diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bbc9ddb10..5a100e374 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -364,6 +364,20 @@ jobs: target: wasm32-wasi helper: "false" - run: pnpm build + # The sanitized seed compiles the compiler's 700 MB module with one clang + # process under AddressSanitizer. It peaks near 14 GB while the Node + # process that emitted the module still holds about 6 GB. Together they + # exceed the runner's 16 GB of memory, so back them with swap. + - name: Enlarge swap for the sanitized seed + if: matrix.flavor == 'san' + run: | + sudo swapoff -a + sudo rm -f /mnt/swapfile + sudo fallocate -l 24G /mnt/swapfile + sudo chmod 600 /mnt/swapfile + sudo mkswap /mnt/swapfile + sudo swapon /mnt/swapfile + free -h - name: Prepare bootstrap seed run: node scripts/bootstrap-seed.mjs env: diff --git a/.oxlintrc.jsonc b/.oxlintrc.jsonc index 0c5cad25f..b0acea28a 100644 --- a/.oxlintrc.jsonc +++ b/.oxlintrc.jsonc @@ -241,6 +241,7 @@ "packages/compiler/src/frontend/comptime-node.ts", "packages/compiler/src/frontend/ts7/world-check.ts", "packages/compiler/src/frontend/ts7/source-parser.test.ts", + "packages/compiler/src/frontend/jsx-source.test.ts", "packages/compiler/src/frontend/npm-static-rewrite.ts", "packages/compiler/src/frontend/npm-static-bundled-cjs.ts", "packages/compiler/src/library/semantic-source.ts", diff --git a/CHANGELOG.md b/CHANGELOG.md index 31d34e8db..ec0b27dbf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,161 @@ All notable changes to scriptc will be documented in this file. +## 0.4.2 + +### Highlights + +- **Faster builds of large programs:** + - scriptc checks with tsc-ts 0.0.7, which answers contextual types and resolved signatures in batches, so lowering asks for them in one request per chunk instead of one per node. A native build of tsc-ts is 7% faster (110.5 → 103.5 s on 8 vCPUs, lowering 12% faster), with an identical executable. + - The native compiler is built with profile-guided optimization, so tsc-ts builds 6.9% faster cold and 10.7% faster after an edit. + - The native compiler's optimization profile is refreshed, so a native build of tsc-ts is a further 10% faster (124.7 → 111.7 s on 16 vCPUs), with lowering 17% faster. + - The class dispatch passes index their receivers by name instead of testing each one, and they reuse their per-function scans while a function's body is unchanged. + - Checker replies resolve objects the compiler already holds from their ids without decoding them. +- **Leaner builds:** + - The frontend of an optimized executable's first build runs in a child process, so the native code generator starts after its memory is released. The cold build of tsc-ts peaks at 2.21 GB instead of 3.30 GB. + - The compiler collects its heap as lowering and LLVM emission grow it. +- **WebCrypto:** + - The `crypto` global, `node:crypto`'s `webcrypto` and `crypto.subtle` are one object. + - `generateKey`, `importKey`, `exportKey`, `encrypt`, `decrypt`, `deriveBits`, `deriveKey`, `wrapKey` and `unwrapKey` cover HMAC, AES-GCM, AES-CBC, AES-CTR, AES-KW, PBKDF2, HKDF and JSON Web Keys. + - `crypto.subtle` signs and verifies with ECDSA, RSASSA-PKCS1-v1_5, RSA-PSS and Ed25519. + - All of it follows Node 24's argument conversion, algorithm normalization and `DOMException`s. jose's symmetric JWE paths run as in Node. +- **JSX:** `.tsx`, `.jsx` and JavaScript files with JSX compile under the project's `jsx` options. + - `react`, `react-jsx` and `react-jsxdev` lower to the factory calls TypeScript emits, with the same keys, fallbacks and source positions. + - `preserve` and `react-native` are refused with a named diagnostic. +- **Faster worker messages:** + - A message is encoded into one block sized to its contents. + - A typed record is posted from its fields. + - A `'message'` listener with a typed parameter builds it from the message, without a checked-dynamic copy. + - Messages between `MessageChannel` ports take 90 ms where they took 250 ms in 0.4.1 (Node: 337 ms), with a peak RSS of 45 MB instead of 115 MB. +- **Loops over typed arrays vectorize:** a loop to a typed array's length keeps one bounds test for every typed array it indexes. + - Index temporaries such as `const j = i * 4` are computed as integers. + - Uint8ClampedArray stores convert inline. + - Row-major index terms such as `r * cols + j` are tested once before the loop. + - Typed-array callback methods call a literal callback directly on an integer counter. + - An 896 × 896 matrix-vector product with four accumulators takes 55 ms instead of 396 ms (Node: 275 ms). `Uint8Array.map` is 4.5 times as fast and `Int32Array.some` 7.8 times as fast as in 0.4.1. + +### Features + +- **Asymmetric keys and signatures in `node:crypto`:** + - `generateKeyPair`, `generateKeyPairSync`, `createPrivateKey` and `createPublicKey` handle RSA, EC and Ed25519 keys in PEM, DER and JWK. + - `sign`, `verify`, `createSign` and `createVerify` sign and verify with those keys, with Node's output and errors. + - Ed25519 verification matches Node on 2,083 test vectors, including small-order and mixed-order keys. +- **`crypto.createDiffieHellman`, `crypto.getDiffieHellman` and `crypto.createDiffieHellmanGroup`:** + - The modp1, modp2 and modp5 to modp18 groups, explicit primes and generators, and generated safe primes are supported, with Node 24's key widths, `verifyError` bits, key range checks and errors. + - Secrets of 0, 1 and p − 1 are rejected, and so are peer keys outside the RFC 5114 subgroups. + - Primes longer than 8192 bits are refused. +- **TLS:** + - `tls.connect` and `tls.createServer` negotiate `ALPNProtocols`, and both sockets report `alpnProtocol`. `minVersion` and `maxVersion` bound the handshake, and `getProtocol()` reports the version it picked. + - `tls.createServer` takes an `SNICallback` that answers a `SecureContext` synchronously or later. + - `new tls.TLSSocket(socket, { isServer: true, ... })` and `tls.connect({ socket })` run the handshake on an open socket, such as an HTTP `CONNECT` tunnel. + - A client whose peer closes before the handshake completes fails with Node's `ECONNRESET`. +- **Streams:** + - Readable, Writable, Duplex, Transform and PassThrough streams take `objectMode`, `readableObjectMode` and `writableObjectMode`, and an object-mode side carries any value through `push`, `write`, `pipe`, `pipeline`, `for await` and `'data'` listeners. + - `_write` and `_transform` receive the encoding argument as Node passes it. + - `Cipheriv` and `Decipheriv` are Transforms: they can be piped, used as `pipeline()` stages, read and iterated. + - `pipeline()` accepts function stages anywhere among its streams, and an array, a string or a synchronous generator as its source. +- **Packages compiled from their JavaScript:** + - js-yaml and smol-toml compile statically and match Node: ES5 error constructors (`Object.create(Error.prototype)`, `Error.call(this)`, `Error.captureStackTrace`) and classes extending `Date` behave as in Node. + - TypeScript's UMD module wrapper compiles as the module body, and tsc's `__importDefault` and `__importStar` helpers around builtin modules compile as plain `require()` calls. + - Interfaces, type aliases and enums that only a package's `.d.ts` declares keep their types. +- **JavaScript sources:** + - Error constructors can be read as values, so `err.constructor === TypeError` works. + - `workerData` can be destructured. + - Unary `+` converts unions of strings, numbers, booleans, `null` and `undefined` as `Number()` does, so `+process.argv[2]` compiles. + - These compile and behave as in Node: + - chained readline listener registration; + - `Number.parseInt` without a radix; + - `match` and `search` with a possibly undefined regular expression; + - `Buffer.from` over an untyped `ArrayBuffer`, with its byte offset; + - Atomics on untyped arrays. + - Buffer methods run on Buffers held as untyped values. +- **`fs`:** + - The synchronous functions (`readFileSync`, `writeFileSync`, `statSync`, `readdirSync`, `mkdirSync`, `rmSync`, `openSync` and the others) accept `file:` URLs wherever they accept a path, with Node's conversion errors. + - `fs.promises.rm` takes `recursive`, `force`, `maxRetries` and `retryDelay`, and removing a directory without `recursive` throws Node's `ERR_FS_EISDIR`. +- **`TypedArray.of`** creates a typed array of its arguments, and a dynamic iterable spread into `Array.of`, `push` or `TypedArray.of` is drained through the iterator protocol. +- **Global `isFinite` converts its argument with ToNumber,** as `isNaN` does. The Math functions and `String.fromCharCode` evaluate every argument before converting any. +- **`Object.freeze`, `Object.seal` and `Object.preventExtensions` of a function** act on its own properties, and `Object.isFrozen`, `Object.isSealed` and `Object.isExtensible` answer for functions. @noble/hashes' frozen hashers work. +- **EventEmitters used through both typed and untyped references** share one listener list. An untyped `emit` reaches typed listeners, an untyped `emit('error')` follows Node's contract, and `listenerCount`, `eventNames`, `listeners`, `rawListeners`, `removeAllListeners`, `setMaxListeners` and `getMaxListeners` work on untyped values. +- **Template literals, `join`, `Array.prototype.toString` and the default `sort()`** call an object's own or inherited `toString`, as `String()` does, instead of printing `[object Object]`. +- **Classes and generics:** + - A generic factory declared to return a constructor type can return one of its local classes, including an anonymous class expression, and `class X extends factory(...)` extends it. + - Static methods of a function-local class compile. + - Constructor types declared in the program (`new () => unknown`, types that also name static members, construct overloads) hold class values. + - Members typed by a conditional type over a literal class type argument (`Field`) resolve. + - Object literals under an erased generic member may hold callbacks with optional or defaulted parameters. +- **Assignment and update targets wrapped in parentheses or non-null assertions** (`(x) = 1`, `h.sums[k]! += 10`) compile. +- **Untyped values keep what JavaScript code adds:** + - Properties that untyped code adds to a typed object literal stay on it. + - Named properties written to an array through an `unknown` view stay on the array, and `for...in` visits them after the indices. + - An `unknown` value asserted to a structural type with methods keeps its class's methods. + - The type inferred from an empty object literal holds any non-nullish value, as TypeScript's `{}` does. +- **A WebAssembly module the build finds but does not translate** rejects with a `CompileError` that names the reason, for example a proposal outside the translated feature set. +- **SC6101 names the functions that likely run often and use untyped values:** + - It shows up to five, most likely hot first, with why their values are untyped: an `any`, an untyped `JSON.parse` result or an untyped JavaScript file. + - `scriptc coverage --fail-on=dynamic` fails on it, and `--no-dynamic-tier-warning` leaves it out. + +### Performance + +- **Regular expressions skip start positions that their leading character sets rule out,** testing 16 positions at once. Searching 50 MB of text for nine case-insensitive alternations takes 0.83 s instead of 3.88 s (Node: 4.38 s). +- **Loop counters stay integers in more loops:** + - Counters that step beside a counted loop's counter (`for (let i = 0, p = 0; i < n; i++, p += 4)`) stay integers: a pixel-stride loop takes 114 ms instead of 340 ms (Node: 190 ms). + - So do two counters that close in on each other (`lo++, hi--`), 17% faster on a permutation-reversal loop. + - So do counters of loops bounded by a string's length or by an invariant limit other than an array's length, and while-loop counters that nested scans advance. A non-ASCII lexer is 9.5% faster. +- **Loops that call a known callback** test its closure once before the loop and call the function directly, so captured `let` bindings stay in registers. `forEach` sums with a captured accumulator are 25 to 55% faster. +- **A speculative integer loop whose check fails** finishes the iteration outside the ordinary loop, which stays optimizable, and a loop site that keeps failing stops trying. With the hole-free-prefix test that element reads now take first, a pairwise loop over an array of class instances with number fields is about 1.25 times as fast as in 0.4.1. +- **Typed arrays over a SharedArrayBuffer** read and write elements inline as relaxed atomic accesses. A numeric kernel over shared `Float64Array`s takes 0.78 s instead of 1.56 s (Node: 0.84 s). +- **`Math.pow(2, n)` for integer exponents** builds the result from its bits. A counter that masks with `Math.pow(2, length)` takes 11.0 s instead of 20.4 s. +- **Async calls that never suspend run in place,** and so does `await Promise.all` of such calls, with Node's microtask turns and rejection handling. An async pipeline takes 5.5 ms instead of 9.7 ms. +- **Strings:** + - `Number()` of a short integer string returns the integer directly. + - `split` builds its result in one inline block, and `indexOf` and `slice` on ASCII strings skip the UTF-16 index map. A log summary is 20% faster. + - `charAt` and `s[i]` on ASCII strings read the character from a table. + - Chains of one-character `replace` calls (the HTML-escape idiom) replace a block of 16 bytes at a time, 12.8% faster on a template renderer. +- **`JSON.parse` and `JSON.stringify`** scan whole words and append parsed typed arrays in bulk, 22% faster to parse and 10% faster to stringify records. +- **Smaller arrays, maps and program-lifetime instances:** + - The array header is 32 bytes instead of 48. + - Arrays of up to 16 known elements keep their slots in their own block. + - Arrays of a known final length get exactly that capacity. + - Small map entry arrays grow in finer steps. + - On Linux, `ProgramLifetime` instances take a 32-bit header and are referenced in 4 bytes. + - They lower the peak memory of a large TypeScript type checker: the 4-byte references alone take a large project's check from 4.13 GB to 3.75 GB, and the 32-bit header saves a further 5%. +- **Worker messages queued together are delivered in one batch** before immediates run. 20,000 messages interleaved with a worker's output drain in 0.5 s instead of 5.6 s (Node: 0.57 s). +- **The cycle collector** never buffers instances of classes whose fields hold nothing cycle-capable, and its per-object paths are shorter. A workload of cyclic graphs is about 5% faster. +- **Array helpers specialized for a callback that cannot throw** skip the pending-exception test, 4.5% faster on a functional pipeline. +- **Compile time:** + - A file's function declarations are walked once for their references, so lowering a 10 MB single-module bundle takes 69 s instead of 219 s. + - LLVM emission computes its module facts once, 6.9% faster on tsc-ts. + - AST walks visit subtrees by their node ranges and read node words through a `Uint32Array`. + - Program builds no longer import runtime bitcode, so builds through Node and through the native compiler produce identical executables. + +### Fixes + +- **Member reads and method calls on an array hole** held in a binding narrowed to one arm of a wide union throw Node's `Cannot read properties of undefined` TypeError. +- **`AsyncLocalStorage`:** + - A store entered before an async function's first await, or in a body that never suspends, is the caller's store after the call. + - Each synchronous generator step runs in its resumer's context, as in Node. +- **Generic arrow and function-expression bindings used before their declarations** behave as in Node, including the `ReferenceError` of a `let` or `const`. +- **Assignment expressions into a module `let` declared without an initializer** (`new Promise((r) => (resolve = r))`) store the value instead of throwing a `ReferenceError`. +- **The `extends` expression of a class defined inside a function** is evaluated each time the definition runs. +- **A `super()` call that leads an `if` condition's comma chain** compiles. +- **A catch binding tests `instanceof`** correctly for values thrown as `unknown`, rejected promises and runtime errors. +- **zlib callbacks receive `undefined` as their result when the operation fails,** as in Node, and a callback may assign a fallback to it. +- **`Readable.push` and `Writable.write` with `string | null` or `Buffer | null` chunks** no longer crash. +- **Object literals keep their key order** through compiler rewrites and on shapes a typed `JSON.parse` also builds, so `JSON.stringify`, `Object.keys` and `console.log` match Node. +- **`console.log` prints class instances with their class name** in programs that test `instanceof` on unknown values. +- **`String`, `Number` and `Boolean` passed to array methods** convert as their direct calls do, and nested array literals in a recursive JSON-style union build. +- **A dynamic method call that the program also reads through a prototype** finds the method when the instance's property bag does not hold it. +- **Storing an element on `Array.prototype`** fails with SC2020, because holes in native arrays do not read inherited elements. +- **A recursive type alias in a generic signature** no longer crashes the compiler. +- **A `Buffer` view of an untyped typed array** compiles. +- **JSX programs checked with tsgo** compile as they do with tsc-ts. +- **Two programs whose entries share a name and build into one directory** no longer share a cached translation unit, so a later cache hit always relinks the right program. +- **A warm build whose cached LLVM exceeds the engine's string limit** reads it in parts instead of failing with `Invalid string length`. +- **The same program compiles to the same executable wherever it sits on disk** and whatever order its functions lower in, and edits outside a declaration do not rename the helpers and closures inside it. +- **Libraries that use untyped byte arrays** link. + + + ## 0.4.1 ### Highlights @@ -77,8 +232,6 @@ All notable changes to scriptc will be documented in this file. - **TypeScript enum exports in compiled packages stay `undefined` until they are assigned,** as in Node. - **Element writes accept a possibly undefined index** (typed-array writes, compound and update writes, element assignments used as values and destructuring targets) and convert it with ToNumber, as element reads already did. - - ## 0.4.0 ### Breaking changes diff --git a/benchmarks/runtime/README.md b/benchmarks/runtime/README.md index 34baccf5b..e177e36c0 100644 --- a/benchmarks/runtime/README.md +++ b/benchmarks/runtime/README.md @@ -11,6 +11,7 @@ Application-shaped workloads for measuring the speed of compiled executables. Ea | `channel-messages` | cpu | 200,000 MessageChannel messages in one thread, half numbers and half small objects, all posted before the port delivers any | | `csv-numbers` | cpu | `split`, `Number`, `parseInt`, `toFixed` | | `cyclic-graphs` | cpu | object graphs with back references built and dropped: trees with parent and sibling links, rings of records sharing a long-lived registry, closures capturing their owner | +| `dictionary-walks` | cpu | `Record` dictionaries walked with for-in and `Object.keys`/`values`/`entries` while entries are added, updated and deleted | | `functional-pipeline` | cpu | `filter`/`map`/`reduce`/`flatMap` chains, closures, small records | | `intern-table`, `intern-table-map`, `intern-table-1`, `intern-table-map-1` | cpu | worker threads interning the same type-like objects under string keys and records under number keys, then hits and misses: one shared `InternTable` per kind of key, or a module-level `Map` per thread (the `-map` entries), with 8 threads or 1 (the `-1` entries); under Node.js every thread has its own tables | | `json-records` | cpu | `JSON.stringify`, typed `JSON.parse`, Map aggregation | @@ -26,7 +27,7 @@ Application-shaped workloads for measuring the speed of compiled executables. Ea | `region-builds` | cpu | a watch-mode build loop over `LifetimeRegion`: each build binds a program in a new region and releases the previous one, which a cache keeps referenced for two builds | | `template-render` | cpu | template literals, escaping, `map`/`join` | | `thread-slots`, `thread-slots-cache` | cpu | worker threads reading one published type graph from an `InternTable` and keeping per-thread state for each type they visit: a `ThreadSlots` family, or a direct-mapped cache by id in front of a `Map` by id (the `-cache` entry); under Node.js both are per thread | -| `typed-array-kernels` | cpu | element-wise loops that run to one typed array's length and index others at the same position: `Float64Array` axpy, `Float32Array` blend, `Uint8Array` mask, wrapping `Int32Array` add, `Uint8ClampedArray` brighten, RGBA to gray, and `reduce`/`some`/`map` callbacks | +| `typed-array-kernels` | cpu | element-wise loops that run to one typed array's length and index others at the same position: `Float64Array` axpy, `Float32Array` blend, `Uint8Array` mask, wrapping `Int32Array` add, `Uint8ClampedArray` brighten, RGBA to gray, a row-major `Float32Array` matrix-vector product, and `reduce`/`some`/`map` callbacks | | `typed-array-scan`, `typed-array-scan-workers` | cpu | element-by-element scan loops over `Uint8Array`, `Uint16Array`, `Int32Array` and `Float64Array`; the second entry is a worker-enabled executable (it can start a worker), so each access also checks for shared storage | | `unicode-lexer` | cpu | a hand-written lexer over a 2.7M-unit bundled module whose comments and strings are in several languages: `charCodeAt` scans, line starts | | `validate-errors` | cpu | throw/catch on a fraction of inputs, small call chains | diff --git a/benchmarks/runtime/workloads/dictionary-walks.json b/benchmarks/runtime/workloads/dictionary-walks.json new file mode 100644 index 000000000..ebfb6ea3f --- /dev/null +++ b/benchmarks/runtime/workloads/dictionary-walks.json @@ -0,0 +1,6 @@ +{ + "entry": "workloads/dictionary-walks.ts", + "args": [ + "1" + ] +} diff --git a/benchmarks/runtime/workloads/dictionary-walks.ts b/benchmarks/runtime/workloads/dictionary-walks.ts new file mode 100644 index 000000000..30b18be63 --- /dev/null +++ b/benchmarks/runtime/workloads/dictionary-walks.ts @@ -0,0 +1,53 @@ +// String-keyed dictionaries (`Record`) walked with for-in, +// Object.keys, Object.values and Object.entries while entries are added, +// updated and deleted: per-category totals, a pruning pass and a merge. +let seed = 777; +function random(): number { + seed = (Math.imul(seed, 1103515245) + 12345) >>> 0; + return seed / 4294967296; +} + +const scale = Number(process.argv[2] ?? "1"); +const categories = 4000; +const counts: Record = {}; +for (let i = 0; i < Math.floor(200000 * scale); i++) { + const key = "c" + Math.floor(random() * categories); + counts[key] = (counts[key] ?? 0) + 1; +} + +let walked = 0; +let sum = 0; +for (let pass = 0; pass < 40; pass++) { + for (const key in counts) { + walked++; + sum += counts[key]!; + } +} + +// Prune: delete the small categories during the walk, including keys the +// walk has not reached yet. +let pruned = 0; +for (const key in counts) { + const n = counts[key]!; + if (n < 45) { + delete counts[key]; + pruned++; + } + const ahead = "c" + ((Number(key.slice(1)) + 1) % categories); + if (n > 60 && ahead in counts && counts[ahead]! < 45) { + delete counts[ahead]; + pruned++; + } +} + +// Merge into a second dictionary and read it back every way. +const merged: Record = {}; +for (const key of Object.keys(counts)) merged[key.toUpperCase()] = counts[key]! * 2; +let values = 0; +for (const v of Object.values(merged)) values += v; +let entries = 0; +for (const [key, v] of Object.entries(merged)) entries += key.length + v; + +console.log( + `walked ${walked}, sum ${sum}, pruned ${pruned}, kept ${Object.keys(counts).length}, values ${values}, entries ${entries}`, +); diff --git a/benchmarks/runtime/workloads/typed-array-kernels.ts b/benchmarks/runtime/workloads/typed-array-kernels.ts index d865ec914..a17c09708 100644 --- a/benchmarks/runtime/workloads/typed-array-kernels.ts +++ b/benchmarks/runtime/workloads/typed-array-kernels.ts @@ -1,5 +1,6 @@ // Element-wise loops over several typed arrays: each loop runs to one -// array's length and reads or writes the others at the same index. +// array's length and reads or writes the others at the same index. Also a +// row-major matrix-vector product with four accumulators per row. function axpy(a: number, x: Float64Array, y: Float64Array, out: Float64Array): void { for (let i = 0; i < out.length; i++) out[i] = a * x[i]! + y[i]!; @@ -21,6 +22,23 @@ function brighten(src: Uint8ClampedArray, gain: number, out: Uint8ClampedArray): for (let i = 0; i < out.length; i++) out[i] = src[i]! * gain + 8; } +function matvec(w: Float32Array, x: Float32Array, out: Float32Array, cols: number): void { + for (let r = 0; r < out.length; r++) { + const base = r * cols; + let s0 = 0; + let s1 = 0; + let s2 = 0; + let s3 = 0; + for (let j = 0; j < cols; j += 4) { + s0 += w[base + j]! * x[j]!; + s1 += w[base + j + 1]! * x[j + 1]!; + s2 += w[base + j + 2]! * x[j + 2]!; + s3 += w[base + j + 3]! * x[j + 3]!; + } + out[r] = s0 + s1 + s2 + s3; + } +} + // Callback methods over a whole typed array. function callbacks(samples: Float32Array, limit: number, data: Uint8Array): number { const energy = samples.reduce((sum, v) => sum + v * v, 0); @@ -53,6 +71,10 @@ const io = new Int32Array(n); const pixels = new Uint8ClampedArray(n); const bright = new Uint8ClampedArray(n); const rgba = new Uint8Array(n * 4); +const side = 256; +const weights = new Float32Array(side * side); +const vector = new Float32Array(side); +const product = new Float32Array(side); const luma = new Uint8Array(n); let state = 2463534242; const next = (): number => { @@ -73,6 +95,8 @@ for (let i = 0; i < n; i++) { pixels[i] = next() & 255; } for (let i = 0; i < rgba.length; i++) rgba[i] = next() & 255; +for (let i = 0; i < weights.length; i++) weights[i] = (next() % 2001) / 1000 - 1; +for (let i = 0; i < side; i++) vector[i] = (next() % 1001) / 1000; const rounds = Math.round(1200 * scale); let f64Sum = 0; @@ -81,6 +105,7 @@ let byteSum = 0; let intSum = 0; let brightSum = 0; let lumaSum = 0; +let productSum = 0; let callbackSum = 0; for (let r = 0; r < rounds; r++) { // Change one input element per round so no round repeats the last. @@ -91,12 +116,14 @@ for (let r = 0; r < rounds; r++) { ia[at] = r * 65537; pixels[at] = r & 255; rgba[at * 4] = r & 255; + vector[r & (side - 1)] = (r % 1001) / 1000; axpy(1.5, x, y, d); blend((r & 15) / 16, fa, fb, fo); mask(data, key, masked); addWrap(ia, ib, io); brighten(pixels, 0.75 + (r & 7) / 8, bright); gray(rgba, luma); + matvec(weights, vector, product, side); if ((r & 3) === 0) callbackSum += callbacks(fo, 140 + (r & 12), masked); const probe = (r * 104729) & (n - 1); f64Sum += d[probe]!; @@ -105,6 +132,7 @@ for (let r = 0; r < rounds; r++) { intSum = (intSum + io[probe]!) | 0; brightSum += bright[probe]!; lumaSum += luma[probe]!; + productSum += product[probe & (side - 1)]!; } console.log(`float64 ${f64Sum.toFixed(3)}`); console.log(`float32 ${f32Sum.toFixed(3)}`); @@ -112,4 +140,5 @@ console.log(`uint8 ${byteSum}`); console.log(`int32 ${intSum}`); console.log(`clamped ${brightSum}`); console.log(`gray ${lumaSum}`); +console.log(`matvec ${productSum.toFixed(3)}`); console.log(`callbacks ${callbackSum.toFixed(3)}`); diff --git a/docs/content/docs/contracts.mdx b/docs/content/docs/contracts.mdx index 5af1cad2f..d5949c4cf 100644 --- a/docs/content/docs/contracts.mdx +++ b/docs/content/docs/contracts.mdx @@ -349,7 +349,7 @@ class Identifier extends SyntaxNode { } ``` -On Linux, except in builds with sanitizers, the instances also take less memory. An executable keeps them in one address range below 32 GiB that it reserves at startup (only the pages in use take memory), and a field typed `C`, `C | undefined` or `C | null`, where `C` is a program-lifetime class, stores the instance's address in 4 bytes instead of 8. In the example, `parent` takes 4 bytes. +On Linux, except in builds with sanitizers, the instances also take less memory. An executable keeps them in one address range below 32 GiB that it reserves at startup (only the pages in use take memory), and a field typed `C`, `C | undefined` or `C | null`, where `C` is a program-lifetime class, stores the instance's address in 4 bytes instead of 8, as does each element of an array of such a type and each value of a `Map` with `string` keys and such a value type. In the example, `parent` takes 4 bytes. scriptc checks that the promise cannot change what the program computes (SC7008): diff --git a/docs/content/docs/coverage.mdx b/docs/content/docs/coverage.mdx index ae36f736d..954b040db 100644 --- a/docs/content/docs/coverage.mdx +++ b/docs/content/docs/coverage.mdx @@ -38,6 +38,8 @@ When a program has unsupported operations, the report adds a group for each root compile staticallyStatements that compile to native instructions. in dynamic tierOf those, the statements in functions that operate on values typed only at runtime, such as any values and untyped JavaScript. Shown when there are any. See Dynamic tier. blockersUnsupported operations with counts, diagnostic codes, reasons, locations, and hints. + runs at startupDeferred operations in a JavaScript module's top-level code, which runs when the module loads: at startup for a module the program imports. + deferred to runtimeOther deferred operations in JavaScript code. Each throws only if it runs. in unreached codeBlockers in code the entry never reaches. A build never compiles that code, so these cannot fail it. differs from NodeSites that compile but can behave differently from Node.js. See Node divergences. npm package compilationNative package compilation outcomes and refusal reasons. @@ -46,7 +48,7 @@ When a program has unsupported operations, the report adds a group for each root Blockers in unreached code and deferred operations in JavaScript packages are listed separately because they don't fail a build. A deferred operation throws a runtime error if it runs, so a successful build doesn't mean every API of a package works; see [unsupported code inside a package](/docs/dependencies#unsupported-code-inside-a-package). -If a function's signature can't compile, the statements in its body count as not compiling, so the percentage reflects how much of the program compiles. +If a function's signature can't compile, the statements in its body count as not compiling, so the percentage reflects how much of the program compiles. The report still lists the diagnostics of those statements; a use of a parameter whose type can't compile reports `SC2004` back to the signature. @@ -211,6 +213,10 @@ For editors, CI annotations, and other tools, `scriptc coverage --print=diagnost group The id of the root-cause group the diagnostic belongs to. A group's primary site is its first in source order. + + startup + true on a deferred diagnostic in a module's top-level code, which runs when the module loads. Absent otherwise. + diff --git a/docs/content/docs/dependencies.mdx b/docs/content/docs/dependencies.mdx index 85a232bb2..409b9cb30 100644 --- a/docs/content/docs/dependencies.mdx +++ b/docs/content/docs/dependencies.mdx @@ -132,7 +132,7 @@ CommonJS packages compile with Node.js's module semantics. Each `require()` eval ## Unsupported code inside a package -A JavaScript package can contain operations scriptc can't compile, often in functions your program never calls. These don't fail the build: each one compiles to code that throws a runtime error if it ever runs. Coverage reports list them as deferred sites, so review the list and test the parts of the package your program uses. +A JavaScript package can contain operations scriptc can't compile, often in functions your program never calls. These don't fail the build: each one compiles to code that throws a runtime error if it ever runs. Coverage reports list them as deferred sites, so review the list and test the parts of the package your program uses. Sites in a module's top-level code are listed under `runs at startup`: that code runs when the module loads, at startup for a module the program imports. ## Provenance sources (experimental) diff --git a/docs/content/docs/limitations.mdx b/docs/content/docs/limitations.mdx index 5be38a924..e132f27da 100644 --- a/docs/content/docs/limitations.mdx +++ b/docs/content/docs/limitations.mdx @@ -34,6 +34,8 @@ title: "Limitations" ../limitations/language-features/commonjs-require.mdx +../limitations/language-features/jsx.mdx + ### Types and class definitions ../limitations/types-and-class-definitions/bigint.mdx diff --git a/docs/content/limitations/builtin-apis/crypto.mdx b/docs/content/limitations/builtin-apis/crypto.mdx index 641d0d8f9..971065408 100644 --- a/docs/content/limitations/builtin-apis/crypto.mdx +++ b/docs/content/limitations/builtin-apis/crypto.mdx @@ -4,11 +4,11 @@ `copy()` of a Hash whose stream flushed, and stream use of a value after its stream closed, throw a coded SC2020 error. Through `unknown` and `any` slots and untyped JavaScript, Hash and Hmac values keep their identity and support `update()`, `digest()`, and Hash's `copy()`, called or read as values; their stream and event members there throw a coded SC2020 error. `timingSafeEqual` compares any view or ArrayBuffer. - Secret KeyObjects (`createSecretKey`, `generateKey` and `generateKeySync` for `hmac` and `aes`) support `type`, `symmetricKeySize`, `export()`, `equals()`, and `instanceof KeyObject`, and createHmac, hkdf, createCipheriv, and createDecipheriv accept them; asymmetric keys, `KeyObject.from()`, and `toCryptoKey()` are not available. + Secret KeyObjects (`createSecretKey`, `generateKey` and `generateKeySync` for `hmac` and `aes`) support `type`, `symmetricKeySize`, `export()`, `equals()`, and `instanceof KeyObject`, and createHmac, hkdf, createCipheriv, and createDecipheriv accept them; `KeyObject.from()` and `toCryptoKey()` are not available. `randomFill`, `randomFillSync` (every view kind and ArrayBuffer), `randomInt`, `randomUUID`, and `randomUUIDv7` follow Node 24.22, including callback timing; a `randomInt` callback whose error parameter is typed `Error | null` receives `null` where Node passes `undefined`. Scrypt supports its default parameters, numeric options, aliases, and memory limit. Both KDFs accept strings, Buffers, numeric typed arrays, DataViews, and fixed-length ArrayBuffers. - `createCipheriv` and `createDecipheriv` support aes-128, aes-192, and aes-256 in CBC, CTR, and GCM modes and chacha20-poly1305, on native targets, through `update()`, `final()`, `setAAD()`, `setAutoPadding()`, `getAuthTag()`, and `setAuthTag()`; other ciphers and wasm32-wasi builds are refused. + `createCipheriv` and `createDecipheriv` support aes-128, aes-192, and aes-256 in CBC, CTR, GCM, ECB, CFB, CFB8, CFB1, and OFB modes, des-ede3 in CBC, ECB, CFB, CFB8, CFB1, and OFB modes, des-ede in CBC, ECB, CFB, and OFB modes, chacha20, and chacha20-poly1305, on native targets, through `update()`, `final()`, `setAAD()`, `setAutoPadding()`, `getAuthTag()`, and `setAuthTag()`; other ciphers and wasm32-wasi builds are refused. Cipheriv and Decipheriv values are also Transforms: they can be written, piped into, used as `pipeline()` stages, read, iterated with `for await`, and listened to, and their stream properties read as in Node; written chunks go through `update()`, and the end pushes `final()` or reports its error through `'error'`. Through `unknown` and `any` slots and untyped JavaScript, in a program that uses streams, their stream properties and their `on`, `once`, `off`, `pipe`, `unpipe`, `read`, `pause`, `resume`, `setEncoding`, `write`, `end`, `destroy`, `cork`, and `uncork` members work; `write` and `end` take no encoding or callback there, and the other stream members throw a coded SC2020 error. Hash, Hmac, Cipheriv, and Decipheriv streams count a written string's bytes in `writableLength`, where Node counts the string's length. @@ -18,8 +18,31 @@ `createECDH` and `ECDH.convertKey` support prime192v1, secp224r1, prime256v1, secp384r1, secp521r1, secp192k1, secp224k1, secp256k1, brainpoolP256r1, brainpoolP384r1, and brainpoolP512r1, on native targets, through `generateKeys()`, `computeSecret()`, `getPublicKey()`, `getPrivateKey()`, `setPrivateKey()`, and `setPublicKey()`, in compressed, uncompressed, and hybrid forms; the other curves `crypto.getCurves()` lists and wasm32-wasi builds are refused. A call on an ECDH value whose arguments no @types/node overload accepts (`getPublicKey("HEX")`) is refused at compile time, as is `computeSecret(view, encoding)`: Node reads that encoding as the input encoding and answers a Buffer where @types/node declares a string. ECDH property reads and writes throw a coded SC2020 error. - Other public-key operations remain unsupported. + `createDiffieHellman`, `getDiffieHellman`, and `createDiffieHellmanGroup` support the modp1, modp2, modp5, and modp14 through modp18 groups, explicit primes and generators, and generated safe primes, on native targets, through `generateKeys()`, `computeSecret()`, `getPrime()`, `getGenerator()`, `getPublicKey()`, `getPrivateKey()`, `setPublicKey()`, `setPrivateKey()`, and `verifyError`, with Node's private key lengths, DH_check results, and errors; wasm32-wasi builds are refused. + Primes longer than 8192 bits throw a coded SC2020 error; Node accepts primes up to 10000 bits for key generation and secrets. + `setPublicKey()` and `setPrivateKey()` keys longer than 80000 bytes, not counting leading zero bytes, throw a coded SC2020 error, as does a generator that long with a prime below 512 bits; Node accepts them. + A call on a DiffieHellman value whose arguments no @types/node overload accepts, or whose output encoding is typed `any`, is refused at compile time, as are its `Object.prototype` methods; other property reads, and property writes, throw a coded SC2020 error. + `Object.keys()` and `in` do not see a DiffieHellman's own `verifyError` property, and `JSON.stringify()` and `instanceof` are refused. + After `createDiffieHellman(primeLength)` throws `Invalid DH parameters`, the next OpenSSL error Node throws can list the failed generation's errors in `opensslErrorStack`; errors in a compiled binary list only their own. + `crypto.diffieHellman`, which takes DH, EC, or X25519 KeyObjects, and the `DiffieHellman` and `DiffieHellmanGroup` constructors are not available. + `checkPrime`, `checkPrimeSync`, `generatePrime`, and `generatePrimeSync` support bigint, ArrayBuffer, typed-array, Buffer, and DataView values and the `checks`, `safe`, `add`, `rem`, and `bigint` options on native targets, with Node's errors; the callback forms compute before they return and call back in a later macrotask. + Candidates and sizes longer than 262144 bits throw a coded SC2020 error, and wasm32-wasi builds are refused. + Node calls their callbacks with `undefined` as the error, which a callback whose error parameter is typed `Error | null`, as @types/node declares it, receives as `null`. + RSA, EC, and Ed25519 private and public KeyObjects come from `generateKeyPairSync`, `generateKeyPair`, `createPrivateKey`, and `createPublicKey`, on native targets. + The EC curves are prime256v1 (P-256), secp384r1 (P-384), secp521r1 (P-521), and secp256k1. + Keys read and write PEM and DER as PKCS#1, SEC1, PKCS#8, and SubjectPublicKeyInfo, and JWK (Ed25519 keys use PKCS#8, SubjectPublicKeyInfo, and JWK); `createPublicKey` also takes a private key or an X.509 certificate. + They support `type`, `asymmetricKeyType`, `asymmetricKeyDetails`, `export()`, `equals()`, and `instanceof KeyObject`. + `crypto.sign`, `crypto.verify`, `createSign`, and `createVerify` support RSA PKCS#1 v1.5 and PSS padding and ECDSA with DER and IEEE P1363 encodings over SHA-1 and SHA-2 digests, and Ed25519 through `crypto.sign` and `crypto.verify` with a `null` or `undefined` algorithm. + Ed25519 signing, verification, and private keys link when a string literal in the program names `ed25519` (in any case); without one they throw a coded SC2020 error, while Ed25519 public keys still import and export. + Sign and Verify values accumulate `update()` data and are not writable streams. + Encrypted private keys (`passphrase`, `cipher`), the other key types (`rsa-pss`, `dsa`, `ed448`, `x25519`, and the rest), and wasm32-wasi builds throw a coded SC2020 error or are refused. + Comparing keys of different types with `equals()` answers false; Node also leaves an OpenSSL error queued, which a later key operation can report. + `publicEncrypt`/`privateDecrypt` remain unsupported. The WebCrypto `crypto` global, which node:crypto also exports as `webcrypto`, supports `randomUUID()` and `getRandomValues()`, also read as values from untyped JavaScript. - Its `subtle` object, also node:crypto's `subtle` export, supports `generateKey()` and `exportKey()` for HMAC and AES keys, `importKey()` for HMAC, AES, PBKDF2, and HKDF keys, `encrypt()` and `decrypt()` with AES-GCM, AES-CBC, and AES-CTR on native targets, `deriveBits()` and `deriveKey()` with PBKDF2 and HKDF, HMAC `sign()` and `verify()`, and SHA-1 and SHA-2 `digest()`. - Secret keys import and export in the `raw`, `raw-secret`, and `jwk` formats; other SubtleCrypto methods, algorithms, and key formats are refused. + Its `subtle` object, also node:crypto's `subtle` export, supports `generateKey()` and `exportKey()` for HMAC and AES keys, `importKey()` for HMAC, AES, PBKDF2, and HKDF keys, `encrypt()` and `decrypt()` with AES-GCM, AES-CBC, and AES-CTR and `wrapKey()` and `unwrapKey()` of secret keys with AES-KW or those ciphers on native targets, `deriveBits()` and `deriveKey()` with PBKDF2 and HKDF, HMAC `sign()` and `verify()`, and SHA-1 and SHA-2 `digest()`. + Secret keys import and export in the `raw`, `raw-secret`, and `jwk` formats. + `generateKey()`, `importKey()`, `exportKey()`, `sign()`, and `verify()` also support ECDSA (P-256, P-384, and P-521), Ed25519, RSASSA-PKCS1-v1_5, and RSA-PSS, with the `spki`, `pkcs8`, and `jwk` formats and `raw` for ECDSA and Ed25519 public keys; ECDSA, Ed25519, and RSA are refused in wasm32-wasi builds. + ECDSA, Ed25519, and RSA link when a string literal in the program names the algorithm; a name built only at run time throws a coded SC2020 error. + `wrapKey()` and `unwrapKey()` of ECDSA, Ed25519, and RSA keys throw a coded SC2020 error. + Other SubtleCrypto methods, algorithms, and key formats are refused or throw a coded SC2020 error. Native derivations finish before their deferred callbacks, so computation blocks the caller. diff --git a/docs/content/limitations/builtin-apis/streams-and-readline.mdx b/docs/content/limitations/builtin-apis/streams-and-readline.mdx index 4e0832c26..8461cc71c 100644 --- a/docs/content/limitations/builtin-apis/streams-and-readline.mdx +++ b/docs/content/limitations/builtin-apis/streams-and-readline.mdx @@ -1,13 +1,17 @@ -- **Streams and readline:** Stream chunks are Buffers or strings. +- **Streams and readline:** Stream chunks are Buffers or strings, and on an object-mode side any value that converts to a checked-dynamic value. `NodeJS.ReadableStream` and `NodeJS.WritableStream`, and `stream.Readable` and `stream.Writable` under `@types/node`, accept every stream: when a program passes file, zlib, or `node:stream` streams through them, child-process pipes and `process.stdout` and `process.stderr` join those streams as `Readable` and `Writable` instances. `readable.pipe(process.stdout)` and `readable.pipe(process.stderr)` compile and, as in Node, do not end the process stream. When an untyped value reaches `NodeJS.ReadableStream` or `NodeJS.WritableStream`, those two interfaces hold any value, so JavaScript stream objects (constructor-function streams, plain objects with `on` and `pipe`) pass through TypeScript functions typed with them and come back to JavaScript as the same objects. Member calls on such a value use the native stream when it is one and call the object's own members otherwise; `readable.pipe()` into a JavaScript destination follows Node's pipe protocol. A `pipeline` stage and a `for await` source that arrive as untyped values must be native streams, or `Cipheriv` and `Decipheriv` values, at run time; other values throw a not-supported error. Native streams expose no `emit` to JavaScript, so a JavaScript stream whose own `pipe()` emits on its destination (the legacy `Stream` pipe) cannot pipe into one. - `Readable.from` accepts arrays, Sets, Map and Set iterators, generators, async generators, and class iterables and async iterables whose values are strings or Buffers, and in JavaScript any iterable or async iterable of them; a stream that mixes strings and Buffers is unsupported. + `Readable.from` accepts strings, Buffers, arrays, Sets, Maps, Map and Set iterators, generators, async generators, and class iterables and async iterables, and in JavaScript any iterable or async iterable. In JavaScript, a sync iterator value with a `then` method that is not a promise, and an object whose `[Symbol.asyncIterator]` or `[Symbol.iterator]` method is a generator method, are unsupported. - Object-mode streams of other values (including `Readable.from` over a `Map`, whose values are entry arrays) are unsupported. + The `objectMode` option, and the `readableObjectMode` and `writableObjectMode` options of a `Duplex`, `Transform`, or `PassThrough`, take `true` or `false` literals in an options object literal; options passed as a value are read when the stream is built. + In typed code, a `write` or `transform` callback, or a `_write` or `_transform` method, may declare its chunk with any type that converts to a checked-dynamic value: the chunk converts when the callback runs, and a value of another type throws a TypeError there. + An object-mode write passes its encoding argument to `write` and `transform` as Node does (`undefined` when absent, `null` when a callback took its place); a parameter declared `string` receives `""` for `undefined` and `null`, and one declared `string | null | undefined` receives Node's value. + `readable.read()` on an object-mode stream answers its Buffers; another value throws a not-supported error (read such values with `'data'` listeners, `for await`, or `pipe`). + `node:stream/consumers` and readline interfaces read object-mode streams of strings and Buffers; other values throw a not-supported error. `pipeline` accepts function stages anywhere among its streams: async generator functions (`async function* (source)`, iterating `source` with `for await`), an async generator source function (`async function* ({ signal })`), and an async function destination whose value resolves the promise form and reaches the callback's second argument. A stage is written inline, or named by a function declaration or a `const` bound to a function expression that nothing reassigns. The source may also be an array, a string, a synchronous generator object, or a synchronous generator function without parameters; their elements reach the next stage on the same ticks as in Node. diff --git a/docs/content/limitations/builtin-function-values.mdx b/docs/content/limitations/builtin-function-values.mdx index 37ed8eb48..c1bab6680 100644 --- a/docs/content/limitations/builtin-function-values.mdx +++ b/docs/content/limitations/builtin-function-values.mdx @@ -28,7 +28,7 @@ The `captureRejections` option and `events.errorMonitor` listeners are not suppo The stream calls the object's `_read`, `_write`, `_final`, `_transform`, `_flush`, and `_destroy` with the object as `this`, and the stream prototypes' methods and accessors work on it. Its listeners live in the native stream rather than in `_events`, and `_readableState` and `_writableState` are read-only views that answer the commonly read fields. Typed code that receives such an object as a stream class uses its native stream, and piping a native stream into it works. -The options the native streams refuse (`objectMode`, `decodeStrings: false`, `signal`, and others) are refused here too, as are `_writev`, `_construct`, symbol event names, `listeners()`, `rawListeners()`, `wrap()`, strings written in encodings other than UTF-8, and pipes into a destination that overrides `write()` or `end()`. +The options the native streams refuse (`decodeStrings: false`, `signal`, and others) are refused here too, as are `_writev`, `_construct`, symbol event names, `listeners()`, `rawListeners()`, `wrap()`, strings written to a byte-mode stream in encodings other than UTF-8, and pipes into a destination that overrides `write()` or `end()`. Other class constructors passed to `util.inherits` or `util.deprecate` are refused. Deprecation warnings are emitted on the next tick and follow assignments to `process.noDeprecation` and `process.throwDeprecation`; compiled binaries take no Node command-line flags such as `--no-deprecation` or `--trace-deprecation`, and `process.traceDeprecation` is unsupported. A stored `util.deprecate` reports a `length` of 4, where Node reports 3. diff --git a/docs/content/limitations/language-features/jsx.mdx b/docs/content/limitations/language-features/jsx.mdx new file mode 100644 index 000000000..a491dc995 --- /dev/null +++ b/docs/content/limitations/language-features/jsx.mdx @@ -0,0 +1,5 @@ +- **JSX:** JSX in `.tsx`, `.jsx` and JavaScript files compiles with the project's `jsx` setting: `"react"` with `jsxFactory`, `jsxFragmentFactory` or `reactNamespace`, and `"react-jsx"` or `"react-jsxdev"` with `jsxImportSource`, as well as the `@jsx`, `@jsxFrag`, `@jsxImportSource` and `@jsxRuntime` pragmas. + The checker types JSX with these settings, and each element becomes the call TypeScript emits for it, importing the automatic runtime's functions from `/jsx-runtime` or `jsx-dev-runtime`. + `"jsx": "preserve"` and `"react-native"`, which keep JSX in the output, are refused. + The call an element becomes must type-check: a factory or runtime function whose parameters do not accept what an element passes is reported at the element. + With `"react-jsxdev"`, `jsxDEV` receives `undefined` as `self` where `this` has no declared type (in a function that is not a method), which is the value such a function observes when called without a receiver. diff --git a/docs/src/generated/node-v24-compatibility.json b/docs/src/generated/node-v24-compatibility.json index 682e68f99..c341da852 100644 --- a/docs/src/generated/node-v24-compatibility.json +++ b/docs/src/generated/node-v24-compatibility.json @@ -11085,9 +11085,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the call shapes accepted by the compiler lowering.", + "verification": "test-backed" } }, { @@ -11108,9 +11108,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the call shapes accepted by the compiler lowering.", + "verification": "test-backed" } }, { @@ -11177,9 +11177,9 @@ "inherited": true }, "static": { - "status": "refused", - "detail": "Explicitly rejected by a named compiler diagnostic.", - "verification": "explicit-refusal" + "status": "partial", + "detail": "Supports the modp groups, explicit primes and generators, and generated safe primes up to 8192 bits with Node's argument, generator and modulus errors.", + "verification": "test-backed" } }, { @@ -11200,9 +11200,9 @@ "inherited": true }, "static": { - "status": "refused", - "detail": "Explicitly rejected by a named compiler diagnostic.", - "verification": "explicit-refusal" + "status": "partial", + "detail": "Supports the modp groups, explicit primes and generators, and generated safe primes up to 8192 bits with Node's argument, generator and modulus errors.", + "verification": "test-backed" } }, { @@ -11223,9 +11223,9 @@ "inherited": true }, "static": { - "status": "refused", - "detail": "Explicitly rejected by a named compiler diagnostic.", - "verification": "explicit-refusal" + "status": "partial", + "detail": "Supports the modp groups, explicit primes and generators, and generated safe primes up to 8192 bits with Node's argument, generator and modulus errors.", + "verification": "test-backed" } }, { @@ -11247,7 +11247,7 @@ }, "static": { "status": "partial", - "detail": "Implemented for the call shapes accepted by the compiler lowering.", + "detail": "Supports prime192v1, secp224r1, prime256v1, secp384r1, secp521r1, secp192k1, secp224k1, secp256k1, brainpoolP256r1, brainpoolP384r1 and brainpoolP512r1 with Node's curve, key, point, format and encoding errors; other curves Node knows are refused.", "verification": "test-backed" } }, @@ -11338,9 +11338,9 @@ "inherited": true }, "static": { - "status": "refused", - "detail": "Explicitly rejected by a named compiler diagnostic.", - "verification": "explicit-refusal" + "status": "partial", + "detail": "Implemented by a dedicated static compiler/runtime path.", + "verification": "test-backed" } }, { @@ -11361,9 +11361,9 @@ "inherited": true }, "static": { - "status": "refused", - "detail": "Explicitly rejected by a named compiler diagnostic.", - "verification": "explicit-refusal" + "status": "partial", + "detail": "Implemented by a dedicated static compiler/runtime path.", + "verification": "test-backed" } }, { @@ -11407,9 +11407,9 @@ "inherited": true }, "static": { - "status": "refused", - "detail": "Explicitly rejected by a named compiler diagnostic.", - "verification": "explicit-refusal" + "status": "partial", + "detail": "Implemented by a dedicated static compiler/runtime path.", + "verification": "test-backed" } }, { @@ -11430,9 +11430,9 @@ "inherited": true }, "static": { - "status": "refused", - "detail": "Explicitly rejected by a named compiler diagnostic.", - "verification": "explicit-refusal" + "status": "partial", + "detail": "Implemented by a dedicated static compiler/runtime path.", + "verification": "test-backed" } }, { @@ -11545,9 +11545,9 @@ "inherited": true }, "static": { - "status": "refused", - "detail": "Explicitly rejected by a named compiler diagnostic.", - "verification": "explicit-refusal" + "status": "partial", + "detail": "Implemented by a dedicated static compiler/runtime path.", + "verification": "test-backed" } }, { @@ -11568,9 +11568,9 @@ "inherited": true }, "static": { - "status": "refused", - "detail": "Explicitly rejected by a named compiler diagnostic.", - "verification": "explicit-refusal" + "status": "partial", + "detail": "Implemented by a dedicated static compiler/runtime path.", + "verification": "test-backed" } }, { @@ -11614,9 +11614,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the call shapes accepted by the compiler lowering.", + "verification": "test-backed" } }, { @@ -11637,9 +11637,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the call shapes accepted by the compiler lowering.", + "verification": "test-backed" } }, { @@ -11729,9 +11729,9 @@ "inherited": true }, "static": { - "status": "refused", - "detail": "Explicitly rejected by a named compiler diagnostic.", - "verification": "explicit-refusal" + "status": "partial", + "detail": "Supports the modp groups, explicit primes and generators, and generated safe primes up to 8192 bits with Node's argument, generator and modulus errors.", + "verification": "test-backed" } }, { @@ -12327,9 +12327,9 @@ "inherited": true }, "static": { - "status": "refused", - "detail": "Explicitly rejected by a named compiler diagnostic.", - "verification": "explicit-refusal" + "status": "partial", + "detail": "Implemented by a dedicated static compiler/runtime path.", + "verification": "test-backed" } }, { @@ -12373,9 +12373,9 @@ "inherited": true }, "static": { - "status": "refused", - "detail": "Explicitly rejected by a named compiler diagnostic.", - "verification": "explicit-refusal" + "status": "partial", + "detail": "Implemented by a dedicated static compiler/runtime path.", + "verification": "test-backed" } }, { @@ -12856,9 +12856,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Matches Node's private key lengths, DH_check results, public key range checks, secret padding and errors; primes longer than 8192 bits, property writes and property reads other than verifyError are refused.", + "verification": "test-backed" } }, { @@ -12879,9 +12879,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Matches Node's private key lengths, DH_check results, public key range checks, secret padding and errors; primes longer than 8192 bits, property writes and property reads other than verifyError are refused.", + "verification": "test-backed" } }, { @@ -12902,9 +12902,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Matches Node's private key lengths, DH_check results, public key range checks, secret padding and errors; primes longer than 8192 bits, property writes and property reads other than verifyError are refused.", + "verification": "test-backed" } }, { @@ -12925,9 +12925,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Matches Node's private key lengths, DH_check results, public key range checks, secret padding and errors; primes longer than 8192 bits, property writes and property reads other than verifyError are refused.", + "verification": "test-backed" } }, { @@ -12948,9 +12948,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Matches Node's private key lengths, DH_check results, public key range checks, secret padding and errors; primes longer than 8192 bits, property writes and property reads other than verifyError are refused.", + "verification": "test-backed" } }, { @@ -12971,9 +12971,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Matches Node's private key lengths, DH_check results, public key range checks, secret padding and errors; primes longer than 8192 bits, property writes and property reads other than verifyError are refused.", + "verification": "test-backed" } }, { @@ -12994,9 +12994,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Matches Node's private key lengths, DH_check results, public key range checks, secret padding and errors; primes longer than 8192 bits, property writes and property reads other than verifyError are refused.", + "verification": "test-backed" } }, { @@ -13017,9 +13017,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Matches Node's private key lengths, DH_check results, public key range checks, secret padding and errors; primes longer than 8192 bits, property writes and property reads other than verifyError are refused.", + "verification": "test-backed" } }, { @@ -13040,9 +13040,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Matches Node's private key lengths, DH_check results, public key range checks, secret padding and errors; primes longer than 8192 bits, property writes and property reads other than verifyError are refused.", + "verification": "test-backed" } }, { @@ -13063,9 +13063,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Matches Node's private key lengths, DH_check results, public key range checks, secret padding and errors; primes longer than 8192 bits, property writes and property reads other than verifyError are refused.", + "verification": "test-backed" } }, { @@ -13086,9 +13086,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Matches Node's private key lengths, DH_check results, public key range checks, secret padding and errors; primes longer than 8192 bits, property writes and property reads other than verifyError are refused.", + "verification": "test-backed" } }, { @@ -13523,9 +13523,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented by a dedicated static compiler/runtime path.", + "verification": "test-backed" } }, { @@ -13546,9 +13546,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented by a dedicated static compiler/runtime path.", + "verification": "test-backed" } }, { @@ -13684,9 +13684,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented by a dedicated static compiler/runtime path.", + "verification": "test-backed" } }, { @@ -13707,9 +13707,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented by a dedicated static compiler/runtime path.", + "verification": "test-backed" } }, { @@ -13730,9 +13730,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented by a dedicated static compiler/runtime path.", + "verification": "test-backed" } }, { @@ -13753,9 +13753,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented by a dedicated static compiler/runtime path.", + "verification": "test-backed" } }, { @@ -13776,9 +13776,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented by a dedicated static compiler/runtime path.", + "verification": "test-backed" } }, { @@ -13799,9 +13799,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented by a dedicated static compiler/runtime path.", + "verification": "test-backed" } }, { @@ -18880,9 +18880,9 @@ "anchor": "err_crypto_invalid_keylen", "nodeStability": null, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented by a dedicated static compiler/runtime path.", + "verification": "test-backed" } }, { @@ -19006,9 +19006,9 @@ "anchor": "err_crypto_invalid_state", "nodeStability": null, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented by a dedicated static compiler/runtime path.", + "verification": "test-backed" } }, { @@ -19276,9 +19276,9 @@ "anchor": "err_crypto_unknown_dh_group", "nodeStability": null, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented by a dedicated static compiler/runtime path.", + "verification": "test-backed" } }, { @@ -67806,9 +67806,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -68565,9 +68565,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -80277,9 +80277,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -80323,9 +80323,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { diff --git a/internal/compatibility/generated/node-v24-internal.json b/internal/compatibility/generated/node-v24-internal.json index a86967ff3..9393fcaa8 100644 --- a/internal/compatibility/generated/node-v24-internal.json +++ b/internal/compatibility/generated/node-v24-internal.json @@ -19313,17 +19313,33 @@ "tests/corpus/6703-ecdh-private-keys.ts", "tests/corpus/6704-ecdh-set-public-key.cjs", "tests/corpus/6705-ecdh-handles.ts", + "tests/corpus/6706-dh-groups.ts", + "tests/corpus/6707-dh-explicit-params.ts", + "tests/corpus/6708-dh-errors.js", + "tests/corpus/6709-dh-generate.ts", + "tests/corpus/6710-dh-encodings.cjs", + "tests/corpus/6711-dh-handles.ts", "tests/corpus/7055-cipher-streams.ts", "tests/corpus/7056-cipher-stream-errors.ts", "tests/corpus/7057-cipher-streams.mjs", + "tests/corpus/7080-crypto-ed25519.mjs", + "tests/corpus/7082-crypto-key-objects.ts", + "tests/corpus/7083-crypto-sign-verify.ts", + "tests/corpus/7084-crypto-key-pairs.cjs", "tests/corpus/7122-webcrypto-node-crypto.ts", "tests/corpus/7123-webcrypto-node-crypto.cjs", + "tests/corpus/7150-cipher-legacy-modes.ts", + "tests/corpus/7151-cipher-legacy-errors.cjs", + "tests/corpus/7152-crypto-prime.ts", + "tests/corpus/7153-crypto-prime-errors.cjs", "tests/corpus/node24-crypto-kdf-errors.ts", "tests/corpus/node24-crypto-kdf-options.ts", "tests/corpus/node24-crypto-kdf.ts", "tests/corpus/node24-crypto-sha2.ts", "tests/fixtures/node-types/cipher.ts", + "tests/fixtures/node-types/dh.ts", "tests/fixtures/node-types/ecdh.ts", + "tests/fixtures/node-types/keys.ts", "tests/harness/npm-static.test.ts" ] }, @@ -19646,8 +19662,12 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:crypto" + "status": "partial", + "evidence": "surface-manifest:node-builtin.crypto.checkPrime", + "tests": [ + "tests/corpus/7152-crypto-prime.ts", + "tests/corpus/7153-crypto-prime-errors.cjs" + ] }, "anchorSource": "exact" }, @@ -19669,8 +19689,12 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:crypto" + "status": "partial", + "evidence": "surface-manifest:node-builtin.crypto.checkPrimeSync", + "tests": [ + "tests/corpus/7152-crypto-prime.ts", + "tests/corpus/7153-crypto-prime-errors.cjs" + ] }, "anchorSource": "exact" }, @@ -19695,6 +19719,8 @@ "status": "partial", "evidence": "surface-manifest:node-builtin.crypto.createCipheriv", "tests": [ + "tests/corpus/7150-cipher-legacy-modes.ts", + "tests/corpus/7151-cipher-legacy-errors.cjs", "tests/corpus/5250-cipher-aes-gcm.ts", "tests/corpus/5251-cipher-aes-cbc.ts", "tests/corpus/5252-cipher-aes-ctr.ts", @@ -19734,6 +19760,8 @@ "status": "partial", "evidence": "surface-manifest:node-builtin.crypto.createDecipheriv", "tests": [ + "tests/corpus/7150-cipher-legacy-modes.ts", + "tests/corpus/7151-cipher-legacy-errors.cjs", "tests/corpus/5250-cipher-aes-gcm.ts", "tests/corpus/5251-cipher-aes-cbc.ts", "tests/corpus/5252-cipher-aes-ctr.ts", @@ -19770,8 +19798,17 @@ "inherited": true }, "static": { - "status": "refused", - "evidence": "surface-manifest:unsupported:node-builtin.crypto.createDiffieHellman" + "status": "partial", + "evidence": "surface-manifest:node-builtin.crypto.createDiffieHellman", + "tests": [ + "tests/corpus/6706-dh-groups.ts", + "tests/corpus/6707-dh-explicit-params.ts", + "tests/corpus/6708-dh-errors.js", + "tests/corpus/6709-dh-generate.ts", + "tests/corpus/6710-dh-encodings.cjs", + "tests/corpus/6711-dh-handles.ts", + "tests/fixtures/node-types/dh.ts" + ] }, "anchorSource": "exact" }, @@ -19793,8 +19830,17 @@ "inherited": true }, "static": { - "status": "refused", - "evidence": "surface-manifest:unsupported:node-builtin.crypto.createDiffieHellman" + "status": "partial", + "evidence": "surface-manifest:node-builtin.crypto.createDiffieHellman", + "tests": [ + "tests/corpus/6706-dh-groups.ts", + "tests/corpus/6707-dh-explicit-params.ts", + "tests/corpus/6708-dh-errors.js", + "tests/corpus/6709-dh-generate.ts", + "tests/corpus/6710-dh-encodings.cjs", + "tests/corpus/6711-dh-handles.ts", + "tests/fixtures/node-types/dh.ts" + ] }, "anchorSource": "exact" }, @@ -19816,8 +19862,17 @@ "inherited": true }, "static": { - "status": "refused", - "evidence": "surface-manifest:unsupported:node-builtin.crypto.createDiffieHellmanGroup" + "status": "partial", + "evidence": "surface-manifest:node-builtin.crypto.createDiffieHellmanGroup", + "tests": [ + "tests/corpus/6706-dh-groups.ts", + "tests/corpus/6707-dh-explicit-params.ts", + "tests/corpus/6708-dh-errors.js", + "tests/corpus/6709-dh-generate.ts", + "tests/corpus/6710-dh-encodings.cjs", + "tests/corpus/6711-dh-handles.ts", + "tests/fixtures/node-types/dh.ts" + ] }, "anchorSource": "exact" }, @@ -19972,8 +20027,15 @@ "inherited": true }, "static": { - "status": "refused", - "evidence": "surface-manifest:unsupported:node-builtin.crypto.createPrivateKey" + "status": "partial", + "evidence": "compiler-dedicated:crypto.createPrivateKey", + "tests": [ + "tests/corpus/7080-crypto-ed25519.mjs", + "tests/corpus/7082-crypto-key-objects.ts", + "tests/corpus/7083-crypto-sign-verify.ts", + "tests/corpus/7084-crypto-key-pairs.cjs", + "tests/fixtures/node-types/keys.ts" + ] }, "anchorSource": "exact" }, @@ -20018,8 +20080,15 @@ "inherited": true }, "static": { - "status": "refused", - "evidence": "surface-manifest:unsupported:node-builtin.crypto.createPublicKey" + "status": "partial", + "evidence": "compiler-dedicated:crypto.createPublicKey", + "tests": [ + "tests/corpus/7080-crypto-ed25519.mjs", + "tests/corpus/7082-crypto-key-objects.ts", + "tests/corpus/7083-crypto-sign-verify.ts", + "tests/corpus/7084-crypto-key-pairs.cjs", + "tests/fixtures/node-types/keys.ts" + ] }, "anchorSource": "exact" }, @@ -20068,8 +20137,15 @@ "inherited": true }, "static": { - "status": "refused", - "evidence": "surface-manifest:unsupported:node-builtin.crypto.createSign" + "status": "partial", + "evidence": "compiler-dedicated:crypto.createSign", + "tests": [ + "tests/corpus/7080-crypto-ed25519.mjs", + "tests/corpus/7082-crypto-key-objects.ts", + "tests/corpus/7083-crypto-sign-verify.ts", + "tests/corpus/7084-crypto-key-pairs.cjs", + "tests/fixtures/node-types/keys.ts" + ] }, "anchorSource": "exact" }, @@ -20091,8 +20167,15 @@ "inherited": true }, "static": { - "status": "refused", - "evidence": "surface-manifest:unsupported:node-builtin.crypto.createVerify" + "status": "partial", + "evidence": "compiler-dedicated:crypto.createVerify", + "tests": [ + "tests/corpus/7080-crypto-ed25519.mjs", + "tests/corpus/7082-crypto-key-objects.ts", + "tests/corpus/7083-crypto-sign-verify.ts", + "tests/corpus/7084-crypto-key-pairs.cjs", + "tests/fixtures/node-types/keys.ts" + ] }, "anchorSource": "exact" }, @@ -20210,8 +20293,15 @@ "inherited": true }, "static": { - "status": "refused", - "evidence": "surface-manifest:unsupported:node-builtin.crypto.generateKeyPair" + "status": "partial", + "evidence": "compiler-dedicated:crypto.generateKeyPair", + "tests": [ + "tests/corpus/7080-crypto-ed25519.mjs", + "tests/corpus/7082-crypto-key-objects.ts", + "tests/corpus/7083-crypto-sign-verify.ts", + "tests/corpus/7084-crypto-key-pairs.cjs", + "tests/fixtures/node-types/keys.ts" + ] }, "anchorSource": "exact" }, @@ -20233,8 +20323,15 @@ "inherited": true }, "static": { - "status": "refused", - "evidence": "surface-manifest:unsupported:node-builtin.crypto.generateKeyPairSync" + "status": "partial", + "evidence": "compiler-dedicated:crypto.generateKeyPairSync", + "tests": [ + "tests/corpus/7080-crypto-ed25519.mjs", + "tests/corpus/7082-crypto-key-objects.ts", + "tests/corpus/7083-crypto-sign-verify.ts", + "tests/corpus/7084-crypto-key-pairs.cjs", + "tests/fixtures/node-types/keys.ts" + ] }, "anchorSource": "exact" }, @@ -20283,8 +20380,12 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:crypto" + "status": "partial", + "evidence": "surface-manifest:node-builtin.crypto.generatePrime", + "tests": [ + "tests/corpus/7152-crypto-prime.ts", + "tests/corpus/7153-crypto-prime-errors.cjs" + ] }, "anchorSource": "exact" }, @@ -20306,8 +20407,12 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:crypto" + "status": "partial", + "evidence": "surface-manifest:node-builtin.crypto.generatePrimeSync", + "tests": [ + "tests/corpus/7152-crypto-prime.ts", + "tests/corpus/7153-crypto-prime-errors.cjs" + ] }, "anchorSource": "exact" }, @@ -20398,8 +20503,17 @@ "inherited": true }, "static": { - "status": "refused", - "evidence": "surface-manifest:unsupported:node-builtin.crypto.getDiffieHellman" + "status": "partial", + "evidence": "surface-manifest:node-builtin.crypto.getDiffieHellman", + "tests": [ + "tests/corpus/6706-dh-groups.ts", + "tests/corpus/6707-dh-explicit-params.ts", + "tests/corpus/6708-dh-errors.js", + "tests/corpus/6709-dh-generate.ts", + "tests/corpus/6710-dh-encodings.cjs", + "tests/corpus/6711-dh-handles.ts", + "tests/fixtures/node-types/dh.ts" + ] }, "anchorSource": "exact" }, @@ -21107,8 +21221,15 @@ "inherited": true }, "static": { - "status": "refused", - "evidence": "surface-manifest:unsupported:node-builtin.crypto.sign" + "status": "partial", + "evidence": "compiler-dedicated:crypto.sign", + "tests": [ + "tests/corpus/7080-crypto-ed25519.mjs", + "tests/corpus/7082-crypto-key-objects.ts", + "tests/corpus/7083-crypto-sign-verify.ts", + "tests/corpus/7084-crypto-key-pairs.cjs", + "tests/fixtures/node-types/keys.ts" + ] }, "anchorSource": "exact" }, @@ -21157,8 +21278,15 @@ "inherited": true }, "static": { - "status": "refused", - "evidence": "surface-manifest:unsupported:node-builtin.crypto.verify" + "status": "partial", + "evidence": "compiler-dedicated:crypto.verify", + "tests": [ + "tests/corpus/7080-crypto-ed25519.mjs", + "tests/corpus/7082-crypto-key-objects.ts", + "tests/corpus/7083-crypto-sign-verify.ts", + "tests/corpus/7084-crypto-key-pairs.cjs", + "tests/fixtures/node-types/keys.ts" + ] }, "anchorSource": "exact" }, @@ -22208,8 +22336,17 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:crypto" + "status": "partial", + "evidence": "compiler-dedicated:DiffieHellman", + "tests": [ + "tests/corpus/6706-dh-groups.ts", + "tests/corpus/6707-dh-explicit-params.ts", + "tests/corpus/6708-dh-errors.js", + "tests/corpus/6709-dh-generate.ts", + "tests/corpus/6710-dh-encodings.cjs", + "tests/corpus/6711-dh-handles.ts", + "tests/fixtures/node-types/dh.ts" + ] }, "anchorSource": "exact" }, @@ -22231,8 +22368,17 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:crypto" + "status": "partial", + "evidence": "compiler-dedicated:diffieHellman.computeSecret", + "tests": [ + "tests/corpus/6706-dh-groups.ts", + "tests/corpus/6707-dh-explicit-params.ts", + "tests/corpus/6708-dh-errors.js", + "tests/corpus/6709-dh-generate.ts", + "tests/corpus/6710-dh-encodings.cjs", + "tests/corpus/6711-dh-handles.ts", + "tests/fixtures/node-types/dh.ts" + ] }, "anchorSource": "exact" }, @@ -22254,8 +22400,17 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:crypto" + "status": "partial", + "evidence": "compiler-dedicated:diffieHellman.generateKeys", + "tests": [ + "tests/corpus/6706-dh-groups.ts", + "tests/corpus/6707-dh-explicit-params.ts", + "tests/corpus/6708-dh-errors.js", + "tests/corpus/6709-dh-generate.ts", + "tests/corpus/6710-dh-encodings.cjs", + "tests/corpus/6711-dh-handles.ts", + "tests/fixtures/node-types/dh.ts" + ] }, "anchorSource": "exact" }, @@ -22277,8 +22432,17 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:crypto" + "status": "partial", + "evidence": "compiler-dedicated:diffieHellman.getGenerator", + "tests": [ + "tests/corpus/6706-dh-groups.ts", + "tests/corpus/6707-dh-explicit-params.ts", + "tests/corpus/6708-dh-errors.js", + "tests/corpus/6709-dh-generate.ts", + "tests/corpus/6710-dh-encodings.cjs", + "tests/corpus/6711-dh-handles.ts", + "tests/fixtures/node-types/dh.ts" + ] }, "anchorSource": "exact" }, @@ -22300,8 +22464,17 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:crypto" + "status": "partial", + "evidence": "compiler-dedicated:diffieHellman.getPrime", + "tests": [ + "tests/corpus/6706-dh-groups.ts", + "tests/corpus/6707-dh-explicit-params.ts", + "tests/corpus/6708-dh-errors.js", + "tests/corpus/6709-dh-generate.ts", + "tests/corpus/6710-dh-encodings.cjs", + "tests/corpus/6711-dh-handles.ts", + "tests/fixtures/node-types/dh.ts" + ] }, "anchorSource": "exact" }, @@ -22323,8 +22496,17 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:crypto" + "status": "partial", + "evidence": "compiler-dedicated:diffieHellman.getPrivateKey", + "tests": [ + "tests/corpus/6706-dh-groups.ts", + "tests/corpus/6707-dh-explicit-params.ts", + "tests/corpus/6708-dh-errors.js", + "tests/corpus/6709-dh-generate.ts", + "tests/corpus/6710-dh-encodings.cjs", + "tests/corpus/6711-dh-handles.ts", + "tests/fixtures/node-types/dh.ts" + ] }, "anchorSource": "exact" }, @@ -22346,8 +22528,17 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:crypto" + "status": "partial", + "evidence": "compiler-dedicated:diffieHellman.getPublicKey", + "tests": [ + "tests/corpus/6706-dh-groups.ts", + "tests/corpus/6707-dh-explicit-params.ts", + "tests/corpus/6708-dh-errors.js", + "tests/corpus/6709-dh-generate.ts", + "tests/corpus/6710-dh-encodings.cjs", + "tests/corpus/6711-dh-handles.ts", + "tests/fixtures/node-types/dh.ts" + ] }, "anchorSource": "exact" }, @@ -22369,8 +22560,17 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:crypto" + "status": "partial", + "evidence": "compiler-dedicated:diffieHellman.setPrivateKey", + "tests": [ + "tests/corpus/6706-dh-groups.ts", + "tests/corpus/6707-dh-explicit-params.ts", + "tests/corpus/6708-dh-errors.js", + "tests/corpus/6709-dh-generate.ts", + "tests/corpus/6710-dh-encodings.cjs", + "tests/corpus/6711-dh-handles.ts", + "tests/fixtures/node-types/dh.ts" + ] }, "anchorSource": "exact" }, @@ -22392,8 +22592,17 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:crypto" + "status": "partial", + "evidence": "compiler-dedicated:diffieHellman.setPublicKey", + "tests": [ + "tests/corpus/6706-dh-groups.ts", + "tests/corpus/6707-dh-explicit-params.ts", + "tests/corpus/6708-dh-errors.js", + "tests/corpus/6709-dh-generate.ts", + "tests/corpus/6710-dh-encodings.cjs", + "tests/corpus/6711-dh-handles.ts", + "tests/fixtures/node-types/dh.ts" + ] }, "anchorSource": "exact" }, @@ -22415,8 +22624,17 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:crypto" + "status": "partial", + "evidence": "compiler-dedicated:diffieHellman.verifyError", + "tests": [ + "tests/corpus/6706-dh-groups.ts", + "tests/corpus/6707-dh-explicit-params.ts", + "tests/corpus/6708-dh-errors.js", + "tests/corpus/6709-dh-generate.ts", + "tests/corpus/6710-dh-encodings.cjs", + "tests/corpus/6711-dh-handles.ts", + "tests/fixtures/node-types/dh.ts" + ] }, "anchorSource": "exact" }, @@ -22438,8 +22656,17 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:crypto" + "status": "partial", + "evidence": "compiler-dedicated:DiffieHellmanGroup", + "tests": [ + "tests/corpus/6706-dh-groups.ts", + "tests/corpus/6707-dh-explicit-params.ts", + "tests/corpus/6708-dh-errors.js", + "tests/corpus/6709-dh-generate.ts", + "tests/corpus/6710-dh-encodings.cjs", + "tests/corpus/6711-dh-handles.ts", + "tests/fixtures/node-types/dh.ts" + ] }, "anchorSource": "exact" }, @@ -23062,8 +23289,15 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:crypto" + "status": "partial", + "evidence": "compiler-dedicated:asymmetricKeyDetails", + "tests": [ + "tests/corpus/7080-crypto-ed25519.mjs", + "tests/corpus/7082-crypto-key-objects.ts", + "tests/corpus/7083-crypto-sign-verify.ts", + "tests/corpus/7084-crypto-key-pairs.cjs", + "tests/fixtures/node-types/keys.ts" + ] }, "anchorSource": "ancestor" }, @@ -23085,8 +23319,15 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:crypto" + "status": "partial", + "evidence": "compiler-dedicated:asymmetricKeyType", + "tests": [ + "tests/corpus/7080-crypto-ed25519.mjs", + "tests/corpus/7082-crypto-key-objects.ts", + "tests/corpus/7083-crypto-sign-verify.ts", + "tests/corpus/7084-crypto-key-pairs.cjs", + "tests/fixtures/node-types/keys.ts" + ] }, "anchorSource": "ancestor" }, @@ -23223,8 +23464,15 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:crypto" + "status": "partial", + "evidence": "compiler-dedicated:Sign", + "tests": [ + "tests/corpus/7080-crypto-ed25519.mjs", + "tests/corpus/7082-crypto-key-objects.ts", + "tests/corpus/7083-crypto-sign-verify.ts", + "tests/corpus/7084-crypto-key-pairs.cjs", + "tests/fixtures/node-types/keys.ts" + ] }, "anchorSource": "exact" }, @@ -23246,8 +23494,15 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:crypto" + "status": "partial", + "evidence": "compiler-dedicated:sign.sign", + "tests": [ + "tests/corpus/7080-crypto-ed25519.mjs", + "tests/corpus/7082-crypto-key-objects.ts", + "tests/corpus/7083-crypto-sign-verify.ts", + "tests/corpus/7084-crypto-key-pairs.cjs", + "tests/fixtures/node-types/keys.ts" + ] }, "anchorSource": "exact" }, @@ -23269,8 +23524,15 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:crypto" + "status": "partial", + "evidence": "compiler-dedicated:sign.update", + "tests": [ + "tests/corpus/7080-crypto-ed25519.mjs", + "tests/corpus/7082-crypto-key-objects.ts", + "tests/corpus/7083-crypto-sign-verify.ts", + "tests/corpus/7084-crypto-key-pairs.cjs", + "tests/fixtures/node-types/keys.ts" + ] }, "anchorSource": "exact" }, @@ -23292,8 +23554,15 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:crypto" + "status": "partial", + "evidence": "compiler-dedicated:Verify", + "tests": [ + "tests/corpus/7080-crypto-ed25519.mjs", + "tests/corpus/7082-crypto-key-objects.ts", + "tests/corpus/7083-crypto-sign-verify.ts", + "tests/corpus/7084-crypto-key-pairs.cjs", + "tests/fixtures/node-types/keys.ts" + ] }, "anchorSource": "exact" }, @@ -23315,8 +23584,15 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:crypto" + "status": "partial", + "evidence": "compiler-dedicated:verify.update", + "tests": [ + "tests/corpus/7080-crypto-ed25519.mjs", + "tests/corpus/7082-crypto-key-objects.ts", + "tests/corpus/7083-crypto-sign-verify.ts", + "tests/corpus/7084-crypto-key-pairs.cjs", + "tests/fixtures/node-types/keys.ts" + ] }, "anchorSource": "exact" }, @@ -23338,8 +23614,15 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:crypto" + "status": "partial", + "evidence": "compiler-dedicated:verify.verify", + "tests": [ + "tests/corpus/7080-crypto-ed25519.mjs", + "tests/corpus/7082-crypto-key-objects.ts", + "tests/corpus/7083-crypto-sign-verify.ts", + "tests/corpus/7084-crypto-key-pairs.cjs", + "tests/fixtures/node-types/keys.ts" + ] }, "anchorSource": "exact" }, @@ -27503,8 +27786,17 @@ "anchor": "dep0167-weak-diffiehellmangroup-instances-modp1-modp2-modp5", "nodeStability": null, "static": { - "status": "not-applicable", - "evidence": "compiler-chapter-policy:deprecations" + "status": "partial", + "evidence": "compiler-dedicated:DiffieHellmanGroup", + "tests": [ + "tests/corpus/6706-dh-groups.ts", + "tests/corpus/6707-dh-explicit-params.ts", + "tests/corpus/6708-dh-errors.js", + "tests/corpus/6709-dh-generate.ts", + "tests/corpus/6710-dh-encodings.cjs", + "tests/corpus/6711-dh-handles.ts", + "tests/fixtures/node-types/dh.ts" + ] }, "anchorSource": "exact" }, @@ -32246,6 +32538,7 @@ "evidence": "derived:descendants", "tests": [ "tests/corpus/6702-ecdh-errors.js", + "tests/corpus/6708-dh-errors.js", "tests/corpus/caught-system-error-fields.ts", "tests/corpus/error-stack-frames.ts", "tests/corpus/error-stack-limit.ts", @@ -33656,8 +33949,11 @@ "anchor": "err_crypto_invalid_keylen", "nodeStability": null, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:errors" + "status": "partial", + "evidence": "compiler-dedicated:ERR_CRYPTO_INVALID_KEYLEN", + "tests": [ + "tests/corpus/6708-dh-errors.js" + ] }, "anchorSource": "exact" }, @@ -33782,8 +34078,11 @@ "anchor": "err_crypto_invalid_state", "nodeStability": null, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:errors" + "status": "partial", + "evidence": "compiler-dedicated:ERR_CRYPTO_INVALID_STATE", + "tests": [ + "tests/corpus/6708-dh-errors.js" + ] }, "anchorSource": "exact" }, @@ -34052,8 +34351,11 @@ "anchor": "err_crypto_unknown_dh_group", "nodeStability": null, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:errors" + "status": "partial", + "evidence": "compiler-dedicated:ERR_CRYPTO_UNKNOWN_DH_GROUP", + "tests": [ + "tests/corpus/6708-dh-errors.js" + ] }, "anchorSource": "exact" }, @@ -95421,7 +95723,10 @@ "tests/corpus/2557-tls-ca-store.ts", "tests/corpus/2596-net-arg-ladders.cjs", "tests/corpus/6721-tls-alpn-protocols.ts", - "tests/corpus/6722-tls-min-max-version.ts" + "tests/corpus/6722-tls-min-max-version.ts", + "tests/corpus/7087-tls-socket-wrap.ts", + "tests/corpus/7088-tls-socket-connect-tunnel.ts", + "tests/fixtures/node-types/tls-socket-wrap.ts" ] }, "anchorSource": "chapter" @@ -96123,8 +96428,13 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:tls" + "status": "partial", + "evidence": "compiler-feature:tls.tls.TLSSocket", + "tests": [ + "tests/corpus/7087-tls-socket-wrap.ts", + "tests/corpus/7088-tls-socket-connect-tunnel.ts", + "tests/fixtures/node-types/tls-socket-wrap.ts" + ] }, "anchorSource": "exact" }, @@ -96911,8 +97221,13 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:tls" + "status": "partial", + "evidence": "compiler-feature:tls.'secure'", + "tests": [ + "tests/corpus/7087-tls-socket-wrap.ts", + "tests/corpus/7088-tls-socket-connect-tunnel.ts", + "tests/fixtures/node-types/tls-socket-wrap.ts" + ] }, "anchorSource": "exact" }, @@ -106669,6 +106984,9 @@ "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs", + "tests/corpus/7081-webcrypto-ed25519.mjs", + "tests/corpus/7085-webcrypto-ecdsa-rsa.ts", + "tests/corpus/7086-webcrypto-ecdsa-rsa-errors.mjs", "tests/corpus/7120-webcrypto-random-uuid.ts", "tests/corpus/7121-webcrypto-crypto-object.mjs", "tests/corpus/7124-webcrypto-secret-keys.ts", @@ -106677,6 +106995,9 @@ "tests/corpus/7127-webcrypto-aes-errors.mjs", "tests/corpus/7128-webcrypto-derive.ts", "tests/corpus/7129-webcrypto-derive-errors.mjs", + "tests/corpus/7130-webcrypto-wrap-key.ts", + "tests/corpus/7131-webcrypto-wrap-key-errors.mjs", + "tests/corpus/7132-jose-pbes2/main.mjs", "tests/corpus/node24-crypto-kdf-errors.ts", "tests/corpus/node24-crypto-kdf-options.ts", "tests/corpus/node24-crypto-kdf.ts", @@ -110549,6 +110870,9 @@ "status": "partial", "evidence": "compiler-feature:webcrypto.subtle.decrypt", "tests": [ + "tests/corpus/7130-webcrypto-wrap-key.ts", + "tests/corpus/7131-webcrypto-wrap-key-errors.mjs", + "tests/corpus/7132-jose-pbes2/main.mjs", "tests/corpus/7128-webcrypto-derive.ts", "tests/corpus/7129-webcrypto-derive-errors.mjs", "tests/corpus/7126-webcrypto-aes.ts", @@ -110561,7 +110885,10 @@ "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs", - "tests/fixtures/node-types/webcrypto-hmac.ts" + "tests/fixtures/node-types/webcrypto-hmac.ts", + "tests/corpus/7081-webcrypto-ed25519.mjs", + "tests/corpus/7085-webcrypto-ecdsa-rsa.ts", + "tests/corpus/7086-webcrypto-ecdsa-rsa-errors.mjs" ] }, "anchorSource": "exact" @@ -110587,6 +110914,9 @@ "status": "partial", "evidence": "compiler-feature:webcrypto.subtle.deriveBits", "tests": [ + "tests/corpus/7130-webcrypto-wrap-key.ts", + "tests/corpus/7131-webcrypto-wrap-key-errors.mjs", + "tests/corpus/7132-jose-pbes2/main.mjs", "tests/corpus/7128-webcrypto-derive.ts", "tests/corpus/7129-webcrypto-derive-errors.mjs", "tests/corpus/7126-webcrypto-aes.ts", @@ -110599,7 +110929,10 @@ "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs", - "tests/fixtures/node-types/webcrypto-hmac.ts" + "tests/fixtures/node-types/webcrypto-hmac.ts", + "tests/corpus/7081-webcrypto-ed25519.mjs", + "tests/corpus/7085-webcrypto-ecdsa-rsa.ts", + "tests/corpus/7086-webcrypto-ecdsa-rsa-errors.mjs" ] }, "anchorSource": "exact" @@ -110625,6 +110958,9 @@ "status": "partial", "evidence": "compiler-feature:webcrypto.subtle.deriveKey", "tests": [ + "tests/corpus/7130-webcrypto-wrap-key.ts", + "tests/corpus/7131-webcrypto-wrap-key-errors.mjs", + "tests/corpus/7132-jose-pbes2/main.mjs", "tests/corpus/7128-webcrypto-derive.ts", "tests/corpus/7129-webcrypto-derive-errors.mjs", "tests/corpus/7126-webcrypto-aes.ts", @@ -110637,7 +110973,10 @@ "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs", - "tests/fixtures/node-types/webcrypto-hmac.ts" + "tests/fixtures/node-types/webcrypto-hmac.ts", + "tests/corpus/7081-webcrypto-ed25519.mjs", + "tests/corpus/7085-webcrypto-ecdsa-rsa.ts", + "tests/corpus/7086-webcrypto-ecdsa-rsa-errors.mjs" ] }, "anchorSource": "exact" @@ -110663,6 +111002,9 @@ "status": "partial", "evidence": "compiler-feature:webcrypto.subtle.digest", "tests": [ + "tests/corpus/7130-webcrypto-wrap-key.ts", + "tests/corpus/7131-webcrypto-wrap-key-errors.mjs", + "tests/corpus/7132-jose-pbes2/main.mjs", "tests/corpus/7128-webcrypto-derive.ts", "tests/corpus/7129-webcrypto-derive-errors.mjs", "tests/corpus/7126-webcrypto-aes.ts", @@ -110675,7 +111017,10 @@ "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs", - "tests/fixtures/node-types/webcrypto-hmac.ts" + "tests/fixtures/node-types/webcrypto-hmac.ts", + "tests/corpus/7081-webcrypto-ed25519.mjs", + "tests/corpus/7085-webcrypto-ecdsa-rsa.ts", + "tests/corpus/7086-webcrypto-ecdsa-rsa-errors.mjs" ] }, "anchorSource": "exact" @@ -110747,6 +111092,9 @@ "status": "partial", "evidence": "compiler-feature:webcrypto.subtle.encrypt", "tests": [ + "tests/corpus/7130-webcrypto-wrap-key.ts", + "tests/corpus/7131-webcrypto-wrap-key-errors.mjs", + "tests/corpus/7132-jose-pbes2/main.mjs", "tests/corpus/7128-webcrypto-derive.ts", "tests/corpus/7129-webcrypto-derive-errors.mjs", "tests/corpus/7126-webcrypto-aes.ts", @@ -110759,7 +111107,10 @@ "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs", - "tests/fixtures/node-types/webcrypto-hmac.ts" + "tests/fixtures/node-types/webcrypto-hmac.ts", + "tests/corpus/7081-webcrypto-ed25519.mjs", + "tests/corpus/7085-webcrypto-ecdsa-rsa.ts", + "tests/corpus/7086-webcrypto-ecdsa-rsa-errors.mjs" ] }, "anchorSource": "exact" @@ -110785,6 +111136,9 @@ "status": "partial", "evidence": "compiler-feature:webcrypto.subtle.exportKey", "tests": [ + "tests/corpus/7130-webcrypto-wrap-key.ts", + "tests/corpus/7131-webcrypto-wrap-key-errors.mjs", + "tests/corpus/7132-jose-pbes2/main.mjs", "tests/corpus/7128-webcrypto-derive.ts", "tests/corpus/7129-webcrypto-derive-errors.mjs", "tests/corpus/7126-webcrypto-aes.ts", @@ -110797,7 +111151,10 @@ "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs", - "tests/fixtures/node-types/webcrypto-hmac.ts" + "tests/fixtures/node-types/webcrypto-hmac.ts", + "tests/corpus/7081-webcrypto-ed25519.mjs", + "tests/corpus/7085-webcrypto-ecdsa-rsa.ts", + "tests/corpus/7086-webcrypto-ecdsa-rsa-errors.mjs" ] }, "anchorSource": "exact" @@ -110846,6 +111203,9 @@ "status": "partial", "evidence": "compiler-feature:webcrypto.subtle.generateKey", "tests": [ + "tests/corpus/7130-webcrypto-wrap-key.ts", + "tests/corpus/7131-webcrypto-wrap-key-errors.mjs", + "tests/corpus/7132-jose-pbes2/main.mjs", "tests/corpus/7128-webcrypto-derive.ts", "tests/corpus/7129-webcrypto-derive-errors.mjs", "tests/corpus/7126-webcrypto-aes.ts", @@ -110858,7 +111218,10 @@ "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs", - "tests/fixtures/node-types/webcrypto-hmac.ts" + "tests/fixtures/node-types/webcrypto-hmac.ts", + "tests/corpus/7081-webcrypto-ed25519.mjs", + "tests/corpus/7085-webcrypto-ecdsa-rsa.ts", + "tests/corpus/7086-webcrypto-ecdsa-rsa-errors.mjs" ] }, "anchorSource": "exact" @@ -110884,6 +111247,9 @@ "status": "partial", "evidence": "compiler-feature:webcrypto.subtle.importKey", "tests": [ + "tests/corpus/7130-webcrypto-wrap-key.ts", + "tests/corpus/7131-webcrypto-wrap-key-errors.mjs", + "tests/corpus/7132-jose-pbes2/main.mjs", "tests/corpus/7128-webcrypto-derive.ts", "tests/corpus/7129-webcrypto-derive-errors.mjs", "tests/corpus/7126-webcrypto-aes.ts", @@ -110896,7 +111262,10 @@ "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs", - "tests/fixtures/node-types/webcrypto-hmac.ts" + "tests/fixtures/node-types/webcrypto-hmac.ts", + "tests/corpus/7081-webcrypto-ed25519.mjs", + "tests/corpus/7085-webcrypto-ecdsa-rsa.ts", + "tests/corpus/7086-webcrypto-ecdsa-rsa-errors.mjs" ] }, "anchorSource": "exact" @@ -110922,6 +111291,9 @@ "status": "partial", "evidence": "compiler-feature:webcrypto.subtle.sign", "tests": [ + "tests/corpus/7130-webcrypto-wrap-key.ts", + "tests/corpus/7131-webcrypto-wrap-key-errors.mjs", + "tests/corpus/7132-jose-pbes2/main.mjs", "tests/corpus/7128-webcrypto-derive.ts", "tests/corpus/7129-webcrypto-derive-errors.mjs", "tests/corpus/7126-webcrypto-aes.ts", @@ -110934,7 +111306,10 @@ "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs", - "tests/fixtures/node-types/webcrypto-hmac.ts" + "tests/fixtures/node-types/webcrypto-hmac.ts", + "tests/corpus/7081-webcrypto-ed25519.mjs", + "tests/corpus/7085-webcrypto-ecdsa-rsa.ts", + "tests/corpus/7086-webcrypto-ecdsa-rsa-errors.mjs" ] }, "anchorSource": "exact" @@ -110957,8 +111332,29 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:webcrypto" + "status": "partial", + "evidence": "compiler-feature:webcrypto.subtle.unwrapKey", + "tests": [ + "tests/corpus/7130-webcrypto-wrap-key.ts", + "tests/corpus/7131-webcrypto-wrap-key-errors.mjs", + "tests/corpus/7132-jose-pbes2/main.mjs", + "tests/corpus/7128-webcrypto-derive.ts", + "tests/corpus/7129-webcrypto-derive-errors.mjs", + "tests/corpus/7126-webcrypto-aes.ts", + "tests/corpus/7127-webcrypto-aes-errors.mjs", + "tests/corpus/7124-webcrypto-secret-keys.ts", + "tests/corpus/7125-webcrypto-key-errors.mjs", + "tests/fixtures/node-types/webcrypto-keys.ts", + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6292-webcrypto-hmac-errors.ts", + "tests/corpus/6293-webcrypto-digest.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs", + "tests/fixtures/node-types/webcrypto-hmac.ts", + "tests/corpus/7081-webcrypto-ed25519.mjs", + "tests/corpus/7085-webcrypto-ecdsa-rsa.ts", + "tests/corpus/7086-webcrypto-ecdsa-rsa-errors.mjs" + ] }, "anchorSource": "exact" }, @@ -110983,6 +111379,9 @@ "status": "partial", "evidence": "compiler-feature:webcrypto.subtle.verify", "tests": [ + "tests/corpus/7130-webcrypto-wrap-key.ts", + "tests/corpus/7131-webcrypto-wrap-key-errors.mjs", + "tests/corpus/7132-jose-pbes2/main.mjs", "tests/corpus/7128-webcrypto-derive.ts", "tests/corpus/7129-webcrypto-derive-errors.mjs", "tests/corpus/7126-webcrypto-aes.ts", @@ -110995,7 +111394,10 @@ "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs", - "tests/fixtures/node-types/webcrypto-hmac.ts" + "tests/fixtures/node-types/webcrypto-hmac.ts", + "tests/corpus/7081-webcrypto-ed25519.mjs", + "tests/corpus/7085-webcrypto-ecdsa-rsa.ts", + "tests/corpus/7086-webcrypto-ecdsa-rsa-errors.mjs" ] }, "anchorSource": "exact" @@ -111018,8 +111420,29 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:webcrypto" + "status": "partial", + "evidence": "compiler-feature:webcrypto.subtle.wrapKey", + "tests": [ + "tests/corpus/7130-webcrypto-wrap-key.ts", + "tests/corpus/7131-webcrypto-wrap-key-errors.mjs", + "tests/corpus/7132-jose-pbes2/main.mjs", + "tests/corpus/7128-webcrypto-derive.ts", + "tests/corpus/7129-webcrypto-derive-errors.mjs", + "tests/corpus/7126-webcrypto-aes.ts", + "tests/corpus/7127-webcrypto-aes-errors.mjs", + "tests/corpus/7124-webcrypto-secret-keys.ts", + "tests/corpus/7125-webcrypto-key-errors.mjs", + "tests/fixtures/node-types/webcrypto-keys.ts", + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6292-webcrypto-hmac-errors.ts", + "tests/corpus/6293-webcrypto-digest.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs", + "tests/fixtures/node-types/webcrypto-hmac.ts", + "tests/corpus/7081-webcrypto-ed25519.mjs", + "tests/corpus/7085-webcrypto-ecdsa-rsa.ts", + "tests/corpus/7086-webcrypto-ecdsa-rsa-errors.mjs" + ] }, "anchorSource": "exact" }, diff --git a/internal/compatibility/src/generate.mjs b/internal/compatibility/src/generate.mjs index cf77c1f84..662c0abd6 100644 --- a/internal/compatibility/src/generate.mjs +++ b/internal/compatibility/src/generate.mjs @@ -582,17 +582,27 @@ function publicDetail(tier) { if (/^surface-manifest:node-builtin\.crypto\.create(?:Cipheriv|Decipheriv)$/.test(source)) { return "Supports aes-128/192/256 in cbc, ctr, and gcm modes and chacha20-poly1305 with Node's argument, IV, tag-length, and key-length errors; other OpenSSL ciphers and KeyObject keys are refused."; } + if (/^surface-manifest:node-builtin\.crypto\.createECDH$/.test(source)) { + return "Supports prime192v1, secp224r1, prime256v1, secp384r1, secp521r1, secp192k1, secp224k1, secp256k1, brainpoolP256r1, brainpoolP384r1 and brainpoolP512r1 with Node's curve, key, point, format and encoding errors; other curves Node knows are refused."; + } + if ( + /^surface-manifest:node-builtin\.crypto\.(?:createDiffieHellman|createDiffieHellmanGroup|getDiffieHellman)$/.test( + source, + ) + ) { + return "Supports the modp groups, explicit primes and generators, and generated safe primes up to 8192 bits with Node's argument, generator and modulus errors."; + } if (source.startsWith("surface-manifest:")) return "Implemented for the call shapes accepted by the compiler lowering."; if (/^compiler-dedicated:assert\.doesNot(?:Throw|Reject)$/.test(source)) { return "Supports callbacks or native promises with messages, Error classes, regular expressions, and validators accepting unknown values; object expectations and complete AssertionError metadata remain unsupported."; } - if (/^surface-manifest:node-builtin\.crypto\.createECDH$/.test(source)) { - return "Supports prime192v1, secp224r1, prime256v1, secp384r1, secp521r1, secp192k1, secp224k1, secp256k1, brainpoolP256r1, brainpoolP384r1 and brainpoolP512r1 with Node's curve, key, point, format and encoding errors; other curves Node knows are refused."; - } if (/^compiler-dedicated:(?:ECDH|ecdh\.)/.test(source)) { return "Matches Node's key formats, point encodings, private-key range checks, key-pair checks and errors on the supported curves; ECDH.convertKey is supported, and property reads and writes are refused."; } + if (/^compiler-dedicated:(?:DiffieHellman|diffieHellman\.)/.test(source)) { + return "Matches Node's private key lengths, DH_check results, public key range checks, secret padding and errors; primes longer than 8192 bits, property writes and property reads other than verifyError are refused."; + } if (/^compiler-dedicated:(?:Cipheriv|Decipheriv|cipher\.|decipher\.)/.test(source)) { return "Matches Node's update() output lengths, padding, authentication tags, string encodings, and state errors for the supported ciphers, and its stream.Transform behavior: chunks written through update(), final() pushed at the end, and its errors reported through 'error'."; } diff --git a/internal/compatibility/static-support.json b/internal/compatibility/static-support.json index 6b3eaab4c..06aad26cf 100644 --- a/internal/compatibility/static-support.json +++ b/internal/compatibility/static-support.json @@ -42,8 +42,12 @@ "node-builtin.child_process": ["tests/corpus/1361-spawn-events.ts", "tests/corpus/1565-spawn-pipe-streams.ts", "tests/corpus/2911-execfile-callback.ts", "tests/corpus/2939-child-shell-text.ts", "tests/corpus/2946-child-fork-ipc/main.ts", "tests/corpus/3127-spawn-runtime-options.ts", "tests/corpus/3130-child-generic-streams.ts", "tests/corpus/3131-child-rest-listeners.js", "tests/corpus/3132-child-generic-stream-completion.ts"], "node-builtin.cluster": ["tests/corpus/957-builtins-namespace.ts"], "node-builtin.console": ["tests/corpus/console-imports.cjs", "tests/corpus/2440-console-inspect-args.ts", "tests/corpus/console-native-value.js", "tests/harness/npm-static.test.ts"], - "node-builtin.crypto.createCipheriv": ["tests/corpus/5250-cipher-aes-gcm.ts", "tests/corpus/5251-cipher-aes-cbc.ts", "tests/corpus/5252-cipher-aes-ctr.ts", "tests/corpus/5253-cipher-chacha20-poly1305.ts", "tests/corpus/5254-cipher-errors.js", "tests/corpus/5255-cipher-encodings.cjs", "tests/corpus/5256-cipher-deprecation.cjs", "tests/corpus/5257-cipher-vectors.ts", "tests/corpus/5258-cipher-handles.js", "tests/corpus/5259-cipher-chunked.ts", "tests/corpus/7055-cipher-streams.ts", "tests/corpus/7056-cipher-stream-errors.ts", "tests/corpus/7057-cipher-streams.mjs", "tests/fixtures/node-types/cipher.ts"], - "node-builtin.crypto.createDecipheriv": ["tests/corpus/5250-cipher-aes-gcm.ts", "tests/corpus/5251-cipher-aes-cbc.ts", "tests/corpus/5252-cipher-aes-ctr.ts", "tests/corpus/5253-cipher-chacha20-poly1305.ts", "tests/corpus/5254-cipher-errors.js", "tests/corpus/5255-cipher-encodings.cjs", "tests/corpus/5256-cipher-deprecation.cjs", "tests/corpus/5257-cipher-vectors.ts", "tests/corpus/5258-cipher-handles.js", "tests/corpus/5259-cipher-chunked.ts", "tests/corpus/7055-cipher-streams.ts", "tests/corpus/7056-cipher-stream-errors.ts", "tests/corpus/7057-cipher-streams.mjs", "tests/fixtures/node-types/cipher.ts"], + "node-builtin.crypto.checkPrime": ["tests/corpus/7152-crypto-prime.ts", "tests/corpus/7153-crypto-prime-errors.cjs"], + "node-builtin.crypto.checkPrimeSync": ["tests/corpus/7152-crypto-prime.ts", "tests/corpus/7153-crypto-prime-errors.cjs"], + "node-builtin.crypto.createCipheriv": ["tests/corpus/7150-cipher-legacy-modes.ts", "tests/corpus/7151-cipher-legacy-errors.cjs", "tests/corpus/5250-cipher-aes-gcm.ts", "tests/corpus/5251-cipher-aes-cbc.ts", "tests/corpus/5252-cipher-aes-ctr.ts", "tests/corpus/5253-cipher-chacha20-poly1305.ts", "tests/corpus/5254-cipher-errors.js", "tests/corpus/5255-cipher-encodings.cjs", "tests/corpus/5256-cipher-deprecation.cjs", "tests/corpus/5257-cipher-vectors.ts", "tests/corpus/5258-cipher-handles.js", "tests/corpus/5259-cipher-chunked.ts", "tests/corpus/7055-cipher-streams.ts", "tests/corpus/7056-cipher-stream-errors.ts", "tests/corpus/7057-cipher-streams.mjs", "tests/fixtures/node-types/cipher.ts"], + "node-builtin.crypto.createDecipheriv": ["tests/corpus/7150-cipher-legacy-modes.ts", "tests/corpus/7151-cipher-legacy-errors.cjs", "tests/corpus/5250-cipher-aes-gcm.ts", "tests/corpus/5251-cipher-aes-cbc.ts", "tests/corpus/5252-cipher-aes-ctr.ts", "tests/corpus/5253-cipher-chacha20-poly1305.ts", "tests/corpus/5254-cipher-errors.js", "tests/corpus/5255-cipher-encodings.cjs", "tests/corpus/5256-cipher-deprecation.cjs", "tests/corpus/5257-cipher-vectors.ts", "tests/corpus/5258-cipher-handles.js", "tests/corpus/5259-cipher-chunked.ts", "tests/corpus/7055-cipher-streams.ts", "tests/corpus/7056-cipher-stream-errors.ts", "tests/corpus/7057-cipher-streams.mjs", "tests/fixtures/node-types/cipher.ts"], + "node-builtin.crypto.createDiffieHellman": ["tests/corpus/6706-dh-groups.ts", "tests/corpus/6707-dh-explicit-params.ts", "tests/corpus/6708-dh-errors.js", "tests/corpus/6709-dh-generate.ts", "tests/corpus/6710-dh-encodings.cjs", "tests/corpus/6711-dh-handles.ts", "tests/fixtures/node-types/dh.ts"], + "node-builtin.crypto.createDiffieHellmanGroup": ["tests/corpus/6706-dh-groups.ts", "tests/corpus/6707-dh-explicit-params.ts", "tests/corpus/6708-dh-errors.js", "tests/corpus/6709-dh-generate.ts", "tests/corpus/6710-dh-encodings.cjs", "tests/corpus/6711-dh-handles.ts", "tests/fixtures/node-types/dh.ts"], "node-builtin.crypto.createECDH": ["tests/corpus/6700-ecdh-shared-secret.ts", "tests/corpus/6701-ecdh-formats.ts", "tests/corpus/6702-ecdh-errors.js", "tests/corpus/6703-ecdh-private-keys.ts", "tests/corpus/6704-ecdh-set-public-key.cjs", "tests/corpus/6705-ecdh-handles.ts", "tests/fixtures/node-types/ecdh.ts"], "node-builtin.crypto": ["tests/corpus/1358-crypto-random.ts", "tests/corpus/1534-crypto-hash-chain.ts", "tests/corpus/2556-crypto-introspection.ts", "tests/corpus/2890-crypto-static-utility.ts", "tests/harness/npm-static.test.ts", "tests/corpus/node24-crypto-sha2.ts", "tests/corpus/node24-crypto-kdf.ts", "tests/corpus/node24-crypto-kdf-errors.ts", "tests/corpus/node24-crypto-kdf-options.ts"], "node-builtin.dgram": ["tests/corpus/2597-dgram-send-ladders.cjs"], @@ -118,6 +122,9 @@ "node-builtin.os.version": ["tests/corpus/5140-os-host-facts.ts"], "node-builtin.os.constants": ["tests/corpus/5141-os-constants.ts"], "node-builtin.fs.constants": ["tests/corpus/6207-fs-constants-object.ts", "tests/corpus/6208-fs-constants-require.cjs"], + "node-builtin.crypto.generatePrime": ["tests/corpus/7152-crypto-prime.ts", "tests/corpus/7153-crypto-prime-errors.cjs"], + "node-builtin.crypto.generatePrimeSync": ["tests/corpus/7152-crypto-prime.ts", "tests/corpus/7153-crypto-prime-errors.cjs"], + "node-builtin.crypto.getDiffieHellman": ["tests/corpus/6706-dh-groups.ts", "tests/corpus/6707-dh-explicit-params.ts", "tests/corpus/6708-dh-errors.js", "tests/corpus/6709-dh-generate.ts", "tests/corpus/6710-dh-encodings.cjs", "tests/corpus/6711-dh-handles.ts", "tests/fixtures/node-types/dh.ts"], "node-builtin.crypto.getRandomValues": ["tests/corpus/6204-crypto-get-random-values.ts", "tests/corpus/6205-crypto-get-random-values.js"], "node-builtin.url.parse": ["tests/corpus/5220-url-parse-warnings.ts", "tests/corpus/5221-url-parse-shapes.ts", "tests/corpus/5222-url-parse-hosts.ts", "tests/corpus/5223-url-parse-query.ts", "tests/corpus/5227-url-legacy-dependency/main.ts", "tests/corpus/5229-url-legacy-errors.cjs", "packages/runtime/test/url-legacy.test.ts", "tests/fixtures/node-types/url-legacy.ts"], "node-builtin.url.format": ["tests/corpus/5224-url-format-objects.ts", "tests/corpus/5225-url-format-whatwg.ts", "tests/corpus/5227-url-legacy-dependency/main.ts", "tests/corpus/5229-url-legacy-errors.cjs", "packages/runtime/test/url-legacy.test.ts", "tests/fixtures/node-types/url-legacy.ts"], @@ -187,14 +194,14 @@ "stdlib.set": ["tests/corpus/typed-collection-iterators.ts", "tests/corpus/typed-collection-iterator-references.ts", "tests/corpus/checked-collection-iterators.ts"], "stdlib.headers": ["tests/harness/fetch-conformance.test.ts"], "stdlib.request": ["tests/harness/fetch-conformance.test.ts"], - "stdlib.webcrypto.subtle": ["tests/corpus/7128-webcrypto-derive.ts", "tests/corpus/7129-webcrypto-derive-errors.mjs", "tests/corpus/7126-webcrypto-aes.ts", "tests/corpus/7127-webcrypto-aes-errors.mjs", "tests/corpus/7124-webcrypto-secret-keys.ts", "tests/corpus/7125-webcrypto-key-errors.mjs", "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"], + "stdlib.webcrypto.subtle": ["tests/corpus/7130-webcrypto-wrap-key.ts", "tests/corpus/7131-webcrypto-wrap-key-errors.mjs", "tests/corpus/7132-jose-pbes2/main.mjs", "tests/corpus/7128-webcrypto-derive.ts", "tests/corpus/7129-webcrypto-derive-errors.mjs", "tests/corpus/7126-webcrypto-aes.ts", "tests/corpus/7127-webcrypto-aes-errors.mjs", "tests/corpus/7124-webcrypto-secret-keys.ts", "tests/corpus/7125-webcrypto-key-errors.mjs", "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"], "stdlib.webcrypto.CryptoKey": ["tests/corpus/7124-webcrypto-secret-keys.ts", "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"], "stdlib.response": ["tests/harness/fetch-conformance.test.ts"], "stdlib.readable-stream": ["tests/harness/fetch-conformance.test.ts"], "stdlib.abort": ["tests/harness/fetch-conformance.test.ts"] }, "features": [ - { "chapter": "webcrypto", "symbols": ["subtle.importKey", "subtle.generateKey", "subtle.exportKey", "subtle.encrypt", "subtle.decrypt", "subtle.deriveBits", "subtle.deriveKey", "subtle.sign", "subtle.verify", "subtle.digest"], "status": "partial", "evidence": ["tests/corpus/7128-webcrypto-derive.ts", "tests/corpus/7129-webcrypto-derive-errors.mjs", "tests/corpus/7126-webcrypto-aes.ts", "tests/corpus/7127-webcrypto-aes-errors.mjs", "tests/corpus/7124-webcrypto-secret-keys.ts", "tests/corpus/7125-webcrypto-key-errors.mjs", "tests/fixtures/node-types/webcrypto-keys.ts", "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs", "tests/fixtures/node-types/webcrypto-hmac.ts"] }, + { "chapter": "webcrypto", "symbols": ["subtle.importKey", "subtle.generateKey", "subtle.exportKey", "subtle.encrypt", "subtle.decrypt", "subtle.wrapKey", "subtle.unwrapKey", "subtle.deriveBits", "subtle.deriveKey", "subtle.sign", "subtle.verify", "subtle.digest"], "status": "partial", "evidence": ["tests/corpus/7130-webcrypto-wrap-key.ts", "tests/corpus/7131-webcrypto-wrap-key-errors.mjs", "tests/corpus/7132-jose-pbes2/main.mjs", "tests/corpus/7128-webcrypto-derive.ts", "tests/corpus/7129-webcrypto-derive-errors.mjs", "tests/corpus/7126-webcrypto-aes.ts", "tests/corpus/7127-webcrypto-aes-errors.mjs", "tests/corpus/7124-webcrypto-secret-keys.ts", "tests/corpus/7125-webcrypto-key-errors.mjs", "tests/fixtures/node-types/webcrypto-keys.ts", "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs", "tests/fixtures/node-types/webcrypto-hmac.ts", "tests/corpus/7081-webcrypto-ed25519.mjs", "tests/corpus/7085-webcrypto-ecdsa-rsa.ts", "tests/corpus/7086-webcrypto-ecdsa-rsa-errors.mjs"] }, { "chapter": "webcrypto", "signatures": ["Class: `SubtleCrypto`", "`subtle` Type: {SubtleCrypto}"], "status": "partial", "evidence": ["tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"] }, { "chapter": "webcrypto", "signatures": ["Class: `CryptoKey`", "`cryptoKey.algorithm`", "`extractable` Type: {boolean}", "`type` Type: {string} One of `'secret'`, `'private'`, or `'public'`.", "`usages` Type: {string\\[]}"], "status": "partial", "evidence": ["tests/corpus/7124-webcrypto-secret-keys.ts", "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"] }, { "chapter": "webcrypto", "signatures": ["Class: `Crypto`"], "status": "partial", "evidence": ["tests/corpus/7120-webcrypto-random-uuid.ts", "tests/corpus/7121-webcrypto-crypto-object.mjs", "tests/corpus/6293-webcrypto-digest.ts"] }, @@ -369,6 +376,7 @@ { "chapter": "console", "signatures": ["Class: `Console`"], "status": "partial", "evidence": ["tests/corpus/4900-console-instances.ts", "tests/corpus/4901-console-instance-errors.ts"] }, { "chapter": "tls", "signatures": ["`alpnProtocol` Type: {string|boolean|null}"], "status": "partial", "evidence": ["tests/corpus/6721-tls-alpn-protocols.ts"] }, { "chapter": "tls", "signatures": ["`tlsSocket.getProtocol()`"], "status": "partial", "evidence": ["tests/corpus/6722-tls-min-max-version.ts"] }, + { "chapter": "tls", "signatures": ["Class: `tls.TLSSocket`", "Event: `'secure'`"], "status": "partial", "evidence": ["tests/corpus/7087-tls-socket-wrap.ts", "tests/corpus/7088-tls-socket-connect-tunnel.ts", "tests/fixtures/node-types/tls-socket-wrap.ts"] }, { "chapter": "webstreams", "signatures": ["`readableStream.values([options])`"], "status": "partial", "evidence": ["tests/corpus/2862-web-readable-async-iteration.ts"] }, { "chapter": "webstreams", "signatures": ["`streamConsumers.text(stream)`", "`streamConsumers.json(stream)`", "`streamConsumers.buffer(stream)`"], "status": "partial", "evidence": ["tests/corpus/2629-stream-consumers.ts", "tests/corpus/2630-stream-consumers-errors.ts"] }, { "chapter": "webstreams", "signatures": ["`streamConsumers.arrayBuffer(stream)`"], "status": "partial", "evidence": ["tests/corpus/4875-stream-consumers-array-buffer.ts"] }, @@ -417,10 +425,13 @@ { "chapter": "crypto", "symbols": ["Cipheriv", "cipher.update", "cipher.final", "cipher.setAAD", "cipher.getAuthTag", "cipher.setAutoPadding", "Decipheriv", "decipher.update", "decipher.final", "decipher.setAAD", "decipher.setAuthTag", "decipher.setAutoPadding"], "status": "partial", "evidence": ["tests/corpus/5250-cipher-aes-gcm.ts", "tests/corpus/5251-cipher-aes-cbc.ts", "tests/corpus/5252-cipher-aes-ctr.ts", "tests/corpus/5253-cipher-chacha20-poly1305.ts", "tests/corpus/5254-cipher-errors.js", "tests/corpus/5255-cipher-encodings.cjs", "tests/corpus/5256-cipher-deprecation.cjs", "tests/corpus/5257-cipher-vectors.ts", "tests/corpus/5258-cipher-handles.js", "tests/corpus/5259-cipher-chunked.ts", "tests/corpus/7055-cipher-streams.ts", "tests/corpus/7056-cipher-stream-errors.ts", "tests/corpus/7057-cipher-streams.mjs", "tests/fixtures/node-types/cipher.ts"] }, { "chapter": "crypto", "symbols": ["crypto.webcrypto", "subtle"], "status": "partial", "evidence": ["tests/corpus/7122-webcrypto-node-crypto.ts", "tests/corpus/7123-webcrypto-node-crypto.cjs"] }, { "chapter": "crypto", "symbols": ["ECDH", "ecdh.computeSecret", "ecdh.generateKeys", "ecdh.getPrivateKey", "ecdh.getPublicKey", "ecdh.setPrivateKey", "ecdh.setPublicKey"], "status": "partial", "evidence": ["tests/corpus/6700-ecdh-shared-secret.ts", "tests/corpus/6701-ecdh-formats.ts", "tests/corpus/6702-ecdh-errors.js", "tests/corpus/6703-ecdh-private-keys.ts", "tests/corpus/6704-ecdh-set-public-key.cjs", "tests/corpus/6705-ecdh-handles.ts", "tests/fixtures/node-types/ecdh.ts"] }, + { "chapter": "crypto", "symbols": ["DiffieHellman", "DiffieHellmanGroup", "diffieHellman.computeSecret", "diffieHellman.generateKeys", "diffieHellman.getGenerator", "diffieHellman.getPrime", "diffieHellman.getPrivateKey", "diffieHellman.getPublicKey", "diffieHellman.setPrivateKey", "diffieHellman.setPublicKey", "diffieHellman.verifyError"], "status": "partial", "evidence": ["tests/corpus/6706-dh-groups.ts", "tests/corpus/6707-dh-explicit-params.ts", "tests/corpus/6708-dh-errors.js", "tests/corpus/6709-dh-generate.ts", "tests/corpus/6710-dh-encodings.cjs", "tests/corpus/6711-dh-handles.ts", "tests/fixtures/node-types/dh.ts"] }, + { "chapter": "errors", "symbols": ["ERR_CRYPTO_UNKNOWN_DH_GROUP", "ERR_CRYPTO_INVALID_KEYLEN", "ERR_CRYPTO_INVALID_STATE"], "status": "partial", "evidence": ["tests/corpus/6708-dh-errors.js"] }, { "chapter": "errors", "symbols": ["ERR_CRYPTO_ECDH_INVALID_FORMAT", "ERR_CRYPTO_ECDH_INVALID_PUBLIC_KEY"], "status": "partial", "evidence": ["tests/corpus/6702-ecdh-errors.js"] }, { "symbols": ["crypto.createHash", "Hash", "hash.update", "hash.digest", "hash.copy", "crypto.createHmac", "Hmac", "hmac.update", "hmac.digest", "crypto.hash", "crypto.pbkdf2", "crypto.pbkdf2Sync"], "status": "partial", "evidence": ["tests/corpus/5760-crypto-hash-stream.ts", "tests/corpus/5768-crypto-hash-unknown.ts", "tests/corpus/5769-crypto-hash-unknown.cjs", "tests/corpus/5761-crypto-hash-stream-states.ts", "tests/corpus/5762-crypto-hash-stream.cjs", "tests/corpus/2890-crypto-static-utility.ts", "tests/corpus/node24-crypto-sha2.ts", "tests/corpus/5435-crypto-hash-algorithms.ts", "tests/corpus/5436-crypto-hash-names.js", "tests/corpus/5437-crypto-hmac-algorithms.ts", "tests/corpus/5438-crypto-kdf-digests.ts", "tests/corpus/5440-crypto-hash-encodings.js", "tests/corpus/5441-crypto-hash-output-length.ts", "tests/harness/npm-static.test.ts"] }, { "symbols": ["crypto.randomFill", "crypto.randomFillSync", "crypto.randomUUID", "crypto.randomUUIDv7"], "status": "supported", "evidence": ["tests/corpus/5442-crypto-random-values.ts", "tests/corpus/5443-crypto-random-arguments.js", "tests/corpus/1358-crypto-random.ts"] }, { "symbols": ["crypto.createSecretKey", "crypto.generateKey", "crypto.generateKeySync", "KeyObject", "keyObject.export", "keyObject.equals", "keyObject.symmetricKeySize", "keyObject.type"], "status": "partial", "evidence": ["tests/corpus/5445-crypto-secret-key-objects.ts", "tests/corpus/5446-crypto-secret-key-errors.js"] }, + { "symbols": ["crypto.createPrivateKey", "crypto.createPublicKey", "crypto.generateKeyPair", "crypto.generateKeyPairSync", "crypto.sign", "crypto.verify", "crypto.createSign", "crypto.createVerify", "Sign", "sign.update", "sign.sign", "Verify", "verify.update", "verify.verify", "asymmetricKeyType", "asymmetricKeyDetails"], "status": "partial", "evidence": ["tests/corpus/7080-crypto-ed25519.mjs", "tests/corpus/7082-crypto-key-objects.ts", "tests/corpus/7083-crypto-sign-verify.ts", "tests/corpus/7084-crypto-key-pairs.cjs", "tests/fixtures/node-types/keys.ts"] }, { "symbols": ["crypto.getRandomValues"], "status": "supported", "evidence": ["tests/corpus/6204-crypto-get-random-values.ts", "tests/corpus/6205-crypto-get-random-values.js"] }, { "symbols": ["crypto.timingSafeEqual"], "status": "supported", "evidence": ["tests/corpus/5444-crypto-timing-safe-equal.js", "tests/corpus/2890-crypto-static-utility.ts"] }, { "symbols": ["crypto.randomInt"], "status": "partial", "evidence": ["tests/corpus/5442-crypto-random-values.ts", "tests/corpus/2890-crypto-static-utility.ts"] }, diff --git a/packages/cli-darwin-arm64/package.json b/packages/cli-darwin-arm64/package.json index 482e5f1e5..23584b60a 100644 --- a/packages/cli-darwin-arm64/package.json +++ b/packages/cli-darwin-arm64/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/cli-darwin-arm64", - "version": "0.4.1", + "version": "0.4.2", "description": "Native scriptc compiler for darwin-arm64", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/cli-darwin-x64/package.json b/packages/cli-darwin-x64/package.json index b1ced2c03..d9e6dafc6 100644 --- a/packages/cli-darwin-x64/package.json +++ b/packages/cli-darwin-x64/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/cli-darwin-x64", - "version": "0.4.1", + "version": "0.4.2", "description": "Native scriptc compiler for darwin-x64", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/cli-linux-arm64-gnu/package.json b/packages/cli-linux-arm64-gnu/package.json index 0aa213279..5b8cb4db2 100644 --- a/packages/cli-linux-arm64-gnu/package.json +++ b/packages/cli-linux-arm64-gnu/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/cli-linux-arm64-gnu", - "version": "0.4.1", + "version": "0.4.2", "description": "Native scriptc compiler for linux-arm64-gnu", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/cli-linux-arm64-musl/package.json b/packages/cli-linux-arm64-musl/package.json index 4ac5434ae..0901602ec 100644 --- a/packages/cli-linux-arm64-musl/package.json +++ b/packages/cli-linux-arm64-musl/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/cli-linux-arm64-musl", - "version": "0.4.1", + "version": "0.4.2", "description": "Native scriptc compiler for linux-arm64-musl", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/cli-linux-x64-gnu/package.json b/packages/cli-linux-x64-gnu/package.json index 7473c36bc..f01478aad 100644 --- a/packages/cli-linux-x64-gnu/package.json +++ b/packages/cli-linux-x64-gnu/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/cli-linux-x64-gnu", - "version": "0.4.1", + "version": "0.4.2", "description": "Native scriptc compiler for linux-x64-gnu", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/cli-linux-x64-musl/package.json b/packages/cli-linux-x64-musl/package.json index 08e048cd1..a6986a66a 100644 --- a/packages/cli-linux-x64-musl/package.json +++ b/packages/cli-linux-x64-musl/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/cli-linux-x64-musl", - "version": "0.4.1", + "version": "0.4.2", "description": "Native scriptc compiler for linux-x64-musl", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/cli-win32-x64-msvc/package.json b/packages/cli-win32-x64-msvc/package.json index 54e323839..6b03811b8 100644 --- a/packages/cli-win32-x64-msvc/package.json +++ b/packages/cli-win32-x64-msvc/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/cli-win32-x64-msvc", - "version": "0.4.1", + "version": "0.4.2", "description": "Native scriptc compiler for win32-x64-msvc", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/cli/package.json b/packages/cli/package.json index d66ba3247..034747c29 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "scriptc", - "version": "0.4.1", + "version": "0.4.2", "description": "Compile ordinary TypeScript and JavaScript to small, fast native executables and WebAssembly modules that run without Node.js", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/cli/test/executable-cache.test.ts b/packages/cli/test/executable-cache.test.ts index a0131f14f..ee8027765 100644 --- a/packages/cli/test/executable-cache.test.ts +++ b/packages/cli/test/executable-cache.test.ts @@ -1,12 +1,13 @@ import { execFile } from "node:child_process"; import { createRequire } from "node:module"; -import { chmod, mkdir, mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises"; +import { chmod, mkdir, mkdtemp, readdir, readFile, rm, stat, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { delimiter, dirname, join } from "node:path"; import { promisify } from "node:util"; import { expect, test } from "vitest"; import { precompiledRuntimePackTarget } from "../../compiler/src/startup-cache.js"; import { platformLinkerSupportsPersistentCache } from "../../compiler/src/backend/linker.js"; +import { toolchainEnvironmentCachePolicy } from "../../compiler/src/backend/toolchain-environment.js"; const execFileAsync = promisify(execFile); const require = createRequire(import.meta.url); @@ -102,6 +103,59 @@ test("exact executable repeats skip lowering while edits and damaged outputs sta } }, 120_000); +// Sanitize builds compile the LLVM text with the C driver rather than linking a precompiled +// runtime pack, and that route's native cache proof records the output directory. +test.skipIf(process.platform === "win32")( + "sanitize builds compile uncached and keep the executable on unchanged repeats", + async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-cli-executable-sanitize-")); + const cacheRoot = join(dir, "cache"); + const entry = join(dir, "main.ts"); + const outPath = join(dir, "program"); + const build = async (env: NodeJS.ProcessEnv = {}): Promise<{ stderr: string }> => { + const { stderr } = await execFileAsync( + process.execPath, + ["--import", tsxLoader, cliEntry, "build", entry, "--sanitize", "-o", outPath], + { + env: { ...process.env, SCRIPTC_CACHE_DIR: cacheRoot, SCRIPTC_TIMING: "1", ...env }, + maxBuffer: 4 * 1024 * 1024, + }, + ); + return { stderr }; + }; + const run = async (): Promise => + (await execFileAsync(outPath, [], { maxBuffer: 1024 * 1024 })).stdout; + try { + await mkdir(cacheRoot, { mode: 0o700 }); + await writeFile(entry, 'console.log("sanitized");\n'); + + // With the native cache off, clang compiles the build's private copy of the LLVM text. + await build({ SCRIPTC_NO_CACHE: "1" }); + expect(await run()).toBe("sanitized\n"); + + expect((await build()).stderr).toContain("scriptc lowering"); + expect(await run()).toBe("sanitized\n"); + const built = await stat(outPath); + const tuTime = (await stat(join(dir, "main.ll"))).mtimeMs; + + // The output directory does not change after the native cache proof is recorded, so an + // unchanged repeat keeps the executable instead of linking it again. + expect((await build()).stderr).not.toContain("scriptc lowering"); + if (toolchainEnvironmentCachePolicy(process.env).completeArtifacts) { + const repeated = await stat(outPath); + expect(repeated.ino).toBe(built.ino); + expect(repeated.mtimeMs).toBe(built.mtimeMs); + } + expect(await run()).toBe("sanitized\n"); + expect((await stat(join(dir, "main.ll"))).mtimeMs).toBe(tuTime); + expect((await readdir(dir)).sort()).toEqual(["cache", "main.ll", "main.ts", "program"]); + } finally { + await rm(dir, { recursive: true, force: true }); + } + }, + 240_000, +); + test.skipIf(process.platform === "win32")( "an executable appearing earlier on unchanged PATH invalidates the early binary", async () => { diff --git a/packages/cli/test/native-link-info.test.ts b/packages/cli/test/native-link-info.test.ts index 56ca46878..a7a5e718a 100644 --- a/packages/cli/test/native-link-info.test.ts +++ b/packages/cli/test/native-link-info.test.ts @@ -72,7 +72,7 @@ describe.runIf(supported)("macOS arm64 native link info", () => { minimum_os: "14.0", }, program: { object, entry_symbol: "main" }, - runtime_abi: { version: 25, marker: "scr_runtime_abi_v25" }, + runtime_abi: { version: 27, marker: "scr_runtime_abi_v27" }, runtime_pack: { kind: "precompiled", package: "@scriptc/runtime-darwin-arm64", @@ -165,6 +165,6 @@ describe.runIf(supported)("macOS arm64 native link info", () => { () => null, (failure: { stderr?: string }) => failure, ); - expect(error?.stderr).toContain("scr_runtime_abi_v25"); + expect(error?.stderr).toContain("scr_runtime_abi_v27"); }); }); diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts index 936283d40..519608393 100644 --- a/packages/compiler/ambient/scriptc-node-fallback.d.ts +++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts @@ -544,15 +544,17 @@ declare var DOMException: { readonly prototype: DOMException; }; -/* WebCrypto's secret-key slice (scr_webcrypto.c): crypto.subtle's - * importKey, generateKey and exportKey for HMAC and AES keys (importKey - * also for PBKDF2 and HKDF keys), AES-GCM/CBC/CTR encrypt and decrypt, - * PBKDF2 and HKDF deriveBits and deriveKey, HMAC sign/verify and the SHA-1/SHA-2 digests, typed as - * exactly that surface (the declarations mirror @types/node's webcrypto - * shapes), plus the global's randomUUID and getRandomValues. Every other - * SubtleCrypto method and crypto member is absent, so a use is an honest - * type error. CryptoKey values are native handles whose four members read - * through the handle. */ +/* WebCrypto (scr_webcrypto.c): crypto.subtle's importKey, generateKey and + * exportKey for HMAC and AES keys (importKey also for PBKDF2 and HKDF + * keys), AES-GCM/CBC/CTR encrypt and decrypt, wrapKey and unwrapKey of + * secret keys (AES-KW or those ciphers), PBKDF2 and HKDF deriveBits and + * deriveKey, HMAC sign/verify, generateKey, importKey, exportKey, sign + * and verify for ECDSA, Ed25519, RSASSA-PKCS1-v1_5 and RSA-PSS, and the + * SHA-1/SHA-2 digests, typed as exactly that surface (the declarations + * mirror @types/node's webcrypto shapes), plus the global's randomUUID and + * getRandomValues. Every other SubtleCrypto method and crypto member is + * absent, so a use is an honest type error. CryptoKey values are native + * handles whose four members read through the handle. */ declare var crypto: Crypto; /* The Crypto and SubtleCrypto constructors (feature probes only) stay * `unknown`; the values keep this declaration order. */ @@ -661,6 +663,42 @@ interface JsonWebKey { x?: string; y?: string; } +interface EcKeyGenParams extends Algorithm { + namedCurve: string; +} +interface EcKeyImportParams extends Algorithm { + namedCurve: string; +} +interface EcdsaParams extends Algorithm { + hash: HashAlgorithmIdentifier; +} +interface RsaHashedKeyGenParams extends Algorithm { + modulusLength: number; + publicExponent: Uint8Array; + hash: HashAlgorithmIdentifier; +} +interface RsaHashedImportParams extends Algorithm { + hash: HashAlgorithmIdentifier; +} +interface RsaPssParams extends Algorithm { + saltLength: number; +} +interface EcKeyAlgorithm extends KeyAlgorithm { + namedCurve: string; +} +interface RsaHashedKeyAlgorithm extends KeyAlgorithm { + modulusLength: number; + publicExponent: Uint8Array; + hash: KeyAlgorithm; +} +interface CryptoKeyPair { + privateKey: CryptoKey; + publicKey: CryptoKey; +} +/* HMAC and AES keys generate, import and export as 'raw' and 'jwk' + * (PBKDF2 and HKDF keys import as 'raw'); ECDSA and RSA keys generate, + * import and export as 'spki', 'pkcs8' and 'jwk', and ECDSA public keys as + * 'raw'. */ interface SubtleCrypto { decrypt( algorithm: AesGcmParams | AesCbcParams | AesCtrParams, @@ -687,6 +725,11 @@ interface SubtleCrypto { ): Promise; exportKey(format: "jwk", key: CryptoKey): Promise; exportKey(format: Exclude, key: CryptoKey): Promise; + generateKey( + algorithm: EcKeyGenParams | RsaHashedKeyGenParams | "Ed25519" | { name: "Ed25519" }, + extractable: boolean, + keyUsages: readonly KeyUsage[], + ): Promise; generateKey( algorithm: AesKeyGenParams | HmacKeyGenParams, extractable: boolean, @@ -695,24 +738,43 @@ interface SubtleCrypto { importKey( format: "jwk", keyData: JsonWebKey, - algorithm: AlgorithmIdentifier | HmacImportParams, + algorithm: AlgorithmIdentifier | HmacImportParams | EcKeyImportParams | RsaHashedImportParams, extractable: boolean, keyUsages: readonly KeyUsage[], ): Promise; importKey( format: Exclude, keyData: BufferSource, - algorithm: AlgorithmIdentifier | HmacImportParams, + algorithm: AlgorithmIdentifier | HmacImportParams | EcKeyImportParams | RsaHashedImportParams, + extractable: boolean, + keyUsages: readonly KeyUsage[], + ): Promise; + sign( + algorithm: AlgorithmIdentifier | EcdsaParams | RsaPssParams, + key: CryptoKey, + data: BufferSource, + ): Promise; + unwrapKey( + format: KeyFormat, + wrappedKey: BufferSource, + unwrappingKey: CryptoKey, + unwrapAlgorithm: AlgorithmIdentifier | AesGcmParams | AesCbcParams | AesCtrParams, + unwrappedKeyAlgorithm: AlgorithmIdentifier | HmacImportParams, extractable: boolean, keyUsages: readonly KeyUsage[], ): Promise; - sign(algorithm: AlgorithmIdentifier, key: CryptoKey, data: BufferSource): Promise; verify( - algorithm: AlgorithmIdentifier, + algorithm: AlgorithmIdentifier | EcdsaParams | RsaPssParams, key: CryptoKey, signature: BufferSource, data: BufferSource, ): Promise; + wrapKey( + format: KeyFormat, + key: CryptoKey, + wrappingKey: CryptoKey, + wrapAlgorithm: AlgorithmIdentifier | AesGcmParams | AesCbcParams | AesCtrParams, + ): Promise; } interface Crypto { readonly subtle: SubtleCrypto; @@ -2773,8 +2835,20 @@ declare module "crypto" { import { Transform } from "stream"; export type KeyObjectType = "secret" | "public" | "private"; export interface JsonWebKey { - kty?: string; + crv?: string; + d?: string; + dp?: string; + dq?: string; + e?: string; k?: string; + kty?: string; + n?: string; + p?: string; + q?: string; + qi?: string; + x?: string; + y?: string; + [key: string]: unknown; } export interface JwkKeyExportOptions { format: "jwk"; @@ -2782,16 +2856,183 @@ declare module "crypto" { export interface KeyExportOptions { format?: "buffer" | undefined; } - /** Secret keys only; the asymmetric key types are not available. */ + export type KeyFormat = "pem" | "der" | "jwk"; + export interface AsymmetricKeyExportOptions { + type: "pkcs1" | "spki" | "pkcs8" | "sec1"; + format: T; + } + export interface AsymmetricKeyDetails { + modulusLength?: number; + publicExponent?: bigint; + namedCurve?: string; + } + /* Secret keys, and RSA and EC private and public keys. */ export class KeyObject { private constructor(); type: KeyObjectType; symmetricKeySize?: number; asymmetricKeyType?: string; + asymmetricKeyDetails?: AsymmetricKeyDetails; + export(options: AsymmetricKeyExportOptions<"pem">): string | Buffer; + export(options: AsymmetricKeyExportOptions<"der">): Buffer; export(options?: KeyExportOptions): Buffer; export(options: JwkKeyExportOptions): JsonWebKey; equals(otherKeyObject: KeyObject): boolean; } + export interface PrivateKeyInput { + key: string | Buffer; + format?: "pem" | "der" | undefined; + type?: "pkcs1" | "pkcs8" | "sec1" | undefined; + encoding?: string | undefined; + } + export interface PublicKeyInput { + key: string | Buffer; + format?: "pem" | "der" | undefined; + type?: "pkcs1" | "spki" | undefined; + encoding?: string | undefined; + } + export interface JsonWebKeyInput { + key: JsonWebKey; + format: "jwk"; + } + export function createPrivateKey( + key: PrivateKeyInput | string | Buffer | JsonWebKeyInput, + ): KeyObject; + export function createPublicKey( + key: PublicKeyInput | string | Buffer | KeyObject | JsonWebKeyInput, + ): KeyObject; + export interface KeyPairKeyObjectResult { + publicKey: KeyObject; + privateKey: KeyObject; + } + export interface KeyPairSyncResult { + publicKey: T1; + privateKey: T2; + } + export interface RSAKeyPairKeyObjectOptions { + modulusLength: number; + publicExponent?: number | undefined; + } + export interface ECKeyPairKeyObjectOptions { + namedCurve: string; + } + export interface RSAKeyPairOptions { + modulusLength: number; + publicExponent?: number | undefined; + publicKeyEncoding: { type: "pkcs1" | "spki"; format: PubF }; + privateKeyEncoding: { type: "pkcs1" | "pkcs8"; format: PrivF }; + } + export interface ECKeyPairOptions { + namedCurve: string; + publicKeyEncoding: { type: "pkcs1" | "spki"; format: PubF }; + privateKeyEncoding: { type: "sec1" | "pkcs8"; format: PrivF }; + } + export function generateKeyPairSync( + type: "rsa", + options: RSAKeyPairOptions<"pem", "pem">, + ): KeyPairSyncResult; + export function generateKeyPairSync( + type: "rsa", + options: RSAKeyPairOptions<"der", "der">, + ): KeyPairSyncResult; + export function generateKeyPairSync( + type: "rsa", + options: RSAKeyPairKeyObjectOptions, + ): KeyPairKeyObjectResult; + export function generateKeyPairSync( + type: "ec", + options: ECKeyPairOptions<"pem", "pem">, + ): KeyPairSyncResult; + export function generateKeyPairSync( + type: "ec", + options: ECKeyPairOptions<"der", "der">, + ): KeyPairSyncResult; + export function generateKeyPairSync( + type: "ec", + options: ECKeyPairKeyObjectOptions, + ): KeyPairKeyObjectResult; + export interface ED25519KeyPairOptions { + publicKeyEncoding: { type: "spki"; format: PubF }; + privateKeyEncoding: { type: "pkcs8"; format: PrivF }; + } + export function generateKeyPairSync( + type: "ed25519", + options: ED25519KeyPairOptions<"pem", "pem">, + ): KeyPairSyncResult; + export function generateKeyPairSync( + type: "ed25519", + options: ED25519KeyPairOptions<"der", "der">, + ): KeyPairSyncResult; + export function generateKeyPairSync(type: "ed25519", options?: {}): KeyPairKeyObjectResult; + export function generateKeyPair( + type: "ed25519", + options: {} | undefined, + callback: (err: Error | null, publicKey: KeyObject, privateKey: KeyObject) => void, + ): void; + export function generateKeyPair( + type: "rsa", + options: RSAKeyPairKeyObjectOptions, + callback: (err: Error | null, publicKey: KeyObject, privateKey: KeyObject) => void, + ): void; + export function generateKeyPair( + type: "ec", + options: ECKeyPairKeyObjectOptions, + callback: (err: Error | null, publicKey: KeyObject, privateKey: KeyObject) => void, + ): void; + export interface SigningOptions { + padding?: number | undefined; + saltLength?: number | undefined; + dsaEncoding?: "der" | "ieee-p1363" | undefined; + } + export interface SignPrivateKeyInput extends PrivateKeyInput, SigningOptions {} + export interface SignKeyObjectInput extends SigningOptions { + key: KeyObject; + } + export interface VerifyPublicKeyInput extends PublicKeyInput, SigningOptions {} + export interface VerifyKeyObjectInput extends SigningOptions { + key: KeyObject; + } + export function sign( + algorithm: string | null | undefined, + data: ArrayBufferView, + key: KeyObject | string | Buffer | SignKeyObjectInput | SignPrivateKeyInput, + ): Buffer; + export function verify( + algorithm: string | null | undefined, + data: ArrayBufferView, + key: KeyObject | string | Buffer | VerifyKeyObjectInput | VerifyPublicKeyInput, + signature: ArrayBufferView, + ): boolean; + /* Sign and Verify accumulate update() data; they are not usable as + * writable streams. */ + export class Sign { + private constructor(); + update(data: string | ArrayBufferView): this; + update(data: string, inputEncoding: Encoding): this; + sign( + privateKey: KeyObject | string | Buffer | SignKeyObjectInput | SignPrivateKeyInput, + ): Buffer; + sign( + privateKey: KeyObject | string | Buffer | SignKeyObjectInput | SignPrivateKeyInput, + outputFormat: BinaryToTextEncoding, + ): string; + } + export class Verify { + private constructor(); + update(data: string | ArrayBufferView): this; + update(data: string, inputEncoding: Encoding): this; + verify( + object: KeyObject | string | Buffer | VerifyKeyObjectInput | VerifyPublicKeyInput, + signature: ArrayBufferView, + ): boolean; + verify( + object: KeyObject | string | Buffer | VerifyKeyObjectInput | VerifyPublicKeyInput, + signature: string, + signatureEncoding?: BinaryToTextEncoding, + ): boolean; + } + export function createSign(algorithm: string): Sign; + export function createVerify(algorithm: string): Verify; export function createSecretKey(key: ArrayBufferView | ArrayBufferLike): KeyObject; export function createSecretKey(key: string, encoding: BufferEncoding): KeyObject; export function generateKeySync(type: "hmac" | "aes", options: { length: number }): KeyObject; @@ -2930,7 +3171,8 @@ declare module "crypto" { callback: (error: Error | null, buffer: Buffer) => void, ): void; export type BinaryLike = string | ArrayBuffer | ArrayBufferView; - /* Cipheriv/Decipheriv: aes-128/192/256 in cbc, ctr, and gcm modes and + /* Cipheriv/Decipheriv: aes-128/192/256 in cbc, ctr, gcm, ecb, cfb, cfb8, + * cfb1, and ofb modes, des-ede3 and des-ede, chacha20, and * chacha20-poly1305. Both are Transforms: written chunks go through * update(), and the end pushes final(). */ export type CipherGCMTypes = "aes-128-gcm" | "aes-192-gcm" | "aes-256-gcm"; @@ -3048,6 +3290,123 @@ declare module "crypto" { setPrivateKey(privateKey: string, encoding: BinaryToTextEncoding): void; } export function createECDH(curveName: string): ECDH; + export class DiffieHellman { + private constructor(); + generateKeys(): Buffer; + generateKeys(encoding: BinaryToTextEncoding): string; + computeSecret( + otherPublicKey: ArrayBufferView, + inputEncoding?: null, + outputEncoding?: null, + ): Buffer; + computeSecret( + otherPublicKey: string, + inputEncoding: BinaryToTextEncoding, + outputEncoding?: null, + ): Buffer; + computeSecret( + otherPublicKey: ArrayBufferView, + inputEncoding: null, + outputEncoding: BinaryToTextEncoding, + ): string; + computeSecret( + otherPublicKey: string, + inputEncoding: BinaryToTextEncoding, + outputEncoding: BinaryToTextEncoding, + ): string; + getPrime(): Buffer; + getPrime(encoding: BinaryToTextEncoding): string; + getGenerator(): Buffer; + getGenerator(encoding: BinaryToTextEncoding): string; + getPublicKey(): Buffer; + getPublicKey(encoding: BinaryToTextEncoding): string; + getPrivateKey(): Buffer; + getPrivateKey(encoding: BinaryToTextEncoding): string; + setPublicKey(publicKey: ArrayBufferView): void; + setPublicKey(publicKey: string, encoding: BufferEncoding): void; + setPrivateKey(privateKey: ArrayBufferView): void; + setPrivateKey(privateKey: string, encoding: BufferEncoding): void; + verifyError: number; + } + export type DiffieHellmanGroup = Omit; + export function createDiffieHellman(primeLength: number, generator?: number): DiffieHellman; + export function createDiffieHellman( + prime: ArrayBuffer | ArrayBufferView, + generator?: number | ArrayBuffer | ArrayBufferView, + ): DiffieHellman; + export function createDiffieHellman( + prime: ArrayBuffer | ArrayBufferView, + generator: string, + generatorEncoding: BinaryToTextEncoding, + ): DiffieHellman; + export function createDiffieHellman( + prime: string, + primeEncoding: BinaryToTextEncoding, + generator?: number | ArrayBuffer | ArrayBufferView, + ): DiffieHellman; + export function createDiffieHellman( + prime: string, + primeEncoding: BinaryToTextEncoding, + generator: string, + generatorEncoding: BinaryToTextEncoding, + ): DiffieHellman; + export function getDiffieHellman(groupName: string): DiffieHellmanGroup; + export function createDiffieHellmanGroup(name: string): DiffieHellmanGroup; + export type LargeNumberLike = ArrayBufferView | SharedArrayBuffer | ArrayBuffer | bigint; + export interface GeneratePrimeOptions { + add?: LargeNumberLike | undefined; + rem?: LargeNumberLike | undefined; + safe?: boolean | undefined; + bigint?: boolean | undefined; + } + export interface GeneratePrimeOptionsBigInt extends GeneratePrimeOptions { + bigint: true; + } + export interface GeneratePrimeOptionsArrayBuffer extends GeneratePrimeOptions { + bigint?: false | undefined; + } + export function generatePrime( + size: number, + callback: (err: Error | null, prime: ArrayBuffer) => void, + ): void; + export function generatePrime( + size: number, + options: GeneratePrimeOptionsBigInt, + callback: (err: Error | null, prime: bigint) => void, + ): void; + export function generatePrime( + size: number, + options: GeneratePrimeOptionsArrayBuffer, + callback: (err: Error | null, prime: ArrayBuffer) => void, + ): void; + export function generatePrime( + size: number, + options: GeneratePrimeOptions, + callback: (err: Error | null, prime: ArrayBuffer | bigint) => void, + ): void; + export function generatePrimeSync(size: number): ArrayBuffer; + export function generatePrimeSync(size: number, options: GeneratePrimeOptionsBigInt): bigint; + export function generatePrimeSync( + size: number, + options: GeneratePrimeOptionsArrayBuffer, + ): ArrayBuffer; + export function generatePrimeSync( + size: number, + options: GeneratePrimeOptions, + ): ArrayBuffer | bigint; + export interface CheckPrimeOptions { + checks?: number | undefined; + } + export function checkPrime( + value: LargeNumberLike, + callback: (err: Error | null, result: boolean) => void, + ): void; + export function checkPrime( + value: LargeNumberLike, + options: CheckPrimeOptions, + callback: (err: Error | null, result: boolean) => void, + ): void; + export function checkPrimeSync(candidate: LargeNumberLike, options?: CheckPrimeOptions): boolean; export interface ScryptOptions { N?: number; r?: number; @@ -4858,8 +5217,8 @@ declare module "net" { * unshift — read answers exactly n buffered bytes or null. */ read(size?: number): Buffer | null; unshift(chunk: Uint8Array): void; - /* secureConnect/session are TLS-socket events declared on the shared - * socket surface (the lowering collapses TLSSocket onto this kind): + /* secureConnect/secure/session are TLS-socket events declared on the + * shared socket surface (the lowering collapses TLSSocket onto this kind): * the runtime gates on the transport, so a plain socket's * registration never fires — Node's exact split. 'session' fires * once with the serialized session (the received-ticket event). */ @@ -4867,7 +5226,15 @@ declare module "net" { /* After setEncoding the chunk is a string. */ on(event: "data", listener: (chunk: string) => void): void; on( - event: "end" | "connect" | "ready" | "timeout" | "readable" | "finish" | "secureConnect", + event: + | "end" + | "connect" + | "ready" + | "timeout" + | "readable" + | "finish" + | "secureConnect" + | "secure", listener: () => void, ): void; on(event: "close", listener: (hadError: boolean) => void): void; @@ -4883,7 +5250,15 @@ declare module "net" { /* After setEncoding the chunk is a string. */ addListener(event: "data", listener: (chunk: string) => void): void; addListener( - event: "end" | "connect" | "ready" | "timeout" | "readable" | "finish" | "secureConnect", + event: + | "end" + | "connect" + | "ready" + | "timeout" + | "readable" + | "finish" + | "secureConnect" + | "secure", listener: () => void, ): void; addListener(event: "close", listener: (hadError: boolean) => void): void; @@ -4898,7 +5273,15 @@ declare module "net" { /* After setEncoding the chunk is a string. */ once(event: "data", listener: (chunk: string) => void): void; once( - event: "end" | "connect" | "ready" | "timeout" | "readable" | "finish" | "secureConnect", + event: + | "end" + | "connect" + | "ready" + | "timeout" + | "readable" + | "finish" + | "secureConnect" + | "secure", listener: () => void, ): void; once(event: "close", listener: (hadError: boolean) => void): void; @@ -4949,9 +5332,10 @@ declare module "net" { on(event: "timeout", listener: (socket: Socket) => void): void; on(event: "error", listener: (err: Error) => void): void; /* The WebSocket handover: fires INSTEAD of 'request' for - * Connection: upgrade requests, with the raw socket + head bytes. */ + * Connection: upgrade requests, with the raw socket + head bytes; + * 'connect' is its twin for CONNECT requests (HTTP tunneling). */ on( - event: "upgrade", + event: "upgrade" | "connect", listener: (req: import("http").IncomingMessage, socket: Socket, head: Buffer) => void, ): void; /* http.createServer(handler)'s event twin (a no-parser net server @@ -4967,7 +5351,7 @@ declare module "net" { addListener(event: "timeout", listener: (socket: Socket) => void): void; addListener(event: "error", listener: (err: Error) => void): void; addListener( - event: "upgrade", + event: "upgrade" | "connect", listener: (req: import("http").IncomingMessage, socket: Socket, head: Buffer) => void, ): void; addListener( @@ -4980,7 +5364,7 @@ declare module "net" { once(event: "timeout", listener: (socket: Socket) => void): void; once(event: "error", listener: (err: Error) => void): void; once( - event: "upgrade", + event: "upgrade" | "connect", listener: (req: import("http").IncomingMessage, socket: Socket, head: Buffer) => void, ): void; once( @@ -5451,6 +5835,7 @@ declare module "tls" { * split — authorizationError is the verify-failure CODE STRING * (DEPTH_ZERO_SELF_SIGNED_CERT, ...), or null. */ export interface TLSSocket extends Socket { + readonly encrypted: true; authorized: boolean; authorizationError: string | null; /* null before the handshake, then the negotiated protocol or false */ @@ -5462,6 +5847,21 @@ declare module "tls" { /* whether the handshake resumed a cached session */ isSessionReused(): boolean; } + /** new tls.TLSSocket(socket, options): the server-mode wrap of an open + * socket (a server connection or an http CONNECT socket). The options + * record reads at runtime; isServer, cert, key, secureContext, + * ALPNProtocols, minVersion and maxVersion are implemented. */ + export interface TLSSocketOptions { + isServer?: boolean; + cert?: string | Uint8Array; + key?: string | Uint8Array; + secureContext?: SecureContext; + ALPNProtocols?: string[] | Uint8Array; + minVersion?: string; + maxVersion?: string; + [option: string]: unknown; + } + export const TLSSocket: new (socket: Socket, options: TLSSocketOptions) => TLSSocket; export interface CipherNameAndProtocol { name: string; standardName: string; @@ -5470,12 +5870,14 @@ declare module "tls" { export const DEFAULT_MIN_VERSION: string; export const DEFAULT_MAX_VERSION: string; /** tls.connect options — the RUNTIME options record (members read at - * runtime; port/host/rejectUnauthorized/ca/servername are the + * runtime; port/host/socket/rejectUnauthorized/ca/servername are the * honestly-implemented set, every other documented member throws the * runtime fence — the options-record stance's divergence-66 shape). */ export interface ConnectionOptions { port?: number; host?: string; + /* an open socket to run the client handshake over */ + socket?: Socket; rejectUnauthorized?: boolean; ca?: string | Uint8Array | ReadonlyArray; servername?: string; @@ -6710,11 +7112,10 @@ declare module "node:events" { * write(chunk, enc, cb) {} })`, Duplex/Transform/PassThrough, push/read, * flowing vs paused, write/end backpressure + 'drain', pipe/unpipe, and * destroy — with Node's event orderings. Chunks are BYTES (Buffer), with - * utf8 strings converted at push/write like Node; objectMode and - * setEncoding are declared so uses fence at their sites with their own - * words. `class My extends Readable` (the _read override form) is a - * separate feature and fences at the class declaration. The runtime-fired - * events carry per-class FORCED argument tuples ('data' is one Buffer, + * strings converted at push/write like Node, and any value on an + * object-mode side (objectMode, readableObjectMode, writableObjectMode — + * the object-mode constructor overloads come first). The runtime-fired + * events carry per-class FORCED argument tuples ('data' is one chunk, * 'error' one Error, 'pipe'/'unpipe' one Readable, the rest empty) — the * overloads below hand tsc the same contract so unannotated listener * parameters contextually type in .js sources. */ @@ -6820,6 +7221,51 @@ declare module "stream" { signal?: AbortSignal; } + /* Object-mode options (objectMode: true, or a Duplex's or Transform's + * readableObjectMode / writableObjectMode): chunks and completion data + * are any values, and an object-mode write's encoding argument reaches + * write/transform as passed (undefined when absent, null when a + * callback took its place). The constructors take these before the + * byte-mode options, so a byte-mode stream's callbacks keep Buffer + * chunks. */ + type ObjectModeEncoding = string | null | undefined; + type ObjectModeFlag = + | { objectMode: true } + | { readableObjectMode: true } + | { writableObjectMode: true }; + interface ObjectWritableOptions extends Omit { + objectMode: true; + write?( + this: Writable, + chunk: any, + encoding: ObjectModeEncoding, + callback: (error?: Error | null) => void, + ): void; + } + type ObjectDuplexOptions = Omit & { + write?( + this: Duplex, + chunk: any, + encoding: ObjectModeEncoding, + callback: (error?: Error | null) => void, + ): void; + } & ObjectModeFlag; + type ObjectTransformOptions = Omit & { + transform?( + this: Transform, + chunk: any, + encoding: ObjectModeEncoding, + callback: (error?: Error | null, data?: any) => void, + ): void; + flush?(this: Transform, callback: (error?: Error | null, data?: any) => void): void; + write?( + this: Transform, + chunk: any, + encoding: ObjectModeEncoding, + callback: (error?: Error | null) => void, + ): void; + } & ObjectModeFlag; + /* The operators' options: concurrency and highWaterMark bound map's * in-flight callbacks and queued results. */ interface ArrayOptions { @@ -6839,7 +7285,10 @@ declare module "stream" { [Symbol.asyncIterator](): AsyncIterableIterator; iterator(options?: { destroyOnReturn?: boolean }): AsyncIterableIterator; push(chunk: Buffer | string | null, encoding?: string): boolean; + /* An object-mode stream's values. */ + push(chunk: any, encoding?: string): boolean; unshift(chunk: Buffer | string, encoding?: string): void; + unshift(chunk: any, encoding?: string): void; read(size?: number): Buffer | null; pause(): this; resume(): this; @@ -6946,6 +7395,7 @@ declare module "stream" { } class Writable extends EventEmitter { + constructor(opts: ObjectWritableOptions); constructor(opts?: WritableOptions); /* The underscore-method surface (see Readable). */ _write(chunk: any, encoding: string, callback: (error?: Error | null) => void): void; @@ -6961,9 +7411,19 @@ declare module "stream" { encoding: string, callback?: (error?: Error | null) => void, ): boolean; + /* An object-mode stream's values (the encoding argument reaches + * _write as passed). */ + write(chunk: any, callback?: (error?: Error | null) => void): boolean; + write( + chunk: any, + encoding: ObjectModeEncoding, + callback?: (error?: Error | null) => void, + ): boolean; end(callback?: () => void): this; end(chunk: Buffer | string, callback?: () => void): this; end(chunk: Buffer | string, encoding: string, callback?: () => void): this; + end(chunk: any, callback?: () => void): this; + end(chunk: any, encoding: ObjectModeEncoding, callback?: () => void): this; cork(): void; uncork(): void; setDefaultEncoding(encoding: string): this; @@ -7013,6 +7473,7 @@ declare module "stream" { * mirrors @types/node: extends Readable, the writable half declared * again. The lowering treats both halves as first-class. */ class Duplex extends Readable { + constructor(opts: ObjectDuplexOptions); constructor(opts?: DuplexOptions); /* The underscore-method surface (see Readable). */ _write(chunk: any, encoding: string, callback: (error?: Error | null) => void): void; @@ -7027,9 +7488,19 @@ declare module "stream" { encoding: string, callback?: (error?: Error | null) => void, ): boolean; + /* An object-mode stream's values (the encoding argument reaches + * _write as passed). */ + write(chunk: any, callback?: (error?: Error | null) => void): boolean; + write( + chunk: any, + encoding: ObjectModeEncoding, + callback?: (error?: Error | null) => void, + ): boolean; end(callback?: () => void): this; end(chunk: Buffer | string, callback?: () => void): this; end(chunk: Buffer | string, encoding: string, callback?: () => void): this; + end(chunk: any, callback?: () => void): this; + end(chunk: any, encoding: ObjectModeEncoding, callback?: () => void): this; cork(): void; uncork(): void; setDefaultEncoding(encoding: string): this; @@ -7101,16 +7572,18 @@ declare module "stream" { } class Transform extends Duplex { + constructor(opts: ObjectTransformOptions); constructor(opts?: TransformOptions); /* The underscore-method surface (see Readable). */ _transform( chunk: any, encoding: string, - callback: (error?: Error | null, data?: Buffer | string) => void, + callback: (error?: Error | null, data?: any) => void, ): void; - _flush(callback: (error?: Error | null, data?: Buffer | string) => void): void; + _flush(callback: (error?: Error | null, data?: any) => void): void; } class PassThrough extends Transform { + constructor(opts: ObjectTransformOptions); constructor(opts?: TransformOptions); } diff --git a/packages/compiler/native/cli.profdata.gz b/packages/compiler/native/cli.profdata.gz index 55523e765..e2f344a4d 100644 Binary files a/packages/compiler/native/cli.profdata.gz and b/packages/compiler/native/cli.profdata.gz differ diff --git a/packages/compiler/package.json b/packages/compiler/package.json index 98d984d4f..a1d66963d 100644 --- a/packages/compiler/package.json +++ b/packages/compiler/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/compiler", - "version": "0.4.1", + "version": "0.4.2", "description": "The scriptc compiler — TypeScript/JavaScript frontend, typed IR, LLVM backend", "license": "Apache-2.0", "homepage": "https://scriptc.dev", @@ -40,7 +40,7 @@ }, "dependencies": { "@scriptc/runtime": "workspace:*", - "tsc-ts": "0.0.5", + "tsc-ts": "0.0.7", "typescript": "7.0.2", "typescript5": "npm:typescript@5.9.3" }, diff --git a/packages/compiler/src/backend/executable-features.ts b/packages/compiler/src/backend/executable-features.ts index f7cae8508..672fddd96 100644 --- a/packages/compiler/src/backend/executable-features.ts +++ b/packages/compiler/src/backend/executable-features.ts @@ -56,6 +56,9 @@ export function executableLinkFeatures(mod: IrModule): NativeLinkFeatures { tlsCa: features.tlsCa, cipher: features.cipher, ecdh: features.ecdh, + dh: features.dh, + pki: features.pki, + ed25519: features.ed25519, webcrypto: features.webcrypto, perfLoop: features.perfLoop, }; diff --git a/packages/compiler/src/backend/library-pack.ts b/packages/compiler/src/backend/library-pack.ts index 805198df1..e9f52f7a0 100644 --- a/packages/compiler/src/backend/library-pack.ts +++ b/packages/compiler/src/backend/library-pack.ts @@ -60,6 +60,9 @@ export async function compilePackedLibrary( tls: false, cipher: false, ecdh: false, + dh: false, + pki: false, + ed25519: false, webcrypto: false, perfLoop: false, tlsCa: false, diff --git a/packages/compiler/src/backend/llvm/array-search.ts b/packages/compiler/src/backend/llvm/array-search.ts index d61d91e5c..41b64d418 100644 --- a/packages/compiler/src/backend/llvm/array-search.ts +++ b/packages/compiler/src/backend/llvm/array-search.ts @@ -10,6 +10,8 @@ * SameValueZero are both pointer identity (class instances, records, * arrays, maps, sets, typed arrays): 64-bit compares, four slots per * branch. + * - sc_arr_index_of_cref, the same over the 4-byte slots of compact class + * references (SCR_ELEM_CREF), against the needle's 32-bit reference. * - sc_arr_index_of_str, for strings: pointer identity, then the length, * then the bytes. Strings of up to 16 bytes compare as two words; a short * string's word is the eight bytes ending at its last byte, which start @@ -26,10 +28,12 @@ import type { IrType } from "../../ir/ir.js"; import { SCR_ARR_FIELD } from "./common.js"; import { FN_ATTRS } from "./shapes.js"; -export type ArraySearchKind = "ptr" | "str"; +export type ArraySearchKind = "ptr" | "cref" | "str"; -/** The inline search for arrays of `elem`, or null for the runtime search. */ -export function arraySearchKind(elem: IrType): ArraySearchKind | null { +/** The inline search for arrays of `elem`, or null for the runtime search. + * `compact`: the arrays hold compact class references (SCR_ELEM_CREF). */ +export function arraySearchKind(elem: IrType, compact: boolean): ArraySearchKind | null { + if (compact) return elem.kind === "object" ? "cref" : null; switch (elem.kind) { case "object": case "record": @@ -46,7 +50,7 @@ export function arraySearchKind(elem: IrType): ArraySearchKind | null { } export function arraySearchHelperName(kind: ArraySearchKind): string { - return kind === "ptr" ? "sc_arr_index_of_ptr" : "sc_arr_index_of_str"; + return `sc_arr_index_of_${kind}`; } /** The definitions of the helpers in `kinds`; they declare their slow paths @@ -76,9 +80,18 @@ export function emitArraySearchHelpers( ` %dp = getelementptr inbounds %ScrArr, ptr %a, i32 0, i32 ${SCR_ARR_FIELD.data}`, ` %data = load ptr, ptr %dp`, ); - if (kind === "ptr") { + if (kind === "ptr" || kind === "cref") { + // A compact reference is the address over 8 (classes.ts + // compactRefLoadLines). + const slot = kind === "ptr" ? "i64" : "i32"; defs.push( - ` %needle = ptrtoint ptr %v to i64`, + ...(kind === "ptr" + ? [` %needle = ptrtoint ptr %v to i64`] + : [ + ` %vaddr = ptrtoint ptr %v to i64`, + ` %vref = lshr i64 %vaddr, 3`, + ` %needle = trunc i64 %vref to i32`, + ]), // Four slots per step with one branch, then one at a time. ` br label %quad`, `quad:`, @@ -89,9 +102,9 @@ export function emitArraySearchHelpers( `quadbody:`, ...[0, 1, 2, 3].flatMap((k) => [ ` %qi${k} = add ${sz} %q, ${k}`, - ` %qp${k} = getelementptr inbounds i64, ptr %data, ${sz} %qi${k}`, - ` %qs${k} = load i64, ptr %qp${k}`, - ` %qh${k} = icmp eq i64 %qs${k}, %needle`, + ` %qp${k} = getelementptr inbounds ${slot}, ptr %data, ${sz} %qi${k}`, + ` %qs${k} = load ${slot}, ptr %qp${k}`, + ` %qh${k} = icmp eq ${slot} %qs${k}, %needle`, ]), ` %qh01 = or i1 %qh0, %qh1`, ` %qh23 = or i1 %qh2, %qh3`, @@ -107,9 +120,9 @@ export function emitArraySearchHelpers( ` %done = icmp eq ${sz} %i, %len`, ` br i1 %done, label %miss, label %body`, `body:`, - ` %sp = getelementptr inbounds i64, ptr %data, ${sz} %i`, - ` %s = load i64, ptr %sp`, - ` %hit = icmp eq i64 %s, %needle`, + ` %sp = getelementptr inbounds ${slot}, ptr %data, ${sz} %i`, + ` %s = load ${slot}, ptr %sp`, + ` %hit = icmp eq ${slot} %s, %needle`, ` br i1 %hit, label %found, label %cont`, ); } else { diff --git a/packages/compiler/src/backend/llvm/blocks.ts b/packages/compiler/src/backend/llvm/blocks.ts index 6d22bf3bf..354e631c6 100644 --- a/packages/compiler/src/backend/llvm/blocks.ts +++ b/packages/compiler/src/backend/llvm/blocks.ts @@ -95,8 +95,8 @@ export class BlockBuilder { this.terminate(`br label %${label}`); } - condBr(v: string, t: string, f: string): void { - this.terminate(`br i1 ${v}, label %${t}, label %${f}`); + condBr(v: string, t: string, f: string, attachment = ""): void { + this.terminate(`br i1 ${v}, label %${t}, label %${f}${attachment}`); } /** Emit a double-indexed `for (i = 0; i < len; i++)` loop. `next` is diff --git a/packages/compiler/src/backend/llvm/byte-windows.ts b/packages/compiler/src/backend/llvm/byte-windows.ts index 5fffa1428..9741cc547 100644 --- a/packages/compiler/src/backend/llvm/byte-windows.ts +++ b/packages/compiler/src/backend/llvm/byte-windows.ts @@ -43,6 +43,7 @@ export function findInitializedByteLoopBindings( if (stmt.init) list([stmt.init], inner); list(stmt.body, inner); } + if (stmt.kind === "while") entries.set(stmt, new Set(known)); if (stmt.kind === "while" || stmt.kind === "doWhile") list(stmt.body, new Set(known)); } } diff --git a/packages/compiler/src/backend/llvm/bytes-loop-versions.test.ts b/packages/compiler/src/backend/llvm/bytes-loop-versions.test.ts index 28194c7b3..2d70e9014 100644 --- a/packages/compiler/src/backend/llvm/bytes-loop-versions.test.ts +++ b/packages/compiler/src/backend/llvm/bytes-loop-versions.test.ts @@ -134,6 +134,39 @@ test("a counted loop's affine accesses get one guard and exact operand ranges", expect(version.ranges.get(shifted)).toEqual({ min: 1, max: Number.MAX_SAFE_INTEGER }); }); +test("invariant terms without a range are tested by the guard", () => { + // for (let i = 0; i < n; i++) out[i] = src[k + i], with `k` a parameter + const shifted = bin("+", ref("k"), ref("i")); + const loop = counted([set(ref("out", f64s), ref("i"), get(ref("src", f64s), shifted))]); + const kernel = fn([loop]); + const version = match(kernel, loop)!; + expect([...version.guarded]).toEqual(["v1:k"]); + expect(version.affine.map((c) => c.terms.map((t) => t.key))).toEqual([[], ["v1:k"]]); + // In the fast copy the term has the range the guard tests. + expect(version.ranges.get(shifted.kind === "bin" ? shifted.left : shifted)).toEqual({ + min: -(2 ** 32), + max: 2 ** 32, + }); + const mod: IrModule = { + irVersion: 17, + sourceFile: loc.file, + entry: "__main", + functions: [ + { name: "__main", params: [], returnType: VOID, locals: [], body: [], loc }, + kernel, + ], + }; + const body = /^define internal [^\n]*@sc_bf_kernel[^]*?^}/m.exec(emitLlvmModule(mod))![0]; + const guard = body.slice(0, body.search(/^bytes\.version\.(fast|slow)\d+:$/m)); + // -2^32 <= k <= 2^32, and k is the double of its frozen integer. + expect(guard).toMatch(/fcmp oge double %\S+, 0xC1F0000000000000/); + expect(guard).toMatch(/fcmp ole double %\S+, 0x41F0000000000000/); + expect(guard).toMatch(/freeze i64/); + const fast = /^bytes\.version\.fast\d+:$[^]*?^bytes\.version\.done\d+:$/m.exec(body)![0]; + expect(fast).not.toContain("bytes.index"); + expect(body.slice(0, body.indexOf(fast))).toContain("bytes.index.invalid"); +}); + test("statically ranged indices compare one constant with the length", () => { const masked = bin("&", bin("^", ref("i"), ref("k")), num(255)); const loop = counted([set(ref("out", f64s), num(0), get(ref("table", i32s), masked))]); @@ -144,6 +177,11 @@ test("statically ranged indices compare one constant with the length", () => { ]); expect(version.affine).toEqual([]); expect(version.counted).toBeNull(); + // A uint32 index (an array counter's range) gets no guard: no typed array + // in practice has 2^31 elements. + const wide = bin(">>>", bin("^", ref("i"), ref("k")), num(0)); + const unbounded = counted([set(ref("out", f64s), num(0), get(ref("table", i32s), wide))]); + expect(match(fn([unbounded]), unbounded)!.fixed).toEqual([{ receiver: 0, end: 0 }]); }); test("scaled counters and body temps resolve to their receiver and index", () => { @@ -325,6 +363,63 @@ test("index temps of a version's counter are integers in the fast copy", () => { expect(fast).not.toContain("bytes.index"); }); +test("while loop counters below a bound version their accesses", () => { + // while (i < n) { out[i] = src[i + 1]; i = i + 1 } + const step: IrStmt = { kind: "assign", localId: "i", value: bin("+", ref("i"), num(1)), loc }; + const whileLoop = (body: IrStmt[]): IrStmt & { kind: "while" } => ({ + kind: "while", + cond: { kind: "bin", op: "<", left: ref("i"), right: ref("n"), type: BOOL, loc }, + body, + loc, + }); + const copy = whileLoop([ + set(ref("out", f64s), ref("i"), get(ref("src", f64s), bin("+", ref("i"), num(1)))), + step, + ]); + const declared = (loop: IrStmt): IrFunction => + fn([{ kind: "varDecl", localId: "i", init: ref("k"), loc }, loop]); + const versionOf = (loop: IrStmt & { kind: "while" }): BytesLoopVersion | null => { + const f = declared(loop); + const ranges = analyzeIntegerRanges(f); + return matchBytesLoopVersion(loop, null, { + locals: new Map(f.locals.map((l) => [l.id, l])), + invariantGlobals: new Set(), + captured: new Set(), + ranges, + initialized: findInitializedByteLoopBindings(f).get(loop) ?? new Set(), + proven: new Set(), + workers: false, + owned: new Set(), + }); + }; + const version = versionOf(copy)!; + expect(version.bounds.size).toBe(2); + expect(version.bounded.map((c) => [version.receivers[c.receiver]!.ref, c.slack])).toEqual([ + [ref("out", f64s), 0], + [ref("src", f64s), 1], + ]); + // `out[i]` needs `i` to start at 0 or above (`src[i + 1]` only at -1). + expect(version.counters.map((c) => [c.ref.kind === "varRef" && c.ref.localId, c.min])).toEqual([ + ["i", 0], + ]); + // An access after the counter advances in the same pass stays checked. + const advanced = whileLoop([step, set(ref("out", f64s), ref("i"), num(0))]); + expect(versionOf(advanced)).toBeNull(); + const mod: IrModule = { + irVersion: 17, + sourceFile: loc.file, + entry: "__main", + functions: [ + { name: "__main", params: [], returnType: VOID, locals: [], body: [], loc }, + declared(copy), + ], + }; + const body = /^define internal [^\n]*@sc_bf_kernel[^]*?^}/m.exec(emitLlvmModule(mod))![0]; + const fast = /^bytes\.version\.fast\d+:$[^]*?^bytes\.version\.done\d+:$/m.exec(body)![0]; + expect(fast).not.toContain("bytes.index"); + expect(body.slice(0, body.indexOf(fast))).toContain("bytes.index.invalid"); +}); + test("Float16Array accesses version in elements and share the element tag", () => { const f16s: IrType = { kind: "bytes", elem: "f16" }; const withHalves = (body: IrStmt[]): IrFunction => { diff --git a/packages/compiler/src/backend/llvm/bytes-loop-versions.ts b/packages/compiler/src/backend/llvm/bytes-loop-versions.ts index 8c7f668a0..e797bcbb9 100644 --- a/packages/compiler/src/backend/llvm/bytes-loop-versions.ts +++ b/packages/compiler/src/backend/llvm/bytes-loop-versions.ts @@ -1,4 +1,4 @@ -import type { IrExpr, IrLocal, IrStmt } from "../../ir/ir.js"; +import { F64, type IrExpr, type IrLocal, type IrStmt } from "../../ir/ir.js"; import type { IntegerRange, IntegerRanges } from "../../ir/integer-ranges.js"; import type { IntegerCountedForLoop } from "../../ir/integer-loops.js"; import type { LlvmEmitterContext } from "./expr-context.js"; @@ -7,6 +7,7 @@ import { everyExprChild, everyStmtChild } from "../../ir/traverse.js"; import { exactInteger, widenInteger } from "./integer-values.js"; import { emitCountedLoopLimit, emitCountedLoopSafety } from "./counted-loops.js"; import { emitBytesHeaderLength, emitBytesSharedStorage } from "./expr-bytes.js"; +import { f64Lit } from "./common.js"; type Node = IrExpr | IrStmt; type VarRef = IrExpr & { kind: "varRef" }; @@ -22,6 +23,24 @@ interface KeyedTerm extends Term { key: string; } +/** Every access of `receiver` at a while loop counter `c + d` happens while + * `c < bound` (`c <= bound` when `inclusive`) holds and before the counter + * advances, so it is in bounds when `bound + slack <= length`. */ +interface BoundedCheck { + receiver: number; + bound: IrExpr; + slack: number; +} + +/** A while loop counter whose entry value the guard tests: an exact + * integer of at least `min`. A counter that `| 0` wraps also needs every + * bound of its loops at most `wrapLimit`, so that it never reaches 2^31. */ +interface CounterCheck { + ref: VarRef; + min: number; + wrapBounds: { bound: IrExpr; limit: number }[]; +} + /** Every access of `receiver` at a static index range ends below `end + 1`. */ interface FixedCheck { receiver: number; @@ -50,6 +69,12 @@ export interface BytesLoopVersion { ownedReceivers: number[]; fixed: FixedCheck[]; affine: AffineCheck[]; + /** While loops: accesses at a counter below a loop-invariant bound. */ + bounded: BoundedCheck[]; + counters: CounterCheck[]; + /** Keys of affine terms the guard tests at run time: exact integers + * within GUARDED_TERM. */ + guarded: ReadonlySet; /** The counted loop whose iteration space the affine checks cover. */ counted: IntegerCountedForLoop | null; /** The guard also requires every counter value within [-bound, bound]. */ @@ -79,10 +104,13 @@ export interface BytesLoopFacts { } const MAX_COST = 400; -const MAX_FIXED_END = 2 ** 32; +const MAX_FIXED_END = 2 ** 31; const MAX_CHECKS = 8; const MAX_OFFSET = 2 ** 31; const SAFE = Number.MAX_SAFE_INTEGER; +/** The range the guard requires of an invariant index term without a + * proven one: every index of a typed array of at most 2^32 elements. */ +const GUARDED_TERM: IntegerRange = { min: -(2 ** 32), max: 2 ** 32 }; const INVARIANT_OPS = new Set(["+", "-", "*", "&", "|", "^", "<<", ">>", ">>>"]); /** Loop versioning for typed-array bounds. A typed array's length and data @@ -95,7 +123,9 @@ const INVARIANT_OPS = new Set(["+", "-", "*", "&", "|", "^", "<<", ">>", ">>>"]) * `hi < length`, such as `table[x & 0xff]` or a constant-trip counter; * - an index `k * counter + invariant + c` of a counted loop needs its first * and last values in bounds, such as `out[base + i]` for `i < n` or - * `view.getUint32(i * 4)`. + * `view.getUint32(i * 4)`. An invariant without a proven integer range, + * such as `base` for `const base = r * cols`, must also be an exact + * integer between -2^32 and 2^32, which the guard tests. * * In worker programs, every access of a view that may be shared tests its * shared storage. A view's storage is also fixed at construction, so the @@ -107,7 +137,7 @@ const INVARIANT_OPS = new Set(["+", "-", "*", "&", "|", "^", "<<", ">>", ">>>"]) * whenever the guard fails, and keeps every out-of-bounds read, ignored * write and NaN-index behavior. */ export function matchBytesLoopVersion( - loop: IrStmt & { kind: "for" }, + loop: IrStmt & { kind: "for" | "while" }, counted: IntegerCountedForLoop | null, facts: BytesLoopFacts, ): BytesLoopVersion | null { @@ -146,10 +176,10 @@ export function matchBytesLoopVersion( // writes is not invariant either: `for (let i = (a = b, 0); ...)` must // not keep the bounds the guard proved for the old `a`. if ( - (loop.init !== null && !scanStmt(loop.init)) || + (loop.kind === "for" && loop.init !== null && !scanStmt(loop.init)) || !loop.body.every(scanStmt) || (loop.cond !== null && !scanExpr(loop.cond)) || - (loop.update !== null && !scanStmt(loop.update)) + (loop.kind === "for" && loop.update !== null && !scanStmt(loop.update)) ) return null; // A body temp stands for its initializer only when it is a constant @@ -198,9 +228,8 @@ export function matchBytesLoopVersion( } return invariant(e.localId) ? e : null; }; - // A pure integer operand evaluated again before the loop. - const invariantKey = (e: IrExpr): string | null => { - if (!facts.ranges.get(e)) return null; + // A pure loop-invariant number, evaluated again before the loop. + const pureKey = (e: IrExpr): string | null => { if (e.kind === "numLit") return Number.isSafeInteger(e.value) ? `n${e.value}` : null; if (e.kind === "varRef") return invariant(e.localId) ? `v${e.localId.length}:${e.localId}` : null; @@ -210,12 +239,27 @@ export function matchBytesLoopVersion( e.left.type.kind === "f64" && e.right.type.kind === "f64" ) { - const left = invariantKey(e.left), - right = invariantKey(e.right); + const left = pureKey(e.left), + right = pureKey(e.right); return left !== null && right !== null ? `(${e.op} ${left} ${right})` : null; } return null; }; + // Invariant terms without a proven range, such as `base` in + // `w[base + j]` with `const base = r * cols` for a parameter `cols`. The + // guard tests each one: an exact integer (not -0) within GUARDED_TERM. + const guardedRanges = new Map(); + const guardedNodes = new Map(); + // The key of a loop-invariant index term, with a proven range or guarded. + const invariantKey = (e: IrExpr): string | null => { + const key = pureKey(e); + if (key === null || facts.ranges.get(e)) return key; + if (e.kind === "numLit") return null; + guardedRanges.set(e, GUARDED_TERM); + guardedNodes.set(e, key); + return key; + }; + const termRange = (e: IrExpr): IntegerRange => facts.ranges.get(e) ?? guardedRanges.get(e)!; interface Affine { /** The counter's coefficient; 0 when the value is loop-invariant. */ counter: number; @@ -223,9 +267,10 @@ export function matchBytesLoopVersion( offset: number; } // Every operand of an affine index is `counter * k + Σ terms + offset`. - // Invariant terms need proven ranges. Operands that involve the counter - // need none: the guard bounds the counter so that each of them is a safe - // integer, which makes the index's double arithmetic exact. + // Invariant terms need proven ranges or the guard's test (guarded terms). + // Operands that involve the counter need none: the guard bounds the + // counter so that each of them is a safe integer, which makes the index's + // double arithmetic exact. const shapes = new Map(); const affine = (e: IrExpr): Affine | null => { if (shapes.has(e)) return shapes.get(e)!; @@ -309,10 +354,48 @@ export function matchBytesLoopVersion( }; const termMagnitude = (terms: Term[]): number => terms.reduce((sum, t) => { - const r = facts.ranges.get(t.expr)!; + const r = termRange(t.expr); return sum + Math.max(Math.abs(r.min), Math.abs(r.max)); }, 0); + const whileAccesses = loop.kind === "while" ? whileCounterAccesses(loop) : null; + function whileCounterAccesses(loop: IrStmt & { kind: "while" }) { + return findWhileCounterAccesses(loop, { + counterLocal: (id) => { + const local = facts.locals.get(id); + return ( + local?.type.kind === "f64" && + !local.boxed && + !local.tdz && + !facts.captured.has(id) && + facts.initialized.has(id) + ); + }, + boundKey: (e) => { + if (e.type.kind !== "f64") return null; + // The guard evaluates the bound before the loop: a typed array's + // length needs the binding itself, not a temp the body declares. + if ( + e.kind === "bytesIntrinsic" && + e.method === "length" && + e.receiver.kind === "varRef" && + e.receiver.type.kind === "bytes" && + invariant(e.receiver.localId) + ) + return `bytes-length:${e.receiver.localId}`; + if ( + e.kind === "strIntrinsic" && + e.method === "length" && + e.args.length === 0 && + e.receiver.kind === "varRef" && + e.receiver.type.kind === "string" && + invariant(e.receiver.localId) + ) + return `string-length:${e.receiver.localId}`; + return pureKey(e); + }, + }); + } const receivers: { ref: VarRef; global: boolean }[] = []; const receiverIndex = new Map(); const fixed = new Map(); @@ -332,6 +415,11 @@ export function matchBytesLoopVersion( // Worker programs: accesses through invariant receivers, proven or not, // become thread-owned when the guard finds no shared storage. const owned = new Map(); + // While loops: accesses at a counter below a bound, by receiver and bound. + const boundedChecks = new Map< + string, + { check: BoundedCheck; nodes: { node: Node; access: WhileAccess }[] } + >(); const access = (node: Node, array: IrExpr, index: IrExpr, width: number): void => { const ref = root(array); if (!ref) return; @@ -366,11 +454,30 @@ export function matchBytesLoopVersion( }); return; } + const bounded = whileAccesses?.accesses.get(node); + const loopCounter = bounded && whileAccesses!.counters.get(bounded.counter); + if (bounded && loopCounter && (!loopCounter.wraps || loopCounter.bounds !== null)) { + const receiver = receiverOf(ref); + const key = `${receiver}|${bounded.key}|${bounded.adj}`; + const slack = bounded.offset + width - 1 + bounded.adj; + const group = boundedChecks.get(key); + if (group) { + group.check.slack = Math.max(group.check.slack, slack); + group.nodes.push({ node, access: bounded }); + } else + boundedChecks.set(key, { + check: { receiver, bound: bounded.bound, slack }, + nodes: [{ node, access: bounded }], + }); + return; + } const range = facts.ranges.get(index); if (!range) return; const end = range.max + width - 1; - // Only a length a buffer can have: an outer loop must not pay for a - // guard that bounds an inner loop's unbounded counter. + // Only an end below 2^31: an outer loop must not pay for a guard that + // bounds an inner loop's unbounded counter, and the counter of a loop to + // an array's length reaches 2^32 - 2, a length no typed array in + // practice has. if (range.min < 0 || end >= MAX_FIXED_END) return; const receiver = receiverOf(ref); const group = fixed.get(receiver); @@ -392,6 +499,7 @@ export function matchBytesLoopVersion( return everyStmtChild(s, visitExpr, visitStmt); }; loop.body.every(visitStmt); + if (loop.kind === "while") visitExpr(loop.cond); // Bound the counter so every counter operand stays a safe integer. let bound = SAFE; const affineGroups = [...affineChecks.values()].filter((group) => { @@ -407,8 +515,18 @@ export function matchBytesLoopVersion( }); const fixedKept = [...fixed.values()].slice(0, MAX_CHECKS); const affineKept = affineGroups.slice(0, MAX_CHECKS - fixedKept.length); + const boundedKept = [...boundedChecks.values()].slice( + 0, + Math.max(0, MAX_CHECKS - fixedKept.length - affineKept.length), + ); const ownedKept = [...owned.entries()].slice(0, MAX_CHECKS); - if (fixedKept.length === 0 && affineKept.length === 0 && ownedKept.length === 0) return null; + if ( + fixedKept.length === 0 && + affineKept.length === 0 && + boundedKept.length === 0 && + ownedKept.length === 0 + ) + return null; const bounds = new Set(); const ownedNodes = new Set(); const used = new Set(); @@ -433,7 +551,7 @@ export function matchBytesLoopVersion( let min = Math.min(a, b) + shape.offset, max = Math.max(a, b) + shape.offset; for (const term of shape.terms) { - const r = facts.ranges.get(term.expr)!; + const r = termRange(term.expr); min += term.sign > 0 ? r.min : -r.max; max += term.sign > 0 ? r.max : -r.min; } @@ -447,6 +565,45 @@ export function matchBytesLoopVersion( ranges.set(operand, { min: Math.max(min, known.min), max: Math.min(max, known.max) }); } } + // Guarded terms of the kept checks, and the range the guard proves for + // every node of each in the fast copy. + const guarded = new Set(); + for (const group of affineKept) + for (const term of group.check.terms) if (guardedRanges.has(term.expr)) guarded.add(term.key); + for (const [node, key] of guardedNodes) if (guarded.has(key)) ranges.set(node, GUARDED_TERM); + // While loop counters of the kept checks: each entry value is an exact + // integer that no access offset takes below 0. In the fast copy each + // access's index is below its length, and its counter read at least the + // most negative offset. + const narrow = (e: IrExpr, range: IntegerRange): void => { + const known = ranges.get(e) ?? facts.ranges.get(e); + ranges.set( + e, + known ? { min: Math.max(known.min, range.min), max: Math.min(known.max, range.max) } : range, + ); + }; + const counterMins = new Map(); + for (const group of boundedKept) { + used.add(group.check.receiver); + for (const { node, access } of group.nodes) { + bounds.add(node); + const min = 0 - access.offset; + counterMins.set(access.counter, Math.max(counterMins.get(access.counter) ?? -SAFE, min)); + narrow(access.ref, { min: -MAX_OFFSET, max: SAFE }); + if (access.index !== access.ref) narrow(access.index, { min: 0, max: SAFE }); + } + } + const counterChecks: CounterCheck[] = []; + for (const [id, min] of counterMins) { + const info = whileAccesses!.counters.get(id)!; + counterChecks.push({ + ref: { kind: "varRef", localId: id, type: F64, loc: loop.loc }, + min, + wrapBounds: info.wraps + ? info.bounds!.map((b) => ({ bound: b.bound, limit: 2 ** 31 - info.sum - b.adj })) + : [], + }); + } // Renumber the receivers the kept checks use. const order = [...used].sort((a, b) => a - b); const renumber = new Map(order.map((old, index) => [old, index])); @@ -470,12 +627,320 @@ export function matchBytesLoopVersion( minOffset: g.check.minOffset, maxEnd: g.check.maxEnd, })), + bounded: boundedKept.map((g) => ({ + receiver: renumber.get(g.check.receiver)!, + bound: g.check.bound, + slack: g.check.slack, + })), + counters: counterChecks, + guarded, counted: affineKept.length > 0 ? counted : null, counterBound: needsBound ? bound : null, ranges, }; } +/** A typed-array access at a while loop counter: `counter + offset`, read + * or written while `counter < bound` (`<=` when `adj` is 1) holds. */ +interface WhileAccess { + counter: string; + /** The index, and the counter's read in it. */ + index: IrExpr; + ref: IrExpr; + bound: IrExpr; + key: string; + adj: number; + offset: number; +} + +interface WhileCounter { + /** Some increment is `c = (c + k) | 0`. */ + wraps: boolean; + /** The sum of every increment literal in the loop. */ + sum: number; + /** One bound from the condition of the loop and of every nested while + * loop that advances the counter; null when one of them has none. */ + bounds: { bound: IrExpr; adj: number }[] | null; +} + +/** `counter = counter + k`, `counter = (counter + k) | 0` or `counter++`. */ +function counterStep(s: IrStmt): { id: string; k: number; wraps: boolean } | null { + if (s.kind === "exprStmt" && s.expr.kind === "incDec" && s.expr.op === "+") + return { id: s.expr.localId, k: 1, wraps: false }; + if (s.kind !== "assign") return null; + let value = s.value, + wraps = false; + if ( + value.kind === "bin" && + value.op === "|" && + value.right.kind === "numLit" && + value.right.value === 0 && + value.left.kind === "bin" + ) { + value = value.left; + wraps = true; + } + if ( + value.kind !== "bin" || + value.op !== "+" || + value.left.kind !== "varRef" || + value.left.localId !== s.localId || + value.right.kind !== "numLit" || + !Number.isSafeInteger(value.right.value) || + value.right.value <= 0 || + value.right.value > MAX_OFFSET + ) + return null; + return { id: s.localId, k: value.right.value, wraps }; +} + +/** Typed-array accesses of a while loop that happen at a counter below a + * loop-invariant bound. A counter is a number local that the loop changes + * only by positive integer increments, written as statements of the body, + * of its blocks and branches, or of nested while loops. The condition of + * the loop, an `if` or a `&&` chain that compares the counter below a bound + * (`while (pos < end && bytes[pos] === 32) pos++`) makes it below that + * bound until the next increment on the path. The counter starts at its + * entry value and only grows (a `| 0` increment never wraps when every + * bound is at most 2^31 minus the increments), so an access at `counter + + * d` is in bounds for the whole loop when the entry value is at least `-d` + * and `bound + d < length` (`+ 1` for `<=`). */ +function findWhileCounterAccesses( + loop: IrStmt & { kind: "while" }, + host: { + counterLocal: (id: string) => boolean; + /** A key for a bound expression whose value the loop never changes. */ + boundKey: (e: IrExpr) => string | null; + }, +): { accesses: Map; counters: Map } { + const steps = new Map(); + const refused = new Set(); + // Increments count only as statements of the loop's straight-line + // structure; any other write refuses the binding. + const writes = (list: readonly IrStmt[], direct: boolean): void => { + for (const s of list) { + const step = counterStep(s); + if (step && direct) { + const prior = steps.get(step.id); + steps.set(step.id, { + sum: (prior?.sum ?? 0) + step.k, + wraps: (prior?.wraps ?? false) || step.wraps, + }); + continue; + } + if (step) refused.add(step.id); + const nested = + direct && + (s.kind === "if" || s.kind === "while" || (s.kind === "block" && !s.labels?.length)); + if (s.kind === "assign" || s.kind === "varDecl" || s.kind === "forOf") refused.add(s.localId); + if (s.kind === "tryCatch" && s.catchLocalId) refused.add(s.catchLocalId); + everyStmtChild( + s, + (e) => (exprWrites(e), true), + (c) => (writes([c], nested), true), + ); + } + }; + const exprWrites = (e: IrExpr): void => { + if (e.kind === "assignExpr" || e.kind === "incDec") refused.add(e.localId); + everyExprChild( + e, + (c) => (exprWrites(c), true), + (c) => (writes([c], false), true), + ); + }; + exprWrites(loop.cond); + writes(loop.body, true); + const counter = (id: string): boolean => + steps.has(id) && !refused.has(id) && host.counterLocal(id); + + type Fact = { bound: IrExpr; key: string; adj: number }; + type Clean = ReadonlyMap; + // The bounds a condition puts on counters while it holds. + const facts = (cond: IrExpr, into: Map): void => { + if (cond.kind === "toBool") return facts(cond.operand, into); + if (cond.kind === "logical" && cond.op === "&&") { + facts(cond.left, into); + facts(cond.right, into); + return; + } + if (cond.kind !== "bin") return; + const below = cond.op === "<" || cond.op === "<=", + above = cond.op === ">" || cond.op === ">="; + const c = below ? cond.left : above ? cond.right : null, + bound = below ? cond.right : cond.left; + if (c?.kind !== "varRef" || !counter(c.localId) || into.has(c.localId)) return; + const key = host.boundKey(bound); + if (key === null) return; + into.set(c.localId, { bound, key, adj: cond.op === "<=" || cond.op === ">=" ? 1 : 0 }); + }; + // The facts that hold after either of two paths. + const meet = (a: Clean | null, b: Clean | null): Clean | null => { + if (a === null) return b; + if (b === null) return a; + const out = new Map(); + for (const [id, fact] of a) if (b.get(id) === fact) out.set(id, fact); + return out; + }; + const withFacts = (clean: Clean, cond: IrExpr): Clean => { + const out = new Map(clean); + facts(cond, out); + return out; + }; + const accesses = new Map(); + const record = (node: Node, index: IrExpr, clean: Clean): void => { + let ref: IrExpr = index, + offset = 0; + if (index.kind === "bin" && (index.op === "+" || index.op === "-")) { + const literal = + index.right.kind === "numLit" ? index.right : index.op === "+" ? index.left : null; + const other = literal === index.right ? index.left : index.right; + if (literal?.kind !== "numLit" || !Number.isSafeInteger(literal.value)) return; + if (Math.abs(literal.value) > MAX_OFFSET || Object.is(literal.value, -0)) return; + offset = index.op === "-" ? -literal.value : literal.value; + ref = other; + } + if (ref.kind !== "varRef") return; + const fact = clean.get(ref.localId); + if (fact) accesses.set(node, { counter: ref.localId, index, ref, ...fact, offset }); + }; + const expr = (e: IrExpr, clean: Clean): void => { + if (e.kind === "bytesIntrinsic") { + const numeric = byteNumberAccess(e); + const index = numeric + ? e.args[numeric.offsetArg] + : e.method === "get" + ? e.args[0] + : undefined; + if (index) record(e, index, clean); + } + if (e.kind === "logical" && e.op === "&&") { + expr(e.left, clean); + expr(e.right, withFacts(clean, e.left)); + return; + } + if (e.kind === "ternary") { + expr(e.cond, clean); + expr(e.then, withFacts(clean, e.cond)); + expr(e.else_, clean); + return; + } + everyExprChild( + e, + (c) => (expr(c, clean), true), + (c) => (stmts([c], clean), true), + ); + }; + // Walks statements in order; the result is the clean state where the + // list falls through, or null when it never does. + const stmts = (list: readonly IrStmt[], clean: Clean): Clean | null => { + let current: Clean | null = clean; + for (const s of list) { + if (current === null) return null; + const step = counterStep(s); + if (step) { + const next: Map = new Map(current); + next.delete(step.id); + current = next; + continue; + } + switch (s.kind) { + case "if": { + expr(s.cond, current); + const then = stmts(s.then, withFacts(current, s.cond)); + const otherwise = stmts(s.else_ ?? [], current); + current = meet(then, otherwise); + break; + } + case "while": { + // A pass of the nested loop may follow its own increments. + const advanced: Map = new Map(current); + for (const id of writtenCounters(s.body)) advanced.delete(id); + expr(s.cond, advanced); + stmts(s.body, withFacts(advanced, s.cond)); + current = advanced; + break; + } + case "block": + if (!s.labels?.length) { + current = stmts(s.body, current); + break; + } + everyStmtChild( + s, + (e) => (expr(e, current!), true), + (c) => (stmts([c], current!), true), + ); + break; + case "bytesSet": + record(s, s.index, current); + everyStmtChild( + s, + (e) => (expr(e, current!), true), + (c) => (stmts([c], current!), true), + ); + break; + case "break": + case "continue": + case "return": + case "throw": + everyStmtChild( + s, + (e) => (expr(e, current!), true), + (c) => (stmts([c], current!), true), + ); + current = null; + break; + default: + everyStmtChild( + s, + (e) => (expr(e, current!), true), + (c) => (stmts([c], current!), true), + ); + break; + } + } + return current; + }; + const writtenCounters = (list: readonly IrStmt[]): Set => { + const out = new Set(); + const visit = (s: IrStmt): boolean => { + const step = counterStep(s); + if (step) out.add(step.id); + return everyStmtChild(s, () => true, visit); + }; + list.every(visit); + return out; + }; + expr(loop.cond, new Map()); + stmts(loop.body, withFacts(new Map(), loop.cond)); + + // Each counter's bounds: the loop's and those of nested while loops that + // advance it. + const counters = new Map(); + const loops: (IrStmt & { kind: "while" })[] = [loop]; + const collectLoops = (s: IrStmt): boolean => { + if (s.kind === "while") loops.push(s); + return everyStmtChild(s, () => true, collectLoops); + }; + loop.body.every(collectLoops); + for (const [id, step] of steps) { + if (!counter(id)) continue; + let bounds: WhileCounter["bounds"] = []; + for (const l of loops) { + if (l !== loop && !writtenCounters(l.body).has(id)) continue; + const fact = withFacts(new Map(), l.cond).get(id); + if (!fact) { + bounds = null; + break; + } + bounds.push({ bound: fact.bound, adj: fact.adj }); + } + counters.set(id, { ...step, bounds }); + } + return { accesses, counters }; +} + /** Branches to `fast` when every access the version proves is in bounds for * every iteration and every view it proves thread-owned has no shared * storage, and to `slow` otherwise (including a counted loop whose @@ -522,6 +987,39 @@ export function emitBytesLoopVersionGuard( B.line(`${ok} = icmp ugt i64 ${lengths[check.receiver]}, ${check.end}`); conditions.push(ok); } + for (const counter of version.counters) { + const value = host.emitExpr(counter.ref); + // A counter held as an integer (an int32 slot) is exact already and + // needs at most its lower bound tested. + const integer = exactInteger(host, value, counter.ref); + if (integer && integer.range.max <= SAFE) { + if (integer.range.min < counter.min) { + const ok = B.tmp(); + B.line(`${ok} = icmp sge i64 ${widenInteger(host, integer)}, ${counter.min}`); + conditions.push(ok); + } + } else conditions.push(emitExactIntegerTest(host, value.name, counter.min, SAFE).ok); + for (const wrap of counter.wrapBounds) { + const bound = host.emitExpr(wrap.bound); + const boundInteger = exactInteger(host, bound, wrap.bound); + if (boundInteger && boundInteger.range.max <= wrap.limit) continue; + const ok = B.tmp(); + B.line(`${ok} = fcmp ole double ${bound.name}, ${f64Lit(wrap.limit)}`); + conditions.push(ok); + } + } + for (const check of version.bounded) { + // `bound <= length - slack` in doubles: both sides are exact (a length + // is below 2^53 and a slack below 2^32), and NaN fails. + const bound = host.emitExpr(check.bound).name; + const length = B.tmp(), + room = B.tmp(), + ok = B.tmp(); + B.line(`${length} = uitofp i64 ${lengths[check.receiver]} to double`); + B.line(`${room} = fsub double ${length}, ${f64Lit(check.slack)}`); + B.line(`${ok} = fcmp ole double ${bound}, ${room}`); + conditions.push(ok); + } if (counted && version.affine.length > 0) { // The first and last counter values, in ascending order. The last value // of a non-unit step is exact: `start + ⌊(limit ∓ 1 - start) / step⌋ * @@ -567,6 +1065,14 @@ export function emitBytesLoopVersionGuard( let base = "0"; for (const term of check.terms) { const value = host.emitExpr(term.expr); + if (version.guarded.has(term.key)) { + const tested = emitExactIntegerTest(host, value.name, GUARDED_TERM.min, GUARDED_TERM.max); + conditions.push(tested.ok); + const sum = B.tmp(); + B.line(`${sum} = ${term.sign > 0 ? "add" : "sub"} i64 ${base}, ${tested.integer}`); + base = sum; + continue; + } const integer = exactInteger(host, value, term.expr); if (!integer) { // Unreachable: every term has a proven range. Stay checked. @@ -614,6 +1120,39 @@ export function emitBytesLoopVersionGuard( else B.condBr(valid, fast, slow); } +/** Tests that a double is an exact integer within [min, max] (not -0; NaN + * fails the ordered comparisons) and gives its i64. The integer is frozen, + * so it is a fixed value even when the test fails. */ +function emitExactIntegerTest( + host: LlvmEmitterContext, + value: string, + min: number, + max: number, +): { ok: string; integer: string } { + const B = host.B; + const low = B.tmp(), + high = B.tmp(), + inRange = B.tmp(), + raw = B.tmp(), + integer = B.tmp(), + back = B.tmp(), + bits = B.tmp(), + backBits = B.tmp(), + exact = B.tmp(), + ok = B.tmp(); + B.line(`${low} = fcmp oge double ${value}, ${f64Lit(min)}`); + B.line(`${high} = fcmp ole double ${value}, ${f64Lit(max)}`); + B.line(`${inRange} = and i1 ${low}, ${high}`); + B.line(`${raw} = fptosi double ${value} to i64`); + B.line(`${integer} = freeze i64 ${raw}`); + B.line(`${back} = sitofp i64 ${integer} to double`); + B.line(`${bits} = bitcast double ${value} to i64`); + B.line(`${backBits} = bitcast double ${back} to i64`); + B.line(`${exact} = icmp eq i64 ${bits}, ${backBits}`); + B.line(`${ok} = and i1 ${inRange}, ${exact}`); + return { ok, integer }; +} + function scaleCounter(host: LlvmEmitterContext, value: string, scale: number): string { if (scale === 1) return value; const scaled = host.B.tmp(); diff --git a/packages/compiler/src/backend/llvm/classes.test.ts b/packages/compiler/src/backend/llvm/classes.test.ts index 2d95e942c..f921900a3 100644 --- a/packages/compiler/src/backend/llvm/classes.test.ts +++ b/packages/compiler/src/backend/llvm/classes.test.ts @@ -1,5 +1,14 @@ import { expect, test } from "vitest"; -import { DYN, F64, STRING, VOID, type IrModule } from "../../ir/ir.js"; +import { + DYN, + F64, + STRING, + VOID, + type IrExpr, + type IrFunction, + type IrModule, + type IrType, +} from "../../ir/ir.js"; import { assignCompactLayouts, buildClassGraph, @@ -222,12 +231,13 @@ test("ProgramLifetime classes pack their fields after a 32-bit header", () => { contracts: { programLifetime: ["Node"] }, }; const llvm = emitLlvmModule(mod, { pointerBits: 64 }); - // Fields go widest first, each at the first offset that fits it. Node's - // weight (an int32 slot: nothing stores a fraction) takes the word the - // header shares (24 bytes, not 32); Leaf keeps Node's offsets and puts its - // bool next to Node's (32 bytes, not 48). - expect(llvm).toContain(`%${mangleClassStruct("Node")} = type { i32, i32, ptr, i8 }`); - expect(llvm).toContain(`%${mangleClassStruct("Leaf")} = type { i32, i32, ptr, i8, i8, ptr }`); + // Scalars narrower than 8 bytes go first, then the rest, widest first, + // each at the first offset that fits it. Node's weight (an int32 slot: + // nothing stores a fraction) takes the word the header shares and its + // flag the next one (24 bytes, not 32); Leaf keeps Node's offsets and puts + // its bool next to Node's (32 bytes, not 48). + expect(llvm).toContain(`%${mangleClassStruct("Node")} = type { i32, i32, i8, ptr }`); + expect(llvm).toContain(`%${mangleClassStruct("Leaf")} = type { i32, i32, i8, i8, ptr, ptr }`); // Without the contract, the layout is the declared one. const mortal = emitLlvmModule({ ...mod, contracts: {} }, { pointerBits: 64 }); expect(mortal).toContain(`%${mangleClassStruct("Leaf")} = type { i64, ptr, i8, i32, ptr, i8 }`); @@ -332,6 +342,99 @@ test("references between ProgramLifetime instances are 32-bit indexes into their expect(classFieldIndex(leafMeta, "label").compressed).toBe(false); }); +test("arrays of ProgramLifetime instances keep 32-bit references in 4-byte slots", () => { + const node = { kind: "object" as const, className: "Node" }; + const nodes = { kind: "array" as const, elem: node }; + const ref = (localId: string, type: IrType): IrExpr => ({ kind: "varRef", localId, type, loc }); + const fn = (name: string, body: IrExpr, returnType: IrType): IrFunction => ({ + name, + params: [ + { name: "a", localId: "a", type: nodes }, + { name: "v", localId: "v", type: node }, + ], + returnType, + locals: [ + { id: "a", name: "a", type: nodes, mutable: false }, + { id: "v", name: "v", type: node, mutable: false }, + ], + body: [{ kind: "return", value: body, loc }], + loc, + }); + const mod: IrModule = { + irVersion: 17, + sourceFile: loc.file, + entry: "main", + functions: [ + { name: "main", params: [], returnType: VOID, locals: [], body: [], loc }, + fn( + "first", + { + kind: "arrayGet", + arr: ref("a", nodes), + index: { kind: "numLit", value: 0, type: F64, loc }, + type: node, + loc, + }, + node, + ), + fn( + "add", + { + kind: "arrIntrinsic", + method: "push", + receiver: ref("a", nodes), + args: [ref("v", node)], + type: F64, + loc, + }, + F64, + ), + fn("make", { kind: "arrayLit", elems: [ref("v", node)], type: nodes, loc }, nodes), + fn( + "find", + { + kind: "arrIntrinsic", + method: "indexOf", + receiver: ref("a", nodes), + args: [ref("v", node)], + type: F64, + loc, + }, + F64, + ), + ], + classes: [{ name: "Node", fields: [{ name: "kids", type: nodes }], methods: [], loc }], + contracts: { programLifetime: ["Node"] }, + }; + const body = (llvm: string, name: string): string => + llvm.split(/^define /m).find((f) => f.includes(`; ${name}\n`)) ?? ""; + const llvm = emitLlvmModule(mod, { pointerBits: 64, objectAudit: false, lowRegion: true }); + // The element type's ops say so (SCR_ARR_OPS_COMPACT), so the runtime makes + // the arrays SCR_ELEM_CREF. + expect(llvm).toMatch( + /^@sc_arrops\.\S+\.c = internal global \{ ptr, ptr, ptr, i32, i32 \} \{ [^}]*, i32 0, i32 1 \}$/m, + ); + // A read loads the 32-bit reference and shifts it; an append stores one. + const first = body(llvm, "first"); + expect(first).toMatch(/getelementptr inbounds i32, ptr %\S+, i64 \S+\n\s+%\S+ = load i32/); + // The states follow 4-byte slots. + expect(first).toContain("getelementptr inbounds [5 x i8]"); + expect(first).toMatch(/= inttoptr i64 %\S+ to ptr/); + expect(body(llvm, "add")).toMatch( + /= lshr i64 %\S+, 3\n\s+%\S+ = trunc i64 %\S+ to i32\n\s+store i32/, + ); + // indexOf scans the 4-byte slots for the needle's reference. + expect(llvm).toMatch(/call double @sc_arr_index_of_cref\(ptr %\S+, ptr %\S+\)/); + expect(/^define internal double @sc_arr_index_of_cref[^]*?^}/m.exec(llvm)?.[0]).toContain( + "load i32, ptr %qp0", + ); + // Elsewhere the arrays keep 8-byte slots. + const elsewhere = emitLlvmModule(mod, { pointerBits: 64, objectAudit: false }); + expect(elsewhere).not.toMatch(/sc_arrops\.\S+\.c /); + expect(body(elsewhere, "first")).not.toContain("inttoptr"); + expect(elsewhere).not.toContain("sc_arr_index_of_cref"); +}); + test("a recycled block is cleared from the end of a ProgramLifetime instance's 32-bit header", () => { const BOOL = { kind: "bool" as const }; const mod: IrModule = { diff --git a/packages/compiler/src/backend/llvm/classes.ts b/packages/compiler/src/backend/llvm/classes.ts index 3073bd1bd..8f179828d 100644 --- a/packages/compiler/src/backend/llvm/classes.ts +++ b/packages/compiler/src/backend/llvm/classes.ts @@ -703,11 +703,13 @@ export function classFieldTypes(host: ShapeHost, cls: IrClassDef, int32?: Int32S /** Gives the classes `compact` admits (whole hierarchies: the test is the * root's) the 32-bit header and packed fields. A class keeps its base's - * field offsets, and places each of its own fields, widest first, at the - * first offset after the header that fits it: in a hole another field's - * alignment left, in the base's tail padding, or at the end. So 32-bit and - * smaller fields share 8-byte words. The struct lists the header and the - * fields by offset, which LLVM's layout of the type reproduces (checked). */ + * field offsets, and places each of its own fields at the first offset + * after the header that fits it: in a hole another field's alignment left, + * in the base's tail padding, or at the end. Scalars narrower than 8 bytes + * (flags, ids, counts) go first, widest first, so they share the header's + * cache line; then the other fields, widest first. So 32-bit and smaller + * fields share 8-byte words. The struct lists the header and the fields by + * offset, which LLVM's layout of the type reproduces (checked). */ export function assignCompactLayouts( metaMap: ReadonlyMap, compact: (root: LlClassMeta) => boolean, @@ -751,10 +753,12 @@ export function assignCompactLayouts( ); return COMPACT_REF; }); + const smallScalar = (f: { type: string; field: number; size: number }): number => + f.size < 8 && f.type !== "ptr" && !compressed[f.field] ? 0 : 1; const own = types .map((type, field) => ({ type, field, ...fieldStorage(type) })) .slice(baseCount) - .sort((a, b) => b.size - a.size || a.field - b.field); + .sort((a, b) => smallScalar(a) - smallScalar(b) || b.size - a.size || a.field - b.field); for (const f of own) { const sorted = [...elems].sort((a, b) => a.offset - b.offset); let at = -1; @@ -860,9 +864,9 @@ export function emitClassShapes( nullable?: NullableRefFields, int32?: Int32Slots, ): { typeDefs: string[]; defs: string[] } { - // Whether compact instances live in the program-lifetime region (any - // field holds a 32-bit reference to one; emitter.ts assignCompactLayouts). - const regionRefs = [...metaMap.values()].some((m) => m.compressed.some(Boolean)); + // Whether compact instances live in the program-lifetime region, where + // fields and arrays can refer to them in 32 bits (LlEmitter.compactRegion). + const regionRefs = host.compactRegion?.() === true; const typeDefs: string[] = []; const defs: string[] = []; const emitted = (mod.classes ?? []).filter((c) => !c.runtime); diff --git a/packages/compiler/src/backend/llvm/counted-loops.ts b/packages/compiler/src/backend/llvm/counted-loops.ts index 02943b852..ca2dd6b92 100644 --- a/packages/compiler/src/backend/llvm/counted-loops.ts +++ b/packages/compiler/src/backend/llvm/counted-loops.ts @@ -1,4 +1,9 @@ -import type { IntegerCountedForLoop, IntegerPairedForLoop } from "../../ir/integer-loops.js"; +import type { + IntegerCountedForLoop, + IntegerLoopFollower, + IntegerPairedForLoop, +} from "../../ir/integer-loops.js"; +import { FOLLOWER_GUARD_LIMIT } from "../../ir/integer-loops.js"; import type { IrExpr } from "../../ir/ir.js"; import type { LlvmEmitterContext } from "./expr-context.js"; import { f64Lit } from "./common.js"; @@ -61,9 +66,47 @@ export function emitCountedLoopSafety( ); } if (loop.startGuarded) safe = andSafe(host, safe, emitExactStart(host, loop.start)); + for (const follower of loop.followers) { + if (follower.startGuarded) safe = andSafe(host, safe, emitExactStart(host, follower.start)); + if (follower.tripGuarded) safe = andSafe(host, safe, emitFollowerReach(host, loop, follower)); + } return safe; } +/** `|start| + |step| * span / |driver step| <= 2^52` for a follower, where + * `span` is the distance from the driver's start to the limit. The body runs + * at most `span / |driver step| + 1` times, so every value the follower holds + * on a body entry is within the bound. NaN and infinities fail the ordered + * test. */ +function emitFollowerReach( + host: LlvmEmitterContext, + loop: IntegerCountedForLoop, + follower: IntegerLoopFollower, +): string { + const B = host.B; + const start = host.emitExpr(loop.start), + limit = host.emitExpr(loop.limit), + from = host.emitExpr(follower.start); + const span = B.tmp(), + reach = B.tmp(), + magnitude = B.tmp(), + total = B.tmp(), + ok = B.tmp(); + B.line( + loop.step > 0 + ? `${span} = fsub double ${limit.name}, ${start.name}` + : `${span} = fsub double ${start.name}, ${limit.name}`, + ); + B.line( + `${reach} = fmul double ${span}, ${f64Lit(Math.abs(follower.step) / Math.abs(loop.step))}`, + ); + host.declare(`declare double @llvm.fabs.f64(double)`); + B.line(`${magnitude} = call double @llvm.fabs.f64(double ${from.name})`); + B.line(`${total} = fadd double ${magnitude}, ${reach}`); + B.line(`${ok} = fcmp ole double ${total}, ${f64Lit(FOLLOWER_GUARD_LIMIT)}`); + return ok; +} + /** The run-time test of a guarded paired loop: each unproven start is an * exact safe integer. */ export function emitPairedLoopSafety(host: LlvmEmitterContext, loop: IntegerPairedForLoop): string { diff --git a/packages/compiler/src/backend/llvm/dense-array-access.ts b/packages/compiler/src/backend/llvm/dense-array-access.ts index 4488f90cb..0ea014059 100644 --- a/packages/compiler/src/backend/llvm/dense-array-access.ts +++ b/packages/compiler/src/backend/llvm/dense-array-access.ts @@ -27,6 +27,8 @@ import { storeArrCount, } from "./common.js"; import type { LlValue, LlvmEmitterContext } from "./expr-context.js"; +import { compactRefLoadLines, compactRefStoreLines } from "./classes.js"; +import { elemAccess } from "./shapes.js"; import { exactInteger, widenInteger } from "./integer-values.js"; const SCR_ARR_VALUE = 1; @@ -209,26 +211,40 @@ function statePointer( return statePtr; } -/** The dense slot type: boolean arrays store one byte per element, every - * other element kind an 8-byte slot. */ -function slotType(acc: ArrayAccess): "i8" | "i64" { - return acc === "bool" ? "i8" : "i64"; +/** How an array's dense slots hold its elements: the access kind, or + * `cref` for references to compact class instances (SCR_ELEM_CREF: a 4-byte + * slot holding the address over 8; LlEmitter.compactArrayElem). */ +type SlotKind = ArrayAccess | "cref"; + +function slotKind(host: LlvmEmitterContext, acc: ArrayAccess, element: IrType): SlotKind { + return acc === "ref" && host.compactArrayElem(element) ? "cref" : acc; +} + +/** The dense slot type: boolean arrays store one byte per element, compact + * references four, every other element kind an 8-byte slot. */ +function slotType(kind: SlotKind): "i8" | "i32" | "i64" { + return kind === "bool" ? "i8" : kind === "cref" ? "i32" : "i64"; } /** Bytes per dense slot (scr_arr_slot_width). */ -export function slotWidth(acc: ArrayAccess): number { - return acc === "bool" ? 1 : 8; +function slotWidth(kind: SlotKind): number { + return kind === "bool" ? 1 : kind === "cref" ? 4 : 8; +} + +/** Bytes per dense slot of an array of `element` (scr_arr_slot_width). */ +export function denseSlotWidth(host: LlvmEmitterContext, element: IrType): number { + return slotWidth(slotKind(host, elemAccess(element), element)); } function valuePointer( host: LlvmEmitterContext, guard: Pick, - acc: ArrayAccess, + kind: SlotKind, ): string { const B = host.B; const valuePtr = B.tmp(); B.line( - `${valuePtr} = getelementptr inbounds ${slotType(acc)}, ptr ${guard.data}, ${host.sizeType} ${guard.offset}`, + `${valuePtr} = getelementptr inbounds ${slotType(kind)}, ptr ${guard.data}, ${host.sizeType} ${guard.offset}`, ); host.markMemoryPointer(valuePtr, "array:elements"); return valuePtr; @@ -300,6 +316,7 @@ export function emitDenseArrayGet( const B = host.B; const ty = acc === "f64" ? "double" : acc === "bool" ? "i1" : "ptr"; const ret = acc === "bool" ? "zeroext i1" : ty; + const kind = slotKind(host, acc, element); host.declare(`declare ${ret} @${getter}(ptr, double)`); if (acc === "ref" && retain && !isRefCounted(element)) { const t = B.tmp(); @@ -322,7 +339,7 @@ export function emitDenseArrayGet( slow, true, valueLabel, - slotWidth(acc), + slotWidth(kind), proven, induction, ); @@ -333,7 +350,7 @@ export function emitDenseArrayGet( B.condBr(present, valueLabel, slow); } B.startBlock(valueLabel); - const ptr = valuePointer(host, guard, acc); + const ptr = valuePointer(host, guard, kind); let fast: string; let fastLabel = valueLabel; if (acc === "f64") { @@ -347,7 +364,13 @@ export function emitDenseArrayGet( } else { const raw = B.tmp(), nonnull = B.tmp(); - B.line(`${raw} = load ptr, ptr ${ptr}${host.fieldAliasAttachment(ptr)}`); + if (kind === "cref") { + const ref = B.tmp(); + B.line(`${ref} = load i32, ptr ${ptr}${host.fieldAliasAttachment(ptr)}`); + for (const line of compactRefLoadLines(ref, raw, `${raw}.r`)) B.line(line.trimStart()); + } else { + B.line(`${raw} = load ptr, ptr ${ptr}${host.fieldAliasAttachment(ptr)}`); + } B.line(`${nonnull} = icmp ne ptr ${raw}, null`); fastLabel = B.newLabel("arr.get.ref"); B.condBr(nonnull, fastLabel, slow); @@ -448,6 +471,7 @@ export function emitDenseArraySet( ): void { const B = host.B; const ty = acc === "f64" ? "double" : acc === "bool" ? "i1" : "ptr"; + const kind = slotKind(host, acc, element); const setter = `scr_arr_set_${acc}`; host.declare(`declare void @${setter}(ptr, double, ${acc === "bool" ? "i1 zeroext" : ty})`); const call = (attrs: string): void => @@ -467,7 +491,7 @@ export function emitDenseArraySet( slow, false, filledLabel, - slotWidth(acc), + slotWidth(kind), proven, ); const { offset, len } = guard; @@ -478,15 +502,20 @@ export function emitDenseArraySet( const bits = B.tmp(); B.line(`${bits} = zext i1 ${value} to i8`); B.line(`store i8 ${bits}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`); + } else if (kind === "cref") { + const ref = B.tmp(); + for (const line of compactRefStoreLines(value, ref, `${ref}.r`)) B.line(line.trimStart()); + B.line(`store i32 ${ref}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`); } else { B.line(`store ptr ${value}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`); } }; const statePtr = statePointer(host, guard); - const valuePtr = valuePointer(host, guard, acc); + const valuePtr = valuePointer(host, guard, kind); let oldState: string | null = null; let old: string | null = null; - if (acc === "ref") { + // Compact instances are never released: a replaced one needs nothing. + if (kind === "ref") { oldState = B.tmp(); old = B.tmp(); B.line(`${oldState} = load i8, ptr ${statePtr}${host.fieldAliasAttachment(statePtr)}`); @@ -537,7 +566,7 @@ export function emitDenseArraySet( storeArrCount(host, nextLen, lenPtr, host.fieldAliasAttachment(lenPtr)); B.br(storedLabel); B.startBlock(storedLabel); - if (acc === "ref" && oldState && old) { + if (kind === "ref" && oldState && old) { const hadValue = B.tmp(); B.line(`${hadValue} = icmp eq i8 ${oldState}, ${SCR_ARR_VALUE}`); const releaseLabel = B.newLabel("arr.set.release"); @@ -549,9 +578,9 @@ export function emitDenseArraySet( // Below the bound the slot holds a present value: replace it, leaving // the state alone, and release a replaced reference after the store. B.startBlock(filledLabel); - const filledValuePtr = valuePointer(host, guard, acc); + const filledValuePtr = valuePointer(host, guard, kind); let filledOld: string | null = null; - if (acc === "ref") { + if (kind === "ref") { filledOld = B.tmp(); B.line( `${filledOld} = load ptr, ptr ${filledValuePtr}${host.fieldAliasAttachment(filledValuePtr)}`, @@ -578,10 +607,12 @@ export function emitDenseArrayPop( host: LlvmEmitterContext, array: string, acc: ArrayAccess, + element: IrType, empty: string, slow: string, ): string { const B = host.B; + const kind = slotKind(host, acc, element); const size = host.sizeType; const header = (field: number): string => { const ptr = B.tmp(); @@ -615,16 +646,23 @@ export function emitDenseArrayPop( states = B.tmp(); B.line(`${data} = load ptr, ptr ${dataPtr}${host.fieldAliasAttachment(dataPtr)}`); loadArrCount(host, cap, capPtr, host.fieldAliasAttachment(capPtr)); - scrArrStates(host, states, data, cap, slotWidth(acc)); - const valuePtr = valuePointer(host, { offset: last, data }, acc); + scrArrStates(host, states, data, cap, slotWidth(kind)); + const valuePtr = valuePointer(host, { offset: last, data }, kind); const statePtr = statePointer(host, { offset: last, states }); - const slotTy = slotType(acc); + const slotTy = slotType(kind); let raw = B.tmp(); B.line(`${raw} = load ${slotTy}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`); if (slotTy === "i8") { const wide = B.tmp(); B.line(`${wide} = zext i8 ${raw} to i64`); raw = wide; + } else if (slotTy === "i32") { + // The 8-byte slot of a compact reference is its address. + const wide = B.tmp(), + address = B.tmp(); + B.line(`${wide} = zext i32 ${raw} to i64`); + B.line(`${address} = shl nuw nsw i64 ${wide}, 3`); + raw = address; } B.line(`store ${slotTy} 0, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`); B.line(`store i8 0, ptr ${statePtr}${host.fieldAliasAttachment(statePtr)}`); @@ -648,10 +686,12 @@ export function emitDenseArrayPush( host: LlvmEmitterContext, array: string, acc: ArrayAccess, + element: IrType | null, value: string, ): string { const B = host.B; const size = host.sizeType; + const kind = element === null ? acc : slotKind(host, acc, element); const ty = acc === "f64" ? "double" : acc === "bool" ? "i1" : "ptr"; const pusher = `scr_arr_push_${acc}`; host.declare(`declare double @${pusher}(ptr, ${acc === "bool" ? "i1 zeroext" : ty})`); @@ -685,16 +725,20 @@ export function emitDenseArrayPush( B.line(`${spare} = sub nuw ${size} ${cap}, ${len}`); B.line(`${near} = getelementptr inbounds i8, ptr ${data}, ${size} ${spare}`); B.line( - `${statePtr} = getelementptr inbounds [${slotWidth(acc)} x i8], ptr ${near}, ${size} ${cap}, ${size} -1`, + `${statePtr} = getelementptr inbounds [${slotWidth(kind)} x i8], ptr ${near}, ${size} ${cap}, ${size} -1`, ); host.markMemoryPointer(statePtr, "array:present"); - const valuePtr = valuePointer(host, { offset: len, data }, acc); + const valuePtr = valuePointer(host, { offset: len, data }, kind); if (acc === "f64") { B.line(`store double ${value}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`); } else if (acc === "bool") { const bits = B.tmp(); B.line(`${bits} = zext i1 ${value} to i8`); B.line(`store i8 ${bits}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`); + } else if (kind === "cref") { + const ref = B.tmp(); + for (const line of compactRefStoreLines(value, ref, `${ref}.r`)) B.line(line.trimStart()); + B.line(`store i32 ${ref}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`); } else { B.line(`store ptr ${value}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`); } diff --git a/packages/compiler/src/backend/llvm/dense-array-loops.test.ts b/packages/compiler/src/backend/llvm/dense-array-loops.test.ts index 405ad67e7..f7a4bc892 100644 --- a/packages/compiler/src/backend/llvm/dense-array-loops.test.ts +++ b/packages/compiler/src/backend/llvm/dense-array-loops.test.ts @@ -1,5 +1,6 @@ import { expect, test } from "vitest"; import { BOOL, F64, arrayOf, type IrExpr, type IrLocal, type IrStmt } from "../../ir/ir.js"; +import type { IntegerCountedForLoop } from "../../ir/integer-loops.js"; import { countedDenseShape, matchDenseArrayLoop, @@ -111,16 +112,24 @@ test("loops that may resize, rebind or move storage keep the ordinary copy only" expect([...rebound!.arrays.keys()]).toEqual(["a"]); }); +const counted = ( + fields: Pick, +): IntegerCountedForLoop => ({ + localId: "i", + guarded: false, + limitGuarded: false, + startGuarded: false, + guardLimit: 0, + followers: [], + ...fields, +}); + test("counted shapes need a non-negative counter strictly below the length", () => { const ascending = (limit: IrExpr, inclusive: boolean, min = 0) => - countedDenseShape(loop([]), { - localId: "i", - start: num(min), - limit, - step: 1, - inclusive, - range: { min, max: 100 }, - }); + countedDenseShape( + loop([]), + counted({ start: num(min), limit, step: 1, inclusive, range: { min, max: 100 } }), + ); const minus = (offset: number): IrExpr => ({ kind: "bin", op: "-", @@ -129,19 +138,58 @@ test("counted shapes need a non-negative counter strictly below the length", () type: F64, loc, }); - expect(ascending(method("a", "length"), false)?.length.localId).toBe("a"); - expect(ascending(minus(1), true)?.length.localId).toBe("a"); + expect(ascending(method("a", "length"), false)?.length?.localId).toBe("a"); + expect(ascending(minus(1), true)?.length?.localId).toBe("a"); expect(ascending(method("a", "length"), true)).toBeNull(); expect(ascending(minus(1), false, -1)).toBeNull(); + // Another invariant limit bounds the counter itself; the entry test + // compares it with every indexed array's hole-free prefix. + const limited = ascending(ref("n"), false); + expect(limited?.length).toBeNull(); + expect(limited?.limit?.limit).toEqual(ref("n")); + expect(ascending(ref("n"), false, -1)).toBeNull(); const descending = (offset: number) => - countedDenseShape(loop([]), { - localId: "i", - start: minus(offset), - limit: num(0), - step: -1, - inclusive: true, - range: { min: 0, max: 100 }, - }); - expect(descending(1)?.length.localId).toBe("a"); + countedDenseShape( + loop([]), + counted({ + start: minus(offset), + limit: num(0), + step: -1, + inclusive: true, + range: { min: 0, max: 100 }, + }), + ); + expect(descending(1)?.length?.localId).toBe("a"); expect(descending(0)).toBeNull(); }); + +test("a loop to another invariant limit covers each array whose length it keeps", () => { + const limited: DenseLoopShape = { + counter: "i", + length: null, + limit: counted({ + start: num(0), + limit: ref("n"), + step: 1, + inclusive: false, + range: { min: 0, max: 100 }, + }), + range: { min: 0, max: 100 }, + }; + const matchLimited = (body: IrStmt[], unaliased = new Set()) => + matchDenseArrayLoop(loop(body), limited, locals, unaliased, new Set(["a", "b"])); + const copy = write("b", read("a")); + const dense = matchLimited([copy]); + expect([...dense!.arrays.keys()].sort()).toEqual(["a", "b"]); + expect(dense!.length).toBeNull(); + expect(dense!.limit?.limit).toEqual(ref("n")); + // An array the loop may resize is not covered; neither is one it may + // share storage with. + const resizing = [ + { kind: "exprStmt" as const, expr: read("a"), loc }, + { kind: "exprStmt" as const, expr: method("b", "getNumber", [ref("i")]), loc }, + { kind: "exprStmt" as const, expr: method("b", "push", [num(1)]), loc }, + ]; + expect([...matchLimited(resizing, new Set(["b"]))!.arrays.keys()]).toEqual(["a"]); + expect(matchLimited(resizing)).toBeNull(); +}); diff --git a/packages/compiler/src/backend/llvm/dense-array-loops.ts b/packages/compiler/src/backend/llvm/dense-array-loops.ts index 708825344..184ff1353 100644 --- a/packages/compiler/src/backend/llvm/dense-array-loops.ts +++ b/packages/compiler/src/backend/llvm/dense-array-loops.ts @@ -15,22 +15,33 @@ * test bounds `a.length` by 2^31 - 1, so the fast copy's counter is an int32. * Arrays longer than that are sparse in practice (a dense one would need * 16 GB of slots) and take the ordinary copy, as does every array with a - * hole, present undefined or sparse storage below `a.length`. */ + * hole, present undefined or sparse storage below `a.length`. + * + * A counted loop with another invariant limit, such as `j < n` after + * `const n = a.length` or `j < 5`, is covered the same way: the test bounds + * the loop's exclusive limit by 2^31 - 1 and by each indexed array's + * `filled`, and the loop must provably never change any of their lengths. */ import type { IrExpr, IrLocal, IrStmt } from "../../ir/ir.js"; import type { IntegerRange } from "../../ir/integer-ranges.js"; import { preservesArrayLength } from "../../ir/array-length-stability.js"; import { everyExprChild, everyStmtChild } from "../../ir/traverse.js"; +import type { IntegerCountedForLoop } from "../../ir/integer-loops.js"; import type { LlvmEmitterContext } from "./expr-context.js"; import { loadArrCount, SCR_ARR_FIELD } from "./common.js"; +import { emitCountedLoopLimit } from "./counted-loops.js"; export const INT32_MAX = 2147483647; export interface DenseArrayLoop { /** The loop's counter binding. */ counter: string; - /** The array whose length bounds the counter. */ - length: IrExpr & { kind: "varRef" }; + /** The array whose length bounds the counter, or null for a counted loop + * bounded by `limit`. */ + length: (IrExpr & { kind: "varRef" }) | null; + /** The ascending counted loop whose invariant limit bounds the counter, + * when `length` is null. */ + limit: IntegerCountedForLoop | null; /** Arrays whose accesses at the counter are proven to be inside the * hole-free prefix in the fast copy, with a read of each binding. */ arrays: ReadonlyMap; @@ -39,12 +50,15 @@ export interface DenseArrayLoop { } /** The fast copy duplicates the loop; keep the duplicated code small. */ -const MAX_LOOP_NODES = 120; +const MAX_LOOP_NODES = 160; /** How a matched integer loop bounds its counter below `length.length`. */ export interface DenseLoopShape { counter: string; - length: IrExpr & { kind: "varRef" }; + /** The array whose length bounds the counter, or null for `limit`. */ + length: (IrExpr & { kind: "varRef" }) | null; + /** An ascending counted loop bounded by another invariant limit. */ + limit?: IntegerCountedForLoop; /** The counter's proven range on every pass of the ordinary loop; the * fast copy keeps its minimum, which must be at least 0. */ range: IntegerRange; @@ -73,22 +87,18 @@ function lengthOffset(e: IrExpr): { receiver: IrExpr & { kind: "varRef" }; offse /** The array bounding a counted loop's counter below its length: an * ascending loop compared below `a.length - c`, or a descending one starting - * at `a.length - c` (c >= 1). */ + * at `a.length - c` (c >= 1). Another ascending loop is bounded by its own + * invariant limit. */ export function countedDenseShape( loop: IrStmt & { kind: "for" }, - counted: { - localId: string; - start: IrExpr; - limit: IrExpr; - step: number; - inclusive: boolean; - range: IntegerRange; - }, + counted: IntegerCountedForLoop, ): DenseLoopShape | null { if (counted.range.min < 0) return null; if (counted.step > 0) { const bound = lengthOffset(counted.limit); - if (!bound || bound.offset < (counted.inclusive ? 1 : 0)) return null; + if (!bound) + return { counter: counted.localId, length: null, limit: counted, range: counted.range }; + if (bound.offset < (counted.inclusive ? 1 : 0)) return null; return { counter: counted.localId, length: bound.receiver, range: counted.range }; } // The start is evaluated once, before the loop; the counter only falls. @@ -143,9 +153,11 @@ export function matchDenseArrayLoop( return everyStmtChild(s, expr, stmt); }; if (!loop.body.every(stmt) || !expr(loop.cond) || nested) return null; + const nodesOfLoop = [loop.cond, ...loop.body]; if ( + length !== null && !preservesArrayLength( - [loop.cond, ...loop.body], + nodesOfLoop, { receiver: length.localId, inRange: counter }, locals, unaliased, @@ -161,7 +173,14 @@ export function matchDenseArrayLoop( local.boxed || local.tdz || disturbed.has(id) || - (id !== length.localId && !initialized.has(id)) + (id !== length?.localId && !initialized.has(id)) + ) + continue; + // Without a bounding array, the entry test reads each array's `filled` + // once: the loop must not change any of their lengths. + if ( + length === null && + !preservesArrayLength(nodesOfLoop, { receiver: id, inRange: counter }, locals, unaliased) ) continue; arrays.set(id, ref); @@ -170,13 +189,15 @@ export function matchDenseArrayLoop( return { counter, length, + limit: length === null ? (shape.limit ?? null) : null, arrays, range: { min: shape.range.min, max: Math.min(shape.range.max, INT32_MAX - 1) }, }; } -/** The fast copy's entry test: the bounding length is at most 2^31 - 1 and - * at most every indexed array's hole-free prefix bound. */ +/** The fast copy's entry test: the bounding length (or the counted loop's + * exclusive limit) is at most 2^31 - 1 and at most every indexed array's + * hole-free prefix bound. */ export function emitDenseLoopGuard(host: LlvmEmitterContext, loop: DenseArrayLoop): string { const B = host.B; const size = host.sizeType; @@ -186,7 +207,29 @@ export function emitDenseLoopGuard(host: LlvmEmitterContext, loop: DenseArrayLoo host.markMemoryPointer(ptr, "array:header"); return ptr; }; - const receiver = host.emitStableReceiver(loop.length, []); + if (loop.length === null) { + // A limit at or below the start runs no pass, and passes the test. + const limit = emitCountedLoopLimit(host, loop.limit!); + const small = B.tmp(); + B.line(`${small} = icmp sle i64 ${limit}, ${INT32_MAX}`); + let valid = small; + for (const ref of loop.arrays.values()) { + const array = host.emitStableReceiver(ref, []); + const filledPtr = header(array.name, SCR_ARR_FIELD.filled), + filled = B.tmp(), + wide = B.tmp(), + covered = B.tmp(), + both = B.tmp(); + B.line(`${filled} = load i32, ptr ${filledPtr}${host.fieldAliasAttachment(filledPtr)}`); + B.line(`${wide} = zext i32 ${filled} to i64`); + B.line(`${covered} = icmp sle i64 ${limit}, ${wide}`); + B.line(`${both} = and i1 ${valid}, ${covered}`); + valid = both; + } + return valid; + } + const length = loop.length; + const receiver = host.emitStableReceiver(length, []); const lenPtr = header(receiver.name, SCR_ARR_FIELD.len), len = B.tmp(), small = B.tmp(); @@ -194,7 +237,7 @@ export function emitDenseLoopGuard(host: LlvmEmitterContext, loop: DenseArrayLoo B.line(`${small} = icmp ule ${size} ${len}, ${INT32_MAX}`); let valid = small; for (const [id, ref] of loop.arrays) { - const array = id === loop.length.localId ? receiver : host.emitStableReceiver(ref, []); + const array = id === length.localId ? receiver : host.emitStableReceiver(ref, []); const filledPtr = header(array.name, SCR_ARR_FIELD.filled), filled = B.tmp(), covered = B.tmp(), diff --git a/packages/compiler/src/backend/llvm/dyn.ts b/packages/compiler/src/backend/llvm/dyn.ts index 6911ef98b..5c9d8e60f 100644 --- a/packages/compiler/src/backend/llvm/dyn.ts +++ b/packages/compiler/src/backend/llvm/dyn.ts @@ -108,6 +108,15 @@ const DYN_HANDLE_UNBOX_FN: Record = { dgramSocket: "scr_dgram_from_dyn", }; +/** ScrMessageKind (scr_message.h): the node kinds message readers test. */ +const MESSAGE_KIND = { + UNDEFINED: 0, + NULL: 1, + BOOL: 2, + NUMBER: 3, + STRING: 4, +} as const; + export const DYN_KIND = { NULL: 0, BOOL: 1, @@ -173,6 +182,8 @@ export class LlDyn { private readonly dynFuncThunks = new Map(); private readonly dynFuncBoxes = new Map(); private readonly dynFuncAdapters = new Map(); + private readonly messageListeners = new Map(); + private readonly messageReaders = new Map(); private readonly promiseDynAdapters = new Map(); private readonly mapDynOps = new Map(); private dynToStrFn: string | null = null; @@ -4610,6 +4621,297 @@ export class LlDyn { return name; } + /** `sc_dml_(ptr c, ptr message) -> i1`, the typed message entry of a + * closure registered as a 'message' listener (ScrDynMessageListener): + * builds the closure's parameter from the message's nodes and calls the + * closure. It answers false, without calling it, when the message does + * not have the parameter's exact layout, and the listener then runs + * through its thunk on the decoded message, as before. Null when the + * signature's parameter has no message reader (messageReadable). */ + messageListenerHelper(t: IrType & { kind: "func" }): string | null { + const key = typeKey(t); + const existing = this.messageListeners.get(key); + if (existing !== undefined) return existing; + const param = t.params[0]; + const supported = + t.params.length === 1 && + t.rest !== true && + param !== undefined && + (t.ret.kind === "void" || t.ret.kind === "f64" || t.ret.kind === "bool") && + this.messageReadable(param, new Set()); + if (!supported) { + this.messageListeners.set(key, null); + return null; + } + const name = `sc_dml_${this.messageListeners.size}`; + this.messageListeners.set(key, name); + const B = new BlockBuilder(); + B.entryAllocas.push(`%fail = alloca i1`); + B.line(`store i1 false, ptr %fail`); + const value = B.tmp(); + B.line( + `${value} = call ${this.valTy(param)} @${this.messageReaderHelper(param)}(ptr %m, ${this.host.sizeType} 0, ptr %fail)`, + ); + const failed = B.tmp(); + B.line(`${failed} = load i1, ptr %fail`); + const lMiss = B.newLabel("dml.miss"); + const lCall = B.newLabel("dml.call"); + B.condBr(failed, lMiss, lCall); + B.startBlock(lMiss); + B.terminate(`ret i1 false`); + B.startBlock(lCall); + // The closure consumes its parameter; a pending exception stays for + // the emitter, as after the thunk. + const fnp = B.tmp(); + const fn = B.tmp(); + B.line(`${fnp} = getelementptr inbounds %ScrClosure, ptr %c, i64 0, i32 1`); + B.line(`${fn} = load ptr, ptr ${fnp}`); + const args = `ptr %c, ${this.valTy(param)} ${value}`; + if (t.ret.kind === "void") B.line(`call void ${fn}(${args})`); + else B.line(`${B.tmp()} = call ${this.valTy(t.ret)} ${fn}(${args})`); + B.terminate(`ret i1 true`); + this.defs.push( + `define internal zeroext i1 @${name}(ptr %c, ptr %m) ${FN_ATTRS} { ; message entry for ${llvmCommentText(key)}`, + B.render(), + `}`, + ``, + ); + return name; + } + + /** Whether a message reader can build T: numbers, booleans and strings; + * records of them with only declared fields, none of union type; and + * boxed unions of those arms and null or undefined without a literal + * discriminator, with at most one number, boolean and string arm. */ + private messageReadable(t: IrType, seen: Set): boolean { + switch (t.kind) { + case "f64": + case "bool": + case "string": + return true; + case "record": { + const shape = this.host.recordsById.get(t.shapeId); + if (!shape || shape.tuple || shape.indexValue || shape.fields.length === 0) return false; + if (seen.has(t.shapeId) || this.isRecursiveShape(t)) return false; + seen.add(t.shapeId); + const readable = shape.fields.every( + (f) => + !f.name.startsWith("%") && + f.type.kind !== "union" && + llFieldType(f.type, this.host.inlineUnions.record(f.type)) !== INLINE_UNION_FIELD && + this.messageReadable(f.type, seen), + ); + seen.delete(t.shapeId); + return readable; + } + case "union": { + const def = this.host.unionsById.get(t.unionId); + // A discriminated union's checked conversion also tests the arm's + // literal discriminator, which the readers do not. + if (!def || def.discriminant || this.host.nullableUnions.has(t.unionId)) return false; + const scalars = new Set(); + for (const arm of def.arms) { + if (arm.kind === "nullT" || arm.kind === "undefinedT") continue; + if (arm.kind === "f64" || arm.kind === "bool" || arm.kind === "string") { + if (scalars.has(arm.kind)) return false; + scalars.add(arm.kind); + continue; + } + if (arm.kind !== "record" || !this.messageReadable(arm, seen)) return false; + } + return true; + } + default: + return false; + } + } + + /** `sc_dmr_(ptr message, size node, ptr fail) -> T`: T (+1) built + * from a message node, for a type messageReadable accepts. Sets *fail + * and returns a dummy when the node does not have T's layout: the arm + * the checked conversion would choose, a record's declared keys in + * declaration order and no others. */ + private messageReaderHelper(t: IrType): string { + const key = typeKey(t); + const existing = this.messageReaders.get(key); + if (existing) return existing; + const name = `sc_dmr_${this.messageReaders.size}`; + this.messageReaders.set(key, name); + const host = this.host; + const S = host.sizeType; + if (t.kind !== "record") host.declare(`declare i32 @scr_message_kind(ptr, ${S})`); + const retTy = this.valTy(t); + const dummy = retTy === "double" ? f64Lit(0) : retTy === "i1" ? "false" : "null"; + const B = new BlockBuilder(); + const lFail = B.newLabel("dmr.fail"); + // scr_message_fields tests a record's kind itself. + const kind = B.tmp(); + if (t.kind !== "record") B.line(`${kind} = call i32 @scr_message_kind(ptr %m, ${S} %n)`); + const requireKind = (want: number): void => { + const ok = B.tmp(); + B.line(`${ok} = icmp eq i32 ${kind}, ${want}`); + const lOk = B.newLabel("dmr.kind"); + B.condBr(ok, lOk, lFail); + B.startBlock(lOk); + }; + switch (t.kind) { + case "f64": + case "bool": { + host.declare(`declare double @scr_message_number(ptr, ${S})`); + requireKind(t.kind === "f64" ? MESSAGE_KIND.NUMBER : MESSAGE_KIND.BOOL); + const n = B.tmp(); + B.line(`${n} = call double @scr_message_number(ptr %m, ${S} %n)`); + if (t.kind === "f64") B.terminate(`ret double ${n}`); + else { + const b = B.tmp(); + B.line(`${b} = fcmp une double ${n}, ${f64Lit(0)}`); + B.terminate(`ret i1 ${b}`); + } + break; + } + case "string": { + host.declare(`declare ptr @scr_message_string(ptr, ${S})`); + requireKind(MESSAGE_KIND.STRING); + const str = B.tmp(); + B.line(`${str} = call ptr @scr_message_string(ptr %m, ${S} %n)`); + B.terminate(`ret ptr ${str}`); + break; + } + case "record": { + const shape = host.recordsById.get(t.shapeId)!; + const order = recordSourceFieldOrder(shape); + const fields = new Map(shape.fields.map((f, i) => [f.name, { field: f, index: i }])); + const base = host.nullableUnions.fieldBase(t.shapeId); + const struct = mangleRecordStruct(t.shapeId); + const names = this.recordNameTable(t.shapeId, shape); + host.declare(`declare zeroext i1 @scr_message_fields(ptr, ${S}, ptr, ptr, ${S}, ptr)`); + B.entryAllocas.push(`%kids = alloca [${order.length} x ${S}]`); + const matched = B.tmp(); + B.line( + `${matched} = call zeroext i1 @scr_message_fields(ptr %m, ${S} %n, ptr ${names.names}, ptr ${names.lens}, ${S} ${order.length}, ptr %kids)`, + ); + const lBuild = B.newLabel("dmr.build"); + B.condBr(matched, lBuild, lFail); + B.startBlock(lBuild); + B.line(`%r0 = call ptr @${mangleRecordNew(t.shapeId)}()`); + order.forEach((fieldName, i) => { + const { field, index } = fields.get(fieldName)!; + const kidp = B.tmp(); + const kid = B.tmp(); + B.line( + `${kidp} = getelementptr inbounds [${order.length} x ${S}], ptr %kids, i64 0, i64 ${i}`, + ); + B.line(`${kid} = load ${S}, ptr ${kidp}`); + const v = B.tmp(); + B.line( + `${v} = call ${this.valTy(field.type)} @${this.messageReaderHelper(field.type)}(ptr %m, ${S} ${kid}, ptr %fail)`, + ); + const failed = B.tmp(); + B.line(`${failed} = load i1, ptr %fail`); + const lDrop = B.newLabel("dmr.drop"); + const lStore = B.newLabel("dmr.store"); + B.condBr(failed, lDrop, lStore); + B.startBlock(lDrop); + B.line(`call void ${releaseSym(host, t)}(ptr %r0)`); + B.terminate(`ret ptr null`); + B.startBlock(lStore); + const p = B.tmp(); + B.line( + `${p} = getelementptr inbounds %${struct}, ptr %r0, i64 0, i32 ${index + base} ; .${llvmCommentText(fieldName)}`, + ); + const fieldTy = llFieldType(field.type, host.inlineUnions.record(field.type)); + if (fieldTy === "i8") { + const z = B.tmp(); + B.line(`${z} = zext i1 ${v} to i8`); + B.line(`store i8 ${z}, ptr ${p}`); + } else { + B.line(`store ${fieldTy} ${v}, ptr ${p}`); + } + }); + B.terminate(`ret ptr %r0`); + break; + } + case "union": { + const def = host.unionsById.get(t.unionId)!; + def.arms.forEach((arm, i) => { + const lNext = B.newLabel("dmr.next"); + if (arm.kind === "nullT" || arm.kind === "undefinedT") { + const hit = B.tmp(); + B.line( + `${hit} = icmp eq i32 ${kind}, ${arm.kind === "nullT" ? MESSAGE_KIND.NULL : MESSAGE_KIND.UNDEFINED}`, + ); + const lHit = B.newLabel("dmr.unit"); + B.condBr(hit, lHit, lNext); + B.startBlock(lHit); + B.terminate(`ret ptr ${host.unitInstanceRef(t.unionId, i)}`); + B.startBlock(lNext); + return; + } + if (arm.kind !== "record") { + const want = + arm.kind === "f64" + ? MESSAGE_KIND.NUMBER + : arm.kind === "bool" + ? MESSAGE_KIND.BOOL + : MESSAGE_KIND.STRING; + const hit = B.tmp(); + B.line(`${hit} = icmp eq i32 ${kind}, ${want}`); + const lHit = B.newLabel("dmr.arm"); + B.condBr(hit, lHit, lNext); + B.startBlock(lHit); + } + // A record arm that does not match leaves *fail clear for the + // next arm, like the checked conversion's next-arm match. + const v = B.tmp(); + B.line( + `${v} = call ${this.valTy(arm)} @${this.messageReaderHelper(arm)}(ptr %m, ${S} %n, ptr %fail)`, + ); + if (arm.kind === "record") { + const failed = B.tmp(); + B.line(`${failed} = load i1, ptr %fail`); + const lHit = B.newLabel("dmr.arm"); + const lMiss = B.newLabel("dmr.miss"); + B.condBr(failed, lMiss, lHit); + B.startBlock(lMiss); + B.line(`store i1 false, ptr %fail`); + B.br(lNext); + B.startBlock(lHit); + } + const u = B.tmp(); + if (arm.kind === "f64") { + host.declare(`declare ptr @scr_union_new_f64(i32, double)`); + B.line(`${u} = call ptr @scr_union_new_f64(i32 ${i}, double ${v})`); + } else if (arm.kind === "bool") { + host.declare(`declare ptr @scr_union_new_bool(i32, i1 zeroext)`); + B.line(`${u} = call ptr @scr_union_new_bool(i32 ${i}, i1 ${v})`); + } else { + const rc = vAdapters(host, arm); + host.declare(`declare ptr @scr_union_new_ref(i32, ptr, ptr, ptr, ptr)`); + B.line( + `${u} = call ptr @scr_union_new_ref(i32 ${i}, ptr ${v}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${traceArg(host, arm)})`, + ); + } + B.terminate(`ret ptr ${u}`); + B.startBlock(lNext); + }); + B.br(lFail); + break; + } + default: + throw new InternalCompilerError(`llvm emitter bug: message reader of ${key}`); + } + B.startBlock(lFail); + B.line(`store i1 true, ptr %fail`); + B.terminate(`ret ${retTy} ${dummy}`); + this.defs.push( + `define internal ${retTy === "i1" ? "zeroext i1" : retTy} @${name}(ptr %m, ${S} %n, ptr %fail) ${FN_ATTRS} { ; message reader for ${llvmCommentText(key)}`, + B.render(), + `}`, + ``, + ); + return name; + } + /** The adapter closure body for one TARGET signature: caps[0] is an * untraced obj-box owning the dyn function value. */ dynFuncAdapterHelper(t: IrType & { kind: "func" }): string { diff --git a/packages/compiler/src/backend/llvm/emit-function.ts b/packages/compiler/src/backend/llvm/emit-function.ts index 09e6551c3..135ebc706 100644 --- a/packages/compiler/src/backend/llvm/emit-function.ts +++ b/packages/compiler/src/backend/llvm/emit-function.ts @@ -55,6 +55,7 @@ import { INT32_ABI_SUFFIX, SCALAR_UNION_RESULT } from "./int32-abi.js"; import { f64Lit } from "./common.js"; import { RESULT_ENTRY_PREFIX } from "./record-results.js"; import { emitIntegerClone, emitIntegerDispatcher } from "./integer-clones.js"; +import { runningExtremumUpdates } from "./running-extrema.js"; export function emitFunction(emitter: LlEmitter, fn: IrFunction): string { const B = new BlockBuilder(); @@ -81,8 +82,11 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string { emitter.currentLocals.set(id, plain); emitter.lazyCaptureBoxes.set(id, ""); } + emitter.runningExtrema = emitter.debug === null ? runningExtremumUpdates(fn) : new Set(); emitter.currentConstantCallbacks = emitter.debug === null ? (emitter.constantCallbacks.get(fn.name) ?? new Map()) : new Map(); + emitter.knownCallbacks.clear(); + emitter.callbackVersionsEnabled = emitter.debug === null && !fn.async && !fn.generator; // Preserve concrete source bindings for debugger inspection. Suspended // functions keep the established array lifetime across continuations. emitter.streamingSplitsEnabled = emitter.debug === null && !fn.async && !fn.generator; diff --git a/packages/compiler/src/backend/llvm/emit-module.ts b/packages/compiler/src/backend/llvm/emit-module.ts index 3d9d41072..4a31c03f3 100644 --- a/packages/compiler/src/backend/llvm/emit-module.ts +++ b/packages/compiler/src/backend/llvm/emit-module.ts @@ -35,9 +35,11 @@ import { emitClassMembershipHelper } from "./classes.js"; import { type RegexMatcherCode, compileRegexMatcher } from "./regex-matchers.js"; import { MAP_ITER_DECL, + MAP_SIZE_DECL, MAP_ITER_NUM_DECL, SCR_MAP_NUM_DEAD, NUMBER_MAP_ENTRY_DECL, + STRING_MAP_ENTRY_C_DECL, STRING_MAP_ENTRY_DECL, } from "./map-number-lookup.js"; import { RUNTIME_ABI_MARKER } from "../runtime-abi.js"; @@ -456,15 +458,18 @@ export function emitModuleParts(emitter: LlEmitter): string[] { // RC fast paths test the traced byte (shapes.ts). ...(emitter.rcHelpers === null ? [] : [`%ScrMapRc = type { ${emitter.sizeType}, i8, i8, i8 }`]), // The complete ScrMap and its entries, read by the inline number-key - // lookup (sc_map_entry_f64) and the inline iteration steps - // (sc_map_iter_entry, sc_map_iter_entry_num): field 8 is the i32 entry + // lookup (sc_map_entry_f64), the inline iteration steps + // (sc_map_iter_entry, sc_map_iter_entry_num) and Map.size + // (sc_map_size): field 8 is the i32 entry count, field 9 the i32 live // count, field 10 the i64 absent-key filter, entries is field 11, // nbuckets and buckets fields 13 and 14, the direct index and its i32 // length fields 15 and 16. ...(emitter.decls.has(NUMBER_MAP_ENTRY_DECL) || emitter.decls.has(STRING_MAP_ENTRY_DECL) || + emitter.decls.has(STRING_MAP_ENTRY_C_DECL) || emitter.decls.has(MAP_ITER_DECL) || - emitter.decls.has(MAP_ITER_NUM_DECL) + emitter.decls.has(MAP_ITER_NUM_DECL) || + emitter.decls.has(MAP_SIZE_DECL) ? [ `%ScrMapIx = type { ${emitter.sizeType}, i8, i8, i8, i8, i32, ptr, ptr, i32, i32, i64, ptr, i32, i32, ptr, ptr, i32, i32 }`, ] @@ -479,6 +484,11 @@ export function emitModuleParts(emitter: LlEmitter): string[] { emitter.decls.has(MAP_ITER_NUM_DECL) ? [`%ScrMapEntry = type { i64, i64, i64 }`, `%ScrMapNumEntry = type { i64, i64 }`] : []), + // A compact-value map's { key, hash, val } entry (scr_map.h ScrMapCEntry): + // a 32-bit hash and the value as an address over 8. + ...(emitter.decls.has(STRING_MAP_ENTRY_C_DECL) + ? [`%ScrMapCEntry = type { i64, i32, i32 }`] + : []), // The runtime error prefix { rc, vt, name, message, code, cause } and the // class-object shape { rc, pre, post, ctor, name } — field reads on // builtin errors and classval loads GEP through these. @@ -545,7 +555,7 @@ export function emitModuleParts(emitter: LlEmitter): string[] { ``, `@scr_error_vts = external ${tl}global [${RUNTIME_ERROR_CLASSES.size} x %ScrVt]`, `declare void @scr_init()`, - ...(emitter.usesCompactRefs ? [`declare void @scr_plr_init()`] : []), + ...(emitter.compactRegion ? [`declare void @scr_plr_init()`] : []), `declare void @scr_lib_init(i32, ptr)`, ...(emitter.mod.workers ? [`declare void @scr_runtime_workers_v8()`] : []), ...(emitter.stackChecks?.size ? [`declare void @scr_stack_guard_init()`] : []), @@ -840,7 +850,7 @@ export function emitModuleParts(emitter: LlEmitter): string[] { ` call void @scr_init()`, // Compressed fields hold addresses in the program-lifetime region, which // is reserved below 32 GiB before compiled code runs. - ...(emitter.usesCompactRefs ? [` call void @scr_plr_init()`] : []), + ...(emitter.compactRegion ? [` call void @scr_plr_init()`] : []), // Each context (the main thread, every worker) installs its stack // guard before running compiled code that checks it. ...(emitter.stackChecks?.size ? [` call void @scr_stack_guard_init()`] : []), @@ -1293,6 +1303,18 @@ export function helperDefs(emitter: LlEmitter): string[] { ``, ); } + // Map.size and Set.size read the live entry count (scr_map_size). + if (emitter.decls.has(MAP_SIZE_DECL)) + defs.push( + `define internal double @sc_map_size(ptr %m) ${FN_ATTRS} {`, + `entry:`, + ` %np = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 9`, + ` %n = load i32, ptr %np`, + ` %nf = uitofp i32 %n to double`, + ` ret double %nf`, + `}`, + ``, + ); // String-key Map/Set lookups test the map's absent-key filter inline // (scr_map.c scr_map_filter_bits): a key whose two filter bits are not // both set is absent, so most misses on small and medium maps return @@ -1301,36 +1323,69 @@ export function helperDefs(emitter: LlEmitter): string[] { // and the filter reads its bits 44..55, which are the capacity word's // own. A key without a cached hash, and every possible hit, take the // runtime lookup. - if (emitter.decls.has(STRING_MAP_ENTRY_DECL)) { + // A compact-value map's lookup (sc_map_entry_str_c) tests the same filter + // and calls scr_map_entry_str_c; its iteration step (sc_map_iter_entry_c) + // reads 16-byte entries and their 32-bit hash. + const stringMapEntry = (helper: string, runtime: string): string[] => [ + `define internal ptr @${helper}(ptr %m, ptr %k) ${FN_ATTRS} {`, + `entry:`, + ...(emitter.sizeType === "i64" + ? [ + ` %fp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 10`, + ` %filter = load i64, ptr %fp`, + ` %capp = getelementptr inbounds %ScrStr, ptr %k, i32 0, i32 2`, + ` %cap = load i64, ptr %capp`, + ` %cached = icmp uge i64 %cap, 4294967296`, + ` br i1 %cached, label %test, label %slow`, + `test:`, + ` %s1 = lshr i64 %cap, 44`, + ` %i1 = and i64 %s1, 63`, + ` %b1 = shl i64 1, %i1`, + ` %s2 = lshr i64 %cap, 50`, + ` %i2 = and i64 %s2, 63`, + ` %b2 = shl i64 1, %i2`, + ` %bits = or i64 %b1, %b2`, + ` %have = and i64 %filter, %bits`, + ` %maybe = icmp eq i64 %have, %bits`, + ` br i1 %maybe, label %slow, label %absent`, + `absent:`, + ` ret ptr null`, + ] + : [` br label %slow`]), + `slow:`, + ` %r = call ptr @${runtime}(ptr %m, ptr %k)`, + ` ret ptr %r`, + `}`, + ``, + ]; + if (emitter.decls.has(STRING_MAP_ENTRY_DECL)) + defs.push(...stringMapEntry("sc_map_entry_str", "scr_map_entry_str")); + if (emitter.decls.has(STRING_MAP_ENTRY_C_DECL)) { + const sz = emitter.sizeType; defs.push( - `define internal ptr @sc_map_entry_str(ptr %m, ptr %k) ${FN_ATTRS} {`, + ...stringMapEntry("sc_map_entry_str_c", "scr_map_entry_str_c"), + `define internal ptr @sc_map_iter_entry_c(ptr %m, double %i) ${FN_ATTRS} {`, `entry:`, - ...(emitter.sizeType === "i64" - ? [ - ` %fp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 10`, - ` %filter = load i64, ptr %fp`, - ` %capp = getelementptr inbounds %ScrStr, ptr %k, i32 0, i32 2`, - ` %cap = load i64, ptr %capp`, - ` %cached = icmp uge i64 %cap, 4294967296`, - ` br i1 %cached, label %test, label %slow`, - `test:`, - ` %s1 = lshr i64 %cap, 44`, - ` %i1 = and i64 %s1, 63`, - ` %b1 = shl i64 1, %i1`, - ` %s2 = lshr i64 %cap, 50`, - ` %i2 = and i64 %s2, 63`, - ` %b2 = shl i64 1, %i2`, - ` %bits = or i64 %b1, %b2`, - ` %have = and i64 %filter, %bits`, - ` %maybe = icmp eq i64 %have, %bits`, - ` br i1 %maybe, label %slow, label %absent`, - `absent:`, - ` ret ptr null`, - ] - : [` br label %slow`]), - `slow:`, - ` %r = call ptr @scr_map_entry_str(ptr %m, ptr %k)`, - ` ret ptr %r`, + ` %np = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 8`, + ` %n = load i32, ptr %np`, + ` %nf = uitofp i32 %n to double`, + ` %lo = fcmp oge double %i, 0.0`, + ` %hi = fcmp olt double %i, %nf`, + ` %in = and i1 %lo, %hi`, + ` br i1 %in, label %load, label %none`, + `load:`, + ` %ix = fptoui double %i to ${sz}`, + ` %ep = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 11`, + ` %entries = load ptr, ptr %ep`, + ` %e = getelementptr inbounds %ScrMapCEntry, ptr %entries, ${sz} %ix`, + ` %hp = getelementptr inbounds %ScrMapCEntry, ptr %e, i32 0, i32 1`, + ` %h = load i32, ptr %hp`, + ` %live = icmp ne i32 %h, 0`, + ` br i1 %live, label %yes, label %none`, + `yes:`, + ` ret ptr %e`, + `none:`, + ` ret ptr null`, `}`, ``, ); diff --git a/packages/compiler/src/backend/llvm/emit-statements.ts b/packages/compiler/src/backend/llvm/emit-statements.ts index 7239db7d8..39ff5dcd3 100644 --- a/packages/compiler/src/backend/llvm/emit-statements.ts +++ b/packages/compiler/src/backend/llvm/emit-statements.ts @@ -51,10 +51,11 @@ import { emitCountedLoopSafety, emitPairedLoopSafety, } from "./counted-loops.js"; -import { emitBytesLoopVersionGuard } from "./bytes-loop-versions.js"; +import { emitBytesLoopVersionGuard, type BytesLoopVersion } from "./bytes-loop-versions.js"; import { mangleLocal } from "../mangle.js"; import { LlvmUnsupportedError } from "./unsupported.js"; import type { LlValue } from "./expr-context.js"; +import { everyExprChild, everyStmtChild } from "../../ir/traverse.js"; import { widenInteger } from "./integer-values.js"; import { SCALAR_UNION_RESULT } from "./int32-abi.js"; import { emitLiteralSwitch } from "./switch-dispatch.js"; @@ -76,6 +77,9 @@ import { } from "./string-construction.js"; import { emitStringInputs } from "./string-lifetimes.js"; +/** A callback loop version copies the loop; keep the copies small. */ +const MAX_CALLBACK_VERSION_COST = 400; + export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { const B = emitter.B; emitter.frames.push([]); @@ -469,7 +473,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { B.startBlock(pop); const elem = s.arr.type.elem; const acc = elemAccess(elem); - const raw = emitDenseArrayPop(emitter, arr.name, acc, call, call); + const raw = emitDenseArrayPop(emitter, arr.name, acc, elem, call, call); if (acc === "ref" && isRefCounted(elem)) { const old = B.tmp(); B.line(`${old} = inttoptr i64 ${raw} to ptr`); @@ -782,7 +786,12 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { const trueLabel = B.newLabel("if.t"); const joinLabel = B.newLabel("if.j"); const falseLabel = s.else_ ? B.newLabel("if.f") : joinLabel; - B.condBr(cond, trueLabel, falseLabel); + // A running maximum or minimum is rarely updated; the weights keep + // the branch, where a select would make each element wait on the last. + const weights = emitter.runningExtrema.has(s) + ? `, !prof !{!"branch_weights", i32 1, i32 1000}` + : ""; + B.condBr(cond, trueLabel, falseLabel, weights); B.startBlock(trueLabel); emitter.emitBlock(s.then); B.br(joinLabel); @@ -795,6 +804,21 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { break; } case "while": { + const callbacks = callbackVersionBindings(emitter, s); + if (callbacks) { + emitCallbackVersionedLoop(emitter, s, callbacks); + break; + } + const version = + emitter.bytesLoopVersionsEnabled && + emitter.bytesLoopVersioning.size < MAX_BYTES_LOOP_VERSION_DEPTH && + !emitter.bytesLoopVersioning.has(s) + ? emitter.matchBytesLoopVersion(s, null) + : null; + if (version) { + emitVersionedLoop(emitter, s, version); + break; + } const workerBudget = emitter.workerLoopBudget(s); const le = B.newLabel("loop.e"); emitter.emitLoopCopies(s, true, le, (exit) => { @@ -803,6 +827,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { const lp = workerBudget !== null ? B.newLabel("loop.p") : lc; B.br(lc); B.startBlock(lc); + enterLoopHeader(emitter, lc); B.condBr(emitter.emitCondition(s.cond), lb, exit); B.startBlock(lb); emitter.jumpTargets.push({ @@ -827,6 +852,11 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { break; } case "doWhile": { + const callbacks = callbackVersionBindings(emitter, s); + if (callbacks) { + emitCallbackVersionedLoop(emitter, s, callbacks); + break; + } const workerBudget = emitter.workerLoopBudget(s); // Body first (runs at least once); continue jumps to the CONDITION. const le = B.newLabel("loop.e"); @@ -835,6 +865,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { const lc = B.newLabel("loop.c"); B.br(lb); B.startBlock(lb); + enterLoopHeader(emitter, lb); emitter.jumpTargets.push({ kind: "loop", brkLabel: exit, @@ -855,6 +886,11 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { break; } case "for": { + const callbacks = callbackVersionBindings(emitter, s); + if (callbacks) { + emitCallbackVersionedLoop(emitter, s, callbacks); + break; + } // The init's scope wraps the whole loop (break/continue must NOT // release it — scopeDepth captured after the push, C parity). const integerArrayLoop = @@ -879,8 +915,9 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { !integerLoop && !countedLoop && emitter.countedLoopsEnabled ? matchIntegerPairedForLoop(s, emitter.numericLocals, emitter.integerRanges) : null; + // Byte windows and dense loops reason about one counter. const window = - countedLoop && emitter.byteWindowsEnabled + countedLoop && countedLoop.followers.length === 0 && emitter.byteWindowsEnabled ? matchByteWindow( s, countedLoop, @@ -898,7 +935,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { length: integerArrayLoop.limitReceiver as IrExpr & { kind: "varRef" }, range: { min: 0, max: 4294967294 }, } - : countedLoop && !countedLoop.guarded + : countedLoop && !countedLoop.guarded && countedLoop.followers.length === 0 ? countedDenseShape(s, countedLoop) : null; const dense = @@ -979,35 +1016,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { : countedLoop; const version = versionable ? emitter.matchBytesLoopVersion(s, versionCounted) : null; if (version) { - const fast = B.newLabel("bytes.version.fast"), - slow = B.newLabel("bytes.version.slow"), - done = B.newLabel("bytes.version.done"); - emitBytesLoopVersionGuard(emitter, version, fast, slow); - const originalBounds = emitter.bytesBounds, - originalOwned = emitter.bytesOwned, - originalRanges = emitter.integerRanges; - emitter.bytesLoopVersioning.add(s); - try { - B.startBlock(slow); - emitter.emitStmt(s); - B.br(done); - B.startBlock(fast); - emitter.bytesBounds = new Set([...originalBounds, ...version.bounds]); - if (version.owned.size > 0) - emitter.bytesOwned = new Set([...originalOwned, ...version.owned]); - if (version.ranges.size > 0) - emitter.integerRanges = new Map([...originalRanges, ...version.ranges]); - if (version.counted?.guarded) emitter.provenCountedGuards.add(s); - emitter.emitStmt(s); - B.br(done); - } finally { - emitter.bytesBounds = originalBounds; - emitter.bytesOwned = originalOwned; - emitter.integerRanges = originalRanges; - emitter.provenCountedGuards.delete(s); - emitter.bytesLoopVersioning.delete(s); - } - B.startBlock(done); + emitVersionedLoop(emitter, s, version); break; } let countedJoin: string | null = null; @@ -1030,6 +1039,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { let integerSlot: string | null = null; let countedLimit: string | null = null; const pairedSlots: string[] = []; + const followerSlots: string[] = []; if (integerLoop) { integerSlot = B.slot(); B.entryAllocas.push( @@ -1073,6 +1083,23 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { ? emitter.denseLoop.range : countedLoop.range, }); + for (const follower of countedLoop.followers) { + const slot = B.slot(); + B.entryAllocas.push( + `${slot} = alloca i64 ; integer induction ${emitter.currentLocals.get(follower.localId)!.name}`, + ); + const value = emitter.emitExpr(follower.start); + const followerStart = B.tmp(); + B.line(`${followerStart} = fptosi double ${value.name} to i64`); + B.line(`store i64 ${followerStart}, ptr ${slot}`); + followerSlots.push(slot); + emitter.integerLoopBindings.set(follower.localId, { + slot, + type: "i64", + signed: true, + range: follower.range, + }); + } countedLimit = emitCountedLoopLimit(emitter, countedLoop); } else if (pairedLoop) { for (const counter of [pairedLoop.low, pairedLoop.high]) { @@ -1135,6 +1162,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { s.update || capturedLets.length > 0 || workerBudget !== null ? B.newLabel("loop.u") : lc; B.br(lc); B.startBlock(lc); + enterLoopHeader(emitter, lc); if (integerLoop && integerSlot) { const receiver = emitter.emitStableReceiver(integerLoop.limitReceiver, []); const lenPtr = B.tmp(); @@ -1212,6 +1240,13 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { B.line(`${old} = load i64, ptr ${integerSlot}`); B.line(`${next} = add nsw i64 ${old}, ${countedLoop.step}`); B.line(`store i64 ${next}, ptr ${integerSlot}`); + countedLoop.followers.forEach((follower, i) => { + const was = B.tmp(); + const now = B.tmp(); + B.line(`${was} = load i64, ptr ${followerSlots[i]}`); + B.line(`${now} = add nsw i64 ${was}, ${follower.step}`); + B.line(`store i64 ${now}, ptr ${followerSlots[i]}`); + }); } else if (pairedLoop) { [pairedLoop.low, pairedLoop.high].forEach((counter, i) => { const old = B.tmp(); @@ -1227,7 +1262,11 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { B.startBlock(le); emitter.releaseScope(emitter.scopes.pop()!); if (integerLoop) emitter.integerLoopBindings.delete(integerLoop.localId); - if (countedLoop) emitter.integerLoopBindings.delete(countedLoop.localId); + if (countedLoop) { + emitter.integerLoopBindings.delete(countedLoop.localId); + for (const follower of countedLoop.followers) + emitter.integerLoopBindings.delete(follower.localId); + } if (pairedLoop) { emitter.integerLoopBindings.delete(pairedLoop.low.localId); emitter.integerLoopBindings.delete(pairedLoop.high.localId); @@ -1885,10 +1924,148 @@ export function emitSwitch(emitter: LlEmitter, s: IrStmt & { kind: "switch" }): B.startBlock(end); } +/** The loop copies a callback loop version needs: the bindings the loop + * calls that the constant-callback census resolves to one function, each + * with that function's direct entry. Only small loops of optimized + * synchronous functions, and only bindings declared before the loop, + * qualify: the guard reads each binding before the loop runs. Versions do + * not nest: a loop inside either copy of a version (including the + * versioned loop itself, emitted again for each copy) keeps its calls as + * they are, so a loop nest is emitted at most twice. */ +function callbackVersionBindings( + emitter: LlEmitter, + s: IrStmt & { kind: "for" | "while" | "doWhile" }, +): { ref: IrExpr & { kind: "varRef" }; direct: string }[] | null { + if ( + !emitter.callbackVersionsEnabled || + emitter.currentConstantCallbacks.size === 0 || + emitter.callbackVersioning.size > 0 + ) + return null; + const found = new Map(); + // Bindings the loop declares or writes (a callback declared in its body). + const bound = new Set(); + let cost = 0; + const expr = (e: IrExpr): boolean => { + if (++cost > MAX_CALLBACK_VERSION_COST) return false; + if (e.kind === "assignExpr") bound.add(e.localId); + if (e.kind === "callValue" && e.callee.kind === "varRef" && !found.has(e.callee.localId)) { + const local = emitter.currentLocals.get(e.callee.localId); + const target = + local && local.tdz !== true ? emitter.knownCallbackTarget(e.callee) : undefined; + if (target) found.set(e.callee.localId, { ref: e.callee, direct: target.direct }); + } + return everyExprChild(e, expr, stmt); + }; + const stmt = (x: IrStmt): boolean => { + if (++cost > MAX_CALLBACK_VERSION_COST) return false; + if (x.kind === "varDecl" || x.kind === "assign" || x.kind === "forOf") bound.add(x.localId); + else if (x.kind === "tryCatch" && x.catchLocalId !== null) bound.add(x.catchLocalId); + return everyStmtChild(x, expr, stmt); + }; + if (!stmt(s)) return null; + const bindings = [...found.values()].filter(({ ref }) => !bound.has(ref.localId)); + return bindings.length > 0 ? bindings : null; +} + +/** A callback loop version. The guard tests, once before the loop, that + * each binding's closure holds its known function: a closure's code + * pointer is written only when the closure is built, and the census + * resolves only bindings the function never rebinds. The copy where the + * guard holds calls those functions directly, with no indirect fallback + * that could rewrite the closure or what it captures, so LLVM can keep a + * captured scalar the callback updates in a register. The other copy is the + * ordinary loop. */ +function emitCallbackVersionedLoop( + emitter: LlEmitter, + s: IrStmt & { kind: "for" | "while" | "doWhile" }, + bindings: { ref: IrExpr & { kind: "varRef" }; direct: string }[], +): void { + const B = emitter.B; + let ok = "true"; + for (const { ref, direct } of bindings) { + // A borrowed read: the guard only compares the code pointer. + const closure = emitter.emitReadReceiver(ref).name; + const fnp = B.tmp(), + fn = B.tmp(), + same = B.tmp(); + B.line(`${fnp} = getelementptr inbounds %ScrClosure, ptr ${closure}, i64 0, i32 1`); + B.line(`${fn} = load ptr, ptr ${fnp}`); + B.line(`${same} = icmp eq ptr ${fn}, ${direct}`); + if (ok === "true") ok = same; + else { + const both = B.tmp(); + B.line(`${both} = and i1 ${ok}, ${same}`); + ok = both; + } + } + const fast = B.newLabel("callback.version.fast"), + slow = B.newLabel("callback.version.slow"), + done = B.newLabel("callback.version.done"); + B.condBr(ok, fast, slow); + emitter.callbackVersioning.add(s); + try { + B.startBlock(slow); + emitter.emitStmt(s); + B.br(done); + B.startBlock(fast); + for (const { ref } of bindings) emitter.knownCallbacks.add(ref.localId); + emitter.emitStmt(s); + B.br(done); + } finally { + for (const { ref } of bindings) emitter.knownCallbacks.delete(ref.localId); + emitter.callbackVersioning.delete(s); + } + B.startBlock(done); +} + +/** Entries in a row whose fast copy bailed before a speculative loop site + * stops trying it. */ +const SPECULATION_BAILS = 8; + /** Emit a loop's condition, body and update through `emitCopy`, twice * when speculative-integers.ts plans a fast integer copy. Both copies * share the loop's setup and exit; the ordinary copy comes first so the * fast copy is only emitted when every resume point exists. */ +/** A typed-array loop version: the guard, then the checked copy, which + * runs whenever the guard fails, and the copy whose accesses the guard + * proves in bounds (or thread-owned). */ +function emitVersionedLoop( + emitter: LlEmitter, + s: IrStmt & { kind: "for" | "while" }, + version: BytesLoopVersion, +): void { + const B = emitter.B; + const fast = B.newLabel("bytes.version.fast"), + slow = B.newLabel("bytes.version.slow"), + done = B.newLabel("bytes.version.done"); + emitBytesLoopVersionGuard(emitter, version, fast, slow); + const originalBounds = emitter.bytesBounds, + originalOwned = emitter.bytesOwned, + originalRanges = emitter.integerRanges; + emitter.bytesLoopVersioning.add(s); + try { + B.startBlock(slow); + emitter.emitStmt(s); + B.br(done); + B.startBlock(fast); + emitter.bytesBounds = new Set([...originalBounds, ...version.bounds]); + if (version.owned.size > 0) emitter.bytesOwned = new Set([...originalOwned, ...version.owned]); + if (version.ranges.size > 0) + emitter.integerRanges = new Map([...originalRanges, ...version.ranges]); + if (version.counted?.guarded) emitter.provenCountedGuards.add(s); + emitter.emitStmt(s); + B.br(done); + } finally { + emitter.bytesBounds = originalBounds; + emitter.bytesOwned = originalOwned; + emitter.integerRanges = originalRanges; + emitter.provenCountedGuards.delete(s); + emitter.bytesLoopVersioning.delete(s); + } + B.startBlock(done); +} + export function emitLoopCopies( emitter: LlEmitter, loop: Extract, @@ -1939,23 +2116,66 @@ export function emitLoopCopies( ok = both; } } + // Each entry whose fast copy bails counts at the loop site, and a fast + // copy that completes a speculated write and leaves the loop clears the + // count. After SPECULATION_BAILS bailing entries in a row, the site runs + // the ordinary copy only. The count is a hint: both copies give the same + // results, and a race between workers loses at most an update. + const bails = `@scr.spec.bails.${emitter.speculativeLoopSites++}`; + emitter.declare(`${bails} = internal global i8 0`); + const count = B.tmp(), + trying = B.tmp(); + B.line(`${count} = load atomic i8, ptr ${bails} unordered, align 1`); + B.line(`${trying} = icmp ult i8 ${count}, ${SPECULATION_BAILS}`); + if (ok === "true") ok = trying; + else { + const both = B.tmp(); + B.line(`${both} = and i1 ${ok}, ${trying}`); + ok = both; + } const fast = B.newLabel("spec.fast"), slow = B.newLabel("spec.slow"); B.condBr(ok, fast, slow); const resume = new Map(); for (const s of plan.writes) resume.set(s, { label: B.newLabel("spec.resume"), placed: false }); + // A failed check finishes its iteration in a third copy, entered only + // there: it re-executes the statement and the rest of the body and the + // update, then enters the ordinary copy through its header. Jumping + // into the middle of the ordinary copy instead would give that loop a + // second entry, and LLVM does not optimize an irreducible loop. + const points = [...resume.values()]; + let header: string | null = null; B.startBlock(slow); + emitter.speculationHeader = (label) => { + header = label; + }; + try { + emitCopy(exit); + } finally { + emitter.speculationHeader = null; + } + if (header === null) + throw new InternalCompilerError("llvm emitter bug: speculative loop copy without a header"); + B.startBlock(B.newLabel("spec.tail")); emitter.speculationResume = resume; + emitter.speculationHeader = () => { + B.br(header!); + B.startBlock(B.newLabel("spec.unused")); + }; try { emitCopy(exit); } finally { emitter.speculationResume = null; + emitter.speculationHeader = null; } B.startBlock(fast); - if (![...resume.values()].every((r) => r.placed)) { + if (!points.every((r) => r.placed)) { B.br(slow); return; } + const ran = B.slot(); + B.entryAllocas.push(`${ran} = alloca i1 ; speculated write completed`); + B.line(`store i1 false, ptr ${ran}`); for (const id of plan.locals.keys()) { const shadow = B.slot(); B.entryAllocas.push( @@ -1992,22 +2212,43 @@ export function emitLoopCopies( writes: plan.writes, resume: new Map([...resume].map(([s, r]) => [s, r.label])), deferred, + bails, + ran, }; try { - if (deferred.size === 0) emitCopy(exit); - else { - const leave = B.newLabel("spec.leave"); - emitCopy(leave); - B.startBlock(leave); - emitter.writeBackDeferred(deferred); - B.br(exit); - } + const leave = B.newLabel("spec.leave"); + emitCopy(leave); + B.startBlock(leave); + emitter.writeBackDeferred(deferred); + // An entry that never ran a speculated write (a loop of no iterations) + // says nothing about the site. + const wrote = B.tmp(), + left = B.tmp(), + counted = B.tmp(), + both = B.tmp(), + clear = B.newLabel("spec.clear"); + B.line(`${wrote} = load i1, ptr ${ran}`); + B.line(`${left} = load atomic i8, ptr ${bails} unordered, align 1`); + B.line(`${counted} = icmp ne i8 ${left}, 0`); + B.line(`${both} = and i1 ${wrote}, ${counted}`); + B.condBr(both, clear, exit); + B.startBlock(clear); + B.line(`store atomic i8 0, ptr ${bails} unordered, align 1`); + B.br(exit); } finally { emitter.speculation = null; for (const id of plan.locals.keys()) emitter.integerLoopBindings.delete(id); } } +/** At the top of a loop copy's header: lets a speculative loop find its + * ordinary copy's header, and keeps its third copy out of the loop. */ +function enterLoopHeader(emitter: LlEmitter, header: string): void { + const dispatch = emitter.speculationHeader; + emitter.speculationHeader = null; + dispatch?.(header); +} + /** A proven integer loop (speculative-integers.ts) for this loop, or null. * A shadowed local's double is stale until the loop is left, which only * the loop's exit, a return and an exception do: the loop is planned only @@ -2134,7 +2375,8 @@ export function emitSpeculativeWrite( if (emitter.frames.pop()!.length > 0) throw new InternalCompilerError("llvm emitter bug: speculative write owns a temporary"); B.line(`store i64 ${result}, ptr ${binding.slot}`); - const deferred = emitter.speculation!.deferred; + const { deferred, bails: site, ran } = emitter.speculation!; + if (ran !== undefined) B.line(`store i1 true, ptr ${ran}`); if (!deferred.has(target.localId)) { const number = B.tmp(); B.line(`${number} = sitofp i64 ${result} to double`); @@ -2145,6 +2387,14 @@ export function emitSpeculativeWrite( B.br(done); B.startBlock(failed); emitter.writeBackDeferred(deferred); + if (site !== undefined) { + const count = B.tmp(), + next = B.tmp(); + emitter.declare("declare i8 @llvm.uadd.sat.i8(i8, i8)"); + B.line(`${count} = load atomic i8, ptr ${site} unordered, align 1`); + B.line(`${next} = call i8 @llvm.uadd.sat.i8(i8 ${count}, i8 1)`); + B.line(`store atomic i8 ${next}, ptr ${site} unordered, align 1`); + } B.br(resume!); B.startBlock(done); } diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index e11622b9b..7adf2a89f 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -72,6 +72,7 @@ import { scalarizeNumericRecords } from "../../ir/scalar-records.js"; import { addRecordResultEntries } from "./record-results.js"; import { specializeCallbackCalls } from "../../ir/callback-specialization.js"; import { specializeNumericCalls } from "../../ir/numeric-call-specialization.js"; +import { versionLoopBoundParameters } from "../../ir/parameter-versions.js"; import { everyExpr, everyExprChild, everyStmtChild, everyStmtList } from "../../ir/traverse.js"; import { INT32_RANGE, type IntegerRange, type IntegerRanges } from "../../ir/integer-ranges.js"; import { unshareNodes } from "../../ir/unshare.js"; @@ -442,6 +443,14 @@ export function lowRegionTarget(platform: string, sanitize: boolean): boolean { return platform === "linux" && !sanitize; } +/** Function versions on loop-bounding number parameters + * (ir/parameter-versions.ts). Library builds have no int32 entries, and a + * module that observes call stacks could see the version's frame. */ +function versionParameters(mod: IrModule): IrModule { + if (mod.lib !== undefined || observesStacks(mod)) return mod; + return versionLoopBoundParameters(mod, mangleFunction); +} + export function emitLlvmModule(input: IrModule, options: LlvmTargetOptions = {}): string { // Facts are keyed by node identity: emit the same module whether it was // lowered in memory or read back from its serialized form. @@ -449,20 +458,22 @@ export function emitLlvmModule(input: IrModule, options: LlvmTargetOptions = {}) // Keep source storage intact for debugger inspection in dev builds. return new LlEmitter( options.debugSources === undefined - ? addRecordResultEntries( - scalarizeNumericRecords( - specializeCallbackCalls( - specializeNumericCalls( - foldConstantGlobals( - copyStableCaptures( - resolveSelfCaptures( - hoistFinalFieldReads(freezeInitOnlyGlobals(dropUnwrittenPropertyBags(mod))), + ? versionParameters( + addRecordResultEntries( + scalarizeNumericRecords( + specializeCallbackCalls( + specializeNumericCalls( + foldConstantGlobals( + copyStableCaptures( + resolveSelfCaptures( + hoistFinalFieldReads(freezeInitOnlyGlobals(dropUnwrittenPropertyBags(mod))), + ), ), ), + mangleFunction, ), mangleFunction, ), - mangleFunction, ), ), ) @@ -481,20 +492,22 @@ export function emitLlvmModuleSource( const mod = unshareNodes(input); const parts = new LlEmitter( options.debugSources === undefined - ? addRecordResultEntries( - scalarizeNumericRecords( - specializeCallbackCalls( - specializeNumericCalls( - foldConstantGlobals( - copyStableCaptures( - resolveSelfCaptures( - hoistFinalFieldReads(freezeInitOnlyGlobals(dropUnwrittenPropertyBags(mod))), + ? versionParameters( + addRecordResultEntries( + scalarizeNumericRecords( + specializeCallbackCalls( + specializeNumericCalls( + foldConstantGlobals( + copyStableCaptures( + resolveSelfCaptures( + hoistFinalFieldReads(freezeInitOnlyGlobals(dropUnwrittenPropertyBags(mod))), + ), ), ), + mangleFunction, ), mangleFunction, ), - mangleFunction, ), ), ) @@ -641,9 +654,11 @@ export class LlEmitter { * 32-bit references to compact class instances (LlClassMeta.compressed): * loadField/loadRef decode them and storeField/storeRef encode them. */ readonly compressedFieldPtrs = new Set(); - /** Whether a class has compressed fields: main reserves the region before - * compiled code runs (scr_plr_init). */ - usesCompactRefs = false; + /** Compact class instances live in the program-lifetime region below + * 32 GiB (LlvmTargetOptions.lowRegion, and the program has compact + * classes): main reserves it before compiled code runs (scr_plr_init), and + * fields and arrays of compactRefType hold 32-bit references. */ + compactRegion = false; /** Native class fields with different property names cannot occupy the * same slot of one object. Inherited views of the same property share a @@ -831,6 +846,15 @@ export class LlEmitter { readonly callLifetimes: CallLifetimes; readonly constantCallbacks: Map>; currentConstantCallbacks: ReadonlyMap = new Map(); + /** The current function's running maximum and minimum updates, whose + * branches are weighted as rarely taken (running-extrema.ts). */ + runningExtrema: ReadonlySet = new Set(); + /** Callback bindings whose closures an enclosing callback loop version + * proved to hold their known function: calls take the direct entry with + * no fallback (emitCallbackVersionedLoop). */ + knownCallbacks = new Set(); + readonly callbackVersioning = new Set(); + callbackVersionsEnabled = false; readonly stackCallbacks: StackCallbacks; readonly receiverReaders: AmbientReceiverReaders; readonly stackCaptures: StackCaptures; @@ -1102,9 +1126,20 @@ export class LlEmitter { resume: Map; /** Globals whose double is written back only when the copy is left. */ deferred: Set; + /** The loop site's count of bailing entries in a row (a global), and + * whether this entry completed a speculated write (a slot). A proven + * integer loop has neither. */ + bails?: string; + ran?: string; } | null = null; + /** Speculative loop sites numbered so far in the module. */ + speculativeLoopSites = 0; /** The ordinary copy being emitted: resume labels still to place. */ speculationResume: Map | null = null; + /** Run once at the top of the next loop copy's header + * (`enterLoopHeader`): records the ordinary copy's header, or sends the + * third copy's header there. */ + speculationHeader: ((header: string) => void) | null = null; /** The function being emitted, for speculative globals. */ speculativeFunction = ""; /** Every function that writes each number global (computed on demand). */ @@ -1459,6 +1494,8 @@ export class LlEmitter { } } this.shapeHost = { + compactArrayElem: (elem) => this.compactArrayElem(elem), + compactRegion: () => this.compactRegion, sizeType: this.sizeType, cycleBufOffset: this.cycleBufOffset, tracedShapes: this.tracedShapes, @@ -1588,22 +1625,19 @@ export class LlEmitter { ? (cls, i, isCompact) => { const f = cls.fields[i]!; if (this.inlineUnions.classField(cls.name, f.name)) return false; - const t = f.type; - if (t.kind === "object") return isCompact(t.className); - if (t.kind !== "union") return false; - const single = this.nullableUnions.single(t.unionId); - if (single !== null) - return ( - single.nullTag < 0 && - single.arm.kind === "object" && - isCompact(single.arm.className) - ); - const nullable = this.nullableFields.get(cls.name, f.name); + if (this.compactRefType(f.type, isCompact)) return true; + const nullable = + f.type.kind === "union" && this.nullableUnions.single(f.type.unionId) === null + ? this.nullableFields.get(cls.name, f.name) + : null; return nullable !== null && isCompact(nullable.arm.className); } : undefined, ); - this.usesCompactRefs = [...this.classMeta.values()].some((m) => m.compressed.some(Boolean)); + this.compactRegion = + mod.lib === undefined && + options.lowRegion === true && + [...this.classMeta.values()].some((m) => m.compact); for (const [name, rec] of RUNTIME_ERROR_CLASSES) { const meta = this.classMeta.get(name); if (!meta) break; // hand-written IR without the builtin defs: no stamps @@ -1896,14 +1930,14 @@ export class LlEmitter { /** Whether a fiberless body running on its caller's stack needs the * frame scr_async_inline_enter/exit keep: it isolates the Error.stack - * frame chain, which only stack-observing programs push, and the - * AsyncLocalStorage context, which only programs that enter stores - * change (RuntimeFeatures asyncContext), as a fresh fiber would. Other - * programs run the body with a plain call. The frame also counts the - * bodies running on a fiber's stack for the runtime's trap on a fiberless - * body that suspends. */ + * frame chain, which only stack-observing programs push, as a fresh fiber + * would. Other programs run the body with a plain call. The body runs in + * its caller's AsyncLocalStorage context either way, as Node runs an async + * body up to its first await. The frame also counts the bodies running on + * a fiber's stack for the runtime's trap on a fiberless body that + * suspends. */ get fiberlessFrames(): boolean { - return this.stackTraces || this.schedulesInContext; + return this.stackTraces; } /** The spawn wrapper of a fiberless async function: runs the body on the @@ -2215,7 +2249,7 @@ export class LlEmitter { B.condBr(thrown, rejected, clean); B.startBlock(clean); if (acc === "ref") this.toHeap(value); - this.arrPush(values, acc, value.name); + this.arrPush(values, acc, value.name, elem); B.br(next); B.startBlock(rejected); const promise = B.tmp(); @@ -3568,6 +3602,30 @@ export class LlEmitter { return { ptr: p, type, inline: this.inlineUnions.classField(className, field), compressed }; } + /** Whether values of `t` are NULL or a compact class instance: `C`, + * `C | undefined` or `C | null` for a compact class `C`. `C | null | + * undefined` is not: its null is a sentinel outside the region. */ + compactRefType( + t: IrType, + isCompact: (className: string) => boolean = (name) => + this.classMeta.get(name)?.compact === true, + ): boolean { + if (t.kind === "object") return isCompact(t.className); + if (t.kind !== "union") return false; + const single = this.nullableUnions.single(t.unionId); + return ( + single !== null && + single.nullTag < 0 && + single.arm.kind === "object" && + isCompact(single.arm.className) + ); + } + + /** Whether arrays of `elem` hold 32-bit references (SCR_ELEM_CREF). */ + compactArrayElem(elem: IrType): boolean { + return this.compactRegion && this.compactRefType(elem); + } + /** Loads the reference a field pointer addresses: a pointer, or the * decoded 32-bit reference of a compressed field (compressedFieldPtrs). * `tagged`: the load carries the field's alias tags. */ @@ -3832,7 +3890,7 @@ export class LlEmitter { /** A binding's storage: a module global, a plain local slot, or a boxed * local (the slot holds the capture BOX; access goes through it). */ matchBytesLoopVersion( - s: IrStmt & { kind: "for" }, + s: IrStmt & { kind: "for" | "while" }, counted: IntegerCountedForLoop | null, ): ReturnType { const fn = this.bytesLoopFunction; @@ -3901,6 +3959,25 @@ export class LlEmitter { return b; } + /** The function a call through `callee` is known to reach (a literal + * closure, or a binding the constant-callback census resolves) and the + * entry it calls directly. Async and generator functions stay indirect. */ + knownCallbackTarget(callee: IrExpr): { known: IrFunction; direct: string } | undefined { + const knownName = + callee.kind === "closure" + ? callee.fnName + : callee.kind === "varRef" + ? this.currentConstantCallbacks.get(callee.localId) + : undefined; + const known = knownName ? this.fnByName.get(knownName) : undefined; + if (!known || known.async || known.generator) return undefined; + if (known.captures === undefined) { + this.fnValues.add(known.name); + return { known, direct: `@${mangleWrapper(known.name)}` }; + } + return { known, direct: `@${this.callTarget(known.name)}` }; + } + /** The payload slot of a capture box (ScrBox.slot, field 5). */ boxSlot(box: string): string { const slot = this.B.tmp(); @@ -3914,12 +3991,18 @@ export class LlEmitter { const r = B.tmp(); // Scalar payloads are read in place (scr_box_get_f64/bool are a // memcpy and a nonzero test of the same slot), so LLVM can keep a - // captured number in a register across a loop of calls. + // captured number in a register across a loop of calls. They have an + // alias category of their own: every other access to a box's payload + // is untagged or a runtime call. if (acc === "f64") { - B.line(`${r} = load double, ptr ${this.boxSlot(box)}`); + B.line( + `${r} = load double, ptr ${this.boxSlot(box)}${this.memoryAliasAttachment("box:scalar")}`, + ); } else if (acc === "bool") { const raw = B.tmp(); - B.line(`${raw} = load i64, ptr ${this.boxSlot(box)}`); + B.line( + `${raw} = load i64, ptr ${this.boxSlot(box)}${this.memoryAliasAttachment("box:scalar")}`, + ); B.line(`${r} = icmp ne i64 ${raw}, 0`); } else { this.declare(`declare ptr @scr_box_get_ref(ptr)`); @@ -3933,11 +4016,15 @@ export class LlEmitter { const B = this.B; const acc = boxAccess(t); if (acc === "f64") { - B.line(`store double ${value}, ptr ${this.boxSlot(box)}`); + B.line( + `store double ${value}, ptr ${this.boxSlot(box)}${this.memoryAliasAttachment("box:scalar")}`, + ); } else if (acc === "bool") { const raw = B.tmp(); B.line(`${raw} = zext i1 ${value} to i64`); - B.line(`store i64 ${raw}, ptr ${this.boxSlot(box)}`); + B.line( + `store i64 ${raw}, ptr ${this.boxSlot(box)}${this.memoryAliasAttachment("box:scalar")}`, + ); } else { this.declare(`declare void @scr_box_set_ref(ptr, ptr)`); B.line(`call void @scr_box_set_ref(ptr ${box}, ptr ${value})`); @@ -3986,7 +4073,7 @@ export class LlEmitter { const acc = boxAccess(t); const cell = B.tmp(); B.line(`${cell} = ${arrNewCall(this.shapeHost, t, "1")} ; TDZ cell`); - this.arrPush(cell, acc === "bool" ? "bool" : "f64", value); + this.arrPush(cell, acc === "bool" ? "bool" : "f64", value, null); this.declare(`declare void @scr_box_set_ref(ptr, ptr)`); B.line(`call void @scr_box_set_ref(ptr ${box}, ptr ${cell})`); } @@ -6136,8 +6223,8 @@ export class LlEmitter { return tagInSet(this, uName, unionId, tags); } - arrPush(arr: string, acc: "f64" | "bool" | "ref", value: string): string { - return arrPush(this, arr, acc, value); + arrPush(arr: string, acc: "f64" | "bool" | "ref", value: string, element: IrType | null): string { + return arrPush(this, arr, acc, value, element); } emitArrayCopyLoop(dst: string, src: string, acc: "f64" | "bool" | "ref"): void { diff --git a/packages/compiler/src/backend/llvm/expr-calls.ts b/packages/compiler/src/backend/llvm/expr-calls.ts index f0aa3b276..4bcf2a3d5 100644 --- a/packages/compiler/src/backend/llvm/expr-calls.ts +++ b/packages/compiler/src/backend/llvm/expr-calls.ts @@ -29,7 +29,6 @@ import { mangleClassStruct, mangleFnClosure, mangleFunction, - mangleWrapper, mangleVtStruct, mangleAsyncDirectSpawn, } from "../mangle.js"; @@ -839,21 +838,9 @@ export function emitCallExpr( host.declare("declare void @scr_dyn_this_push_dyn(ptr)"); B.line(`call void @scr_dyn_this_push_dyn(ptr ${receiver?.name ?? "null"})`); }; - const knownName = - e.callee.kind === "closure" - ? e.callee.fnName - : e.callee.kind === "varRef" - ? host.currentConstantCallbacks.get(e.callee.localId) - : undefined; - const candidate = knownName ? host.fnByName.get(knownName) : undefined; - const known = candidate && !candidate.async && !candidate.generator ? candidate : undefined; - let direct: string | undefined; - if (known) { - if (known.captures === undefined) { - host.fnValues.add(known.name); - direct = `@${mangleWrapper(known.name)}`; - } else direct = `@${host.callTarget(known.name)}`; - } + const target = host.knownCallbackTarget(e.callee); + const known = target?.known; + const direct = target?.direct; // A known closure body that borrows reference parameters is entered // directly with lent arguments, as a direct call to a borrowing body // is: an argument is read without a reference when neither a later @@ -939,7 +926,13 @@ export function emitCallExpr( let pending = host.indirectMayThrow; let pendingGuard: string | undefined; let t: string; - if (direct && e.callee.kind === "closure") { + // A literal closure, or a binding an enclosing callback loop version + // proved to hold its known function, enters it directly. + if ( + direct && + (e.callee.kind === "closure" || + (e.callee.kind === "varRef" && host.knownCallbacks.has(e.callee.localId))) + ) { t = invoke(lending ?? direct, !host.receiverReaders.ignores(known!)); if (quiet) pending = false; } else { diff --git a/packages/compiler/src/backend/llvm/expr-containers.ts b/packages/compiler/src/backend/llvm/expr-containers.ts index 8970aa3be..071ef7f92 100644 --- a/packages/compiler/src/backend/llvm/expr-containers.ts +++ b/packages/compiler/src/backend/llvm/expr-containers.ts @@ -31,8 +31,10 @@ import { emitMapIterCount, emitMapIterEntry, emitMapIterSlot, + compactMapValues, mapEntryLayout, emitMapIterStart, + emitMapSize, emitNumberMapGetRef, emitNumberMapGetScalar, emitNumberMapHas, @@ -102,13 +104,17 @@ export function tagInSet( return acc; } +/** `element`: the array's element type, or null for arrays whose + * elements are not compact class references (dense-array-access.ts + * slotKind). */ export function arrPush( host: LlvmEmitterContext, arr: string, acc: "f64" | "bool" | "ref", value: string, + element: IrType | null, ): string { - return emitDenseArrayPush(host, arr, acc, value); + return emitDenseArrayPush(host, arr, acc, element, value); } /** Move evaluated values through one bounded slot buffer per call site. */ @@ -116,6 +122,7 @@ export function emitArrayValues( host: LlvmEmitterContext, arr: string, acc: "f64" | "bool" | "ref", + element: IrType, values: LlValue[], prepend = false, sharedBuffer?: string, @@ -129,7 +136,7 @@ export function emitArrayValues( return result; } if (values.length === 1) { - if (!prepend) return arrPush(host, arr, acc, values[0]!.name); + if (!prepend) return arrPush(host, arr, acc, values[0]!.name, element); const ty = acc === "f64" ? "double" : acc === "bool" ? "i1" : "ptr"; host.declare( `declare double @scr_arr_unshift_${acc}(ptr, ${ty}${acc === "bool" ? " zeroext" : ""})`, @@ -628,7 +635,7 @@ export function emitArrIntrinsic( // no-op zero-argument call. const vs = e.args.map((a) => host.emitExpr(a)); host.emitPublishedArrayGuard(r.name, e.receiver.type, 1, f64Lit(0), f64Lit(vs.length)); - return { name: emitArrayValues(host, r.name, acc, vs), type: e.type }; + return { name: emitArrayValues(host, r.name, acc, elem, vs), type: e.type }; } case "pushSpread": { // `a.push(...src)`: append src's elements in order (borrowed src, @@ -660,7 +667,7 @@ export function emitArrIntrinsic( // from right to left so the final front order is source order. const vs = e.args.map((a) => host.emitExpr(a)); host.emitPublishedArrayGuard(r.name, e.receiver.type, 4, f64Lit(0), f64Lit(vs.length)); - return { name: emitArrayValues(host, r.name, acc, vs, true), type: e.type }; + return { name: emitArrayValues(host, r.name, acc, elem, vs, true), type: e.type }; } case "unshiftSpread": { // The runtime snapshots the borrowed source and handles self-spread. @@ -702,7 +709,7 @@ export function emitArrIntrinsic( let slowLabel = ""; if (method === "pop") { slowLabel = B.newLabel("remove.slow"); - denseRaw = emitDenseArrayPop(host, r.name, acc, la, slowLabel); + denseRaw = emitDenseArrayPop(host, r.name, acc, elem, la, slowLabel); denseLabel = B.currentLabel(); B.br(lp); B.startBlock(slowLabel); @@ -771,7 +778,7 @@ export function emitArrIntrinsic( // by content, everything else by pointer). Strict equality. Arrays of // objects and strings scan inline (array-search.ts). const v = host.emitExpr(e.args[0]!); - const search = acc === "ref" ? arraySearchKind(elem) : null; + const search = acc === "ref" ? arraySearchKind(elem, host.compactArrayElem(elem)) : null; if (search !== null) { host.arraySearchKinds.add(search); const t = B.tmp(); @@ -789,7 +796,7 @@ export function emitArrIntrinsic( // Borrowed needle, SameValueZero (NaN matches NaN). For the inline // searches SameValueZero is strict equality. const v = host.emitExpr(e.args[0]!); - const search = acc === "ref" ? arraySearchKind(elem) : null; + const search = acc === "ref" ? arraySearchKind(elem, host.compactArrayElem(elem)) : null; if (search !== null) { host.arraySearchKinds.add(search); const at = B.tmp(), @@ -1060,8 +1067,10 @@ function newCollection(host: LlvmEmitterContext, key: IrType, value: IrType): st const kind = mapKeyKindNum(key, arms); const m = B.tmp(); host.declare(`declare ptr @scr_map_new(i32, i32, ptr)`); + // SCR_MAP_VAL_CREF (3): string keys and compact values (compactMapValues). + const valKind = compactMapValues(host, key, value) ? 3 : mapValKindNum(value); B.line( - `${m} = call ptr @scr_map_new(i32 ${kind}, i32 ${mapValKindNum(value)}, ptr ${collectionOps(host, key, value)})`, + `${m} = call ptr @scr_map_new(i32 ${kind}, i32 ${valKind}, ptr ${collectionOps(host, key, value)})`, ); return m; } @@ -1124,6 +1133,9 @@ export function emitMapLikeIntrinsic( const key = receiverType.kind === "map" ? receiverType.key : receiverType.elem; const kAcc = mapKeyAccess(key); const kTy = mapKeyParamType(kAcc); + // The map keeps compact values in 16-byte entries (SCR_MAP_VAL_CREF). + const compact = receiverType.kind === "map" && compactMapValues(host, key, receiverType.value); + const layout = mapEntryLayout(key, compact); const method = e.method; // A published Map or Set rejects mutation (the guard precedes the // operands, which each method evaluates itself). @@ -1287,7 +1299,7 @@ export function emitMapLikeIntrinsic( if (numberKey !== null) { raw = emitNumberMapGetRef(host, r.name, numberKey, value); } else if (stringKey !== null) { - raw = emitStringMapGetRef(host, r.name, stringKey, value); + raw = emitStringMapGetRef(host, r.name, stringKey, value, compact); } else { host.declare(`declare ptr @scr_map_get_${kAcc}_ref(ptr, ${kTy})`); raw = B.tmp(); @@ -1365,7 +1377,7 @@ export function emitMapLikeIntrinsic( return host.wrapNullable(raw, raw, value, valueTag, e.type, undefTag); } if (stringKey !== null && isRefCounted(value)) { - const raw = emitStringMapGetRef(host, r.name, stringKey, value); + const raw = emitStringMapGetRef(host, r.name, stringKey, value, compact); return host.wrapNullable(raw, raw, value, valueTag, e.type, undefTag); } host.declare(`declare ptr @scr_map_get_${kAcc}_ref(ptr, ${kTy})`); @@ -1413,7 +1425,8 @@ export function emitMapLikeIntrinsic( case "has": { const k = borrowInputs ? host.emitReadReceiver(e.args[0]!) : host.emitExpr(e.args[0]!); if (kAcc === "f64") return { name: emitNumberMapHas(host, r.name, k.name), type: e.type }; - if (kAcc === "str") return { name: emitStringMapHas(host, r.name, k.name), type: e.type }; + if (kAcc === "str") + return { name: emitStringMapHas(host, r.name, k.name, compact), type: e.type }; host.declare(`declare zeroext i1 @scr_map_has_${kAcc}(ptr, ${kTy})`); const t = B.tmp(); B.line(`${t} = call zeroext i1 @scr_map_has_${kAcc}(ptr ${r.name}, ${kTy} ${k.name})`); @@ -1426,12 +1439,8 @@ export function emitMapLikeIntrinsic( B.line(`${t} = call zeroext i1 @scr_map_delete_${kAcc}(ptr ${r.name}, ${kTy} ${k.name})`); return { name: t, type: e.type }; } - case "size": { - host.declare(`declare double @scr_map_size(ptr)`); - const t = B.tmp(); - B.line(`${t} = call double @scr_map_size(ptr ${r.name})`); - return { name: t, type: e.type }; - } + case "size": + return { name: emitMapSize(host, r.name), type: e.type }; case "clear": host.declare(`declare void @scr_map_clear(ptr)`); B.line(`call void @scr_map_clear(ptr ${r.name})`); @@ -1442,7 +1451,7 @@ export function emitMapLikeIntrinsic( return { name: emitMapIterStart(host, r.name), type: e.type }; case "iterLive": { const i = host.emitExpr(e.args[0]!); - const entry = emitMapIterEntry(host, r.name, i.name, mapEntryLayout(key)); + const entry = emitMapIterEntry(host, r.name, i.name, layout); const t = B.tmp(); B.line(`${t} = icmp ne ptr ${entry}, null`); return { name: t, type: e.type }; @@ -1461,7 +1470,7 @@ export function emitMapLikeIntrinsic( kAcc === "str" ? "ref" : kAcc, key, { decl: `declare ${retTy} @${symbol}(ptr, double)`, symbol }, - mapEntryLayout(key), + layout, ); return host.own({ name: t, type: e.type }); } @@ -1488,7 +1497,7 @@ export function emitMapLikeIntrinsic( decl: `declare ${vAcc === "bool" ? "zeroext i1" : retTy} @${symbol}(ptr, double)`, symbol, }, - mapEntryLayout(key), + layout, ); return host.own({ name: t, type: e.type }); } diff --git a/packages/compiler/src/backend/llvm/expr-dynamic.ts b/packages/compiler/src/backend/llvm/expr-dynamic.ts index 563a900f8..9af677586 100644 --- a/packages/compiler/src/backend/llvm/expr-dynamic.ts +++ b/packages/compiler/src/backend/llvm/expr-dynamic.ts @@ -16,6 +16,7 @@ import { streamTypedRefEligible } from "../../ir/analysis.js"; import { DYN, type IrExpr, + type IrType, isDynTypedRefType, isRefCounted, isUnitType, @@ -221,6 +222,19 @@ export function emitDynamicExpr( host.own({ name: prepared, type: { kind: "dyn" } }); host.emitPendingCheck(); const args = e.args.map((a) => emitBorrowedInput(host, a)); + // A closure registered as a 'message' listener gets a typed message + // entry: ports deliver messages of its parameter's layout without + // decoding them to checked-dynamic values first. + const listener = messageListenerArg(e); + if (listener !== null) { + const entry = host.dyn.messageListenerHelper(listener); + if (entry !== null) { + host.declare(`declare void @scr_dyn_func_set_message(ptr, ptr, ptr)`); + B.line( + `call void @scr_dyn_func_set_message(ptr ${args[1]!.name}, ptr @${host.dyn.dynFuncThunkHelper(listener)}, ptr @${entry})`, + ); + } + } let argsPtr = "null"; if (args.length > 0) { const arr = B.slot(); @@ -1282,9 +1296,7 @@ function emitUnionEqWrappedScalar( B.startBlock(same); const eq = B.tmp(); if (arm.kind === "f64") { - host.declare(`declare double @scr_union_get_f64(ptr)`); - const v = B.tmp(); - B.line(`${v} = call double @scr_union_get_f64(ptr ${union.name})`); + const v = host.unionGetF64(union.name); if (e.sameValue) { host.declare(`declare zeroext i1 @scr_num_same_value(double, double)`); B.line(`${eq} = call zeroext i1 @scr_num_same_value(double ${v}, double ${plain.name})`); @@ -1292,9 +1304,7 @@ function emitUnionEqWrappedScalar( B.line(`${eq} = fcmp oeq double ${v}, ${plain.name}`); } } else { - host.declare(`declare zeroext i1 @scr_union_get_bool(ptr)`); - const v = B.tmp(); - B.line(`${v} = call zeroext i1 @scr_union_get_bool(ptr ${union.name})`); + const v = host.unionGetBool(union.name); B.line(`${eq} = icmp eq i1 ${v}, ${plain.name}`); } B.line(`store i1 ${eq}, ptr ${slot}`); @@ -1323,3 +1333,24 @@ export const INHERITED_OBJECT_MEMBERS: ReadonlySet = new Set([ "hasOwnProperty", "propertyIsEnumerable", ]); + +/** EventEmitter methods that register their second argument as a listener. */ +const LISTENER_METHODS: ReadonlySet = new Set([ + "on", + "once", + "addListener", + "prependListener", + "prependOnceListener", +]); + +/** The closure type of `recv.on("message", listener)` and the other + * listener registrations with a typed closure for the 'message' event, or + * null. */ +function messageListenerArg(e: ExprOf<"dynInvoke">): (IrType & { kind: "func" }) | null { + if (!LISTENER_METHODS.has(e.method) || e.args.length !== 2) return null; + const [event, listener] = e.args; + if (event?.kind !== "dynFrom" || event.value.kind !== "strLit" || event.value.value !== "message") + return null; + if (listener?.kind !== "dynFrom" || listener.value.type.kind !== "func") return null; + return listener.value.type; +} diff --git a/packages/compiler/src/backend/llvm/expr-primitives.ts b/packages/compiler/src/backend/llvm/expr-primitives.ts index 354004e9e..02a9f971f 100644 --- a/packages/compiler/src/backend/llvm/expr-primitives.ts +++ b/packages/compiler/src/backend/llvm/expr-primitives.ts @@ -32,7 +32,7 @@ import { emitPowerOfTwoRemainder, emitSignedIntegerRemainder, } from "./integer-remainder.js"; -import { emitDenseArrayGet, emitDenseArrayState, slotWidth } from "./dense-array-access.js"; +import { denseSlotWidth, emitDenseArrayGet, emitDenseArrayState } from "./dense-array-access.js"; import { emitWrappingSum, markWrappingOperands } from "./wrapping-sums.js"; export function emitLiteralExpr( @@ -879,7 +879,7 @@ export function emitContainerExpr( buffer = B.tmp(); B.entryAllocas.push(`${buffer} = alloca [${Math.min(e.elems.length, 64)} x i64]`); } - if (pending.length > 0) emitArrayValues(host, arr, acc, pending, false, buffer); + if (pending.length > 0) emitArrayValues(host, arr, acc, elem, pending, false, buffer); pending = []; }; e.elems.forEach((el, i) => { @@ -924,7 +924,7 @@ export function emitContainerExpr( } const bound = B.tmp(); B.line(`${bound} = fsub double ${n.name}, ${f64Lit(1)}`); - B.countedLoop(bound, () => host.arrPush(arr, acc, fill), "ole"); + B.countedLoop(bound, () => host.arrPush(arr, acc, fill, elem), "ole"); return out; } case "arrayGet": { @@ -962,7 +962,7 @@ export function emitContainerExpr( idx, e.index, "has", - slotWidth(elemAccess(e.arr.type.elem)), + denseSlotWidth(host, e.arr.type.elem), ); return { name: t, type: e.type }; } @@ -977,7 +977,7 @@ export function emitContainerExpr( idx, e.index, "state", - slotWidth(elemAccess(e.arr.type.elem)), + denseSlotWidth(host, e.arr.type.elem), ); return { name: t, type: e.type }; } diff --git a/packages/compiler/src/backend/llvm/expr-stream-callbacks.ts b/packages/compiler/src/backend/llvm/expr-stream-callbacks.ts index 9be757a20..0570d0ea7 100644 --- a/packages/compiler/src/backend/llvm/expr-stream-callbacks.ts +++ b/packages/compiler/src/backend/llvm/expr-stream-callbacks.ts @@ -15,23 +15,55 @@ export function streamDataAdapter( sym = `sc_ee_dad_${host.resolveThunks.size}`; host.resolveThunks.set(key, sym); const p = cbT.params[0]; - if ( - cbT.params.length > 1 || - (p && p.kind !== "bytes" && p.kind !== "string" && p.kind !== "dyn" && p.kind !== "union") - ) { + if (cbT.params.length > 1) { throw new InternalCompilerError( "llvm emitter bug: stream data listener param shape (frontend must fence)", ); } + // A `Buffer | string` union wraps the filled slot; any other union (or + // other type) converts from the chunk's checked-dynamic value. + const bytesOrString = + p?.kind === "union" && + (() => { + const def = host.unionsById.get(p.unionId); + return ( + def !== undefined && + !host.nullableUnions.has(p.unionId) && + def.arms.length === 2 && + def.arms.some((a) => a.kind === "bytes" && a.elem === "u8") && + def.arms.some((a) => a.kind === "string") + ); + })(); host.declare(`declare ptr @scr_box_get_ref(ptr)`); host.declare(`declare void @scr_closure_release(ptr)`); + // The emit ABI's three slots (scr_stream_emit_data): the bytes chunk, + // the string chunk, and an object-mode entry's value. const d: string[] = [ - `define internal void @${sym}(ptr %cb, ptr %a0, ptr %a1) ${FN_ATTRS} { ; stream 'data' adapter ${typeKey(cbT)}`, + `define internal void @${sym}(ptr %cb, ptr %a0, ptr %a1, ptr %a2) ${FN_ATTRS} { ; stream 'data' adapter ${typeKey(cbT)}`, `entry:`, ` %capsp = getelementptr inbounds %ScrClosure, ptr %cb, i64 1`, ` %bx = load ptr, ptr %capsp`, ` %orig = call ptr @scr_box_get_ref(ptr %bx) ; the listener, +1`, ]; + const finishTyped = (arg: string, argTy: string): void => { + d.push( + ` %fnp = getelementptr inbounds %ScrClosure, ptr %orig, i64 0, i32 1`, + ` %fn = load ptr, ptr %fnp`, + ); + const retTy = host.llType(cbT.ret); + const argList = `ptr %orig, ${argTy} ${arg}`; + if (retTy === "void") { + d.push(` call void %fn(${argList})`); + } else { + d.push(` %ret = call ${retTy} %fn(${argList})`); + if (isRefCounted(cbT.ret)) { + d.push( + ` call void ${releaseSym(host.shapeHost, cbT.ret)}(ptr %ret) ; discarded listener result`, + ); + } + } + d.push(` call void @scr_closure_release(ptr %orig)`, ` ret void`); + }; const finish = (arg: string | null): void => { d.push( ` %fnp = getelementptr inbounds %ScrClosure, ptr %orig, i64 0, i32 1`, @@ -57,8 +89,8 @@ export function streamDataAdapter( } else if (p.kind === "bytes" || p.kind === "string") { const msg = p.kind === "bytes" - ? "a 'data' listener declaring a Buffer chunk received a string (the stream has an encoding set)" - : "a 'data' listener declaring a string chunk received a Buffer (call setEncoding, or declare the chunk as a Buffer)"; + ? "a 'data' listener declaring a Buffer chunk received a string or an object-mode value (the stream has an encoding set, or is in object mode)" + : "a 'data' listener declaring a string chunk received a Buffer or an object-mode value (call setEncoding, or declare the chunk's type)"; const slot = p.kind === "bytes" ? "%a0" : "%a1"; host.declare(`declare void @scr_throw_error_msg(i32, ptr, ${host.sizeType})`); d.push( @@ -73,7 +105,7 @@ export function streamDataAdapter( ); finish("%r0"); d.push(`}`, ``); - } else if (p.kind === "union") { + } else if (p.kind === "union" && bytesOrString) { // A `Buffer | string` chunk: wrap whichever slot the stream filled at // its arm of the (boxed) union. const def = host.unionsById.get(p.unionId); @@ -94,6 +126,9 @@ export function streamDataAdapter( host.declare(`declare void @scr_bytes_release_v(ptr)`); host.declare(`declare ptr @scr_str_retain_v(ptr)`); host.declare(`declare void @scr_str_release_v(ptr)`); + host.declare(`declare void @scr_throw_error_msg(i32, ptr, ${host.sizeType})`); + const unionMsg = + "a 'data' listener declaring a Buffer | string chunk received an object-mode value that is neither (declare the chunk's type)"; d.push( ` %uslot = alloca ptr`, ` %isb = icmp ne ptr %a0, null`, @@ -104,6 +139,15 @@ export function streamDataAdapter( ` store ptr %ub, ptr %uslot`, ` br label %go`, `str:`, + // an object-mode value that is neither: the catchable error the + // one-type adapters throw + ` %nos = icmp eq ptr %a1, null`, + ` br i1 %nos, label %bad, label %strok`, + `bad:`, + ` call void @scr_throw_error_msg(i32 1, ptr ${host.cstr(unionMsg)}, ${host.sizeType} ${Buffer.byteLength(unionMsg, "utf8")})`, + ` call void @scr_closure_release(ptr %orig)`, + ` ret void`, + `strok:`, ` %rs = call ptr @scr_str_retain_v(ptr %a1)`, ` %us = call ptr @scr_union_new_ref(i32 ${strTag}, ptr %rs, ptr @scr_str_retain_v, ptr @scr_str_release_v, ptr null)`, ` store ptr %us, ptr %uslot`, @@ -114,11 +158,20 @@ export function streamDataAdapter( finish("%uv"); d.push(`}`, ``); } else { - // dyn: box by runtime tag — the JS lane's adapter parameter. + // dyn: the object-mode value, else box by runtime tag — the JS lane's + // adapter parameter. Any other type converts from that value. host.declare(`declare ptr @scr_dyn_new_buffer(ptr)`); host.declare(`declare ptr @scr_dyn_new_str(ptr)`); + host.declare(`declare ptr @scr_dyn_retain_v(ptr)`); d.push( ` %dslot = alloca ptr`, + ` %isv = icmp ne ptr %a2, null`, + ` br i1 %isv, label %val, label %chunk`, + `val:`, + ` %dvv = call ptr @scr_dyn_retain_v(ptr %a2)`, + ` store ptr %dvv, ptr %dslot`, + ` br label %go`, + `chunk:`, ` %isb = icmp ne ptr %a0, null`, ` br i1 %isb, label %buf, label %str`, `buf:`, @@ -132,7 +185,25 @@ export function streamDataAdapter( `go:`, ` %dv = load ptr, ptr %dslot`, ); - finish("%dv"); + if (p.kind === "dyn") { + finish("%dv"); + } else { + const helper = host.dyn.dynCheckHelper(p, false); + const ty = host.llType(p); + host.declare(`declare void @scr_dyn_release(ptr)`); + host.declare(`declare zeroext i1 @scr_exc_pending()`); + d.push( + ` %tv = call ${ty === "i1" ? "zeroext i1" : ty} @${helper}(ptr %dv, ptr null)`, + ` call void @scr_dyn_release(ptr %dv)`, + ` %pend = call zeroext i1 @scr_exc_pending()`, + ` br i1 %pend, label %fail, label %conv`, + `fail:`, + ` call void @scr_closure_release(ptr %orig)`, + ` ret void`, + `conv:`, + ); + finishTyped("%tv", ty); + } d.push(`}`, ``); } host.resolveThunkDefs.push(...d); @@ -164,12 +235,25 @@ export function streamDoneFnFor( const dataT = kind === "t" || kind === "l" ? doneT.params[1] : undefined; let bytesTag = -1; let strTag = -1; + // data of any type other than a union of Buffer, string, undefined and + // null (an object-mode Transform's value) completes through the + // value entry points with the data boxed. + let valueData = false; if (dataT !== undefined) { - if (dataT.kind !== "union") - throw new InternalCompilerError("llvm emitter bug: stream done data param not a union"); - const def = host.unionsById.get(dataT.unionId); - bytesTag = def ? def.arms.findIndex((a) => a.kind === "bytes") : -1; - strTag = def ? def.arms.findIndex((a) => a.kind === "string") : -1; + const def = dataT.kind === "union" ? host.unionsById.get(dataT.unionId) : undefined; + valueData = + def === undefined || + !def.arms.every( + (a) => + (a.kind === "bytes" && a.elem === "u8") || + a.kind === "string" || + a.kind === "undefinedT" || + a.kind === "nullT", + ); + if (!valueData) { + bytesTag = def!.arms.findIndex((a) => a.kind === "bytes"); + strTag = def!.arms.findIndex((a) => a.kind === "string"); + } } const entry = kind === "w" @@ -179,10 +263,16 @@ export function streamDoneFnFor( : kind === "d" ? "scr_stream_destroy_done" : kind === "t" - ? "scr_stream_transform_done" - : "scr_stream_flush_done"; - const twoSlot = kind === "t" || kind === "l"; - host.declare(`declare void @${entry}(ptr, ptr${twoSlot ? ", ptr, ptr" : ""})`); + ? valueData + ? "scr_stream_transform_done_value" + : "scr_stream_transform_done" + : valueData + ? "scr_stream_flush_done_value" + : "scr_stream_flush_done"; + const twoSlot = (kind === "t" || kind === "l") && !valueData; + host.declare( + `declare void @${entry}(ptr, ptr${twoSlot ? ", ptr, ptr" : valueData ? ", ptr" : ""})`, + ); host.declare(`declare ptr @scr_box_get_ref(ptr)`); host.declare(`declare void @scr_stream_release_v(ptr)`); host.declare(`declare ptr @scr_error_retain_v(ptr)`); @@ -190,7 +280,7 @@ export function streamDoneFnFor( const params = [ "ptr %self", ...(errT !== undefined ? ["ptr %e"] : []), - ...(dataT !== undefined ? ["ptr %d"] : []), + ...(dataT !== undefined ? [`${host.llType(dataT)} %d`] : []), ]; const d: string[] = [ `define internal void @${sym}(${params.join(", ")}) ${FN_ATTRS} { ; stream '${kind}' done ${typeKey(doneT)}`, @@ -234,7 +324,15 @@ export function streamDoneFnFor( unwrap("%d", strTag, "@scr_str_retain_v", "%sslot"); } d.push(` %ev = load ptr, ptr %eslot`); - if (twoSlot) { + if (valueData) { + const conv = host.dyn.toDynHelper(dataT!); + host.declare(`declare void @scr_dyn_release(ptr)`); + d.push( + ` %dv = call ptr @${conv}(${host.llType(dataT!)} %d)`, + ` call void @${entry}(ptr %s, ptr %ev, ptr %dv) ; moves err; borrows s and the value`, + ` call void @scr_dyn_release(ptr %dv)`, + ); + } else if (twoSlot) { d.push( ` %bv = load ptr, ptr %bslot`, ` %sv = load ptr, ptr %sslot`, @@ -245,7 +343,8 @@ export function streamDoneFnFor( } d.push(` call void @scr_stream_release_v(ptr %s)`); if (errT !== undefined) d.push(` call void ${releaseSym(host.shapeHost, errT)}(ptr %e)`); - if (dataT !== undefined) d.push(` call void ${releaseSym(host.shapeHost, dataT)}(ptr %d)`); + if (dataT !== undefined && isRefCounted(dataT)) + d.push(` call void ${releaseSym(host.shapeHost, dataT)}(ptr %d)`); d.push(` ret void`, `}`, ``); host.resolveThunkDefs.push(...d); return sym; @@ -611,18 +710,14 @@ export function streamCbThunkFor( : pos === 1; if (p.kind === "dyn") { if (isChunkPos) { - host.declare(`declare ptr @scr_dyn_new_buffer(ptr)`); - d.push(` %dc${i} = call ptr @scr_dyn_new_buffer(ptr %chunk)`); + // the chunk's value: a Buffer, or an object-mode side's value + host.declare(`declare ptr @scr_stream_chunk_value(ptr, ptr)`); + d.push(` %dc${i} = call ptr @scr_stream_chunk_value(ptr %s, ptr %chunk)`); passed.push(`ptr %dc${i}`); } else if (isEncPos) { - host.declare(`declare ptr @scr_str_new(ptr, ${host.sizeType})`); - host.declare(`declare ptr @scr_dyn_new_str(ptr)`); - host.declare(`declare void @scr_str_release(ptr)`); - d.push( - ` %es${i} = call ptr @scr_str_new(ptr ${host.cstr("buffer")}, ${host.sizeType} 6)`, - ` %ed${i} = call ptr @scr_dyn_new_str(ptr %es${i})`, - ` call void @scr_str_release(ptr %es${i})`, - ); + // 'buffer', or the encoding argument an object-mode write passed + host.declare(`declare ptr @scr_stream_chunk_encoding_value(ptr)`); + d.push(` %ed${i} = call ptr @scr_stream_chunk_encoding_value(ptr %s)`); passed.push(`ptr %ed${i}`); } else if (isErrPos) { // finished/pipeline succeed with UNDEFINED (Node calls the eos @@ -673,15 +768,71 @@ export function streamCbThunkFor( } continue; } - if (isChunkPos) { - host.declare(`declare ptr @scr_bytes_retain_v(ptr)`); - d.push(` %rc${i} = call ptr @scr_bytes_retain_v(ptr %chunk)`); + if (isChunkPos && p.kind === "bytes") { + // A Buffer, which an object-mode side's value must be (else a + // TypeError, thrown out of the write). + host.declare(`declare ptr @scr_stream_chunk_bytes(ptr, ptr)`); + d.push( + ` %rc${i} = call ptr @scr_stream_chunk_bytes(ptr %s, ptr %chunk)`, + ` %rcbad${i} = icmp eq ptr %rc${i}, null`, + ` br i1 %rcbad${i}, label %rcfail${i}, label %rcok${i}`, + `rcfail${i}:`, + ...(hasThis ? [` call void @scr_stream_release_v(ptr %sr)`] : []), + ` ret void`, + `rcok${i}:`, + ); + if (hasThis) host.declare(`declare void @scr_stream_release_v(ptr)`); passed.push(`ptr %rc${i}`); - } else if (isEncPos) { - // Node's encoding for decoded (Buffer) chunks is 'buffer'. - host.declare(`declare ptr @scr_str_new(ptr, ${host.sizeType})`); - d.push(` %en${i} = call ptr @scr_str_new(ptr ${host.cstr("buffer")}, ${host.sizeType} 6)`); + } else if (isChunkPos) { + // Any other declared type converts from the chunk's value (an + // object-mode side's value, or the Buffer), or throws out of the + // write. + const helper = host.dyn.dynCheckHelper(p, false); + const ty = host.llType(p); + host.declare(`declare ptr @scr_stream_chunk_value(ptr, ptr)`); + host.declare(`declare void @scr_dyn_release(ptr)`); + host.declare(`declare zeroext i1 @scr_exc_pending()`); + if (hasThis) host.declare(`declare void @scr_stream_release_v(ptr)`); + d.push( + ` %cv${i} = call ptr @scr_stream_chunk_value(ptr %s, ptr %chunk)`, + ` %ct${i} = call ${ty === "i1" ? "zeroext i1" : ty} @${helper}(ptr %cv${i}, ptr null)`, + ` call void @scr_dyn_release(ptr %cv${i})`, + ` %ctbad${i} = call zeroext i1 @scr_exc_pending()`, + ` br i1 %ctbad${i}, label %ctfail${i}, label %ctok${i}`, + `ctfail${i}:`, + ...(hasThis ? [` call void @scr_stream_release_v(ptr %sr)`] : []), + ` ret void`, + `ctok${i}:`, + ); + passed.push(`${ty} %ct${i}`); + } else if (isEncPos && p.kind === "string") { + // Node's encoding for decoded (Buffer) chunks is 'buffer'; an + // object-mode side passes the write's encoding argument ("" for + // undefined and null). + host.declare(`declare ptr @scr_stream_chunk_encoding(ptr)`); + d.push(` %en${i} = call ptr @scr_stream_chunk_encoding(ptr %s)`); passed.push(`ptr %en${i}`); + } else if (isEncPos) { + // Another declared type (string | null | undefined) converts from + // the encoding argument's value. + const helper = host.dyn.dynCheckHelper(p, false); + const ty = host.llType(p); + host.declare(`declare ptr @scr_stream_chunk_encoding_value(ptr)`); + host.declare(`declare void @scr_dyn_release(ptr)`); + host.declare(`declare zeroext i1 @scr_exc_pending()`); + if (hasThis) host.declare(`declare void @scr_stream_release_v(ptr)`); + d.push( + ` %ev${i} = call ptr @scr_stream_chunk_encoding_value(ptr %s)`, + ` %et${i} = call ${ty === "i1" ? "zeroext i1" : ty} @${helper}(ptr %ev${i}, ptr null)`, + ` call void @scr_dyn_release(ptr %ev${i})`, + ` %etbad${i} = call zeroext i1 @scr_exc_pending()`, + ` br i1 %etbad${i}, label %etfail${i}, label %etok${i}`, + `etfail${i}:`, + ...(hasThis ? [` call void @scr_stream_release_v(ptr %sr)`] : []), + ` ret void`, + `etok${i}:`, + ); + passed.push(`${ty} %et${i}`); } else if (isErrPos) { // destroy's error argument: `Error | null` — wrap type-directedly. // The finished/pipeline callback ("e") may declare `Error | null | diff --git a/packages/compiler/src/backend/llvm/lib-filesystem.ts b/packages/compiler/src/backend/llvm/lib-filesystem.ts index 6d5fe652d..7d9be632d 100644 --- a/packages/compiler/src/backend/llvm/lib-filesystem.ts +++ b/packages/compiler/src/backend/llvm/lib-filesystem.ts @@ -163,7 +163,7 @@ function emitObjEntryPairs(host: LlvmEmitterContext, e: LibCallExpr): LlValue { B.line(`${row} = call ptr @${mangleRecordNew(rowT.shapeId)}()`); host.storeField(host.recordFieldPtr(row, rowT.shapeId, "0").ptr, STRING, key, false); host.storeField(host.recordFieldPtr(row, rowT.shapeId, "1").ptr, DYN, item, false); - host.arrPush(arr, "ref", row); // push takes ownership of the row + host.arrPush(arr, "ref", row, null); // push takes ownership of the row const next = B.tmp(); B.line(`${next} = add ${size} ${i}, 1`); B.line(`store ${size} ${next}, ptr ${iSlot}`); @@ -536,7 +536,7 @@ export function emitFilesystemLibCall(host: LlvmEmitterContext, e: LibCallExpr): const parent = B.tmp(); B.line(`${parent} = call ptr @scr_fs_glob_parent(ptr ${snap}, ${host.sizeType} ${i}) ; +1`); B.line(`store ptr ${parent}, ptr ${host.recordFieldPtr(row, recT.shapeId, "parentPath").ptr}`); - host.arrPush(arr, "ref", row); // push takes ownership of the row + host.arrPush(arr, "ref", row, null); // push takes ownership of the row const i2 = B.tmp(); B.line(`${i2} = add ${host.sizeType} ${i}, 1`); B.line(`store ${host.sizeType} ${i2}, ptr ${iSlot}`); @@ -620,7 +620,7 @@ export function emitFilesystemLibCall(host: LlvmEmitterContext, e: LibCallExpr): B.line( `store ptr ${host.retainHeap(path.name, STRING)}, ptr ${host.recordFieldPtr(row, recT.shapeId, "parentPath").ptr}`, ); - host.arrPush(arr, "ref", row); // push takes ownership of the row + host.arrPush(arr, "ref", row, null); // push takes ownership of the row const i2 = B.tmp(); B.line(`${i2} = add ${host.sizeType} ${i}, 1`); B.line(`store ${host.sizeType} ${i2}, ptr ${iSlot}`); @@ -955,7 +955,7 @@ export function emitPathUrlLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll B.startBlock(lj); const rows = B.tmp(); B.line(`${rows} = load ptr, ptr ${rowsSlot}`); - host.arrPush(rows, "ref", row); // push takes ownership of the row + host.arrPush(rows, "ref", row, null); // push takes ownership of the row B.line(`call void @scr_arr_release(ptr ${rows})`); B.line(`call void @scr_str_release(ptr ${nm})`); const i2 = B.tmp(); diff --git a/packages/compiler/src/backend/llvm/lib-process.ts b/packages/compiler/src/backend/llvm/lib-process.ts index 699e52575..3e8a9bff5 100644 --- a/packages/compiler/src/backend/llvm/lib-process.ts +++ b/packages/compiler/src/backend/llvm/lib-process.ts @@ -924,32 +924,41 @@ export function emitErrorsEventsLibCall(host: LlvmEmitterContext, e: LibCallExpr } if (e.fn === "emitter.emitData") { // A user emit('data', chunk) on a stream-rooted receiver: fill the - // matching payload slot of the two-slot 'data' ABI, NULL the other. + // matching payload slot of the three-slot 'data' ABI (bytes, string, + // object-mode value), NULL the others; a checked-dynamic chunk fills + // them by tag (scr_stream_emit_value). const args = e.args.map((a) => host.emitExpr(a)); const chunkT = e.args[2]!.type; - const both = - chunkT.kind === "string" - ? [`ptr null`, `ptr ${args[2]!.name}`] - : [`ptr ${args[2]!.name}`, `ptr null`]; - host.declare(`declare zeroext i1 @scr_emitter_emit(ptr, ptr, ...)`); const t = B.tmp(); - B.line( - `${t} = call zeroext i1 (ptr, ptr, ...) @scr_emitter_emit(ptr ${args[0]!.name}, ptr ${args[1]!.name}, ${both.join(", ")})`, - ); + if (chunkT.kind === "dyn") { + host.declare(`declare zeroext i1 @scr_stream_emit_value(ptr, ptr, ptr)`); + B.line( + `${t} = call zeroext i1 @scr_stream_emit_value(ptr ${args[0]!.name}, ptr ${args[1]!.name}, ptr ${args[2]!.name})`, + ); + } else { + const slots = + chunkT.kind === "string" + ? [`ptr null`, `ptr ${args[2]!.name}`, `ptr null`] + : [`ptr ${args[2]!.name}`, `ptr null`, `ptr null`]; + host.declare(`declare zeroext i1 @scr_emitter_emit(ptr, ptr, ...)`); + B.line( + `${t} = call zeroext i1 (ptr, ptr, ...) @scr_emitter_emit(ptr ${args[0]!.name}, ptr ${args[1]!.name}, ${slots.join(", ")})`, + ); + } host.emitPendingCheck(); return e.type.kind === "void" ? { name: "", type: e.type } : { name: t, type: e.type }; } if (e.fn === "emitter.onData" || e.fn === "emitter.onDataDyn") { // The stream-'data' registration: same runtime entries as - // emitter.on/onDyn, but the DATA adapter (the two-slot payload ABI - // — scr_stream_emit_data) behind the arity-2 fixed shim. + // emitter.on/onDyn, but the DATA adapter (the three-slot payload ABI + // — scr_stream_emit_data) behind the arity-3 fixed shim. const isDyn = e.fn === "emitter.onDataDyn"; const cbT = e.args[isDyn ? 3 : 2]!.type; if (cbT.kind !== "func") throw new InternalCompilerError(`llvm emitter bug: ${e.fn} listener not a func`); const args = e.args.map((a) => host.emitExpr(a)); const adapterFn = host.streamDataAdapter(cbT); - host.declare(`declare void @scr_ee_inv_fixed2(ptr, ptr)`); + host.declare(`declare void @scr_ee_inv_fixed3(ptr, ptr)`); const t = B.tmp(); if (isDyn) { host.toHeap(args[3]!); @@ -959,7 +968,7 @@ export function emitErrorsEventsLibCall(host: LlvmEmitterContext, e: LibCallExpr ); B.line( `${t} = call ptr @scr_emitter_on_dyn(ptr ${args[0]!.name}, ptr ${args[1]!.name}, ` + - `ptr ${args[2]!.name}, ptr ${wrapped}, ptr @scr_ee_inv_fixed2, i1 ${args[4]!.name}, i1 ${args[5]!.name})`, + `ptr ${args[2]!.name}, ptr ${wrapped}, ptr @scr_ee_inv_fixed3, i1 ${args[4]!.name}, i1 ${args[5]!.name})`, ); } else { host.declare(`declare ptr @scr_closure_retain_v(ptr)`); @@ -972,7 +981,7 @@ export function emitErrorsEventsLibCall(host: LlvmEmitterContext, e: LibCallExpr ); B.line( `${t} = call ptr @scr_emitter_on_via(ptr ${args[0]!.name}, ptr ${args[1]!.name}, ` + - `ptr ${args[2]!.name}, ptr ${wrapped}, ptr @scr_ee_inv_fixed2, i1 ${args[3]!.name}, i1 ${args[4]!.name})`, + `ptr ${args[2]!.name}, ptr ${wrapped}, ptr @scr_ee_inv_fixed3, i1 ${args[3]!.name}, i1 ${args[4]!.name})`, ); } const out = host.own({ name: t, type: e.type }); diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index c08d7b720..8c598a3d3 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -47,7 +47,7 @@ export const LIB_FN_SYMS: Record = { "dyn.fromEntries": "scr_dyn_from_entries", "bytes.constructor": "scr_bytes_constructor", "bytes.construct": "scr_bytes_construct", - "bytes.instanceOf": "scr_bytes_instanceof", + "bytes.instanceOf": "scr_bytes_instanceof_at", "arrayBuffer.constructor": "scr_array_buffer_constructor", "sharedArrayBuffer.new": "scr_shared_array_buffer_new", "sharedArrayBuffer.is": "scr_shared_array_buffer_is", @@ -578,6 +578,12 @@ export const LIB_FN_SYMS: Record = { "crypto.cipherNew": "scr_crypto_cipher_new", "crypto.ecdhNew": "scr_crypto_ecdh_new", "crypto.ecdhConvertKey": "scr_crypto_ecdh_convert_key", + "crypto.dhNew": "scr_crypto_dh_new", + "crypto.checkPrimeSync": "scr_crypto_check_prime_sync", + "crypto.checkPrimeCb": "scr_crypto_check_prime_async", + "crypto.generatePrimeSync": "scr_crypto_generate_prime_sync", + "crypto.generatePrimeCb": "scr_crypto_generate_prime_async", + "crypto.dhGroupNew": "scr_crypto_dh_group_new", "crypto.hmacNewStr": "scr_crypto_hmac_new_str", "crypto.hmacNewBytes": "scr_crypto_hmac_new_bytes", "crypto.hashUpdateStr": "scr_crypto_hash_update_str", @@ -615,11 +621,13 @@ export const LIB_FN_SYMS: Record = { "crypto.generateKeySync": "scr_crypto_generate_key_sync", "crypto.keyObjectInput": "scr_crypto_key_object_input", "crypto.isKeyObject": "scr_crypto_is_key_object", - "webcrypto.importKey": "scr_webcrypto_import_key", "webcrypto.generateKey": "scr_webcrypto_generate_key", + "webcrypto.importKey": "scr_webcrypto_import_key", "webcrypto.exportKey": "scr_webcrypto_export_key", "webcrypto.encrypt": "scr_webcrypto_encrypt", "webcrypto.decrypt": "scr_webcrypto_decrypt", + "webcrypto.wrapKey": "scr_webcrypto_wrap_key", + "webcrypto.unwrapKey": "scr_webcrypto_unwrap_key", "webcrypto.deriveBits": "scr_webcrypto_derive_bits", "webcrypto.deriveKey": "scr_webcrypto_derive_key", "webcrypto.sign": "scr_webcrypto_sign", @@ -1039,6 +1047,14 @@ export const LIB_FN_SYMS: Record = { "crypto.x509ValidFromStr": "scr_crypto_x509_valid_from_str", "crypto.x509ValidTo": "scr_crypto_x509_valid_to", "crypto.x509ValidToStr": "scr_crypto_x509_valid_to_str", + "crypto.createPrivateKey": "scr_crypto_create_private_key", + "crypto.createPublicKey": "scr_crypto_create_public_key", + "crypto.generateKeyPairSync": "scr_crypto_generate_key_pair_sync", + "crypto.generateKeyPairCb": "scr_crypto_generate_key_pair_async", + "crypto.signOneshot": "scr_crypto_sign_oneshot", + "crypto.verifyOneshot": "scr_crypto_verify_oneshot", + "crypto.createSign": "scr_crypto_create_sign", + "crypto.createVerify": "scr_crypto_create_verify", "fs.writeFileModeSync": "scr_fs_write_file_mode", "fs.chownSync": "scr_fs_chown", "fs.lchownSync": "scr_fs_lchown", @@ -1113,10 +1129,13 @@ export const LIB_FN_SYMS: Record = { "readable.pushStr": "scr_stream_push_str", "readable.pushStrEnc": "scr_stream_push_str_enc", "readable.pushEncoding": "scr_stream_set_push_encoding", + "stream.objectMode": "scr_stream_object_mode", + "stream.chunkDone": "scr_stream_chunk_done", "readable.pushNull": "scr_stream_push_null", "readable.pushDyn": "scr_stream_push_dyn", "readable.unshift": "scr_stream_unshift", "readable.unshiftStr": "scr_stream_unshift_str", + "readable.unshiftDyn": "scr_stream_unshift_dyn", "readable.pause": "scr_stream_pause", "readable.resume": "scr_stream_resume", "readable.isPaused": "scr_stream_is_paused", @@ -1584,6 +1603,9 @@ export function mockTimerSym( } export const USES_TIMERS_LIB_FNS = new Set([ + "crypto.generateKeyPairCb", + "crypto.checkPrimeCb", + "crypto.generatePrimeCb", "process.emitWarning", // Constructor-function streams defer their events on the stream ticks. "dyn.builtinStreamClass", @@ -1595,6 +1617,8 @@ export const USES_TIMERS_LIB_FNS = new Set([ "webcrypto.generateKey", "webcrypto.encrypt", "webcrypto.decrypt", + "webcrypto.wrapKey", + "webcrypto.unwrapKey", "webcrypto.deriveBits", "webcrypto.deriveKey", "webcrypto.sign", @@ -1667,6 +1691,7 @@ export const USES_TIMERS_LIB_FNS = new Set([ "readable.pushDyn", "readable.unshift", "readable.unshiftStr", + "readable.unshiftDyn", "readable.read", "readable.readDyn", "readable.setEncoding", @@ -1698,6 +1723,7 @@ export const USES_TIMERS_LIB_FNS = new Set([ "console.write", "writable.writeU", "writable.writeDyn", + "writable.writeEnc", "writable.end", "writable.uncork", "stream.destroy", diff --git a/packages/compiler/src/backend/llvm/lib-stream.ts b/packages/compiler/src/backend/llvm/lib-stream.ts index 5d198d306..6d6c6f99c 100644 --- a/packages/compiler/src/backend/llvm/lib-stream.ts +++ b/packages/compiler/src/backend/llvm/lib-stream.ts @@ -419,10 +419,34 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV host.emitPendingCheck(); return e.type.kind === "void" ? { name: "", type: e.type } : { name: t, type: e.type }; } + if (e.fn === "writable.writeEnc") { + // (recv, flags, chunk[, encoding][, cb]) — flags: 1 encoding, 2 + // callback (MOVES). Without an encoding the argument is undefined, or + // null when the callback took its place. + const flagsArg = e.args[1]!; + if (flagsArg.kind !== "numLit") + throw new InternalCompilerError("llvm emitter bug: writable.writeEnc flags not a literal"); + const flags = flagsArg.value; + const args = e.args.map((a) => host.emitExpr(a)); + let at = 3; + const enc = flags & 1 ? args[at++]!.name : writeEncNull(host, flags & 2 ? "null" : "undef"); + let cb = "null"; + if (flags & 2) { + host.toHeap(args[at]!); + cb = args[at]!.name; + } + host.declare(`declare zeroext i1 @scr_stream_write_enc(ptr, ptr, ptr, ptr)`); + const t = B.tmp(); + B.line( + `${t} = call zeroext i1 @scr_stream_write_enc(ptr ${args[0]!.name}, ptr ${args[2]!.name}, ptr ${enc}, ptr ${cb})`, + ); + host.emitPendingCheck(); + return e.type.kind === "void" ? { name: "", type: e.type } : { name: t, type: e.type }; + } if (e.fn === "writable.end") { - // (recv, flags[, chunk][, cb]) — flags: 1 bytes chunk, 2 string - // chunk, 8 dyn chunk (write first, Node's end(chunk) decomposition), - // 4 callback. + // (recv, flags[, chunk][, encoding][, cb]) — flags: 1 bytes chunk, 2 + // string chunk, 8 dyn chunk (write first, Node's end(chunk) + // decomposition), 16 an encoding after the dyn chunk, 4 callback. const flagsArg = e.args[1]!; if (flagsArg.kind !== "numLit") throw new InternalCompilerError("llvm emitter bug: writable.end flags not a literal"); @@ -432,19 +456,27 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV let chunkB = "null"; let chunkS = "null"; let chunkD: string | null = null; + let encD = "null"; if (flags & 1) chunkB = args[at++]!.name; else if (flags & 2) chunkS = args[at++]!.name; else if (flags & 8) chunkD = args[at++]!.name; + if (flags & 16) encD = args[at++]!.name; + else if (chunkD !== null && flags & 4) encD = writeEncNull(host, "null"); let cbName = "null"; if (flags & 4) { host.toHeap(args[at]!); cbName = args[at]!.name; } if (chunkD !== null) { - host.declare(`declare zeroext i1 @scr_stream_write_dyn(ptr, ptr, ptr)`); + // end(chunk, cb) writes with a null encoding argument (Node's end + // shifts the callback out of it) + host.declare(`declare zeroext i1 @scr_stream_write_enc(ptr, ptr, ptr, ptr)`); B.line( - `${B.tmp()} = call zeroext i1 @scr_stream_write_dyn(ptr ${args[0]!.name}, ptr ${chunkD}, ptr null)`, + `${B.tmp()} = call zeroext i1 @scr_stream_write_enc(ptr ${args[0]!.name}, ptr ${chunkD}, ptr ${encD}, ptr null)`, ); + // a write that throws (an unknown encoding) leaves the stream as it + // was: Node's end() throws before it ends the stream + host.emitPendingCheck(); } host.declare(`declare ptr @scr_stream_end(ptr, ptr, ptr, ptr)`); const t = B.tmp(); @@ -771,3 +803,13 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV // adapters; the handful of union-wrapped reads use the envGet shapes. return host.emitGenericLibCall(e); } + +/** The encoding argument a write passes when the call has none: undefined + * (a null pointer), or the null value when a callback took its place. */ +function writeEncNull(host: LlvmEmitterContext, which: "null" | "undef"): string { + if (which === "undef") return "null"; + host.declare(`declare ptr @scr_dyn_new_null()`); + const t = host.B.tmp(); + host.B.line(`${t} = call ptr @scr_dyn_new_null()`); + return t; +} diff --git a/packages/compiler/src/backend/llvm/lib-tls.ts b/packages/compiler/src/backend/llvm/lib-tls.ts index c5c77d3a6..1d1cf659c 100644 --- a/packages/compiler/src/backend/llvm/lib-tls.ts +++ b/packages/compiler/src/backend/llvm/lib-tls.ts @@ -121,6 +121,13 @@ export function emitTlsLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValu case "tls.sockOnSecureConnect": host.toHeap(args[1]!); return finishRuntimeCall(host, e, "scr_tls_sock_on_secure_connect", a); + case "tls.sockOnSecure": + host.toHeap(args[1]!); + return finishRuntimeCall(host, e, "scr_tls_sock_on_secure", a); + case "tls.wrapSocket": + return finishRuntimeCall(host, e, "scr_tls_wrap_socket", [a[0]!, a[1]!, ptr()]); + case "tls.wrapSocketCtx": + return finishRuntimeCall(host, e, "scr_tls_wrap_socket", a); case "tls.sockOnSession": { const cb = args[1]!, type = callbackType(cb); diff --git a/packages/compiler/src/backend/llvm/local-array-reads.test.ts b/packages/compiler/src/backend/llvm/local-array-reads.test.ts index b09029865..7af495d42 100644 --- a/packages/compiler/src/backend/llvm/local-array-reads.test.ts +++ b/packages/compiler/src/backend/llvm/local-array-reads.test.ts @@ -940,3 +940,17 @@ test("a dense read tests the hole-free prefix before the capacity", () => { expect(ir.lastIndexOf("local.array.range", firstCap)).toBeGreaterThan(firstFilled); } }); + +test("an optional read at a proven integer index skips the double round trip", () => { + const module = fixture(); + const call = module.functions[2]!.body[0]!; + if (call.kind !== "varDecl" || call.init?.kind !== "call") throw new Error("missing call"); + call.init.args[1] = { kind: "bin", op: "|", left: ref("i", F64), right: num(0), type: F64, loc }; + expect(candidates(module).get("value")?.borrow).toBe(true); + expect(validateModule(module)).toEqual([]); + // The int32 index is the dense offset on 64-bit targets; a 32-bit target + // converts the double, as a borrowed read does. + expect(workBody(module, 64)).not.toContain("local.array.exact"); + expect(workBody(module, 64)).toContain("local.array.value"); + expect(workBody(module, 32)).toContain("local.array.exact"); +}); diff --git a/packages/compiler/src/backend/llvm/local-array-reads.ts b/packages/compiler/src/backend/llvm/local-array-reads.ts index 45d046e1f..8df187736 100644 --- a/packages/compiler/src/backend/llvm/local-array-reads.ts +++ b/packages/compiler/src/backend/llvm/local-array-reads.ts @@ -15,6 +15,13 @@ import { ReferenceEffects } from "./reference-effects.js"; import { SCR_ARR_FIELD, loadArrCount, scrArrStateAt } from "./common.js"; import { exactInteger, widenInteger } from "./integer-values.js"; import { READ_FALLBACK } from "./dense-array-access.js"; +import { compactRefLoadLines } from "./classes.js"; + +/** The dense slot load of a reference read from an array of type + * `arrayType`: a pointer, or a compact class reference (SCR_ELEM_CREF). */ +function referenceLoad(host: LlvmEmitterContext, arrayType: IrType): "ptr" | "cref" { + return arrayType.kind === "array" && host.compactArrayElem(arrayType.elem) ? "cref" : "ptr"; +} export interface LocalArrayRead { type: IrType; @@ -469,7 +476,7 @@ function emitDenseReferenceArrayRead( no: string, slow: string, join: string, - loadType: "ptr" | "i64" | "i8" = "ptr", + loadType: "ptr" | "cref" | "i64" | "i8" = "ptr", ): void { const B = host.B; const header = (field: number): string => { @@ -555,7 +562,14 @@ function emitDenseReferenceArrayRead( const statePtr = B.tmp(), denseState = B.tmp(), densePresent = B.tmp(); - scrArrStateAt(host, statePtr, data, cap, loadType === "i8" ? 1 : 8, offset); + scrArrStateAt( + host, + statePtr, + data, + cap, + loadType === "i8" ? 1 : loadType === "cref" ? 4 : 8, + offset, + ); host.markMemoryPointer(statePtr, "array:present"); B.line(`${denseState} = load i8, ptr ${statePtr}${host.fieldAliasAttachment(statePtr)}`); B.line(`${densePresent} = icmp eq i8 ${denseState}, 1`); @@ -564,13 +578,20 @@ function emitDenseReferenceArrayRead( B.startBlock(valueLabel); const valuePtr = B.tmp(), raw = B.tmp(); - // Boolean arrays store one byte per element; every other kind an 8-byte slot. - const slotType = loadType === "i8" ? "i8" : "i64"; + // Boolean arrays store one byte per element, compact references four, + // every other kind an 8-byte slot. + const slotType = loadType === "i8" ? "i8" : loadType === "cref" ? "i32" : "i64"; B.line( `${valuePtr} = getelementptr inbounds ${slotType}, ptr ${data}, ${host.sizeType} ${offset}`, ); host.markMemoryPointer(valuePtr, "array:elements"); - B.line(`${raw} = load ${loadType}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`); + if (loadType === "cref") { + const ref = B.tmp(); + B.line(`${ref} = load i32, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`); + for (const line of compactRefLoadLines(ref, raw, `${raw}.r`)) B.line(line.trimStart()); + } else { + B.line(`${raw} = load ${loadType}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`); + } present(raw); B.br(join); } @@ -586,6 +607,36 @@ export function emitBorrowedArrayRead( const B = host.B; const array = host.emitStableReceiver(read.arr, [read.index]); const loopIndex = host.emitIntegerLoopIndex(read.index); + if (loopIndex !== null && host.provenDenseIndex(read.arr, read.index)) { + // In a dense array loop's fast copy, the entry test proved the counter + // below the array's hole-free prefix: the slot holds a present value. + const dataPtr = B.tmp(), + data = B.tmp(), + valuePtr = B.tmp(), + value = B.tmp(); + B.line( + `${dataPtr} = getelementptr inbounds %ScrArr, ptr ${array.name}, i32 0, i32 ${SCR_ARR_FIELD.data}`, + ); + host.markMemoryPointer(dataPtr, "array:header"); + B.line(`${data} = load ptr, ptr ${dataPtr}${host.fieldAliasAttachment(dataPtr)}`); + if (referenceLoad(host, read.arr.type) === "cref") { + // Compact references: 4-byte slots holding the address over 8. + const ref = B.tmp(); + B.line( + `${valuePtr} = getelementptr inbounds i32, ptr ${data}, ${host.sizeType} ${loopIndex}`, + ); + host.markMemoryPointer(valuePtr, "array:elements"); + B.line(`${ref} = load i32, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`); + for (const line of compactRefLoadLines(ref, value, `${value}.r`)) B.line(line.trimStart()); + B.line(`store ptr ${value}, ptr ${slot}`); + return; + } + B.line(`${valuePtr} = getelementptr inbounds i64, ptr ${data}, ${host.sizeType} ${loopIndex}`); + host.markMemoryPointer(valuePtr, "array:elements"); + B.line(`${value} = load ptr, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`); + B.line(`store ptr ${value}, ptr ${slot}`); + return; + } const index = host.emitExpr(read.index); // A proven integer index (an int32 local, say) needs no double // round-trip; a negative one compares above the capacity as unsigned. @@ -604,6 +655,7 @@ export function emitBorrowedArrayRead( slow, slow, join, + referenceLoad(host, read.arr.type), ); B.startBlock(slow); host.declare("declare ptr @scr_arr_borrow_ref(ptr, double)"); @@ -626,8 +678,15 @@ export function emitLocalArrayRead( ): { slot: string; type: IrType } | null { const B = host.B; const array = host.emitStableReceiver(read.array, [read.index]); - const integerIndex = inline ? host.emitIntegerLoopIndex(read.index) : null; + const loopIndex = inline ? host.emitIntegerLoopIndex(read.index) : null; const index = host.emitExpr(read.index); + // A proven integer index (a counter with a known range, say) needs no + // double round trip, as in a borrowed read. + const exact = + inline && loopIndex === null && host.sizeType === "i64" + ? exactInteger(host, index, read.index) + : null; + const integerIndex = loopIndex ?? (exact ? widenInteger(host, exact) : null); const nullable = read.type.kind === "union" ? host.nullableUnions.get(read.type.unionId) : null; if (nullable) { // A nullable union is the element pointer itself: borrowed from an @@ -647,12 +706,12 @@ export function emitLocalArrayRead( array, index, integerIndex, - integerIndex !== null, + loopIndex !== null, store, no, slow, join, - "ptr", + referenceLoad(host, read.array.type), ); else B.br(slow); B.startBlock(slow); @@ -708,12 +767,12 @@ export function emitLocalArrayRead( array, index, integerIndex, - integerIndex !== null, + loopIndex !== null, storeValue, no, slow, join, - !scalar ? "ptr" : read.element.kind === "bool" ? "i8" : "i64", + !scalar ? referenceLoad(host, read.array.type) : read.element.kind === "bool" ? "i8" : "i64", ); else B.br(slow); B.startBlock(slow); diff --git a/packages/compiler/src/backend/llvm/map-number-lookup.test.ts b/packages/compiler/src/backend/llvm/map-number-lookup.test.ts index 5dc3589e4..989b8b3b0 100644 --- a/packages/compiler/src/backend/llvm/map-number-lookup.test.ts +++ b/packages/compiler/src/backend/llvm/map-number-lookup.test.ts @@ -181,3 +181,63 @@ test("hashed number-key lookups probe inline and call the runtime only for wide ); } }); + +/** work(map): map.size, and keep(map): map returned as an owned value. */ +function sizeFixture(): IrModule { + const map: IrType = { kind: "map", key: F64, value: F64 }; + const params = [{ localId: "map", name: "map", type: map }]; + const locals = params.map((p) => ({ id: p.localId, name: p.name, type: p.type, mutable: true })); + const size: IrExpr = { + kind: "mapIntrinsic", + method: "size", + receiver: ref("map", map), + args: [], + type: F64, + loc, + }; + return { + irVersion: 17, + sourceFile: loc.file, + entry: "main", + records: [], + unions: [], + functions: [ + { name: "main", params: [], locals: [], returnType: VOID, body: [], loc }, + { + name: "work", + params, + returnType: F64, + loc, + locals, + body: [{ kind: "return", value: size, loc }], + }, + { + name: "keep", + params, + returnType: map, + loc, + locals, + body: [{ kind: "return", value: ref("map", map), loc }], + }, + ], + }; +} + +test("Map.size and Map retains need no runtime call in release builds", () => { + const mod = sizeFixture(); + expect(validateModule(mod)).toEqual([]); + for (const bits of [32, 64] as const) { + const module = emitLlvmModule(mod, { pointerBits: bits, inlineRc: "release" }); + const fn = (name: string) => + new RegExp(`^define internal [^\\n]*@sc_(?:b)?f_${name}(?:\\.i32)?\\([^]*?^}`, "m").exec( + module, + )![0]; + // Field 9 is the live entry count (scr_map.h). + expect(module).toMatch(/^define internal double @sc_map_size\(ptr %m\)/m); + expect(module).toContain(`getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 9`); + expect(fn("work")).toContain("call double @sc_map_size("); + expect(module).not.toContain("call double @scr_map_size("); + expect(fn("keep")).toContain("@sc_rc_retain_map("); + expect(module).not.toContain("call ptr @scr_map_retain_v("); + } +}); diff --git a/packages/compiler/src/backend/llvm/map-number-lookup.ts b/packages/compiler/src/backend/llvm/map-number-lookup.ts index 1244bd9c3..6d219b50a 100644 --- a/packages/compiler/src/backend/llvm/map-number-lookup.ts +++ b/packages/compiler/src/backend/llvm/map-number-lookup.ts @@ -15,6 +15,12 @@ * retain inline rather than through the runtime's indirect adapter call, * and scalar reads and has() share the one lookup. * + * A string-key map whose values are compact class instances + * (SCR_MAP_VAL_CREF, LlEmitter.compactArrayElem of the value type) keeps + * 16-byte { key, hash, val } entries with the value as an address over 8: + * its reads go through sc_map_entry_str_c and sc_map_iter_entry_c, and + * decode the 4-byte value. + * * The iteration steps behind for-of, forEach and the other entry walks read * the entries array through the internal sc_map_iter_entry and * sc_map_iter_count helpers: a live entry's key or value loads in place, @@ -24,6 +30,7 @@ */ import type { IrType } from "../../ir/ir.js"; import type { LlvmEmitterContext } from "./expr-context.js"; +import { mapKeyAccess } from "./shapes.js"; export const NUMBER_MAP_ENTRY_DECL = "declare ptr @scr_map_entry_f64(ptr, double)"; @@ -47,18 +54,59 @@ export function emitNumberMapSlot(host: LlvmEmitterContext, entry: string, type: } export const STRING_MAP_ENTRY_DECL = "declare ptr @scr_map_entry_str(ptr, ptr)"; +/** Declared by reads of a compact-value map; its presence makes the emitter + * define sc_map_entry_str_c and sc_map_iter_entry_c (emit-module.ts). */ +export const STRING_MAP_ENTRY_C_DECL = "declare ptr @scr_map_entry_str_c(ptr, ptr)"; -/** The entry for a string `key`, or null when absent. Valid until the next mutation. */ -export function emitStringMapEntry(host: LlvmEmitterContext, map: string, key: string): string { - host.declare(STRING_MAP_ENTRY_DECL); +/** Whether a Map with key type `key` and value type `value` keeps compact + * values (SCR_MAP_VAL_CREF): string keys and a value type whose instances + * are compact (LlEmitter.compactArrayElem). */ +export function compactMapValues(host: LlvmEmitterContext, key: IrType, value: IrType): boolean { + // SCR_MAP_KEY_STR (shapes.ts mapKeyKindNum 1): string access, not a union. + return key.kind !== "union" && mapKeyAccess(key) === "str" && host.compactArrayElem(value); +} + +/** The entry for a string `key`, or null when absent. Valid until the next + * mutation. `compact`: the map keeps compact values (a %ScrMapCEntry). */ +export function emitStringMapEntry( + host: LlvmEmitterContext, + map: string, + key: string, + compact = false, +): string { + host.declare(compact ? STRING_MAP_ENTRY_C_DECL : STRING_MAP_ENTRY_DECL); const entry = host.B.tmp(); - host.B.line(`${entry} = call ptr @sc_map_entry_str(ptr ${map}, ptr ${key})`); + host.B.line( + `${entry} = call ptr @${compact ? "sc_map_entry_str_c" : "sc_map_entry_str"}(ptr ${map}, ptr ${key})`, + ); return entry; } +/** A found compact-value entry's value as a pointer: its 4-byte form, an + * address over 8 (scr_map.h ScrMapCEntry). */ +export function emitCompactMapSlot(host: LlvmEmitterContext, entry: string): string { + const B = host.B; + const slot = B.tmp(), + ref = B.tmp(), + wide = B.tmp(), + addr = B.tmp(), + value = B.tmp(); + B.line(`${slot} = getelementptr inbounds %ScrMapCEntry, ptr ${entry}, i32 0, i32 2`); + B.line(`${ref} = load i32, ptr ${slot}`); + B.line(`${wide} = zext i32 ${ref} to i64`); + B.line(`${addr} = shl nuw nsw i64 ${wide}, 3`); + B.line(`${value} = inttoptr i64 ${addr} to ptr`); + return value; +} + /** Map.has / Set.has under a string key. */ -export function emitStringMapHas(host: LlvmEmitterContext, map: string, key: string): string { - const entry = emitStringMapEntry(host, map, key); +export function emitStringMapHas( + host: LlvmEmitterContext, + map: string, + key: string, + compact = false, +): string { + const entry = emitStringMapEntry(host, map, key, compact); const found = host.B.tmp(); host.B.line(`${found} = icmp ne ptr ${entry}, null`); return found; @@ -92,12 +140,18 @@ export function emitStringMapGetRef( map: string, key: string, value: IrType, + compact = false, ): string { - return emitMapEntryGetRef(host, emitStringMapEntry(host, map, key), value); + return emitMapEntryGetRef(host, emitStringMapEntry(host, map, key, compact), value, compact); } /** The +1 value of a found entry, or null for a missing (null) entry. */ -function emitMapEntryGetRef(host: LlvmEmitterContext, entry: string, value: IrType): string { +function emitMapEntryGetRef( + host: LlvmEmitterContext, + entry: string, + value: IrType, + compact = false, +): string { const B = host.B; const found = B.tmp(); B.line(`${found} = icmp ne ptr ${entry}, null`); @@ -106,7 +160,10 @@ function emitMapEntryGetRef(host: LlvmEmitterContext, entry: string, value: IrTy join = B.newLabel("mapn.join"); B.condBr(found, hit, miss); B.startBlock(hit); - const retained = host.retainFrame(emitNumberMapSlot(host, entry, "ptr"), value); + const retained = host.retainFrame( + compact ? emitCompactMapSlot(host, entry) : emitNumberMapSlot(host, entry, "ptr"), + value, + ); B.br(join); B.startBlock(miss); B.br(join); @@ -199,20 +256,25 @@ export const SCR_MAP_NUM_DEAD = "9218868437227405313"; /** The entry layout a Map or Set of key type `key` stores: * - "num": number keys, whose entries are { key, val } (scr_map.h ScrMapNumEntry); + * - "centry": string keys with compact values (compactMapValues), whose + * entries are { key, hash, val } (ScrMapCEntry); * - "entry": every other typed key, whose entries are { key, val, hash }; * - "either": checked (`unknown`) keys. A `Map` or * `Set` cast from a checked value keeps the collection's own - * storage (scr_dyn_native_collection_check), which can have number keys. */ -export type MapEntryLayout = "num" | "entry" | "either"; + * storage (scr_dyn_native_collection_check), which can have number keys + * or compact values. */ +export type MapEntryLayout = "num" | "centry" | "entry" | "either"; -export function mapEntryLayout(key: IrType): MapEntryLayout { +export function mapEntryLayout(key: IrType, compact = false): MapEntryLayout { + if (compact) return "centry"; return key.kind === "f64" ? "num" : key.kind === "dyn" ? "either" : "entry"; } /** The entry at an iteration index, or null for a tombstone or an index at * or past the entry count (the scr_map_iter_live test). With the "either" * layout, the map's key kind (%ScrMapIx field 1, SCR_MAP_KEY_F64 = 0) picks - * the step at run time. */ + * the step at run time; in a program with compact class instances, so does + * its value kind (field 2, SCR_MAP_VAL_CREF = 3). */ export function emitMapIterEntry( host: LlvmEmitterContext, map: string, @@ -220,14 +282,20 @@ export function emitMapIterEntry( layout: MapEntryLayout, ): string { const B = host.B; - const step = (num: boolean): string => { - host.declare(num ? MAP_ITER_NUM_DECL : MAP_ITER_DECL); + const step = (kind: "num" | "centry" | "entry"): string => { + host.declare(kind === "num" ? MAP_ITER_NUM_DECL : MAP_ITER_DECL); + if (kind === "centry") host.declare(STRING_MAP_ENTRY_C_DECL); const entry = B.tmp(); - const helper = num ? "sc_map_iter_entry_num" : "sc_map_iter_entry"; + const helper = + kind === "num" + ? "sc_map_iter_entry_num" + : kind === "centry" + ? "sc_map_iter_entry_c" + : "sc_map_iter_entry"; B.line(`${entry} = call ptr @${helper}(ptr ${map}, double ${index})`); return entry; }; - if (layout !== "either") return step(layout === "num"); + if (layout !== "either") return step(layout); host.declare(MAP_ITER_DECL); const kindPtr = B.tmp(), kind = B.tmp(), @@ -240,16 +308,31 @@ export function emitMapIterEntry( join = B.newLabel("mapit.layout"); B.condBr(isNum, numLabel, entryLabel); B.startBlock(numLabel); - const numEntry = step(true); + const numEntry = step("num"); const numEnd = B.currentLabel(); B.br(join); B.startBlock(entryLabel); - const otherEntry = step(false); - const entryEnd = B.currentLabel(); + const arms: { value: string; label: string }[] = [{ value: numEntry, label: numEnd }]; + if (host.shapeHost.compactRegion?.() === true) { + const valPtr = B.tmp(), + valKind = B.tmp(), + isCompact = B.tmp(); + B.line(`${valPtr} = getelementptr inbounds %ScrMapIx, ptr ${map}, i32 0, i32 2`); + B.line(`${valKind} = load i8, ptr ${valPtr}`); + B.line(`${isCompact} = icmp eq i8 ${valKind}, 3`); + const compactLabel = B.newLabel("mapit.centry"), + wideLabel = B.newLabel("mapit.wide"); + B.condBr(isCompact, compactLabel, wideLabel); + B.startBlock(compactLabel); + arms.push({ value: step("centry"), label: B.currentLabel() }); + B.br(join); + B.startBlock(wideLabel); + } + arms.push({ value: step("entry"), label: B.currentLabel() }); B.br(join); B.startBlock(join); const entry = B.tmp(); - B.line(`${entry} = phi ptr [ ${numEntry}, %${numEnd} ], [ ${otherEntry}, %${entryEnd} ]`); + B.line(`${entry} = phi ptr ${arms.map((a) => `[ ${a.value}, %${a.label} ]`).join(", ")}`); return entry; } @@ -270,6 +353,16 @@ export function emitMapIterCount(host: LlvmEmitterContext, map: string): string return count; } +export const MAP_SIZE_DECL = "declare double @scr_map_size(ptr)"; + +/** Map.size / Set.size: the live entry count, read inline (sc_map_size). */ +export function emitMapSize(host: LlvmEmitterContext, map: string): string { + host.declare(MAP_SIZE_DECL); + const size = host.B.tmp(); + host.B.line(`${size} = call double @sc_map_size(ptr ${map})`); + return size; +} + /** The key (field 0) or value (field 1) of the live entry at an iteration * index, read inline: a number as double, a boolean as i1, and a string or * reference retained with `type`'s retain (+1), the adapter the collection @@ -294,18 +387,24 @@ export function emitMapIterSlot( join = B.newLabel("mapit.join"); B.condBr(found, hit, miss); B.startBlock(hit); - const slot = B.tmp(); - B.line(`${slot} = getelementptr inbounds %ScrMapEntry, ptr ${entry}, i32 0, i32 ${field}`); const llTy = access === "f64" ? "double" : access === "bool" ? "i1" : "ptr"; - let loaded = B.tmp(); - if (access === "bool") { - const bits = loaded; - B.line(`${bits} = load i64, ptr ${slot}`); - loaded = B.tmp(); - B.line(`${loaded} = icmp ne i64 ${bits}, 0`); + let loaded: string; + if (layout === "centry" && field === 1) { + // A compact value; its type's retain emits no counting. + loaded = host.retainFrame(emitCompactMapSlot(host, entry), type); } else { - B.line(`${loaded} = load ${llTy}, ptr ${slot}`); - if (access === "ref") loaded = host.retainFrame(loaded, type); + const slot = B.tmp(); + B.line(`${slot} = getelementptr inbounds %ScrMapEntry, ptr ${entry}, i32 0, i32 ${field}`); + loaded = B.tmp(); + if (access === "bool") { + const bits = loaded; + B.line(`${bits} = load i64, ptr ${slot}`); + loaded = B.tmp(); + B.line(`${loaded} = icmp ne i64 ${bits}, 0`); + } else { + B.line(`${loaded} = load ${llTy}, ptr ${slot}`); + if (access === "ref") loaded = host.retainFrame(loaded, type); + } } const hitEnd = B.currentLabel(); B.br(join); diff --git a/packages/compiler/src/backend/llvm/map-read-lifetimes.ts b/packages/compiler/src/backend/llvm/map-read-lifetimes.ts index 005f88f13..6b58d314a 100644 --- a/packages/compiler/src/backend/llvm/map-read-lifetimes.ts +++ b/packages/compiler/src/backend/llvm/map-read-lifetimes.ts @@ -15,6 +15,7 @@ import { emitNumberMapGetScalar, emitStringMapGetScalar, emitStringMapGetRef, + compactMapValues, } from "./map-number-lookup.js"; import { mapKeyAccess, mapKeyParamType } from "./shapes.js"; import { rememberMapEntry } from "./map-entry-fusion.js"; @@ -323,7 +324,10 @@ export function emitStackMapRead(host: LlvmEmitterContext, read: LocalMapRead): } else if (isRefCounted(read.value)) { const stringKey = entryStringKey(key); let raw: string; - if (stringKey !== null) raw = emitStringMapGetRef(host, receiver.name, stringKey, read.value); + const mapType = read.receiver.type; + const compact = mapType.kind === "map" && compactMapValues(host, mapType.key, mapType.value); + if (stringKey !== null) + raw = emitStringMapGetRef(host, receiver.name, stringKey, read.value, compact); else { raw = B.tmp(); host.declare(`declare ptr @scr_map_get_${access}_ref(ptr, ${keyType})`); diff --git a/packages/compiler/src/backend/llvm/regex-matchers.test.ts b/packages/compiler/src/backend/llvm/regex-matchers.test.ts index 1acd9c465..d009de7c9 100644 --- a/packages/compiler/src/backend/llvm/regex-matchers.test.ts +++ b/packages/compiler/src/backend/llvm/regex-matchers.test.ts @@ -110,6 +110,25 @@ describe("regex literal matcher code", () => { expect(body("a\\d{1,3}")).not.toContain("<16 x i8>"); }); + test("fixed leading characters filter 16 start positions at once", () => { + // Both alternatives' first characters are tested at every offset and + // combined, and the reads reach seven characters past the block. + const text = body("agggtaaa|tttaccct", "gi"); + expect(text.match(/load <16 x i8>/g)!.length).toBeGreaterThanOrEqual(16); + expect(text).toMatch(/and i16 /); + expect(text).toMatch(/or i16 /); + expect(text).toMatch(/add i32 %t\d+, 7\n/); + // A class sequence with a fixed count extends the filter. + expect(body("[cg]x{3}y")).toMatch(/and i16 /); + // An optional first character, a single first character and a + // variable-length start keep the first-character search. + expect(body("[ab]?cd")).not.toMatch(/and i16 /); + expect(body("[ab]*cd")).not.toMatch(/and i16 /); + expect(body("xy|zw")).toMatch(/and i16 /); + expect(body("[ab]|cd")).not.toMatch(/and i16 /); + expect(body("agggtaaa|tttaccct", "gi", false)).not.toContain("<16 x i8>"); + }); + test("loops no continuation can share push no backtrack entries", () => { // \d+ then a dot; [^\]]+ then ]; the run at the end of the pattern. expect(body("\\d+\\.\\d+")).not.toContain("@scr_regex_stack_grow"); diff --git a/packages/compiler/src/backend/llvm/regex-matchers.ts b/packages/compiler/src/backend/llvm/regex-matchers.ts index 001f081e5..d83d6bae5 100644 --- a/packages/compiler/src/backend/llvm/regex-matchers.ts +++ b/packages/compiler/src/backend/llvm/regex-matchers.ts @@ -589,6 +589,90 @@ function firstOf(node: RxNode): FirstInfo { } } +/** Positions the start filter tests and alternatives it keeps. */ +const PREFIX_LENGTH = 8; +const PREFIX_ALTERNATIVES = 8; + +/** The character sets a match's first characters must belong to: every + * match of the node begins with the characters of some entry of `alts`, + * one set per character. `complete` means every match is exactly as long as + * its entry (so a following node continues the sequences). Null when the + * node gives no usable condition (an alternative that can start with + * anything, or too many alternatives). Assertions add no characters: the + * result stays a necessary condition. */ +interface Prefix { + alts: CharSet[][]; + complete: boolean; +} + +/** Each head followed by each tail, cut to PREFIX_LENGTH sets; null past + * PREFIX_ALTERNATIVES entries. */ +function crossPrefixes(heads: CharSet[][], tails: CharSet[][]): CharSet[][] | null { + if (heads.length * tails.length > PREFIX_ALTERNATIVES) return null; + const out: CharSet[][] = []; + for (const head of heads) + for (const tail of tails) out.push([...head, ...tail].slice(0, PREFIX_LENGTH)); + return out; +} + +/** Whether no entry was cut at PREFIX_LENGTH. */ +function uncut(alts: CharSet[][]): boolean { + return alts.every((alt) => alt.length < PREFIX_LENGTH); +} + +function prefixOf(node: RxNode): Prefix | null { + switch (node.kind) { + case "set": + return { alts: [[node.set]], complete: true }; + case "assert": + return { alts: [[]], complete: true }; + case "cap": + return prefixOf(node.body); + case "alt": { + const alts: CharSet[][] = []; + let complete = true; + for (const alt of node.alts) { + const p = prefixOf(alt); + if (p === null) return null; + alts.push(...p.alts); + complete &&= p.complete; + } + return alts.length <= PREFIX_ALTERNATIVES ? { alts, complete } : null; + } + case "seq": { + let alts: CharSet[][] = [[]]; + for (const item of node.items) { + const p = prefixOf(item); + if (p === null) return { alts, complete: false }; + const joined = crossPrefixes(alts, p.alts); + if (joined === null) return { alts, complete: false }; + alts = joined; + if (!p.complete || !uncut(alts)) return { alts, complete: false }; + } + return { alts, complete: true }; + } + case "rep": { + if (node.max === 0) return { alts: [[]], complete: true }; + if (node.min === 0) return { alts: [[]], complete: false }; + const body = prefixOf(node.body); + if (body === null) return null; + // The first `min` iterations are required; they extend the sequences + // while the body has a fixed length. + let alts = body.alts; + let covered = 1; + while (covered < node.min && body.complete && uncut(alts)) { + const joined = crossPrefixes(alts, body.alts); + if (joined === null) break; + alts = joined; + covered++; + } + const complete = + body.complete && covered === node.min && node.min === node.max && uncut(alts); + return { alts, complete }; + } + } +} + /** Whether a node can match without consuming (assertions included). */ function canMatchEmpty(node: RxNode): boolean { switch (node.kind) { @@ -721,16 +805,74 @@ class RxGen { return wide; } + /** The vector plans of a start filter (prefixOf): per alternative, the + * plans of its first sets up to the first set without one. Null unless + * every alternative keeps at least two positions, where the filter is + * more selective than the first character alone. */ + prefixPlans(prefix: Prefix | null): { ranges: number[][]; negate: boolean }[][] | null { + if (prefix === null || !this.vector) return null; + const plans: { ranges: number[][]; negate: boolean }[][] = []; + for (const alt of prefix.alts) { + const row: { ranges: number[][]; negate: boolean }[] = []; + for (const set of alt) { + const plan = this.vectorPlan(set); + if (plan === null) break; + row.push(plan); + } + if (row.length < 2) return null; + plans.push(row); + } + return plans.length === 0 ? null : plans; + } + + /** Start positions s[index..index+16) where some alternative of the + * filter matches its first sets: an i16 mask. */ + testPrefix(plans: { ranges: number[][]; negate: boolean }[][], index: string): string { + let any = ""; + for (const row of plans) { + let all = ""; + for (let k = 0; k < row.length; k++) { + let at = index; + if (k > 0) { + at = this.tmp(); + this.line(`${at} = add i32 ${index}, ${k}`); + } + const bits = this.testVector(row[k]!, at); + if (all === "") all = bits; + else { + const both = this.tmp(); + this.line(`${both} = and i16 ${all}, ${bits}`); + all = both; + } + } + if (any === "") any = all; + else { + const either = this.tmp(); + this.line(`${either} = or i16 ${any}, ${all}`); + any = either; + } + } + return any; + } + /** The first-character search: from %at, store the next position whose - * character is in `set` and branch to %attempt, or branch to %nomatch. */ - emitSeek(set: CharSet): void { + * character is in `set` and branch to %attempt, or branch to %nomatch. + * With a start filter (`prefix`), the vector loop tests the filter's + * first sets of each alternative at 16 start positions at once and stops + * only where one of them matches all of its sets; a match can start + * nowhere else, so the attempts that remain are the ones that can + * succeed, in the same order. */ + emitSeek(set: CharSet, prefix: Prefix | null = null): void { this.block("seek"); const a = this.tmp(); this.line(`${a} = load i32, ptr %at`); + const filter = this.prefixPlans(prefix); const plan = this.vectorPlan(set); + // The filter reads s[i + reach + 15] at its farthest. + const reach = filter === null ? 0 : Math.max(...filter.map((row) => row.length)) - 1; let from = "seek", start = a; - if (plan !== null) { + if (plan !== null || filter !== null) { const head = this.label("vsh"), load = this.label("vsl"), next = this.label("vsn"), @@ -740,17 +882,21 @@ class RxGen { this.block(head); const i = this.tmp(), i16 = this.tmp(), + rest = this.tmp(), fits = this.tmp(); this.line(`${i} = phi i32 [ ${a}, %seek ], [ ${i16}, %${next} ]`); - this.line(`${i16} = add i32 ${i}, 16`); - this.line(`${fits} = icmp sle i32 ${i16}, %len`); + // The characters left (0 <= i <= len, so this cannot wrap; i + 16 + // can for subjects near 2^31 characters). + this.line(`${rest} = sub i32 %len, ${i}`); + this.line(`${fits} = icmp sge i32 ${rest}, ${16 + reach}`); this.line(`br i1 ${fits}, label %${load}, label %${tail}`); this.block(load); - const mask = this.testVector(plan, i), + const mask = filter !== null ? this.testPrefix(filter, i) : this.testVector(plan!, i), any = this.tmp(); this.line(`${any} = icmp ne i16 ${mask}, 0`); this.line(`br i1 ${any}, label %${hit}, label %${next}`); this.block(next); + this.line(`${i16} = add i32 ${i}, 16`); this.line(`br label %${head}`); this.block(hit); const k = this.lowestBit(mask), @@ -1001,10 +1147,10 @@ class RxGen { check = this.label("chk"); this.block(entry); const p = this.loadPos(), - end = this.tmp(), + rest = this.tmp(), inb = this.tmp(); - this.line(`${end} = add i32 ${p}, ${sets.length}`); - this.line(`${inb} = icmp sle i32 ${end}, %len`); + this.line(`${rest} = sub i32 %len, ${p}`); + this.line(`${inb} = icmp sge i32 ${rest}, ${sets.length}`); this.line(`br i1 ${inb}, label %${check}, label %fail`); this.block(check); let acc = ""; @@ -1023,6 +1169,8 @@ class RxGen { acc = both; } } + const end = this.tmp(); + this.line(`${end} = add i32 ${p}, ${sets.length}`); // Storing before the branch is safe: every resume sets the position. this.storePos(end); this.line(`br i1 ${acc}, label %${succ}, label %fail`); @@ -1229,10 +1377,11 @@ class RxGen { this.block(vhead); const kv = this.tmp(), kv16 = this.tmp(), + left = this.tmp(), fits = this.tmp(); this.line(`${kv} = phi i32 [ 0, %${entry} ], [ ${kv16}, %${vnext} ]`); - this.line(`${kv16} = add i32 ${kv}, 16`); - this.line(`${fits} = icmp sle i32 ${kv16}, ${lim}`); + this.line(`${left} = sub i32 ${lim}, ${kv}`); + this.line(`${fits} = icmp sge i32 ${left}, 16`); this.line(`br i1 ${fits}, label %${vload}, label %${tail}`); this.block(vload); const vat = this.tmp(); @@ -1242,6 +1391,7 @@ class RxGen { this.line(`${all} = icmp eq i16 ${mask}, -1`); this.line(`br i1 ${all}, label %${vnext}, label %${vexit}`); this.block(vnext); + this.line(`${kv16} = add i32 ${kv}, 16`); this.line(`br label %${vhead}`); this.block(vexit); const missing = this.tmp(); @@ -1769,7 +1919,10 @@ export function compileRegexMatcher( fn.push(` store i32 %off.n, ptr %at`); fn.push(` br label %attempt`); } else { - gen.emitSeek(first.set); + // A start filter over the first few characters (a single first byte + // keeps memchr). + const prefix = prefixOf(root); + gen.emitSeek(first.set, gen.prefixPlans(prefix) === null ? null : prefix); } } diff --git a/packages/compiler/src/backend/llvm/running-extrema.test.ts b/packages/compiler/src/backend/llvm/running-extrema.test.ts new file mode 100644 index 000000000..4ce307c5c --- /dev/null +++ b/packages/compiler/src/backend/llvm/running-extrema.test.ts @@ -0,0 +1,97 @@ +import { expect, test } from "vitest"; +import { BOOL, F64, type IrExpr, type IrFunction, type IrStmt } from "../../ir/ir.js"; +import { runningExtremumUpdates } from "./running-extrema.js"; + +const loc = { file: "extrema.ts", start: 0, end: 0 }; +const ref = (localId: string): IrExpr => ({ kind: "varRef", localId, type: F64, loc }); +const num = (value: number): IrExpr => ({ kind: "numLit", value, type: F64, loc }); +const assign = (localId: string, value: IrExpr): IrStmt => ({ + kind: "assign", + localId, + value, + loc, +}); +const decl = (localId: string): IrStmt => ({ kind: "varDecl", localId, init: num(0), loc }); +const compare = (op: "<" | ">", left: IrExpr, right: IrExpr): IrExpr => ({ + kind: "bin", + op, + left, + right, + type: BOOL, + loc, +}); +/** `if (v > max) max = v;` */ +const update = (): IrStmt & { kind: "if" } => ({ + kind: "if", + cond: compare(">", ref("v"), ref("max")), + then: [assign("max", ref("v"))], + else_: null, + loc, +}); +const loop = (body: IrStmt[]): IrStmt => ({ + kind: "while", + cond: compare(">", ref("n"), num(0)), + body, + loc, +}); +const fn = (body: IrStmt[], captures: string[] = []): IrFunction => ({ + name: "scan", + params: [], + locals: ["v", "max", "n"].map((id) => ({ id, name: id, type: F64, mutable: true })), + captures: captures.map((localId) => ({ localId, name: localId, type: F64 })), + returnType: F64, + body, + loc, +}); + +test("a loop's update of an accumulator declared outside it qualifies", () => { + const s = update(); + expect(runningExtremumUpdates(fn([decl("max"), loop([decl("v"), s])])).has(s)).toBe(true); +}); + +test("the local may be on either side of the comparison", () => { + const s = update(); + s.cond = compare("<", ref("max"), ref("v")); + expect(runningExtremumUpdates(fn([decl("max"), loop([s])])).has(s)).toBe(true); +}); + +test("a local declared in the loop is not an accumulator", () => { + const s = update(); + expect(runningExtremumUpdates(fn([loop([decl("max"), s])])).size).toBe(0); +}); + +test("another write in the loop disqualifies the update", () => { + const s = update(); + const body = [decl("max"), loop([assign("max", num(1)), s])]; + expect(runningExtremumUpdates(fn(body)).size).toBe(0); +}); + +test("the assigned value must be the compared expression", () => { + const s = update(); + s.then = [assign("max", num(5))]; + expect(runningExtremumUpdates(fn([decl("max"), loop([s])])).size).toBe(0); +}); + +test("outside a loop only a captured binding qualifies", () => { + const captured = update(); + expect(runningExtremumUpdates(fn([captured], ["max"])).has(captured)).toBe(true); + expect(runningExtremumUpdates(fn([update()])).size).toBe(0); +}); + +test("an else branch disqualifies the update", () => { + const s = update(); + s.else_ = [assign("v", num(0))]; + expect(runningExtremumUpdates(fn([decl("max"), loop([s])])).size).toBe(0); +}); + +test("an update nested in another branch does not qualify", () => { + const s = update(); + const guard: IrStmt = { + kind: "if", + cond: compare(">", ref("v"), num(0)), + then: [s], + else_: null, + loc, + }; + expect(runningExtremumUpdates(fn([decl("max"), loop([guard])])).size).toBe(0); +}); diff --git a/packages/compiler/src/backend/llvm/running-extrema.ts b/packages/compiler/src/backend/llvm/running-extrema.ts new file mode 100644 index 000000000..1ddea603d --- /dev/null +++ b/packages/compiler/src/backend/llvm/running-extrema.ts @@ -0,0 +1,110 @@ +// Running maximum and minimum updates, `if (v > max) { max = v; ... }`. +// With the accumulator in a register LLVM turns such an update into a +// select (`maxsd`, or a compare and mask), so every element waits on the +// previous one's. The update gets rarer as a scan goes on, so a branch +// predicts well; emitStmtBody gives these `if`s branch weights that say the +// update is rarely taken, and LLVM keeps the branch. A clamp of a value read +// in the same iteration is not an accumulator and keeps its select: its +// branch would go whichever way the data goes. An update nested in another +// branch keeps its select too: kept, its branch can stop LLVM from turning +// the enclosing branch into selects, and that branch may follow the data. + +import type { IrExpr, IrFunction, IrStmt } from "../../ir/ir.js"; +import { everyExprChild, everyStmtChild } from "../../ir/traverse.js"; + +type Loop = IrStmt & { kind: "for" | "while" | "doWhile" | "forOf" }; + +const RELATIONAL = new Set(["<", "<=", ">", ">="]); + +/** The `if` statements of `fn` that update a running maximum or minimum: + * no `else`, a numeric `<`, `<=`, `>` or + * `>=` between a local and another expression, and a then-block that + * assigns that expression to the local. The local must carry across + * iterations: inside a loop the `if` is a statement of the innermost loop's + * body, the local is declared outside that loop, and the loop writes it only + * in this `if`; outside any loop the `if` is a statement of the function + * body and the local is a binding the function captures and writes only in + * this `if` (a callback that a loop elsewhere calls). */ +export function runningExtremumUpdates(fn: IrFunction): Set { + const found = new Set(); + const captured = new Set((fn.captures ?? []).map((c) => c.localId)); + const loops: Loop[] = []; + const candidates: { s: IrStmt; local: string; loop: Loop | undefined }[] = []; + const expr = (e: IrExpr): boolean => everyExprChild(e, expr, stmt); + const stmt = (s: IrStmt): boolean => { + if (s.kind === "if") { + const local = updatedExtremum(s); + if (local !== undefined) candidates.push({ s, local, loop: loops[loops.length - 1] }); + } + const loop = + s.kind === "for" || s.kind === "while" || s.kind === "doWhile" || s.kind === "forOf"; + if (loop) loops.push(s); + everyStmtChild(s, expr, stmt); + if (loop) loops.pop(); + return true; + }; + for (const s of fn.body) stmt(s); + for (const { s, local, loop } of candidates) { + const scope = loop ?? (captured.has(local) ? fn.body : undefined); + const body = loop ? loop.body : fn.body; + if (scope !== undefined && body.includes(s) && writes(scope, local) === 1) found.add(s); + } + return found; +} + +/** The local an `if` updates to a new maximum or minimum, if it is one. */ +function updatedExtremum(s: IrStmt & { kind: "if" }): string | undefined { + if (s.else_ !== null && s.else_.length > 0) return undefined; + const c = s.cond; + if (c.kind !== "bin" || !RELATIONAL.has(c.op)) return undefined; + if (c.left.type.kind !== "f64" || c.right.type.kind !== "f64") return undefined; + for (const [local, other] of [ + [c.left, c.right], + [c.right, c.left], + ]) { + if (local!.kind !== "varRef") continue; + const id = local!.localId; + const assigns = s.then.some( + (x) => x.kind === "assign" && x.localId === id && sameExpr(x.value, other!), + ); + if (assigns) return id; + } + return undefined; +} + +/** Declarations and writes of `local` under `scope`. A declaration counts + * as a write too, so a local declared inside a loop does not qualify. */ +function writes(scope: IrStmt | IrStmt[], local: string): number { + let n = 0; + const expr = (e: IrExpr): boolean => { + if ((e.kind === "assignExpr" || e.kind === "incDec") && e.localId === local) n++; + return everyExprChild(e, expr, stmt); + }; + const stmt = (s: IrStmt): boolean => { + if ((s.kind === "assign" || s.kind === "varDecl" || s.kind === "forOf") && s.localId === local) + n++; + else if (s.kind === "tryCatch" && s.catchLocalId === local) n++; + return everyStmtChild(s, expr, stmt); + }; + if (Array.isArray(scope)) for (const s of scope) stmt(s); + else stmt(scope); + return n; +} + +/** Structural equality of two small expressions (`v`, `a[i]`), ignoring + * source locations and types; larger expressions do not compare equal. */ +function sameExpr(a: IrExpr, b: IrExpr): boolean { + let budget = 64; + const same = (x: unknown, y: unknown): boolean => { + if (--budget < 0) return false; + if (typeof x !== "object" || x === null || typeof y !== "object" || y === null) return x === y; + if (Array.isArray(x) !== Array.isArray(y)) return false; + const keys = Object.keys(x).filter((k) => k !== "loc" && k !== "type"); + if (keys.length !== Object.keys(y).filter((k) => k !== "loc" && k !== "type").length) + return false; + return keys.every((k) => + same((x as Record)[k], (y as Record)[k]), + ); + }; + return same(a, b); +} diff --git a/packages/compiler/src/backend/llvm/shapes.ts b/packages/compiler/src/backend/llvm/shapes.ts index 6de92f667..83f025437 100644 --- a/packages/compiler/src/backend/llvm/shapes.ts +++ b/packages/compiler/src/backend/llvm/shapes.ts @@ -71,6 +71,12 @@ export interface ShapeHost { /** The hierarchy root of a class (a program class or a runtime base), * or undefined for classes outside the class graph. */ classRootOf?(className: string): string | undefined; + /** Whether arrays of `elem` hold 32-bit references to compact class + * instances (SCR_ELEM_CREF; LlEmitter.compactArrayElem). */ + compactArrayElem?(elem: IrType): boolean; + /** Whether compact class instances live in the program-lifetime region + * (LlEmitter.compactRegion). */ + compactRegion?(): boolean; /** Byte offset from an object pointer back to ScrCycHdr.buf. */ readonly cycleBufOffset: number; declare(decl: string): void; @@ -564,14 +570,16 @@ export function inlineRcFamily(host: ShapeHost, stem: string, op: RcOp): InlineR } /** Which RC fast paths an emission inlines. `speed` mirrors every family - * with a fast path. `release` mirrors only the string retain and the dyn - * retain and release: the operations that dominate dynamic-value and string - * code, at almost no size cost. String releases stay calls in `release` - * because string-heavy programs have many release sites. */ + * with a fast path. `release` mirrors only the string and Map retains and + * the dyn retain and release: the operations that dominate dynamic-value, + * string and Map code (a Map read from an array or another Map), at almost + * no size cost. String releases stay calls in `release` because + * string-heavy programs have many release sites. */ export type InlineRcMode = "release" | "speed"; const RELEASE_RC_OPS = new Set([ "str:retain", + "map:retain", "dyn:retain", "dyn:release", "js:retain", @@ -1076,18 +1084,30 @@ export function arrNewCall( /** The %ScrArrOps descriptor (retain, release, trace, registry id) of a REF * element type, defined once per module and shared by all its arrays. It is - * writable: the runtime stores its registry id on the first array. */ + * writable: the runtime stores its registry id on the first array. Its tail + * padding is the runtime's flags word (0); arrays of compact class + * references (ShapeHost.compactArrayElem) spell the word out as + * SCR_ARR_OPS_COMPACT. */ export function arrOps(host: ShapeHost, elem: IrType): string { const v = vAdapters(host, elem); const trace = traceArg(host, elem); const part = (sym: string): string => sym.replace(/^@/, "").replace(/[^A-Za-z0-9_.]/g, "_"); const name = `@sc_arrops.${part(v.retain)}.${part(v.release)}.${part(trace)}`; + if (host.compactArrayElem?.(elem) === true) { + host.declare( + `${name}.c = internal global { ptr, ptr, ptr, i32, i32 } { ptr ${v.retain}, ptr ${v.release}, ptr ${trace}, i32 0, i32 ${SCR_ARR_OPS_COMPACT} }`, + ); + return `${name}.c`; + } host.declare( `${name} = internal global %ScrArrOps { ptr ${v.retain}, ptr ${v.release}, ptr ${trace}, i32 0 }`, ); return name; } +/** scr_array.h SCR_ARR_OPS_COMPACT. */ +const SCR_ARR_OPS_COMPACT = 1; + /* ── capture boxes ────────────────────────────────────────────────────── */ /** Box construction call text (boxNewC's dispatch): plain-kind boxes for diff --git a/packages/compiler/src/backend/native-link-info-core.ts b/packages/compiler/src/backend/native-link-info-core.ts index 55301948b..5782f091d 100644 --- a/packages/compiler/src/backend/native-link-info-core.ts +++ b/packages/compiler/src/backend/native-link-info-core.ts @@ -91,6 +91,15 @@ export interface NativeLinkFeatures { /** crypto.createECDH and ECDH.convertKey: scr_ecdh.c plus the mbedTLS * archive. */ ecdh: boolean; + /** crypto.createDiffieHellman, getDiffieHellman and + * createDiffieHellmanGroup: scr_dh.c plus the mbedTLS archive. */ + dh: boolean; + /** Asymmetric KeyObjects, crypto.sign/verify and Sign/Verify: scr_pki.c + * plus the mbedTLS archive. */ + pki: boolean; + /** Ed25519 keys and signatures with pki: scr_ed25519.c (the vendored + * libsodium Ed25519). */ + ed25519: boolean; /** perf_hooks event-loop metrics: scr_perf_loop.c. */ perfLoop: boolean; tlsCa: boolean; diff --git a/packages/compiler/src/backend/native/contracts.ts b/packages/compiler/src/backend/native/contracts.ts index 8df233585..f683ede3e 100644 --- a/packages/compiler/src/backend/native/contracts.ts +++ b/packages/compiler/src/backend/native/contracts.ts @@ -6,6 +6,10 @@ export interface CcOptions { * LLVM backend's .ll — clang compiles IR text natively on the same * command line, so both ride this one seat. */ cPath: string; + /** Where the program TU's bytes are read from, when that is not `cPath`: an invocation-private + * copy that a concurrent build of another program cannot replace. `cPath` stays the spelling + * clang sees (`__FILE__`, debug file names) and the one cache keys name. */ + cSourcePath?: string; /** Path of the native executable to produce. */ outPath: string; /** Additional identity for a translation unit whose complete non-system @@ -250,6 +254,16 @@ export interface CcOptions { /** crypto.createECDH and ECDH.convertKey: compiles scr_ecdh.c and links * the mbedTLS archive (shared with tls). */ ecdh?: boolean; + /** crypto.createDiffieHellman, getDiffieHellman and + * createDiffieHellmanGroup: compiles scr_dh.c and links the mbedTLS + * archive (shared with tls). */ + dh?: boolean; + /** Asymmetric KeyObjects and signatures: compiles scr_pki.c and links + * the mbedTLS archive. */ + pki?: boolean; + /** Ed25519 with pki: compiles scr_ed25519.c (the vendored libsodium + * Ed25519). */ + ed25519?: boolean; /** crypto.subtle importKey/sign/verify/digest: compiles scr_webcrypto.c. */ webcrypto?: boolean; /** perf_hooks event-loop metrics: compiles scr_perf_loop.c. */ diff --git a/packages/compiler/src/backend/native/executable.ts b/packages/compiler/src/backend/native/executable.ts index 229b2f967..714bc09a5 100644 --- a/packages/compiler/src/backend/native/executable.ts +++ b/packages/compiler/src/backend/native/executable.ts @@ -114,6 +114,8 @@ interface RuntimeUnitContext { tlsCa: boolean; cipher: boolean; ecdh: boolean; + dh: boolean; + pki: boolean; lreObjects: readonly string[]; zlibObjects: readonly string[]; tlsArchive: string | null; @@ -297,6 +299,8 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [ unit((u) => u.opts.foreignFfi, "scr_ffi_queue.c"), unit((u) => u.opts.nodeTest, "scr_test.c"), unit((u) => u.opts.webcrypto, "scr_webcrypto.c"), + unit((u) => u.opts.webcrypto === true && u.opts.pki === true, "scr_webcrypto_pki.c"), + unit((u) => u.opts.pki === true && u.opts.ed25519 === true, "scr_ed25519.c"), // The crypto global's object extends scr_async_dyn.c's. unit((u) => u.opts.webcrypto && u.opts.dynAsync, "scr_webcrypto_object.c"), // The CA-store unit rides its own gate OR the tls one: scr_tls.c @@ -310,6 +314,8 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [ ...(u.tls ? [u.rt(join(u.rtDir, "scr_tls.c"))] : []), ...(u.cipher ? [u.rt(join(u.rtDir, "scr_cipher.c"))] : []), ...(u.ecdh ? [u.rt(join(u.rtDir, "scr_ecdh.c"))] : []), + ...(u.dh ? [u.rt(join(u.rtDir, "scr_dh.c"))] : []), + ...(u.pki ? [u.rt(join(u.rtDir, "scr_pki.c"))] : []), ...(u.cipher && u.opts.webcrypto ? [u.rt(join(u.rtDir, "scr_webcrypto_aes.c"))] : []), u.tlsArchive, // mbedTLS's win32 entropy poll is BCryptGenRandom (bcrypt.h). @@ -375,10 +381,13 @@ export async function compileCInternal( const http = (opts.http ?? false) || nativeFetch; const tls = (opts.tls ?? false) || nativeFetch; const tlsCa = (opts.tlsCa ?? false) || tls; - // Symmetric ciphers and ECDH link the same mbedTLS archive as TLS. + // Symmetric ciphers, ECDH and DiffieHellman link the same mbedTLS + // archive as TLS. const cipher = opts.cipher ?? false; const ecdh = opts.ecdh ?? false; - const mbedtls = tls || cipher || ecdh; + const dh = opts.dh ?? false; + const pki = opts.pki ?? false; + const mbedtls = tls || cipher || ecdh || dh || pki; const driver = resolveCc(); const darwinDebugSymbols = needsDarwinDebugSymbols( targetPlatform(driver), @@ -484,7 +493,7 @@ export async function compileCInternal( const [compiler, runtimeHash, programBytes] = await Promise.all([ resolvedTool(driver.argv[0] ?? "clang"), runtimeFingerprint(rtDir), - readFile(opts.cPath), + readFile(opts.cSourcePath ?? opts.cPath), ]); if (compiler !== null) { const effectiveCompiler = @@ -736,6 +745,8 @@ export async function compileCInternal( tlsCa, cipher, ecdh, + dh, + pki, lreObjects, zlibObjects, tlsArchive, @@ -864,7 +875,12 @@ export async function compileCInternal( ); try { await materializeVendorPrerequisites(undefined, privateVendorRoot); - await buildExecutable((p) => p); + await buildExecutable( + (p) => p, + opts.cSourcePath === undefined + ? {} + : { programPath: opts.cSourcePath, compilerVisibleSource: opts.cPath }, + ); } finally { await rm(privateVendorRoot, { recursive: true, force: true }).catch(() => undefined); } @@ -899,7 +915,9 @@ export async function compileCInternal( localArtifact === null ? runtimeFingerprint(rtDir) : Promise.resolve(localArtifact.runtimeHash), - localArtifact === null ? readFile(opts.cPath) : Promise.resolve(localArtifact.programBytes), + localArtifact === null + ? readFile(opts.cSourcePath ?? opts.cPath) + : Promise.resolve(localArtifact.programBytes), ]); compileMetadataStamp = compileMetadataKey === null @@ -968,7 +986,7 @@ export async function compileCInternal( Promise.all([ ccVersion(driver.argv, toolchainEnv, true), runtimeFingerprint(rtDir), - readFile(opts.cPath), + readFile(opts.cSourcePath ?? opts.cPath), ])); } catch (error) { // A version/fingerprint probe is an optimization boundary. If the compiler diff --git a/packages/compiler/src/backend/runtime-abi.ts b/packages/compiler/src/backend/runtime-abi.ts index f02510338..3a41d9176 100644 --- a/packages/compiler/src/backend/runtime-abi.ts +++ b/packages/compiler/src/backend/runtime-abi.ts @@ -2,8 +2,8 @@ * of the link contract: a program object keeps this symbol undefined and the * matching runtime defines it, so incompatible manual links fail before the * program can start. */ -export const RUNTIME_ABI_VERSION = 25 as const; -export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v25" as const; +export const RUNTIME_ABI_VERSION = 27 as const; +export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v27" as const; /** Object consumption is useful today, but the complete scr_* surface may * still change before 1.0. The versioned marker prevents accidental mixing; diff --git a/packages/compiler/src/backend/runtime-pack-targets.test.ts b/packages/compiler/src/backend/runtime-pack-targets.test.ts index 8bdbc8492..af61e5229 100644 --- a/packages/compiler/src/backend/runtime-pack-targets.test.ts +++ b/packages/compiler/src/backend/runtime-pack-targets.test.ts @@ -52,7 +52,7 @@ async function fixture(target: NativeTargetSpec, workers = false) { object_format: target.objectFormat, minimum_os: target.minimumOs, }, - runtime_abi: { version: 25, marker: "scr_runtime_abi_v25" }, + runtime_abi: { version: 27, marker: "scr_runtime_abi_v27" }, compiler: { command: "fixture", identity: "fixture", target: target.llvmTriple }, macros: { executable: [], excluded: ["SCR_LIB"], sanitizer: "external-toolchain-required" }, flavors: { @@ -125,6 +125,9 @@ const features = { tlsCa: false, cipher: false, ecdh: false, + dh: false, + pki: false, + ed25519: false, webcrypto: false, perfLoop: false, }; diff --git a/packages/compiler/src/backend/runtime-pack.test.ts b/packages/compiler/src/backend/runtime-pack.test.ts index a3ac9fe5d..d09509911 100644 --- a/packages/compiler/src/backend/runtime-pack.test.ts +++ b/packages/compiler/src/backend/runtime-pack.test.ts @@ -61,6 +61,9 @@ const BASE: NativeLinkFeatures = { tlsCa: false, cipher: false, ecdh: false, + dh: false, + pki: false, + ed25519: false, webcrypto: false, perfLoop: false, }; @@ -124,7 +127,7 @@ async function fixture(target: NativeTargetSpec = MACOS_ARM64_TARGET) { object_format: target.objectFormat, minimum_os: target.minimumOs, }, - runtime_abi: { version: 25, marker: "scr_runtime_abi_v25" }, + runtime_abi: { version: 27, marker: "scr_runtime_abi_v27" }, compiler: { command: "clang", identity: "fixture clang", diff --git a/packages/compiler/src/backend/target-diagnostics.ts b/packages/compiler/src/backend/target-diagnostics.ts index eb70a0d2c..236ce12cf 100644 --- a/packages/compiler/src/backend/target-diagnostics.ts +++ b/packages/compiler/src/backend/target-diagnostics.ts @@ -2,7 +2,12 @@ import { everyModuleNode, everyStmtList, transformStmtList } from "../ir/travers import type { LlvmUnsupportedError } from "./llvm/emitter.js"; import type { ScrDiagnostic } from "../diagnostics/diagnostic.js"; import { type IrModule, type SrcLoc } from "../ir/ir.js"; -import { moduleUsesFetch } from "../ir/runtime-features.js"; +import { + DH_LIB_FNS, + PKI_LIB_FNS, + moduleUsesFetch, + moduleUsesWebCryptoAsymmetric, +} from "../ir/runtime-features.js"; /** Reflection tables expose methods that the program never calls. On WASI, * keep their host-only implementations as explicit runtime refusals. Typed @@ -208,10 +213,15 @@ export function moduleWasiUnavailableSurface( }; return false; } - if (node.fn === "webcrypto.encrypt" || node.fn === "webcrypto.decrypt") { + if ( + node.fn === "webcrypto.encrypt" || + node.fn === "webcrypto.decrypt" || + node.fn === "webcrypto.wrapKey" || + node.fn === "webcrypto.unwrapKey" + ) { found = { surface: - "WebCrypto encrypt and decrypt (the wasm32-wasi runtime does not include the mbedTLS library)", + "WebCrypto encrypt, decrypt, wrapKey and unwrapKey (the wasm32-wasi runtime does not include the mbedTLS library)", loc, }; return false; @@ -223,6 +233,24 @@ export function moduleWasiUnavailableSurface( }; return false; } + if (DH_LIB_FNS.has(node.fn)) { + found = { + surface: + node.fn === "crypto.dhNew" || node.fn === "crypto.dhGroupNew" + ? "DiffieHellman (the wasm32-wasi runtime does not include the mbedTLS library)" + : "checkPrime and generatePrime (the wasm32-wasi runtime does not include the mbedTLS library)", + loc, + }; + return false; + } + if (PKI_LIB_FNS.has(node.fn)) { + found = { + surface: + "asymmetric keys and signatures (the wasm32-wasi runtime does not include the mbedTLS library)", + loc, + }; + return false; + } if (node.fn === "os.networkInterfaces") { found = { surface: "network-interface enumeration (WASI Preview 1 has no interface API)", @@ -249,5 +277,12 @@ export function moduleWasiUnavailableSurface( if (moduleUsesFetch(mod)) { return { surface: "network-backed fetch (WASI Preview 1 has no socket API)", loc: entryLoc }; } + if (moduleUsesWebCryptoAsymmetric(mod)) { + return { + surface: + "WebCrypto ECDSA and RSA (the wasm32-wasi runtime does not include the mbedTLS library)", + loc: entryLoc, + }; + } return null; } diff --git a/packages/compiler/src/backend/warm-cache.ts b/packages/compiler/src/backend/warm-cache.ts index 2d8ae6641..b48cf6739 100644 --- a/packages/compiler/src/backend/warm-cache.ts +++ b/packages/compiler/src/backend/warm-cache.ts @@ -67,6 +67,9 @@ export async function warmNativeCaches( tls: false, cipher: false, ecdh: false, + dh: false, + pki: false, + ed25519: false, webcrypto: false, perfLoop: false, tlsCa: false, diff --git a/packages/compiler/src/coverage/report.ts b/packages/compiler/src/coverage/report.ts index 37f77f4c6..79017c97a 100644 --- a/packages/compiler/src/coverage/report.ts +++ b/packages/compiler/src/coverage/report.ts @@ -318,12 +318,23 @@ export function renderCoverage(input: CoverageInput, opts: CoverageRenderOptions // statements the module carries): they compile, but executing one // throws its SC-coded error — the report lists them so a "compiling" // JS program never hides its dynamic gaps. + // Fences in a module's top-level code run when the module initializes: + // they are listed first, as sites that run at startup. const fences = input.runtimeFences ?? []; - if (fences.length > 0) { + const startup = fences.filter((d) => d.startup === true); + const later = fences.filter((d) => d.startup !== true); + if (startup.length > 0) { out.push( - ` ${c(YELLOW, "deferred to runtime")} ${fences.length} site${fences.length === 1 ? "" : "s"} ${c(DIM, "(JS statements that throw their fence if executed)")}`, + ` ${c(YELLOW, "runs at startup")} ${startup.length} site${startup.length === 1 ? "" : "s"} ${c(DIM, "(JS statements in module top-level code, which runs when the module loads)")}`, ); - renderRows(groupBlockers(fences, "deferred"), YELLOW); + renderRows(groupBlockers(startup, "deferred"), YELLOW); + out.push(""); + } + if (later.length > 0) { + out.push( + ` ${c(YELLOW, "deferred to runtime")} ${later.length} site${later.length === 1 ? "" : "s"} ${c(DIM, "(JS statements that throw their fence if executed)")}`, + ); + renderRows(groupBlockers(later, "deferred"), YELLOW); out.push(""); } diff --git a/packages/compiler/src/diagnostics/diagnostic.ts b/packages/compiler/src/diagnostics/diagnostic.ts index 17cb2efc0..625ee93af 100644 --- a/packages/compiler/src/diagnostics/diagnostic.ts +++ b/packages/compiler/src/diagnostics/diagnostic.ts @@ -92,6 +92,10 @@ export interface ScrDiagnostic { * the error may not reproduce under the project's tsc. Machine-readable * output reports these under the "environment" category. */ typeWorld?: boolean; + /** A runtime fence in a module's top-level code, outside every function + * and class member body: it throws when the module initializes, which is + * program startup for a module the entry imports. */ + startup?: true; } /** SC3002–SC3004 — native-helper target, installation, and execution diff --git a/packages/compiler/src/diagnostics/envelope.ts b/packages/compiler/src/diagnostics/envelope.ts index 59c285ead..b4de8db77 100644 --- a/packages/compiler/src/diagnostics/envelope.ts +++ b/packages/compiler/src/diagnostics/envelope.ts @@ -67,6 +67,9 @@ export interface LocatedDiagnostic extends SourcePosition { hint: string; /** The id of the root-cause group this site belongs to. */ group: string; + /** Deferred scope only: the statement is module top-level code, so it + * runs when its module loads. */ + startup?: true; } export interface DiagnosticGroup { @@ -290,6 +293,7 @@ export function locateDiagnostic( message: diag.message, hint: hintOf(diag), group: groupId(diag, scope), + ...(scope === "deferred" && diag.startup === true ? { startup: true as const } : {}), }; } diff --git a/packages/compiler/src/executable/executable-cache.test.ts b/packages/compiler/src/executable/executable-cache.test.ts index 94b136c90..5cf8c6588 100644 --- a/packages/compiler/src/executable/executable-cache.test.ts +++ b/packages/compiler/src/executable/executable-cache.test.ts @@ -65,6 +65,9 @@ const native: EarlyExecutableNativeFeatures = { tlsCa: false, cipher: false, ecdh: false, + dh: false, + pki: false, + ed25519: false, webcrypto: false, perfLoop: false, }; @@ -133,8 +136,15 @@ test("early executable cache restores the emitted TU, IR, and native feature gat const hit = await readEarlyExecutableCache(f.root, f.options); expect(hit?.native).toEqual(native); expect(hit?.executableRestored).toBe(false); - expect(await readFile(f.llvmPath, "utf8")).toBe("; generated llvm\n"); - expect(await readFile(f.irPath, "utf8")).toContain("irVersion"); + // The build compiles private copies beside the shared names and installs + // them there when it is done. + expect(hit?.llvmPath).toBe(f.llvmPath); + expect(hit?.irPath).toBe(f.irPath); + expect(hit?.llvmSourcePath).toMatch(/[/\\]\.scriptc-llvm-[^/\\]*\.ll$/); + expect(hit?.irSourcePath).toMatch(/[/\\]\.scriptc-ir-/); + expect(await readFile(hit!.llvmSourcePath!, "utf8")).toBe("; generated llvm\n"); + expect(await readFile(hit!.irSourcePath!, "utf8")).toContain("irVersion"); + await expect(stat(f.llvmPath)).rejects.toThrow(); expect(await readFile(alternateC, "utf8")).toBe("/* user-owned native source */\n"); }); diff --git a/packages/compiler/src/executable/executable-cache.ts b/packages/compiler/src/executable/executable-cache.ts index 2312f5e85..5c3904a91 100644 --- a/packages/compiler/src/executable/executable-cache.ts +++ b/packages/compiler/src/executable/executable-cache.ts @@ -18,6 +18,7 @@ import { compilerReleaseVersion } from "../library/sidecar.js"; import { nativeArtifactDependenciesStillMatch } from "../backend/native/artifact-stamps.js"; import { type NativeArtifactDependency } from "../backend/native/contracts.js"; import { optimizationKeyParts } from "../backend/optimization.js"; +import { privateSiblingPath } from "../backend/build-cache.js"; import type { CompilerImplementationDependency } from "../library/compiler-self-identity.js"; import { installDarwinDebugSymbols, @@ -96,6 +97,9 @@ export interface EarlyExecutableNativeFeatures { tls: boolean; cipher: boolean; ecdh: boolean; + dh: boolean; + pki: boolean; + ed25519: boolean; perfLoop: boolean; tlsCa: boolean; webcrypto: boolean; @@ -179,6 +183,15 @@ interface EarlyExecutableImplementationProof { export interface EarlyExecutableCacheHit { llvmPath: string; irPath?: string; + /** When the executable was not restored: private copies of the cached LLVM text and IR beside + * llvmPath and irPath, which the build compiles from and installs at those names when it is done + * (index.ts BuildSources). A restored executable needs no compile, and its files are installed at + * llvmPath and irPath. */ + llvmSourcePath?: string; + irSourcePath?: string; + /** llvmPath (irPath) already holds the cached bytes: the build leaves it as it is. */ + llvmPathCurrent?: true; + irPathCurrent?: true; /** True when the final executable was restored after native dependencies * validated. False restores only frontend artifacts and must call compileC. */ executableRestored: boolean; @@ -228,6 +241,9 @@ const BOOLEAN_NATIVE_KEYS = [ "tlsCa", "cipher", "ecdh", + "dh", + "pki", + "ed25519", "webcrypto", "perfLoop", ] as const satisfies readonly (keyof EarlyExecutableNativeFeatures)[]; @@ -575,15 +591,6 @@ async function readExecutableCache( (await nativeArtifactDependenciesStillMatch(stamp.nativeDependencies)); if (validateRoute !== undefined && !executableRestored) return null; const paths = outputPaths(options, stamp.native.backend); - if (!(await fileMatches(paths.llvmPath, stamp.files.translationUnit.digest))) { - await installBytes(translationUnit, paths.llvmPath); - } - if ( - ir !== null && - stamp.files.ir !== null && - !(await fileMatches(paths.irPath, stamp.files.ir.digest)) - ) - await installBytes(ir, paths.irPath); if (executableRestored) { try { if (debugSymbols !== null) await installDarwinDebugSymbols(debugSymbols, options.outPath); @@ -598,6 +605,32 @@ async function readExecutableCache( executableRestored = false; } } + // A build that compiles from the cached translation unit compiles its own copy: a concurrent + // build of another program with the same entry name may replace the shared names meanwhile. + let llvmSourcePath: string | undefined; + let irSourcePath: string | undefined; + let llvmPathCurrent = false; + let irPathCurrent = false; + if (executableRestored) { + if (!(await fileMatches(paths.llvmPath, stamp.files.translationUnit.digest))) + await installBytes(translationUnit, paths.llvmPath); + if ( + ir !== null && + stamp.files.ir !== null && + !(await fileMatches(paths.irPath, stamp.files.ir.digest)) + ) + await installBytes(ir, paths.irPath); + } else { + // The extension keeps clang reading the copy as LLVM IR rather than as a linker input. + llvmSourcePath = `${privateSiblingPath(paths.llvmPath, "llvm")}.ll`; + await installBytes(translationUnit, llvmSourcePath); + llvmPathCurrent = await fileMatches(paths.llvmPath, stamp.files.translationUnit.digest); + if (ir !== null && stamp.files.ir !== null) { + irSourcePath = privateSiblingPath(paths.irPath, "ir"); + await installBytes(ir, irSourcePath); + irPathCurrent = await fileMatches(paths.irPath, stamp.files.ir.digest); + } + } const now = new Date(); await Promise.all( [ @@ -619,6 +652,10 @@ async function readExecutableCache( ? { performance: stamp.performance } : {}), ...(ir === null ? {} : { irPath: paths.irPath }), + ...(llvmSourcePath === undefined ? {} : { llvmSourcePath }), + ...(irSourcePath === undefined ? {} : { irSourcePath }), + ...(llvmPathCurrent ? { llvmPathCurrent: true as const } : {}), + ...(irPathCurrent ? { irPathCurrent: true as const } : {}), }; } catch { return null; diff --git a/packages/compiler/src/frontend/jsx-entities.ts b/packages/compiler/src/frontend/jsx-entities.ts new file mode 100644 index 000000000..b9b0070de --- /dev/null +++ b/packages/compiler/src/frontend/jsx-entities.ts @@ -0,0 +1,258 @@ +/** The HTML entity names JSX text and attribute strings decode (HTML 4's + * 253 named character references, the set TypeScript's JSX emit decodes), + * by name. */ +export const JSX_ENTITIES: ReadonlyMap = new Map([ + ["quot", 34], + ["amp", 38], + ["apos", 39], + ["lt", 60], + ["gt", 62], + ["nbsp", 160], + ["iexcl", 161], + ["cent", 162], + ["pound", 163], + ["curren", 164], + ["yen", 165], + ["brvbar", 166], + ["sect", 167], + ["uml", 168], + ["copy", 169], + ["ordf", 170], + ["laquo", 171], + ["not", 172], + ["shy", 173], + ["reg", 174], + ["macr", 175], + ["deg", 176], + ["plusmn", 177], + ["sup2", 178], + ["sup3", 179], + ["acute", 180], + ["micro", 181], + ["para", 182], + ["middot", 183], + ["cedil", 184], + ["sup1", 185], + ["ordm", 186], + ["raquo", 187], + ["frac14", 188], + ["frac12", 189], + ["frac34", 190], + ["iquest", 191], + ["Agrave", 192], + ["Aacute", 193], + ["Acirc", 194], + ["Atilde", 195], + ["Auml", 196], + ["Aring", 197], + ["AElig", 198], + ["Ccedil", 199], + ["Egrave", 200], + ["Eacute", 201], + ["Ecirc", 202], + ["Euml", 203], + ["Igrave", 204], + ["Iacute", 205], + ["Icirc", 206], + ["Iuml", 207], + ["ETH", 208], + ["Ntilde", 209], + ["Ograve", 210], + ["Oacute", 211], + ["Ocirc", 212], + ["Otilde", 213], + ["Ouml", 214], + ["times", 215], + ["Oslash", 216], + ["Ugrave", 217], + ["Uacute", 218], + ["Ucirc", 219], + ["Uuml", 220], + ["Yacute", 221], + ["THORN", 222], + ["szlig", 223], + ["agrave", 224], + ["aacute", 225], + ["acirc", 226], + ["atilde", 227], + ["auml", 228], + ["aring", 229], + ["aelig", 230], + ["ccedil", 231], + ["egrave", 232], + ["eacute", 233], + ["ecirc", 234], + ["euml", 235], + ["igrave", 236], + ["iacute", 237], + ["icirc", 238], + ["iuml", 239], + ["eth", 240], + ["ntilde", 241], + ["ograve", 242], + ["oacute", 243], + ["ocirc", 244], + ["otilde", 245], + ["ouml", 246], + ["divide", 247], + ["oslash", 248], + ["ugrave", 249], + ["uacute", 250], + ["ucirc", 251], + ["uuml", 252], + ["yacute", 253], + ["thorn", 254], + ["yuml", 255], + ["OElig", 338], + ["oelig", 339], + ["Scaron", 352], + ["scaron", 353], + ["Yuml", 376], + ["fnof", 402], + ["circ", 710], + ["tilde", 732], + ["Alpha", 913], + ["Beta", 914], + ["Gamma", 915], + ["Delta", 916], + ["Epsilon", 917], + ["Zeta", 918], + ["Eta", 919], + ["Theta", 920], + ["Iota", 921], + ["Kappa", 922], + ["Lambda", 923], + ["Mu", 924], + ["Nu", 925], + ["Xi", 926], + ["Omicron", 927], + ["Pi", 928], + ["Rho", 929], + ["Sigma", 931], + ["Tau", 932], + ["Upsilon", 933], + ["Phi", 934], + ["Chi", 935], + ["Psi", 936], + ["Omega", 937], + ["alpha", 945], + ["beta", 946], + ["gamma", 947], + ["delta", 948], + ["epsilon", 949], + ["zeta", 950], + ["eta", 951], + ["theta", 952], + ["iota", 953], + ["kappa", 954], + ["lambda", 955], + ["mu", 956], + ["nu", 957], + ["xi", 958], + ["omicron", 959], + ["pi", 960], + ["rho", 961], + ["sigmaf", 962], + ["sigma", 963], + ["tau", 964], + ["upsilon", 965], + ["phi", 966], + ["chi", 967], + ["psi", 968], + ["omega", 969], + ["thetasym", 977], + ["upsih", 978], + ["piv", 982], + ["ensp", 8194], + ["emsp", 8195], + ["thinsp", 8201], + ["zwnj", 8204], + ["zwj", 8205], + ["lrm", 8206], + ["rlm", 8207], + ["ndash", 8211], + ["mdash", 8212], + ["lsquo", 8216], + ["rsquo", 8217], + ["sbquo", 8218], + ["ldquo", 8220], + ["rdquo", 8221], + ["bdquo", 8222], + ["dagger", 8224], + ["Dagger", 8225], + ["bull", 8226], + ["hellip", 8230], + ["permil", 8240], + ["prime", 8242], + ["Prime", 8243], + ["lsaquo", 8249], + ["rsaquo", 8250], + ["oline", 8254], + ["frasl", 8260], + ["euro", 8364], + ["image", 8465], + ["weierp", 8472], + ["real", 8476], + ["trade", 8482], + ["alefsym", 8501], + ["larr", 8592], + ["uarr", 8593], + ["rarr", 8594], + ["darr", 8595], + ["harr", 8596], + ["crarr", 8629], + ["lArr", 8656], + ["uArr", 8657], + ["rArr", 8658], + ["dArr", 8659], + ["hArr", 8660], + ["forall", 8704], + ["part", 8706], + ["exist", 8707], + ["empty", 8709], + ["nabla", 8711], + ["isin", 8712], + ["notin", 8713], + ["ni", 8715], + ["prod", 8719], + ["sum", 8721], + ["minus", 8722], + ["lowast", 8727], + ["radic", 8730], + ["prop", 8733], + ["infin", 8734], + ["ang", 8736], + ["and", 8743], + ["or", 8744], + ["cap", 8745], + ["cup", 8746], + ["int", 8747], + ["there4", 8756], + ["sim", 8764], + ["cong", 8773], + ["asymp", 8776], + ["ne", 8800], + ["equiv", 8801], + ["le", 8804], + ["ge", 8805], + ["sub", 8834], + ["sup", 8835], + ["nsub", 8836], + ["sube", 8838], + ["supe", 8839], + ["oplus", 8853], + ["otimes", 8855], + ["perp", 8869], + ["sdot", 8901], + ["lceil", 8968], + ["rceil", 8969], + ["lfloor", 8970], + ["rfloor", 8971], + ["lang", 9001], + ["rang", 9002], + ["loz", 9674], + ["spades", 9824], + ["clubs", 9827], + ["hearts", 9829], + ["diams", 9830], +]); diff --git a/packages/compiler/src/frontend/jsx-source.test.ts b/packages/compiler/src/frontend/jsx-source.test.ts new file mode 100644 index 000000000..564a61d98 --- /dev/null +++ b/packages/compiler/src/frontend/jsx-source.test.ts @@ -0,0 +1,244 @@ +import ts5 from "typescript5"; +import { afterAll, describe, expect, it } from "vitest"; +import { jsxEmitPlan, decodeEntities, jsxPragmas, jsxTextValue, lowerJsx } from "./jsx-source.js"; +import { closeSourceParser, parseSourceFile } from "./ts7/source-parser-node.js"; + +afterAll(() => closeSourceParser()); + +/** Evaluates a program whose last statement is `result = ...`, with `h` and + * `Fragment` recording the calls the JSX makes. */ +function evaluate(code: string): unknown { + const run = new Function( + "h", + "Fragment", + "React", + `"use strict"; let result; ${code}; return result;`, + ) as (...args: unknown[]) => unknown; + const h = (tag: unknown, props: unknown, ...children: unknown[]): unknown => ({ + tag: typeof tag === "function" ? `fn:${tag.name}` : tag === Fragment ? "#fragment" : tag, + props, + children, + }); + const Fragment = { fragment: true }; + return run(h, Fragment, { createElement: h, Fragment }); +} + +/** TypeScript's own classic-runtime emit of the snippet. */ +function typescriptEmit(source: string, factory?: string): string { + return ts5.transpileModule(source, { + fileName: "case.tsx", + compilerOptions: { + target: ts5.ScriptTarget.ES2022, + module: ts5.ModuleKind.ESNext, + jsx: ts5.JsxEmit.React, + ...(factory === undefined ? {} : { jsxFactory: factory, jsxFragmentFactory: "Fragment" }), + }, + }).outputText; +} + +function rewrite(source: string, factory?: string): string { + const sf = parseSourceFile("/case.tsx", source, "tsx"); + const plan = jsxEmitPlan( + { + jsx: "react", + ...(factory === undefined ? {} : { jsxFactory: factory, jsxFragmentFactory: "Fragment" }), + }, + jsxPragmas(source), + ); + return lowerJsx(sf, plan).text; +} + +const cases: Record = { + "attributes and text": `const name = "w"; const p = { id: 1 }; +result =
Hello {name} & A © + second line + {/* comment */} + {name}{" "}bold +
;`, + fragments: `function Foo() {} Foo.Bar = function Bar() {}; +result = <>;`, + namespaced: `result = ;`, + "keys and nested maps": `const p = { id: 1 }; +result =
{[1, 2].map((n) => {n})}
;`, + "white space only": `result = [
,
+
,
a {"b"} c
];`, + "spreads": `const p = { id: 1 }; const kids = [1, 2]; +result = [
,
,
{...kids}
,
,
];`, + "attribute strings": `result =
;`, + "attribute values": `result =
c={{1}} d={[

]} />;`, + "member and this tags": `class X { Tag = function Tag() {}; render() { return ; } } +result = new X().render();`, + "dashed and capitalized names": `function Comp() {} +result = [, ];`, + "nested JSX in expressions": `const items = ["a", "b"]; +result =

    + {items.length > 1 ?
  • {items[0]}
  • : null} + {items.map((item) => +
  • + {item} +
  • )} +
;`, +}; + +describe("lowerJsx", () => { + for (const [name, source] of Object.entries(cases)) { + it(`matches TypeScript's emit: ${name}`, () => { + const lowered = rewrite(source); + expect(evaluate(lowered)).toEqual(evaluate(typescriptEmit(source))); + expect(lowered.split("\n").length).toBe(source.split("\n").length); + }); + } + + it("uses jsxFactory and the pragmas", () => { + const source = `result = <>;`; + expect(evaluate(rewrite(source, "h"))).toEqual(evaluate(typescriptEmit(source, "h"))); + const pragma = `/** @jsx h */\n/** @jsxFrag Fragment */\nresult = <>;`; + expect(rewrite(pragma)).toContain(`h(Fragment, null`); + expect(evaluate(rewrite(pragma))).toEqual(evaluate(typescriptEmit(pragma))); + }); + + it("keeps every byte outside JSX", () => { + const source = `const a = 1; // \nconst b = "
";\nresult = [a, b, ];\n`; + const lowered = rewrite(source); + expect( + lowered.startsWith(`const a = 1; // \nconst b = "
";\nresult = [a, b, `), + ).toBe(true); + }); + + it("maps rewritten positions back to the source", () => { + const source = `const x = 1;\nresult =
{x}
;\nconst y = 2;\n`; + const sf = parseSourceFile("/case.tsx", source, "tsx"); + const lowered = lowerJsx(sf, { runtime: "classic", factory: "h", fragment: "Fragment" }); + const after = lowered.text.indexOf("const y"); + expect(lowered.originalPosition(after)).toBe(source.indexOf("const y")); + const copied = lowered.text.indexOf("{ a: x }") + "{ a: ".length; + expect(lowered.originalPosition(copied)).toBe(source.indexOf("{x}>") + 1); + expect(lowered.originalPosition(lowered.text.indexOf("h("))).toBe(source.indexOf(" + list + .split(",") + .map((item) => { + const [name, local] = item.trim().split(/\s+as\s+/); + return `const ${local} = runtime.${name};`; + }) + .join(" "), + ) + .replace(/(^|\s)export /gm, "$1"); + const record = + (kind: string) => + (...args: unknown[]): unknown => ({ kind, args: args.map(named) }); + const runtime = { + jsx: record("jsx"), + jsxs: record("jsxs"), + jsxDEV: record("jsxDEV"), + createElement: record("createElement"), + Fragment: "#fragment", + }; + const run = new Function("runtime", `"use strict"; let result; ${body}; return result;`) as ( + runtime: unknown, + ) => unknown; + return run(runtime); +} + +/** Functions compare by name: each evaluation declares its own. */ +function named(value: unknown): unknown { + if (typeof value === "function") return `fn:${value.name}`; + if (Array.isArray(value)) return value.map(named); + if (value !== null && typeof value === "object") + return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, named(item)])); + return value; +} + +function typescriptAutomatic(source: string, development: boolean): string { + return ts5.transpileModule(source, { + fileName: "/case.tsx", + compilerOptions: { + target: ts5.ScriptTarget.ES2022, + module: ts5.ModuleKind.ESNext, + jsx: development ? ts5.JsxEmit.ReactJSXDev : ts5.JsxEmit.ReactJSX, + }, + }).outputText; +} + +function rewriteAutomatic(source: string, development: boolean): string { + const sf = parseSourceFile("/case.tsx", source, "tsx"); + const plan = jsxEmitPlan({ jsx: development ? "react-jsxdev" : "react-jsx" }, jsxPragmas(source)); + return lowerJsx(sf, plan).text; +} + +const automaticCases: Record = { + ...cases, + "keys": `const p = { id: 1 }; +result = [
  • x
  • ,
  • ,
  • ,
  • ,
  • ];`, + "children shapes": `const kids = [1, 2]; +result = [

    ,

    {}

    ,

    +

    ,

    one

    ,

    {1}{2}

    ,

    {...kids}

    ,

    , <>, <>];`, + "names in use": `const _jsx = "taken"; const _jsxs = 1; +result = [

    {_jsx}

    ,

    {_jsxs}{_jsx}

    ];`, +}; + +describe("lowerJsx with the automatic runtime", () => { + for (const [name, source] of Object.entries(automaticCases)) { + for (const development of [false, true]) { + it(`matches TypeScript's ${development ? "react-jsxdev" : "react-jsx"} emit: ${name}`, () => { + const lowered = rewriteAutomatic(`export {};\n${source}`, development); + const expected = typescriptAutomatic(`export {};\n${source}`, development); + expect(evaluateAutomatic(lowered)).toEqual(evaluateAutomatic(expected)); + expect(lowered.split("\n").length).toBe(source.split("\n").length + 1); + }); + } + } + + it("imports the runtime functions on the first statement's line", () => { + const lowered = rewriteAutomatic( + `"use strict";\nexport const a = <>;\n`, + false, + ); + expect(lowered).toBe( + `"use strict";import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime"; \nexport const a = _jsx(_Fragment, { children: _jsx("b", {}, "k") });\n`, + ); + }); + + it("reads @jsxImportSource and @jsxRuntime", () => { + const pragma = `/** @jsxImportSource preact */\nexport const a = ;\n`; + expect(rewriteAutomatic(pragma, false)).toContain(`from "preact/jsx-runtime"`); + const classic = `/** @jsxRuntime classic */\n/** @jsx h */\nexport const a = ;\n`; + expect(rewriteAutomatic(classic, false)).toContain(`h("b", null)`); + }); +}); + +describe("JSX text", () => { + it("trims lines as TypeScript does", () => { + expect(jsxTextValue(" a \n b \n ")).toBe(" a b"); + expect(jsxTextValue("\n \n")).toBeUndefined(); + expect(jsxTextValue(" ")).toBe(" "); + }); + + it("decodes entities", () => { + expect(decodeEntities("<>& AB&bogus;�")).toBe( + "<>& AB&bogus;�", + ); + }); +}); + +describe("jsxPragmas", () => { + it("reads leading multi-line comments, first @jsx and last @jsxImportSource", () => { + expect( + jsxPragmas( + "#!/usr/bin/env node\n/** @jsx h\n * @jsx g */\n// @jsxFrag F\n/* @jsxImportSource a */ /* @jsxImportSource b */\nconst x = 1; /** @jsx late */", + ), + ).toEqual({ jsx: "h", jsxImportSource: "b" }); + }); +}); diff --git a/packages/compiler/src/frontend/jsx-source.ts b/packages/compiler/src/frontend/jsx-source.ts new file mode 100644 index 000000000..915a5d16f --- /dev/null +++ b/packages/compiler/src/frontend/jsx-source.ts @@ -0,0 +1,856 @@ +/* JSX lowering at the source level, before IR lowering. + * + * A program that contains JSX is checked as written: the checker types JSX + * with the project's jsx settings and reports the errors tsc reports. Each + * file that contains JSX is then rewritten with every JSX expression + * replaced by the calls TypeScript emits for it, and the rewritten program is + * what the lowering compiles, through the same import, call and object + * literal lowering as ordinary code. + * + * The rewrite copies every byte outside JSX expressions, and copies the + * expressions inside them (attribute values, children, spreads) with their + * own JSX rewritten. It keeps every line break of a JSX expression, between + * the arguments it emits, so a position keeps its line in the rewritten + * text. */ + +import * as ts from "./ts7/adapter.js"; +import { jsxOptionName } from "./ts7/enums.js"; +import { JSX_ENTITIES } from "./jsx-entities.js"; + +/** The project's jsx compiler options, as the tsconfig spells them. */ +export interface JsxOptions { + jsx?: string | number | undefined; + jsxFactory?: string | undefined; + jsxFragmentFactory?: string | undefined; + jsxImportSource?: string | undefined; + reactNamespace?: string | undefined; +} + +/** The tsconfig name of a `jsx` option value (TS7 options carry JsxEmit + * numbers, parsed tsconfigs may carry the name). */ +export function jsxEmitName(value: string | number | undefined): string | undefined { + if (typeof value === "number") return jsxOptionName(value); + return value?.toLowerCase(); +} + +/** The `@jsx`, `@jsxFrag`, `@jsxImportSource` and `@jsxRuntime` pragmas of + * a file's leading comments, as TypeScript reads them: multi-line comments + * only, the first `@jsx` and `@jsxFrag` and the last `@jsxImportSource` and + * `@jsxRuntime`. */ +export interface JsxPragmas { + jsx?: string; + jsxFrag?: string; + jsxImportSource?: string; + jsxRuntime?: string; +} + +export function jsxPragmas(text: string): JsxPragmas { + const pragmas: JsxPragmas = {}; + for (const comment of leadingMultiLineComments(text)) { + const pattern = /@(\S+)(\s+(?:\S.*)?)?$/gm; + for (let match = pattern.exec(comment); match !== null; match = pattern.exec(comment)) { + const argument = match[2]?.trim().split(/\s+/)[0]; + if (!argument) continue; + switch (match[1]!.toLowerCase()) { + case "jsx": + pragmas.jsx ??= argument; + break; + case "jsxfrag": + pragmas.jsxFrag ??= argument; + break; + case "jsximportsource": + pragmas.jsxImportSource = argument; + break; + case "jsxruntime": + pragmas.jsxRuntime = argument; + break; + } + } + } + return pragmas; +} + +/** The `/* ... *\/` comments before a file's first token (after a shebang). */ +function leadingMultiLineComments(text: string): string[] { + const comments: string[] = []; + let i = text.startsWith("#!") ? text.indexOf("\n") : 0; + if (i < 0) return comments; + while (i < text.length) { + const ch = text.charCodeAt(i); + if (isLineBreak(ch) || isWhiteSpaceSingleLine(ch)) { + i++; + } else if (text.startsWith("//", i)) { + while (i < text.length && !isLineBreak(text.charCodeAt(i))) i++; + } else if (text.startsWith("/*", i)) { + const end = text.indexOf("*/", i + 2); + const close = end < 0 ? text.length : end + 2; + comments.push(text.slice(i, close)); + i = close; + } else { + break; + } + } + return comments; +} + +function isLineBreak(ch: number): boolean { + return ch === 10 || ch === 13 || ch === 0x2028 || ch === 0x2029; +} + +function isWhiteSpaceSingleLine(ch: number): boolean { + return ( + ch === 32 || + ch === 9 || + ch === 11 || + ch === 12 || + ch === 160 || + ch === 133 || + ch === 5760 || + (ch >= 8192 && ch <= 8203) || + ch === 8239 || + ch === 8287 || + ch === 12288 || + ch === 65279 + ); +} + +/** How a file's JSX is emitted, after its pragmas. */ +export type JsxEmitPlan = + | { + runtime: "classic"; + /** The factory call (`h`, `React.createElement`). */ + factory: string; + /** The fragment tag (`Fragment`, `React.Fragment`). */ + fragment: string; + } + | { + runtime: "automatic"; + /** The module `jsx-runtime` (or `jsx-dev-runtime`) is under, and that + * a `createElement` fallback imports from (`react`). */ + importSource: string; + /** jsx: react-jsxdev: `jsxDEV` calls with source locations. */ + development: boolean; + }; + +/** The plan of a file: the automatic runtime with jsx: react-jsx or + * react-jsxdev, an import source or an `@jsxRuntime automatic` pragma (an + * `@jsxRuntime classic` pragma overrides all of them), the classic runtime + * otherwise. */ +export function jsxEmitPlan(options: JsxOptions, pragmas: JsxPragmas): JsxEmitPlan { + const mode = jsxEmitName(options.jsx); + const automatic = + pragmas.jsxRuntime !== "classic" && + (mode === "react-jsx" || + mode === "react-jsxdev" || + options.jsxImportSource !== undefined || + pragmas.jsxImportSource !== undefined || + pragmas.jsxRuntime === "automatic"); + if (automatic) { + return { + runtime: "automatic", + importSource: pragmas.jsxImportSource ?? options.jsxImportSource ?? "react", + development: mode === "react-jsxdev", + }; + } + const namespace = options.reactNamespace ?? "React"; + return { + runtime: "classic", + factory: pragmas.jsx ?? options.jsxFactory ?? `${namespace}.createElement`, + fragment: pragmas.jsxFrag ?? options.jsxFragmentFactory ?? `${namespace}.Fragment`, + }; +} + +/** True when the file contains a JSX expression. */ +export function containsJsx(sf: ts.SourceFile): boolean { + let found = false; + const visit = (node: ts.Node): void => { + if (found) return; + if (isJsxExpressionRoot(node)) { + found = true; + return; + } + ts.forEachChild(node, visit); + }; + visit(sf); + return found; +} + +/** The first JSX expression of the file (for diagnostics), or null. */ +export function firstJsx(sf: ts.SourceFile): ts.Node | null { + let found: ts.Node | null = null; + const visit = (node: ts.Node): void => { + if (found !== null) return; + if (isJsxExpressionRoot(node)) { + found = node; + return; + } + ts.forEachChild(node, visit); + }; + visit(sf); + return found; +} + +type JsxRoot = ts.JsxElement | ts.JsxSelfClosingElement | ts.JsxFragment; + +function isJsxExpressionRoot(node: ts.Node): node is JsxRoot { + return ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node) || ts.isJsxFragment(node); +} + +/** A file's text with its JSX lowered, and the way back from a position of + * that text to the source position it stands for. */ +export interface LoweredJsx { + text: string; + /** A rewritten-text offset as an offset of the original text: copied text + * keeps its offset; emitted call text maps to the JSX it stands for. */ + originalPosition(pos: number): number; + /** An original-text offset in the rewritten text: copied text keeps its + * offset; a position inside replaced JSX maps to the call text emitted + * for the JSX at or before it (its end, for the `end` of a span). */ + rewrittenPosition(pos: number, end: boolean): number; +} + +/** The file's text with its JSX replaced by the calls TypeScript emits for + * the plan, and for the automatic runtime the imports of the runtime + * functions those calls use. */ +export function lowerJsx(sf: ts.SourceFile, plan: JsxEmitPlan): LoweredJsx { + return new JsxRewriter(sf, plan).file(); +} + +/** A run of rewritten text: `copied` text starts at `original`; emitted + * text stands for the source position `original`. */ +interface Segment { + rewritten: number; + original: number; + /** The copied length, 0 for emitted text. */ + copied: number; +} + +class JsxRewriter { + private readonly text: string; + /** Output pieces; `cursor` is the source position whose line breaks the + * output has reached. */ + private readonly pieces: string[] = []; + private readonly segments: Segment[] = []; + private length = 0; + private cursor = 0; + + constructor( + private readonly sf: ts.SourceFile, + private readonly plan: JsxEmitPlan, + ) { + this.text = sf.text; + } + + file(): LoweredJsx { + const insertAt = this.plan.runtime === "automatic" ? this.prologueEnd() : -1; + this.copy(0, this.text.length); + if (insertAt >= 0) this.insertImports(insertAt); + const segments = this.segments; + const total = this.length; + return { + text: this.pieces.join(""), + originalPosition: (pos: number): number => { + let low = 0; + let high = segments.length - 1; + while (low < high) { + const mid = (low + high + 1) >> 1; + if (segments[mid]!.rewritten <= pos) low = mid; + else high = mid - 1; + } + const segment = segments[low]; + if (segment === undefined) return pos; + return segment.copied > 0 + ? segment.original + Math.min(pos - segment.rewritten, segment.copied) + : segment.original; + }, + rewrittenPosition: (pos: number, end: boolean): number => { + // Segments are in source order: the last one starting at or before + // `pos` holds it, or is the emitted text for the JSX holding it. + let low = 0; + let high = segments.length - 1; + while (low < high) { + const mid = (low + high + 1) >> 1; + if (segments[mid]!.original <= pos) low = mid; + else high = mid - 1; + } + const segment = segments[low]; + if (segment === undefined) return pos; + if (segment.copied > 0) + return segment.rewritten + Math.min(pos - segment.original, segment.copied); + return end ? (segments[low + 1]?.rewritten ?? total) : segment.rewritten; + }, + }; + } + + /** Local names of the automatic runtime's functions, by module and + * export, in the order of first use (TypeScript's implicit imports). */ + private readonly imports = new Map>(); + private names: Set | null = null; + private fileNameLocal: string | null = null; + + /** The local name bound to `name` of `module`: `_name`, or `_name_1`, + * `_name_2`, ... past an identifier the file already uses. */ + private importName(name: string, module: string): string { + let names = this.imports.get(module); + if (names === undefined) { + names = new Map(); + this.imports.set(module, names); + } + let local = names.get(name); + if (local === undefined) { + local = this.uniqueName(`_${name}`); + names.set(name, local); + } + return local; + } + + private uniqueName(base: string): string { + if (this.names === null) { + const names = new Set(); + const visit = (node: ts.Node): void => { + if (ts.isIdentifier(node)) names.add(node.text); + ts.forEachChild(node, visit); + }; + visit(this.sf); + this.names = names; + } + let name = base; + for (let i = 1; this.names.has(name); i++) name = `${base}_${i}`; + this.names.add(name); + return name; + } + + /** Where the runtime imports go: after the leading "use strict"-style + * directives, else before the first statement, so the inserted text + * shares that statement's line. */ + private prologueEnd(): number { + const statements = this.sf.statements.nodes; + let end = -1; + for (const statement of statements) { + if (!ts.isExpressionStatement(statement) || !ts.isStringLiteral(statement.expression)) break; + end = statement.end; + } + if (end >= 0) return end; + const first = statements[0]; + return first === undefined ? this.text.length : first.getStart(this.sf); + } + + /** Inserts the imports (or, in a CommonJS file, the requires) of the + * runtime functions the file's JSX used, and react-jsxdev's file name + * constant, at `at`: imports in the reverse order of their modules' first + * use, as TypeScript inserts each after the prologue. */ + private insertImports(at: number): void { + if (this.imports.size === 0 && this.fileNameLocal === null) return; + const esm = ts.isExternalModule(this.sf); + const parts: string[] = []; + for (const [module, names] of [...this.imports].reverse()) { + const specifier = JSON.stringify(module); + if (esm) { + const list = [...names].map(([name, local]) => `${name} as ${local}`).join(", "); + parts.push(`import { ${list} } from ${specifier};`); + } else if (/\.[cm]?jsx?$/.test(this.sf.fileName)) { + const list = [...names].map(([name, local]) => `${name}: ${local}`).join(", "); + parts.push(`const { ${list} } = require(${specifier});`); + } + } + if (this.fileNameLocal !== null) + parts.push(`const ${this.fileNameLocal} = ${JSON.stringify(this.sf.fileName)};`); + if (parts.length === 0) return; + const text = `${parts.join(" ")} `; + let index = 0; + let rewritten = 0; + for (; index < this.segments.length; index++) { + const segment = this.segments[index]!; + if (segment.copied > 0 && segment.original <= at && at <= segment.original + segment.copied) { + rewritten = segment.rewritten + (at - segment.original); + break; + } + } + // Split the copied segment holding `at` and shift what follows. + const holder = this.segments[index]; + const joined = this.pieces.join(""); + this.pieces.length = 0; + this.pieces.push(joined.slice(0, rewritten), text, joined.slice(rewritten)); + const following: Segment[] = [{ rewritten, original: at, copied: 0 }]; + if (holder !== undefined && holder.copied > 0) { + const head = at - holder.original; + const tail = holder.copied - head; + holder.copied = head; + if (tail > 0) + following.push({ rewritten: rewritten + text.length, original: at, copied: tail }); + } + for (let i = index + 1; i < this.segments.length; i++) + this.segments[i]!.rewritten += text.length; + this.segments.splice(index + 1, 0, ...following); + if (holder !== undefined && holder.copied === 0) this.segments.splice(index, 1); + this.length += text.length; + } + + /** Emits source text [start, end) unchanged. */ + private copied(start: number, end: number): void { + if (end <= start) return; + this.segments.push({ rewritten: this.length, original: start, copied: end - start }); + this.pieces.push(this.text.slice(start, end)); + this.length += end - start; + } + + /** Emits call text standing for the source position the cursor reached. */ + private emit(...texts: string[]): void { + for (const text of texts) { + if (text.length === 0) continue; + const last = this.segments[this.segments.length - 1]; + if (last === undefined || last.copied > 0 || last.original !== this.cursor) + this.segments.push({ rewritten: this.length, original: this.cursor, copied: 0 }); + this.pieces.push(text); + this.length += text.length; + } + } + + /** Copies the source range [start, end), with its JSX expressions + * replaced. */ + private copy(start: number, end: number): void { + let at = start; + const visit = (node: ts.Node): void => { + if (node.end <= at || (node !== this.sf && node.getStart(this.sf) >= end)) return; + if (isJsxExpressionRoot(node)) { + const nodeStart = node.getStart(this.sf); + this.copied(at, nodeStart); + if (nodeStart > this.cursor) this.cursor = nodeStart; + this.element(node); + this.sync(node.end); + at = node.end; + return; + } + ts.forEachChild(node, visit); + }; + // The cursor never moves back: a range copied after source that follows + // it (jsx()'s key argument, after the children) emits no line break + // twice. + if (start > this.cursor) this.cursor = start; + visit(this.enclosing(start, end)); + this.copied(at, end); + if (end > this.cursor) this.cursor = end; + } + + /** The smallest node around [start, end) whose children cover it. */ + private enclosing(start: number, end: number): ts.Node { + let node: ts.Node = this.sf; + for (;;) { + let next: ts.Node | undefined; + ts.forEachChild(node, (child) => { + if (next === undefined && child.pos <= start && child.end >= end) next = child; + }); + if (next === undefined || next === node) return node; + node = next; + } + } + + /** Emits the line breaks of the source between the cursor and `pos`. */ + private sync(pos: number): void { + for (let i = this.cursor; i < pos; i++) { + const ch = this.text.charCodeAt(i); + if (ch === 13 && this.text.charCodeAt(i + 1) === 10) { + this.cursor = i; + this.emit("\r\n"); + i++; + } else if (isLineBreak(ch)) { + this.cursor = i; + this.emit(this.text[i]!); + } + } + if (pos > this.cursor) this.cursor = pos; + } + + /** Copies an expression in a JSX expression's place. */ + private expression(node: ts.Node): void { + const start = node.getStart(this.sf); + this.sync(start); + this.copy(start, node.end); + } + + private element(node: JsxRoot): void { + const plan = this.plan; + if (plan.runtime === "classic") { + this.createElement(node, plan.factory, plan.fragment); + return; + } + const opening = ts.isJsxFragment(node) + ? null + : ts.isJsxElement(node) + ? node.openingElement + : node; + if (opening !== null && hasKeyAfterPropsSpread(opening.attributes)) { + // A key after a spread of props is not jsx()'s key argument: the + // element is the import source's createElement call. + this.createElement(node, this.importName("createElement", plan.importSource), ""); + return; + } + this.automaticElement(node, opening, plan.importSource, plan.development); + } + + /** The classic-runtime call: `factory(tag, props or null, ...children)`. */ + private createElement(node: JsxRoot, factory: string, fragment: string): void { + const children = ts.isJsxSelfClosingElement(node) ? [] : node.children.nodes; + if (ts.isJsxFragment(node)) { + this.emit(`${factory}(${fragment}, null`); + } else { + const opening = ts.isJsxElement(node) ? node.openingElement : node; + this.emit(`${factory}(`); + this.tagName(opening.tagName); + this.emit(", "); + this.attributes(opening.attributes.properties.nodes, null); + } + for (const child of children) { + if (ts.isJsxText(child)) { + const value = jsxTextValue(child.text); + if (value === undefined) continue; + this.emit(", "); + this.sync(child.getStart(this.sf)); + this.emit(JSON.stringify(value)); + } else if (ts.isJsxExpression(child)) { + if (child.expression === undefined) continue; + this.emit(", "); + if (child.dotDotDotToken !== undefined) this.emit("..."); + this.expression(child.expression); + } else if (isJsxExpressionRoot(child)) { + this.emit(", "); + this.sync(child.getStart(this.sf)); + this.element(child); + } + this.sync(child.end); + } + this.sync(node.end); + this.emit(")"); + } + + /** The automatic-runtime call: `jsx(tag, props[, key])` with the + * children in props, `jsxs` for more than one child or a spread child, and + * react-jsxdev's `jsxDEV(tag, props, key, isStatic, source, self)`. */ + private automaticElement( + node: JsxRoot, + opening: ts.JsxOpeningElement | ts.JsxSelfClosingElement | null, + importSource: string, + development: boolean, + ): void { + const children: readonly ts.JsxChild[] = ts.isJsxSelfClosingElement(node) + ? [] + : node.children.nodes; + const semantic = children.filter(isSemanticJsxChild); + const first = semantic[0]; + const isStatic = + semantic.length > 1 || + (first !== undefined && ts.isJsxExpression(first) && first.dotDotDotToken !== undefined); + const module = `${importSource}/${development ? "jsx-dev-runtime" : "jsx-runtime"}`; + this.emit(`${this.importName(development ? "jsxDEV" : isStatic ? "jsxs" : "jsx", module)}(`); + // TypeScript's children prop: the one semantic child itself, else an + // array of every child that emits a value. + const single = semantic.length === 1 && !isStatic ? first! : null; + const listed = single === null ? children.filter((child) => jsxChildEmits(child)) : []; + const childrenProp = + children.length > 0 && (single !== null ? jsxChildEmits(single) : listed.length > 0) + ? (): void => { + this.emit("children: "); + if (single !== null) { + this.child(single); + } else { + this.emit("["); + listed.forEach((child, index) => { + if (index > 0) this.emit(", "); + this.child(child); + this.sync(child.end); + }); + this.emit("]"); + } + } + : null; + let key: ts.JsxAttribute | undefined; + if (opening === null) { + this.emit(this.importName("Fragment", module), ", "); + if (childrenProp === null) this.emit("{}"); + else this.attributes([], childrenProp); + } else { + this.tagName(opening.tagName); + this.emit(", "); + const properties = opening.attributes.properties.nodes; + key = properties.find( + (property): property is ts.JsxAttribute => + ts.isJsxAttribute(property) && + ts.isIdentifier(property.name) && + property.name.text === "key", + ); + const attributes = properties.filter((property) => property !== key); + if (attributes.length === 0 && childrenProp === null) this.emit("{}"); + else this.attributes(attributes, childrenProp); + } + if (key !== undefined) { + this.emit(", "); + this.attributeValue(key.initializer); + } + if (development) { + if (key === undefined) this.emit(", void 0"); + const { line, column } = lineAndColumn(this.text, node.pos); + this.fileNameLocal ??= this.uniqueName("_jsxFileName"); + this.emit( + `, ${isStatic}, { fileName: ${this.fileNameLocal}, lineNumber: ${line}, columnNumber: ${column} }, `, + jsxSelf(node), + ); + } + this.sync(node.end); + this.emit(")"); + } + + /** A child's value in the children prop (it emits one: jsxChildEmits). */ + private child(child: ts.JsxChild): void { + if (ts.isJsxText(child)) { + this.sync(child.getStart(this.sf)); + this.emit(JSON.stringify(jsxTextValue(child.text)!)); + } else if (ts.isJsxExpression(child)) { + if (child.dotDotDotToken !== undefined) this.emit("..."); + this.expression(child.expression!); + } else { + this.sync(child.getStart(this.sf)); + this.element(child as JsxRoot); + } + } + + private tagName(tag: ts.JsxTagNameExpression): void { + this.sync(tag.getStart(this.sf)); + if (ts.isIdentifier(tag) && isIntrinsicJsxName(tag.text)) { + this.emit(JSON.stringify(tag.text)); + } else if (ts.isJsxNamespacedName(tag)) { + this.emit(JSON.stringify(`${tag.namespace.text}:${tag.name.text}`)); + } else { + this.emit(this.text.slice(tag.getStart(this.sf), tag.end)); + } + this.cursor = tag.end; + } + + /** The props object of `properties`, then `children` (the automatic + * runtime's children prop), or `null` for no properties and no children + * prop. */ + private attributes( + properties: readonly ts.JsxAttributeLike[], + children: (() => void) | null, + ): void { + if (properties.length === 0 && children === null) { + this.emit("null"); + return; + } + if (properties.length === 0) { + this.emit("{ "); + children!(); + this.emit(" }"); + return; + } + this.emit("{ "); + let first = true; + const separate = (): void => { + if (!first) this.emit(", "); + first = false; + }; + for (const attribute of properties) { + if (ts.isJsxSpreadAttribute(attribute)) { + const value = attribute.expression; + if (ts.isObjectLiteralExpression(value) && !hasProtoProperty(value)) { + // TypeScript inlines the properties of a spread object literal. + for (const property of value.properties.nodes) { + separate(); + this.expression(property); + } + } else { + separate(); + this.emit("..."); + this.expression(value); + } + } else { + separate(); + this.sync(attribute.getStart(this.sf)); + this.emit(attributeName(attribute.name), ": "); + this.attributeValue(attribute.initializer); + } + this.sync(attribute.end); + } + if (children !== null) { + this.emit(", "); + children(); + } + this.emit(" }"); + } + + private attributeValue(value: ts.JsxAttributeValue | undefined): void { + if (value === undefined) { + this.emit("true"); + } else if (ts.isStringLiteral(value)) { + this.sync(value.getStart(this.sf)); + this.emit(JSON.stringify(decodeEntities(value.text))); + } else if (ts.isJsxExpression(value)) { + if (value.expression === undefined) this.emit("true"); + else this.expression(value.expression); + } else { + this.sync(value.getStart(this.sf)); + this.element(value as JsxRoot); + } + } +} + +/** A child TypeScript counts: an expression container with an expression, + * text that is not only white space spanning lines, or an element. */ +function isSemanticJsxChild(child: ts.JsxChild): boolean { + if (ts.isJsxExpression(child)) return child.expression !== undefined; + if (ts.isJsxText(child)) return !child.containsOnlyTriviaWhiteSpaces; + return true; +} + +/** True when the child emits a value (text that trims to nothing and empty + * expression containers emit none). */ +function jsxChildEmits(child: ts.JsxChild): boolean { + if (ts.isJsxText(child)) return jsxTextValue(child.text) !== undefined; + if (ts.isJsxExpression(child)) return child.expression !== undefined; + return true; +} + +/** A `key` attribute after a spread that is not an object literal free of + * spreads: TypeScript emits createElement for such an element. */ +function hasKeyAfterPropsSpread(attributes: ts.JsxAttributes): boolean { + let spread = false; + for (const property of attributes.properties.nodes) { + if ( + ts.isJsxSpreadAttribute(property) && + (!ts.isObjectLiteralExpression(property.expression) || + property.expression.properties.nodes.some((p: ts.ObjectLiteralElementLike) => + ts.isSpreadAssignment(p), + )) + ) { + spread = true; + } else if ( + spread && + ts.isJsxAttribute(property) && + ts.isIdentifier(property.name) && + property.name.text === "key" + ) { + return true; + } + } + return false; +} + +/** The 1-based line and column of `pos`, counting the line breaks + * TypeScript counts (\r\n, \n, \r, U+2028, U+2029) and UTF-16 units. */ +function lineAndColumn(text: string, pos: number): { line: number; column: number } { + let line = 1; + let start = 0; + for (let i = 0; i < pos; i++) { + const ch = text.charCodeAt(i); + if (ch === 13 && text.charCodeAt(i + 1) === 10) continue; + if (isLineBreak(ch)) { + line++; + start = i + 1; + } + } + return { line, column: pos - start + 1 }; +} + +/** react-jsxdev's `self` argument. TypeScript passes `this`; where `this` + * has no declared type (the top level of an ES module and a function that is + * not a method and declares no `this` parameter) the argument is the + * `undefined` such code observes when called without a receiver, since an + * untyped `this` does not type-check under noImplicitThis. */ +function jsxSelf(node: ts.Node): string { + for (let at = node.parent; at !== undefined; at = at.parent) { + if (ts.isArrowFunction(at)) continue; + if ( + ts.isMethodDeclaration(at) || + ts.isConstructorDeclaration(at) || + ts.isGetAccessorDeclaration(at) || + ts.isSetAccessorDeclaration(at) || + ts.isPropertyDeclaration(at) || + ts.isClassStaticBlockDeclaration(at) + ) + return "this"; + if (ts.isFunctionDeclaration(at) || ts.isFunctionExpression(at)) { + const declaresThis = at.parameters.nodes.some( + (parameter: ts.ParameterDeclaration) => + ts.isIdentifier(parameter.name) && parameter.name.text === "this", + ); + const objectMethod = + ts.isFunctionExpression(at) && + at.parent !== undefined && + ts.isPropertyAssignment(at.parent); + return declaresThis || objectMethod ? "this" : "undefined"; + } + if (ts.isSourceFile(at)) return ts.isExternalModule(at) ? "undefined" : "this"; + } + return "undefined"; +} + +/** TypeScript's intrinsic-element test: a lowercase first letter or a dash. */ +function isIntrinsicJsxName(name: string): boolean { + const ch = name.charCodeAt(0); + return (ch >= 97 && ch <= 122) || name.includes("-"); +} + +function attributeName(name: ts.JsxAttributeName): string { + if (ts.isJsxNamespacedName(name)) + return JSON.stringify(`${name.namespace.text}:${name.name.text}`); + return /^[A-Z_]\w*$/i.test(name.text) ? name.text : JSON.stringify(name.text); +} + +function hasProtoProperty(object: ts.ObjectLiteralExpression): boolean { + return object.properties.nodes.some( + (property: ts.ObjectLiteralElementLike) => + ts.isPropertyAssignment(property) && + ((ts.isIdentifier(property.name) && property.name.text === "__proto__") || + (ts.isStringLiteral(property.name) && property.name.text === "__proto__")), + ); +} + +/** A JSX text child's string value: each line trimmed (the first line keeps + * its leading and the last line its trailing white space), lines without + * text dropped, the rest joined with one space and entities decoded. + * Undefined when nothing remains. */ +export function jsxTextValue(text: string): string | undefined { + let value: string | undefined; + let firstNonWhitespace = 0; + let lastNonWhitespace = -1; + const add = (line: string): void => { + const decoded = decodeEntities(line); + value = value === undefined ? decoded : `${value} ${decoded}`; + }; + for (let i = 0; i < text.length; i++) { + const ch = text.charCodeAt(i); + if (isLineBreak(ch)) { + if (firstNonWhitespace !== -1 && lastNonWhitespace !== -1) + add(text.slice(firstNonWhitespace, lastNonWhitespace + 1)); + firstNonWhitespace = -1; + } else if (!isWhiteSpaceSingleLine(ch)) { + lastNonWhitespace = i; + if (firstNonWhitespace === -1) firstNonWhitespace = i; + } + } + if (firstNonWhitespace !== -1) add(text.slice(firstNonWhitespace)); + return value; +} + +/** Decodes `&name;`, `&#ddd;` and `&#xhh;` references. Unknown names and + * numbers past U+10FFFF stay as written. */ +export function decodeEntities(text: string): string { + return text.replace( + /&((#((\d+)|x([\da-fA-F]+)))|(\w+));/g, + ( + match: string, + _all: string | undefined, + _number: string | undefined, + _digits: string | undefined, + decimal: string | undefined, + hex: string | undefined, + word: string | undefined, + ) => { + const code = decimal + ? parseInt(decimal, 10) + : hex + ? parseInt(hex, 16) + : JSX_ENTITIES.get(word!); + return code === undefined || code > 0x10ffff ? match : String.fromCodePoint(code); + }, + ); +} diff --git a/packages/compiler/src/frontend/literal-values.test.ts b/packages/compiler/src/frontend/literal-values.test.ts index 2a85f82bf..1d792b1d4 100644 --- a/packages/compiler/src/frontend/literal-values.test.ts +++ b/packages/compiler/src/frontend/literal-values.test.ts @@ -8,16 +8,15 @@ import { function harness() { const members = new Map(); - const snapshot = new SemanticSnapshot(1, { - text(method, payload) { - const query = JSON.parse(payload) as { objectId: number }; - if (method !== "getTypesOfType" || !members.has(query.objectId)) - throw new Error(`unexpected request: ${method}`); - return JSON.stringify(members.get(query.objectId)); - }, - binary(method) { + const reply = (method: string, payload: string) => { + const query = JSON.parse(payload) as { objectId: number }; + if (method !== "getTypesOfType" || !members.has(query.objectId)) throw new Error(`unexpected request: ${method}`); - }, + return JSON.stringify(members.get(query.objectId)); + }; + const snapshot = new SemanticSnapshot(1, { + text: reply, + binary: (method, payload) => new TextEncoder().encode(reply(method, payload)), }); const project = snapshot.addProject("literal-values", () => undefined); let id = 0; diff --git a/packages/compiler/src/frontend/lowering/builtins/crypto.ts b/packages/compiler/src/frontend/lowering/builtins/crypto.ts index 93c8498ef..47e66f9b2 100644 --- a/packages/compiler/src/frontend/lowering/builtins/crypto.ts +++ b/packages/compiler/src/frontend/lowering/builtins/crypto.ts @@ -1,6 +1,7 @@ import { lowerBuiltinByteInput, errorFirstBytesCallback, errorFirstCallback } from "./arguments.js"; import { isKeyObjectValue } from "./key-object.js"; import { lowerCreateEcdh } from "./ecdh.js"; +import { lowerCreateDiffieHellman, lowerGetDiffieHellman } from "./dh.js"; import { boolLit, dynUndefinedExpr, nodeThrowExpr, varRef } from "../../../ir/build.js"; import * as ts from "../../ts7/adapter.js"; import { type Lowerer, own } from "../lowerer.js"; @@ -411,6 +412,9 @@ export function lowerCryptoModuleCall( }; } if (bi.member === "createECDH") return lowerCreateEcdh(lowerer, expr); + if (bi.member === "createDiffieHellman") return lowerCreateDiffieHellman(lowerer, expr); + if (bi.member === "getDiffieHellman" || bi.member === "createDiffieHellmanGroup") + return lowerGetDiffieHellman(lowerer, expr); if (bi.member === "createHmac") { if (args.length < 1 || args.length > 3 || args.some(ts.isSpreadElement)) { lowerer.noLowering( @@ -696,6 +700,10 @@ export function lowerCryptoModuleCall( loc, }; } + const pki = lowerPkiCall(lowerer, expr, bi.member, args, loc); + if (pki) return pki; + const prime = lowerPrimeCall(lowerer, expr, bi.member, args, loc); + if (prime) return prime; if (bi.member === "createSecretKey") { if (args.length < 1 || args.length > 2 || args.some(ts.isSpreadElement)) lowerer.noLowering( @@ -861,11 +869,29 @@ const IMPLEMENTED_CIPHERS = new Set([ "id-aes192-gcm", "id-aes256-gcm", "chacha20-poly1305", + ...["128", "192", "256"].flatMap((bits) => + ["ecb", "cfb", "cfb8", "cfb1", "ofb"].map((mode) => `aes-${bits}-${mode}`), + ), + "des-ede3-cbc", + "des3", + "des-ede3", + "des-ede3-ecb", + "des-ede3-cfb", + "des-ede3-cfb8", + "des-ede3-cfb1", + "des-ede3-ofb", + "des-ede-cbc", + "des-ede", + "des-ede-ecb", + "des-ede-cfb", + "des-ede-ofb", + "chacha20", ]); const SUPPORTED_CIPHERS_HINT = - "the supported ciphers are aes-128-cbc, aes-192-cbc, aes-256-cbc, aes-128-ctr, aes-192-ctr, " + - "aes-256-ctr, aes-128-gcm, aes-192-gcm, aes-256-gcm, and chacha20-poly1305"; + "the supported ciphers are aes-128, aes-192 and aes-256 in cbc, ctr, gcm, ecb, cfb, cfb8, cfb1 " + + "and ofb modes, des-ede3 in cbc, ecb, cfb, cfb8, cfb1 and ofb modes, des-ede in cbc, ecb, cfb " + + "and ofb modes, chacha20, and chacha20-poly1305"; /** A literal cipher name that Node knows but the runtime does not * implement is refused at compile time. Unknown literal names keep Node's @@ -1339,3 +1365,108 @@ export function isWebCryptoReceiver(lowerer: Lowerer, receiver: ts.Expression): } return false; } + +/** Node calls checkPrime's and generatePrime's callbacks with undefined as + * the error, where @types/node declares Error | null: whether this + * callback's error parameter can hold undefined. One that cannot receives + * null instead. */ +function takesUndefinedError(lowerer: Lowerer, callback: ts.Expression): boolean { + const type = lowerer.irTypeOf(callback); + if (type.kind !== "func") return true; + const error = type.params[0]; + if (error === undefined || error.kind === "dyn" || error.kind === "undefinedT") return true; + if (error.kind !== "union") return false; + return !!lowerer.unions.get(error.unionId)?.arms.some((arm) => arm.kind === "undefinedT"); +} + +/** checkPrime, checkPrimeSync, generatePrime and generatePrimeSync + * (scr_dh.c): checked-dynamic arguments, which the runtime checks as Node + * does (a function in the options position is the callback), and the + * sync forms' boolean, ArrayBuffer or bigint converted to the call site's + * type. Null for other members. */ +function lowerPrimeCall( + lowerer: Lowerer, + expr: ts.CallExpression, + member: string, + args: readonly ts.Expression[], + loc: SrcLoc, +): IrExpr | null { + const forms: Readonly> = { + checkPrimeSync: ["crypto.checkPrimeSync", 1, 2, "checkPrimeSync(candidate[, options])"], + checkPrime: ["crypto.checkPrimeCb", 1, 3, "checkPrime(candidate[, options], callback)"], + generatePrimeSync: ["crypto.generatePrimeSync", 1, 2, "generatePrimeSync(size[, options])"], + generatePrime: ["crypto.generatePrimeCb", 1, 3, "generatePrime(size[, options], callback)"], + }; + const form = forms[member]; + if (!form) return null; + const [fn, min, max, supported] = form; + if (args.length < min || args.length > max || args.some(ts.isSpreadElement)) + lowerer.noLowering( + `crypto.${member} with ${args.length} arguments`, + expr, + `${supported} is supported`, + ); + const values = Array.from({ length: max }, (_, i) => dynArg(lowerer, args[i], loc)); + if (fn === "crypto.checkPrimeCb" || fn === "crypto.generatePrimeCb") { + const callback = args[args.length - 1]; + const nullError = callback !== undefined && !takesUndefinedError(lowerer, callback); + values.push({ kind: "boolLit", value: nullError, type: BOOL, loc }); + return { kind: "libCall", fn, args: values, type: VOID, loc }; + } + const call: IrExpr = { kind: "libCall", fn, args: values, type: DYN, loc }; + const result = lowerer.mapTypeOf(lowerer.typeOf(expr)); + if (result === null || result.kind === "dyn") return call; + return lowerer.coerceInto(expr, call, result); +} + +/** The asymmetric-key and signature members (scr_pki.c): checked-dynamic + * arguments, and the result converted to the call site's type + * (KeyObject, Sign and Verify are checked-dynamic handles; key pairs, + * signatures and verdicts convert to their records, Buffers and + * booleans). Null for other members. */ +function lowerPkiCall( + lowerer: Lowerer, + expr: ts.CallExpression, + member: string, + args: readonly ts.Expression[], + loc: SrcLoc, +): IrExpr | null { + const forms: Readonly> = { + createPrivateKey: ["crypto.createPrivateKey", 1, 1, "createPrivateKey(key)"], + createPublicKey: ["crypto.createPublicKey", 1, 1, "createPublicKey(key)"], + generateKeyPairSync: ["crypto.generateKeyPairSync", 1, 2, "generateKeyPairSync(type, options)"], + generateKeyPair: [ + "crypto.generateKeyPairCb", + 2, + 3, + "generateKeyPair(type, [options,] callback)", + ], + sign: ["crypto.signOneshot", 3, 3, "sign(algorithm, data, key)"], + verify: ["crypto.verifyOneshot", 4, 4, "verify(algorithm, data, key, signature)"], + createSign: ["crypto.createSign", 1, 1, "createSign(algorithm)"], + createVerify: ["crypto.createVerify", 1, 1, "createVerify(algorithm)"], + }; + const form = forms[member]; + if (!form) return null; + const [fn, min, max, supported] = form; + if (args.length < min || args.length > max) + lowerer.noLowering( + `crypto.${member} with ${args.length} arguments`, + expr, + `${supported} is supported`, + ); + // generateKeyPair(type, callback): Node reads a function in the options + // position as the callback. + const shifted = fn === "crypto.generateKeyPairCb" && args.length === 2; + const values: IrExpr[] = []; + for (let i = 0; i < max; i++) { + const node = shifted ? (i === 1 ? undefined : i === 2 ? args[1] : args[i]) : args[i]; + values.push(dynArg(lowerer, node, loc)); + } + if (fn === "crypto.generateKeyPairCb") + return { kind: "libCall", fn, args: values, type: VOID, loc }; + const call: IrExpr = { kind: "libCall", fn, args: values, type: DYN, loc }; + const result = lowerer.mapTypeOf(lowerer.typeOf(expr)); + if (result === null || result.kind === "dyn") return call; + return lowerer.coerceInto(expr, call, result); +} diff --git a/packages/compiler/src/frontend/lowering/builtins/dh.ts b/packages/compiler/src/frontend/lowering/builtins/dh.ts new file mode 100644 index 000000000..eea5a4813 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/builtins/dh.ts @@ -0,0 +1,166 @@ +import * as ts from "../../ts7/adapter.js"; +import { nodeThrowExpr } from "../../../ir/build.js"; +import { type Lowerer } from "../lowerer.js"; +import { locOf } from "../../program.js"; +import { DYN, type IrExpr, type IrStmt } from "../../../ir/ir.js"; +import { v8CalleeText } from "./crypto.js"; +import { dynArguments, refuseUnmatchedOverload } from "./ecdh.js"; + +/* node:crypto's DiffieHellman and DiffieHellmanGroup are native + * checked-dynamic handles (scr_dh.c): both types map to dyn, their methods + * dispatch through the handle, and these helpers lower + * createDiffieHellman, getDiffieHellman, createDiffieHellmanGroup and the + * method calls. The runtime applies Node's argument checks, so the + * constructors take any arguments; method calls are refused when their + * argument types leave the Buffer or string result undetermined. */ + +/** Whether a checker type is node:crypto's DiffieHellman class or its + * DiffieHellmanGroup alias, or a union of them. */ +export function isDiffieHellmanType(lowerer: Lowerer, type: ts.Type): boolean { + if (type.isUnionType()) + return ts.constituentTypes(type).every((part) => isDiffieHellmanType(lowerer, part)); + const symbol = type.getAliasSymbol() ?? type.getSymbol(); + if (!symbol || !lowerer.isStdlibSymbol(symbol)) return false; + const name = ts.symbolName(symbol); + return name === "DiffieHellman" || name === "DiffieHellmanGroup"; +} + +function isDiffieHellmanGroupType(type: ts.Type): boolean { + if (type.isUnionType()) return ts.constituentTypes(type).some(isDiffieHellmanGroupType); + const symbol = type.getAliasSymbol(); + return !!symbol && ts.symbolName(symbol) === "DiffieHellmanGroup"; +} + +function newHandle( + lowerer: Lowerer, + call: ts.CallExpression, + fn: "crypto.dhNew" | "crypto.dhGroupNew", +): IrExpr { + const loc = locOf(call); + const stmts: IrStmt[] = []; + const values = dynArguments(lowerer, call, fn === "crypto.dhNew" ? 4 : 1, stmts, "%dhInput"); + return { + kind: "seqExpr", + stmts, + result: { kind: "libCall", fn, args: values, type: DYN, loc }, + type: DYN, + loc, + }; +} + +/** crypto.createDiffieHellman(sizeOrKey, keyEncoding, generator, + * generatorEncoding): a native DiffieHellman handle. */ +export function lowerCreateDiffieHellman(lowerer: Lowerer, call: ts.CallExpression): IrExpr { + return newHandle(lowerer, call, "crypto.dhNew"); +} + +/** crypto.getDiffieHellman(groupName) and createDiffieHellmanGroup(name): + * a native DiffieHellmanGroup handle. */ +export function lowerGetDiffieHellman(lowerer: Lowerer, call: ts.CallExpression): IrExpr { + return newHandle(lowerer, call, "crypto.dhGroupNew"); +} + +const DH_METHODS = new Set([ + "generateKeys", + "computeSecret", + "getPrime", + "getGenerator", + "getPublicKey", + "getPrivateKey", + "setPublicKey", + "setPrivateKey", +]); + +/** Object.prototype methods, which a DiffieHellman inherits. */ +const OBJECT_METHODS = new Set([ + "constructor", + "hasOwnProperty", + "isPrototypeOf", + "propertyIsEnumerable", + "toLocaleString", + "toString", + "valueOf", +]); + +/** The argument that selects each method's Buffer or string result: the + * output encoding. */ +const OUTPUT_ENCODING_ARG: Record = { + generateKeys: 0, + computeSecret: 2, + getPrime: 0, + getGenerator: 0, + getPublicKey: 0, + getPrivateKey: 0, +}; + +/** An output encoding typed any: the checker's chosen overload then types + * the result as a Buffer or a string that Node may not answer, so the call + * is refused rather than checked against that type at run time. */ +function refuseAnyOutputEncoding(lowerer: Lowerer, call: ts.CallExpression, name: string): void { + const index = OUTPUT_ENCODING_ARG[name]; + const arg = index === undefined ? undefined : call.arguments.nodes[index]; + if (!arg || (lowerer.typeOf(arg).flags & ts.TypeFlags.Any) === 0) return; + lowerer.noLowering( + `DiffieHellman.${name} with an output encoding typed any`, + arg, + "type the encoding as a BinaryToTextEncoding such as 'hex', or as null or undefined for a Buffer", + ); +} + +/** Calls on native DiffieHellman and DiffieHellmanGroup handles. Methods + * dispatch through the handle with checked-dynamic arguments. */ +export function lowerDiffieHellmanCall(lowerer: Lowerer, call: ts.CallExpression): IrExpr | null { + if (call.questionDotToken || !ts.isPropertyAccessExpression(call.expression)) return null; + const access = call.expression; + if (access.questionDotToken) return null; + const receiver = lowerer.typeOf(access.expression); + if (!isDiffieHellmanType(lowerer, receiver)) return null; + const name = access.name.text; + const loc = locOf(call); + const setter = name === "setPublicKey" || name === "setPrivateKey"; + if (!DH_METHODS.has(name) || (setter && isDiffieHellmanGroupType(receiver))) { + if (OBJECT_METHODS.has(name)) + lowerer.noLowering( + `DiffieHellman.${name}`, + call, + "generateKeys(), computeSecret(), getPrime(), getGenerator(), getPublicKey(), getPrivateKey(), " + + "setPublicKey(), setPrivateKey() and verifyError are supported", + ); + // Node reads the member as undefined (a DiffieHellmanGroup has no key + // setters) and throws a TypeError once the receiver and arguments are + // evaluated. + const evaluated = [access.expression, ...call.arguments.nodes].map((node): IrStmt => ({ + kind: "exprStmt", + expr: lowerer.lowerExpr(node), + loc, + })); + return { + kind: "seqExpr", + stmts: evaluated, + result: nodeThrowExpr(1, "", `${v8CalleeText(access)} is not a function`, DYN, loc), + type: DYN, + loc, + }; + } + if (call.arguments.nodes.some(ts.isSpreadElement)) + lowerer.noLowering(`DiffieHellman.${name} with spread arguments`, call); + refuseAnyOutputEncoding(lowerer, call, name); + refuseUnmatchedOverload(lowerer, call, `DiffieHellman.${name}`); + const invoke: IrExpr = { + kind: "dynInvoke", + recv: lowerer.lowerExprExpecting(access.expression, DYN), + method: name, + calleeName: v8CalleeText(access), + args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), + type: DYN, + loc, + }; + // setPublicKey and setPrivateKey answer the receiver where @types/node + // declares void. Other results take the static representation of the + // declared overload the arguments match. + if (setter) return invoke; + const result = lowerer.irTypeOf(call); + return result.kind === "bytes" || result.kind === "string" + ? lowerer.coerceInto(call, invoke, result) + : invoke; +} diff --git a/packages/compiler/src/frontend/lowering/builtins/ecdh.ts b/packages/compiler/src/frontend/lowering/builtins/ecdh.ts index e08be56f4..6e937284c 100644 --- a/packages/compiler/src/frontend/lowering/builtins/ecdh.ts +++ b/packages/compiler/src/frontend/lowering/builtins/ecdh.ts @@ -28,15 +28,16 @@ export function isEcdhType(lowerer: Lowerer, type: ts.Type): boolean { return !!symbol && ts.symbolName(symbol) === "ECDH" && lowerer.isStdlibSymbol(symbol); } -/** The call's arguments as checked-dynamic values in hidden locals, the - * first `count` of them padded with undefined, so every argument is - * evaluated left to right before the runtime checks them. Surplus - * arguments are evaluated and dropped. */ -function dynArguments( +/** The call's arguments as checked-dynamic values in hidden locals named + * `local`, the first `count` of them padded with undefined, so every + * argument is evaluated left to right before the runtime checks them. + * Surplus arguments are evaluated and dropped. */ +export function dynArguments( lowerer: Lowerer, call: ts.CallExpression, count: number, stmts: IrStmt[], + local = "%ecdhInput", ): IrExpr[] { const args = call.arguments.nodes; const loc = locOf(call); @@ -45,7 +46,7 @@ function dynArguments( const input: IrExpr = args[i] ? lowerer.lowerExprExpecting(args[i]!, DYN) : dynUndefinedExpr(loc); - const saved = lowerer.declareHiddenLocal("%ecdhInput", DYN); + const saved = lowerer.declareHiddenLocal(local, DYN); stmts.push({ kind: "varDecl", localId: saved.id, init: input, loc }); values.push(varRef(saved.id, DYN, loc)); } @@ -137,7 +138,11 @@ function refuseViewKeyWithEncoding(lowerer: Lowerer, call: ts.CallExpression, na * not follow (getPublicKey("HEX") answers a string where the encoding-less * overload declares a Buffer), so the call is refused rather than checked * against that type at run time. Arguments typed any are accepted. */ -function refuseUnmatchedOverload(lowerer: Lowerer, call: ts.CallExpression, what: string): void { +export function refuseUnmatchedOverload( + lowerer: Lowerer, + call: ts.CallExpression, + what: string, +): void { const signature = lowerer.checker.getResolvedSignature(call); if (signature === undefined) return; const params = signature.getParameters(); diff --git a/packages/compiler/src/frontend/lowering/builtins/key-object.ts b/packages/compiler/src/frontend/lowering/builtins/key-object.ts index 7e680fa20..ecb4f275a 100644 --- a/packages/compiler/src/frontend/lowering/builtins/key-object.ts +++ b/packages/compiler/src/frontend/lowering/builtins/key-object.ts @@ -4,10 +4,11 @@ import { locOf } from "../../program.js"; import { BOOL, DYN, type IrExpr } from "../../../ir/ir.js"; import { builtinImportOf } from "./module-bindings.js"; -/* node:crypto's secret KeyObject is a native checked-dynamic handle - * (scr_lib.c): the KeyObject type maps to dyn, its getters and methods - * dispatch through the handle, and these helpers lower the class's uses - * that a dyn value cannot answer by itself. */ +/* node:crypto's KeyObject is a native checked-dynamic handle (secret keys + * in scr_lib.c, private and public keys in scr_pki.c), as are Sign and + * Verify: the types map to dyn, their getters and methods dispatch through + * the handle, and these helpers lower the uses that a dyn value cannot + * answer by itself. */ /** Whether `node` names node:crypto's KeyObject class. */ export function isKeyObjectClass(lowerer: Lowerer, node: ts.Expression): boolean { @@ -37,15 +38,31 @@ export function lowerKeyObjectInstanceof( } const KEY_OBJECT_METHODS = new Set(["export", "equals", "toCryptoKey"]); +const SIGNER_METHODS = new Set(["update", "sign", "verify"]); -/** `key.export(...)`, `key.equals(...)` and `key.toCryptoKey(...)`: the +/** Whether a value's checker type is node:crypto's Sign or Verify. */ +function isSignerValue(lowerer: Lowerer, node: ts.Expression): boolean { + const type = lowerer.typeOf(node); + const sym = type.getAliasSymbol() ?? type.getSymbol(); + return ( + !!sym && + (ts.symbolName(sym) === "Sign" || ts.symbolName(sym) === "Verify") && + lowerer.isStdlibSymbol(sym) + ); +} + +/** `key.export(...)`, `key.equals(...)` and `key.toCryptoKey(...)`, and + * the update/sign/verify methods of Sign and Verify (scr_pki.c): the * handle answers them as methods, so they dispatch through dynInvoke * rather than as a member read and a call. */ export function lowerKeyObjectCall(lowerer: Lowerer, call: ts.CallExpression): IrExpr | null { if (call.questionDotToken || !ts.isPropertyAccessExpression(call.expression)) return null; const access = call.expression; - if (access.questionDotToken || !KEY_OBJECT_METHODS.has(access.name.text)) return null; - if (!isKeyObjectValue(lowerer, access.expression)) return null; + if (access.questionDotToken) return null; + const method = access.name.text; + const keyMethod = KEY_OBJECT_METHODS.has(method) && isKeyObjectValue(lowerer, access.expression); + if (!keyMethod && !(SIGNER_METHODS.has(method) && isSignerValue(lowerer, access.expression))) + return null; if (call.arguments.nodes.some(ts.isSpreadElement)) return null; return { kind: "dynInvoke", diff --git a/packages/compiler/src/frontend/lowering/builtins/webcrypto.ts b/packages/compiler/src/frontend/lowering/builtins/webcrypto.ts index fe310294c..8487e5cb7 100644 --- a/packages/compiler/src/frontend/lowering/builtins/webcrypto.ts +++ b/packages/compiler/src/frontend/lowering/builtins/webcrypto.ts @@ -7,10 +7,11 @@ import { stdlibGlobalNameOf } from "../surfaces.js"; import { WEBCRYPTO_DYN_TYPES } from "../../type-mapper.js"; import { isWebCryptoReceiver } from "./crypto.js"; -/* WebCrypto's secret-key slice (scr_webcrypto.c, with encrypt and - * decrypt in scr_webcrypto_aes.c): `crypto.subtle`'s `importKey`, - * `generateKey`, `exportKey`, `encrypt`, `decrypt`, `deriveBits`, - * `deriveKey`, `sign`, `verify` and `digest` +/* WebCrypto (scr_webcrypto.c, with encrypt and decrypt in + * scr_webcrypto_aes.c and the ECDSA, Ed25519, RSASSA-PKCS1-v1_5 and RSA-PSS + * algorithms in scr_webcrypto_pki.c): `crypto.subtle`'s `importKey`, + * `generateKey`, `exportKey`, `encrypt`, `decrypt`, `wrapKey`, + * `unwrapKey`, `deriveBits`, `deriveKey`, `sign`, `verify` and `digest` * called on the crypto global (also through * globalThis, node:crypto's webcrypto and node:crypto's subtle) lower to * runtime entry points that take the argument vector as one dyn array and @@ -20,11 +21,13 @@ import { isWebCryptoReceiver } from "./crypto.js"; * are handle reads. Other SubtleCrypto methods are refused by name. */ const SUBTLE_LOWERED: ReadonlyMap = new Map([ - ["importKey", "webcrypto.importKey"], ["generateKey", "webcrypto.generateKey"], + ["importKey", "webcrypto.importKey"], ["exportKey", "webcrypto.exportKey"], ["encrypt", "webcrypto.encrypt"], ["decrypt", "webcrypto.decrypt"], + ["wrapKey", "webcrypto.wrapKey"], + ["unwrapKey", "webcrypto.unwrapKey"], ["deriveBits", "webcrypto.deriveBits"], ["deriveKey", "webcrypto.deriveKey"], ["sign", "webcrypto.sign"], @@ -33,7 +36,7 @@ const SUBTLE_LOWERED: ReadonlyMap = new Map([ ]); const SUBTLE_HINT = - "the native WebCrypto surface is crypto.subtle's importKey, generateKey and exportKey for HMAC and AES keys (importKey also for PBKDF2 and HKDF keys), encrypt and decrypt with AES-GCM, AES-CBC or AES-CTR, deriveBits and deriveKey with PBKDF2 or HKDF, sign and verify with HMAC, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle"; + "the native WebCrypto surface is crypto.subtle's importKey, generateKey and exportKey for HMAC and AES keys (importKey also for PBKDF2 and HKDF keys), encrypt and decrypt with AES-GCM, AES-CBC or AES-CTR, wrapKey and unwrapKey of secret keys with AES-KW or those ciphers, deriveBits and deriveKey with PBKDF2 or HKDF, sign and verify with HMAC, generateKey, importKey, exportKey, sign and verify for ECDSA, Ed25519, RSASSA-PKCS1-v1_5 and RSA-PSS, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle"; /** Whether `node` is a SubtleCrypto object: `subtle` on a WebCrypto * Crypto object (the global, globalThis.crypto or node:crypto's diff --git a/packages/compiler/src/frontend/lowering/caught-value-dispatch.ts b/packages/compiler/src/frontend/lowering/caught-value-dispatch.ts index 606d10d56..07d68baec 100644 --- a/packages/compiler/src/frontend/lowering/caught-value-dispatch.ts +++ b/packages/compiler/src/frontend/lowering/caught-value-dispatch.ts @@ -11,6 +11,7 @@ import { } from "../../ir/ir.js"; import { everyStmtList, transformStmtList } from "../../ir/traverse.js"; import { classInstanceOf } from "./class-dynamic-dispatch.js"; +import type { DispatchSites } from "./dispatch-sites.js"; import type { ClassInfo } from "./lower-classes.js"; import type { Lowerer } from "./lowerer.js"; @@ -26,25 +27,16 @@ export class CaughtValueDispatch { private readonly payloadHelpers = new Map(); private readonly payloadHelperFns = new Set(); - process(lowerer: Lowerer, functions: readonly IrFunction[]): boolean { + process(lowerer: Lowerer, functions: readonly IrFunction[], sites: DispatchSites): boolean { const consumers: IrFunction[] = []; for (const fn of functions) { if (fn === this.helper) continue; - let consumes = false; - everyStmtList(fn.body, { - stmt: (stmt) => { - if (stmt.kind === "throw" && stmt.value.type.kind === "object") { - const info = lowerer.classes.get(stmt.value.type.className); - if (info) this.thrownClasses.add(info.def.name); - } - return true; - }, - expr: (expr) => { - if (expr.kind === "caughtToDyn") consumes = true; - return true; - }, - }); - if (consumes) consumers.push(fn); + const found = sites.of(fn); + for (const className of found.thrown) { + const info = lowerer.classes.get(className); + if (info) this.thrownClasses.add(info.def.name); + } + if (found.caughtValues) consumers.push(fn); } if (!this.helper && consumers.length === 0) return false; let changed = false; diff --git a/packages/compiler/src/frontend/lowering/class-construction.ts b/packages/compiler/src/frontend/lowering/class-construction.ts index 30f504b4a..7ee53d04e 100644 --- a/packages/compiler/src/frontend/lowering/class-construction.ts +++ b/packages/compiler/src/frontend/lowering/class-construction.ts @@ -13,7 +13,8 @@ import { type IrType, type SrcLoc, } from "../../ir/ir.js"; -import { everyStmtList, transformExpr, transformStmtList } from "../../ir/traverse.js"; +import { transformExpr, transformStmtList } from "../../ir/traverse.js"; +import type { DispatchSites } from "./dispatch-sites.js"; import type { Lowerer } from "./lowerer.js"; import type { ParamShape } from "./call-signatures.js"; import { classPrototypeData } from "./class-prototypes.js"; @@ -24,16 +25,21 @@ const PREFIX = "%class.construct:"; const PACKED = `${PREFIX}packed`; const INSTANCEOF = "%class.instanceof:dynamic"; +/** `value instanceof callee` for an untyped constructor value, through one + * shared helper. `refusal` is the site's message for a runtime + * constructor with no instance test. */ export function checkedClassInstanceOf( lowerer: Lowerer, value: IrExpr, callee: IrExpr, + refusal: string, loc: SrcLoc, ): IrExpr { if (!lowerer.liftedFns.some((fn) => fn.name === INSTANCEOF)) { const params = [ { localId: "value", name: "value", type: DYN }, { localId: "callee", name: "callee", type: DYN }, + { localId: "refusal", name: "refusal", type: STRING }, ]; lowerer.liftedFns.push({ name: INSTANCEOF, @@ -46,7 +52,11 @@ export function checkedClassInstanceOf( value: { kind: "libCall", fn: "bytes.instanceOf", - args: [varRef("value", DYN, loc), varRef("callee", DYN, loc)], + args: [ + varRef("value", DYN, loc), + varRef("callee", DYN, loc), + varRef("refusal", STRING, loc), + ], type: BOOL, loc, }, @@ -56,7 +66,13 @@ export function checkedClassInstanceOf( loc, }); } - return { kind: "call", callee: INSTANCEOF, args: [value, callee], type: BOOL, loc }; + return { + kind: "call", + callee: INSTANCEOF, + args: [value, callee, { kind: "strLit", value: refusal, type: STRING, loc }], + type: BOOL, + loc, + }; } /** Checked construction dispatches to emitted native constructor thunks. */ @@ -254,22 +270,17 @@ export class ClassConstructionDispatch { private prototypeHelper: IrFunction | null = null; private readonly prototypes = new Set(); - process(lowerer: Lowerer, functions: readonly IrFunction[]): boolean { + process(lowerer: Lowerer, functions: readonly IrFunction[], sites: DispatchSites): boolean { const discover = (type: IrType): void => { if (type.kind === "classval") this.constructors.add(type.className); else if (type.kind === "union") lowerer.unions.get(type.unionId)?.arms.forEach(discover); }; const prototypeReads = new Set(); - for (const fn of functions) - everyStmtList(fn.body, { - stmt: () => true, - expr: (expr) => { - if (expr.kind === "dynFrom") discover(expr.value.type); - if (expr.kind === "libCall" && expr.fn === "dyn.classBasePrototype") - prototypeReads.add(fn); - return true; - }, - }); + for (const fn of functions) { + const found = sites.of(fn); + found.values.forEach(discover); + if (found.basePrototypes) prototypeReads.add(fn); + } let changed = false; for (const fn of functions) { if (fn === this.prototypeHelper || !prototypeReads.has(fn)) continue; diff --git a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts index fefe85d0c..af3212ef0 100644 --- a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts +++ b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts @@ -1,4 +1,5 @@ import { primitiveConstructorOr } from "./lower-exprs.js"; +import { InternalCompilerError } from "../../errors.js"; import { dynUndefinedExpr, helperLoc, nodeThrowExpr, varRef } from "../../ir/build.js"; import { constructedValue } from "./constructor-returns.js"; import { eventTargetApiMembers, eventTargetInheritedMember } from "./lower-event-target.js"; @@ -77,6 +78,7 @@ import { isClassCallback } from "./class-callbacks.js"; import { CaughtValueDispatch } from "./caught-value-dispatch.js"; import { refreshDescriptorGuards } from "./class-descriptors.js"; import { ClassConstructionDispatch } from "./class-construction.js"; +import { DispatchSites } from "./dispatch-sites.js"; import { emitterRooted, lowerDynamicEmitterEmit, @@ -226,7 +228,7 @@ const KEY_CALLS = new Map([ ["dyn.keySetComputed", "value"], ]); -function toPrimitiveHint(expr: IrExpr): ToPrimitiveHint | null { +export function toPrimitiveHint(expr: IrExpr): ToPrimitiveHint | null { if (expr.kind === "toString") return expr.operand.type.kind === "dyn" ? "string" : null; if (expr.kind === "dynKeyGet") return expr.key.type.kind === "dyn" ? "string" : null; if (expr.kind !== "libCall") return null; @@ -467,6 +469,79 @@ function sortAdded(lowerer: Lowerer, set: Set, from: number): void { for (const name of added) set.add(name); } +/** The receivers of one property-receiver list (propertyReceivers) by the + * names that can make them answer a member, as positions in the list. A + * receiver whose root answers names of its own (an emitter, a stream, an + * EventTarget or a Date) is in `rooted` instead. */ +interface ReceiverIndex { + plans: readonly PropertyReceiver[]; + /** By a method or generic method its base chain declares. */ + methods: Map; + /** By a method, generic method or accessor its base chain declares (an + * accessor under its key and under its property name), or a field it has. */ + members: Map; + rooted: number[]; +} + +function receiverIndex(lowerer: Lowerer, plans: readonly PropertyReceiver[]): ReceiverIndex { + const methods = new Map(); + const members = new Map(); + const rooted: number[] = []; + const add = (table: Map, name: string, position: number): void => { + const positions = table.get(name); + if (positions === undefined) table.set(name, [position]); + else if (positions[positions.length - 1] !== position) positions.push(position); + }; + plans.forEach(({ info }, position) => { + if ( + emitterRooted(lowerer, info) || + info.builtinStream !== undefined || + eventTargetApiMembers(info) !== null || + dateRootOf(info) !== null + ) { + rooted.push(position); + return; + } + for (let c: ClassInfo | null = info; c; c = c.base) { + for (const name of c.methods.keys()) { + add(methods, name, position); + // A `get:`/`set:` key is an accessor of the property after the + // prefix, or a method whose quoted name has that prefix; the test + // looks up both. + add(members, name, position); + if (name.startsWith("get:") || name.startsWith("set:")) + add(members, name.slice(4), position); + } + if (c.genericMethods) + for (const name of c.genericMethods.keys()) { + add(methods, name, position); + add(members, name, position); + } + } + for (const name of info.fields.keys()) add(members, name, position); + }); + for (const table of [methods, members]) + for (const positions of table.values()) positions.sort((a, b) => a - b); + return { plans, methods, members, rooted }; +} + +/** The ascending union of two ascending position lists. */ +function mergePositions(left: readonly number[], right: readonly number[]): number[] { + const out: number[] = []; + let i = 0; + let j = 0; + while (i < left.length || j < right.length) { + const next = + j >= right.length || (i < left.length && left[i]! <= right[j]!) ? left[i++]! : right[j++]!; + if (out[out.length - 1] !== next) out.push(next); + } + return out; +} + +/** SCRIPTC_CHECK_DISPATCH_CANDIDATES=1 checks every indexed receiver selection + * against a test of every receiver. */ +const CHECK_CANDIDATES = process.env["SCRIPTC_CHECK_DISPATCH_CANDIDATES"] === "1"; + export function classPropertiesHelper(lowerer: Lowerer, loc: SrcLoc, readOnly = false): IrFunction { const name = readOnly ? READ_PROPERTIES : "%dyn.class.properties"; lowerer.shareHelper(name); @@ -780,6 +855,8 @@ export class ClassDynamicDispatch { private readonly generatorDispatch = new GeneratorDynamicDispatch(); private readonly caughtDispatch = new CaughtValueDispatch(); private readonly constructionDispatch = new ClassConstructionDispatch(); + /** What each function's body holds for this pass and its sub-passes. */ + readonly sites = new DispatchSites(); /** Property names a delete can remove (null: any name), for the field * creation tracking decision. */ private deletedNames: Set | null | undefined; @@ -862,12 +939,13 @@ export class ClassDynamicDispatch { ) fn.body.shift(); } - let changed = this.caughtDispatch.process(lowerer, functions); + let changed = this.caughtDispatch.process(lowerer, functions, this.sites); changed = refreshErrorMethodDispatch(lowerer, functions) || changed; changed = refreshClassMethodValueSelections(lowerer) || changed; changed = refreshErrorPropertyDispatch(lowerer, functions) || changed; - changed = this.generatorDispatch.process(lowerer, functions, this.generated) || changed; - changed = this.constructionDispatch.process(lowerer, functions) || changed; + changed = + this.generatorDispatch.process(lowerer, functions, this.generated, this.sites) || changed; + changed = this.constructionDispatch.process(lowerer, functions, this.sites) || changed; const seenTypes = new Set(); const rewrite = new Set(); const toPrimitiveSites = new Set(); @@ -891,100 +969,18 @@ export class ClassDynamicDispatch { }; const boxedBefore = this.boxed.size; const boxedConstructorsBefore = this.boxedConstructors.size; - for (const fn of functions) - everyStmtList(fn.body, { - stmt: (statement) => { - // Field-creation instrumentation can box a base-constructor receiver - // even when only its derived instances otherwise cross the boundary. - if ( - statement.kind === "fieldSet" && - lowerer.classes.get(statement.className)?.def.tracksOwnFields && - isClassOwnEnumerableFieldName(statement.field) - ) - discover(statement.obj.type); - return true; - }, - expr: (expr) => { - if ( - expr.kind === "jsonStringify" || - (expr.kind === "libCall" && - (expr.fn === "json.stringifyValue" || expr.fn === "json.stringifyReplacer")) - ) { - this.serializesClasses = true; - } - if ( - toPrimitiveHint(expr) !== null || - (expr.kind === "libCall" && (expr.fn === "dyn.valueOf" || expr.fn === "dyn.toString")) - ) - toPrimitiveSites.add(fn); - switch (expr.kind) { - case "dynFrom": - discover(expr.value.type); - break; - case "fieldIncDec": - if ( - lowerer.classes.get(expr.className)?.def.tracksOwnFields && - isClassOwnEnumerableFieldName(expr.field) - ) - discover(expr.obj.type); - break; - case "call": - if ( - (expr.callee === "%dyn.class.properties" || - expr.callee === "%dyn.class.readProperties") && - expr.args[0]?.kind === "dynFrom" && - isDynTypedRefType(expr.args[0].value.type) - ) - rewrite.add(fn); - break; - case "dynHasKey": - hasKeySites.add(fn); - break; - case "caughtTest": - if (expr.test === "instanceof") caughtSites.add(fn); - break; - case "caughtNarrow": - case "caughtCheck": - if (expr.type.kind === "object") caughtSites.add(fn); - break; - case "dynKeyGet": - case "dynInvoke": - rewrite.add(fn); - break; - case "dynCheck": - if (isDynTypedRefType(expr.type)) rewrite.add(fn); - // A dynamic function checked into a native function type - // receives its arguments as dynamic values: class instances - // passed to it cross the boundary when it is called (a JS - // `list.map((rec) => rec.match(x))` callback with an untyped - // parameter). - if (expr.type.kind === "func") expr.type.params.forEach(discover); - break; - case "libCall": - if ( - expr.fn === "bytes.construct" || - expr.fn === "dyn.keySet" || - expr.fn === "dyn.keySetComputed" || - expr.fn === "dyn.iterator" || - expr.fn === "dyn.arrayFromIterator" || - expr.fn === "dyn.toString" || - expr.fn === "dyn.stringConstructor" || - expr.fn === "dyn.forInKeys" || - expr.fn === "dyn.forInKeyStrings" || - expr.fn === "dyn.hasKeyComputed" || - expr.fn === "dyn.hasOwnComputed" || - expr.fn === "dyn.propertyIsEnumerableComputed" || - expr.fn === "dyn.defineProperty" || - expr.fn === "dyn.getPrototype" || - expr.fn === "dyn.reflectGet" || - expr.fn === "dyn.reflectSet" - ) - rewrite.add(fn); - break; - } - return true; - }, - }); + for (const fn of functions) { + const found = this.sites.of(fn); + // A class starts tracking its own fields during this pass. + for (const { type, fieldClass } of found.boxing) + if (fieldClass === undefined || lowerer.classes.get(fieldClass)?.def.tracksOwnFields) + discover(type); + if (found.serializes) this.serializesClasses = true; + if (found.conversions) toPrimitiveSites.add(fn); + if (found.dispatches) rewrite.add(fn); + if (found.keyTests) hasKeySites.add(fn); + if (found.caughtClassTests) caughtSites.add(fn); + } // The classes this pass found join the tables in a fixed order, not in // the order the scan met them. sortAdded(lowerer, this.boxed, boxedBefore); @@ -1342,10 +1338,30 @@ export class ClassDynamicDispatch { } refreshDescriptorGuards(lowerer); const byMethod = new Map(); + // A query asks every receiver about one name, and the program's dispatch + // sites ask about thousands: index the receivers by name once per list. + let index: ReceiverIndex | null = null; + const indexOf = (plans: readonly PropertyReceiver[]): ReceiverIndex => + index?.plans === plans ? index : (index = receiverIndex(lowerer, plans)); + /** The receivers of `plans` that can answer `name`, in list order: those + * `table` lists under it and the rooted ones. Emit-override + * specializations (`emit:`) join a class's methods while bodies + * lower, so they keep every receiver. */ + const receiversFor = ( + plans: readonly PropertyReceiver[], + name: string, + table: "methods" | "members", + ): readonly PropertyReceiver[] => { + if (name.startsWith("emit:")) return plans; + const found = indexOf(plans); + const positions = (table === "methods" ? found.methods : found.members).get(name); + return mergePositions(positions ?? [], found.rooted).map((position) => plans[position]!); + }; const candidates = (method: string): ClassInfo[] => { const found = byMethod.get(method); if (found) return found; - const matching = this.propertyReceivers(lowerer, true).flatMap(({ info }) => { + const plans = this.propertyReceivers(lowerer, true); + const test = ({ info }: PropertyReceiver): ClassInfo[] => { if (!info || info.fields.has(method) || info.builtinError) return []; if (emitterRooted(lowerer, info) && EMITTER_METHODS.has(method)) return [info]; if ( @@ -1364,7 +1380,13 @@ export class ClassDynamicDispatch { return findMethodOn(lowerer, info, method) || findGenericMethodOn(lowerer, info, method) ? [info] : []; - }); + }; + const matching = receiversFor(plans, method, "methods").flatMap(test); + if (CHECK_CANDIDATES) { + const every = plans.flatMap(test); + if (every.length !== matching.length || every.some((info, i) => info !== matching[i])) + throw new InternalCompilerError(`indexed candidates for ${method} differ`); + } byMethod.set(method, matching); return matching; }; @@ -2898,24 +2920,40 @@ export class ClassDynamicDispatch { }); changed = true; } - for (const plan of this.propertyReceivers(lowerer, true)) { + // Only the receivers that declare or hold the name, and the rooted + // ones, can pass the test, except for names it decides on every + // receiver. Fields added while bodies lower (the property bag and the + // attribute words) have compiler names, which take every receiver. + const plans = this.propertyReceivers(lowerer, true); + const anyReceiver = + !isClassOwnEnumerableFieldName(dispatch.name) || + dispatch.name === "constructor" || + dispatch.name === "stack"; + const skips = (plan: PropertyReceiver): boolean => + !isClassOwnEnumerableFieldName(dispatch.name) || + (!(plan.info.builtinStream !== undefined && streamProperty(dispatch.name)) && + !eventTargetInheritedMember(plan.info, dispatch.name) && + !dateInheritedMember(lowerer, plan.info, dispatch.name) && + !findMethodOn(lowerer, plan.info, dispatch.name) && + !findGenericMethodOn(lowerer, plan.info, dispatch.name) && + dispatch.name !== "constructor" && + !plan.info.fields.has(dispatch.name) && + !(dispatch.name === "stack" && lowerer.isSubclassOf(plan.info.def.name, "%Error")) && + !findMethodOn(lowerer, plan.info, `get:${dispatch.name}`) && + !findMethodOn(lowerer, plan.info, `set:${dispatch.name}`)); + const selected = anyReceiver ? plans : receiversFor(plans, dispatch.name, "members"); + if ( + CHECK_CANDIDATES && + plans.some( + (plan) => !dispatch.classes.has(plan.key) && !skips(plan) && !selected.includes(plan), + ) + ) + throw new InternalCompilerError(`indexed receivers for ${dispatch.name} differ`); + for (const plan of selected) { const { info } = plan; // Classes answered in an earlier pass skip the member lookups. if (dispatch.classes.has(plan.key)) continue; - if ( - !isClassOwnEnumerableFieldName(dispatch.name) || - (!(info.builtinStream !== undefined && streamProperty(dispatch.name)) && - !eventTargetInheritedMember(info, dispatch.name) && - !dateInheritedMember(lowerer, info, dispatch.name) && - !findMethodOn(lowerer, info, dispatch.name) && - !findGenericMethodOn(lowerer, info, dispatch.name) && - dispatch.name !== "constructor" && - !info.fields.has(dispatch.name) && - !(dispatch.name === "stack" && lowerer.isSubclassOf(info.def.name, "%Error")) && - !findMethodOn(lowerer, info, `get:${dispatch.name}`) && - !findMethodOn(lowerer, info, `set:${dispatch.name}`)) - ) - continue; + if (skips(plan)) continue; dispatch.classes.add(plan.key); const loc = dispatch.fn.loc; const { receiver, condition } = this.propertyReceiver(plan, loc); diff --git a/packages/compiler/src/frontend/lowering/class-protocols.ts b/packages/compiler/src/frontend/lowering/class-protocols.ts index 81aa90a1f..76091b13e 100644 --- a/packages/compiler/src/frontend/lowering/class-protocols.ts +++ b/packages/compiler/src/frontend/lowering/class-protocols.ts @@ -101,6 +101,8 @@ export class ClassProtocols { const sites: { source: ts.Expression; target: () => ts.Type | undefined; + /** The node whose contextual type `target` answers. */ + contextual?: ts.Expression; assertion?: true; }[] = []; /** Expressions whose values are observed as whole objects. */ @@ -122,7 +124,8 @@ export class ClassProtocols { const callbackParams: { fn: ts.SignatureDeclaration; index: number; name: ts.Node }[] = []; const contextual = (expr: ts.Expression) => () => this.checker.getContextualType(expr); const flow = (expr: ts.Expression | undefined): void => { - if (expr !== undefined) sites.push({ source: expr, target: contextual(expr) }); + if (expr !== undefined) + sites.push({ source: expr, target: contextual(expr), contextual: expr }); }; const write = (target: ts.Expression): void => { while (ts.isParenthesizedExpression(target) || ts.isNonNullExpression(target)) @@ -329,7 +332,11 @@ export class ClassProtocols { facts.loose.push({ node: looseArguments[i]!, call: looseCalls[i]!, symbols }); } this.facts = facts; - const pending: { sourceType: ts.Type; target: () => ts.Type | undefined }[] = []; + const pending: { + sourceType: ts.Type; + target: () => ts.Type | undefined; + contextual?: ts.Expression; + }[] = []; /** Views a value of unknown origin is asserted to and that expose a * method of a class the view accepts (see methodViewSources). */ const methodViews = new Set(); @@ -352,7 +359,11 @@ export class ClassProtocols { } continue; } - pending.push({ sourceType, target: site.target }); + pending.push( + site.contextual === undefined + ? { sourceType, target: site.target } + : { sourceType, target: site.target, contextual: site.contextual }, + ); } for (const { fn, index, name } of callbackParams) { attempt(() => { @@ -377,6 +388,19 @@ export class ClassProtocols { // flows and origins are complete. const before = new Set(marked); const targets = new Map(); + // The fixpoint asks the target of each site whose source carries + // identity: one batch asks those of the sites that carry it now, and a + // site that starts to as marks grow asks its own. + attempt(() => + this.checker.prefetchContextualTypes( + pending.flatMap((entry) => + entry.contextual !== undefined && + attempt(() => this.carriesIdentity(entry.sourceType, marked)) === true + ? [entry.contextual] + : [], + ), + ), + ); let changed = true; while (changed) { changed = false; diff --git a/packages/compiler/src/frontend/lowering/containers/callback-arguments.ts b/packages/compiler/src/frontend/lowering/containers/callback-arguments.ts index 9db04e95b..d95640978 100644 --- a/packages/compiler/src/frontend/lowering/containers/callback-arguments.ts +++ b/packages/compiler/src/frontend/lowering/containers/callback-arguments.ts @@ -53,6 +53,8 @@ export function lowerArrayCallback( bindUntyped = false, expectedReturn?: IrType, ): { fnArg: IrExpr & { type: IrType & { kind: "func" } }; arity: number } { + // `xs.map(((x) => x))`: a parenthesized callback is the function itself. + while (ts.isParenthesizedExpression(argNode)) argNode = argNode.expression; // The ambient `Boolean` as the callback (`flags.every(Boolean)`, // `xs.some(Boolean)`, `xs.map(Boolean)`): Boolean(value) answers the // ToBoolean of its FIRST argument and ignores the rest, so the callback diff --git a/packages/compiler/src/frontend/lowering/deferred-module-initializers.test.ts b/packages/compiler/src/frontend/lowering/deferred-module-initializers.test.ts index 28f05bfce..4696659bc 100644 --- a/packages/compiler/src/frontend/lowering/deferred-module-initializers.test.ts +++ b/packages/compiler/src/frontend/lowering/deferred-module-initializers.test.ts @@ -159,6 +159,15 @@ test("fills every empty copy with the exact file and span while preserving popul for (const untouched of [otherFile, otherStart, otherEnd]) expect(untouched.body).toEqual([]); }); +test("reports the functions whose placeholders it fills", () => { + const { initializers, pending, lowerer } = fixture(); + const functions = [fn("init", [block(loc)]), fn("use", [read("registry")])]; + pending.set("registry", [{ loc, lower: () => [value(4)] }]); + const filled: IrFunction[] = []; + expect(initializers.process(lowerer, functions, (f) => filled.push(f))).toBe(true); + expect(filled).toEqual([functions[0]]); +}); + test("an update of the binding demands its initializer", () => { const { initializers, pending, lowerer } = fixture(); const updated = block(loc), diff --git a/packages/compiler/src/frontend/lowering/deferred-module-initializers.ts b/packages/compiler/src/frontend/lowering/deferred-module-initializers.ts index eb22b4085..a44df8f75 100644 --- a/packages/compiler/src/frontend/lowering/deferred-module-initializers.ts +++ b/packages/compiler/src/frontend/lowering/deferred-module-initializers.ts @@ -129,7 +129,14 @@ export class DeferredModuleInitializers { return block; } - process(lowerer: Lowerer, functions: readonly IrFunction[]): boolean { + /** Materializes the initializers that reached bodies observe. `filled` + * receives each function whose body changes inside an unchanged statement + * list. */ + process( + lowerer: Lowerer, + functions: readonly IrFunction[], + filled?: (fn: IrFunction) => void, + ): boolean { if (this.pending.size === 0) return false; // Scans keep only the reads of deferred bindings. for (const key of this.pending.keys()) { @@ -192,7 +199,10 @@ export class DeferredModuleInitializers { } if (holders.length === 0) continue; // These bodies changed inside unchanged statement lists. - for (const fn of holders) this.scans.delete(fn); + for (const fn of holders) { + this.scans.delete(fn); + filled?.(fn); + } // An inserted body can introduce a placeholder needed by another // action in this pass. Unattached bodies are not part of the live IR. for (const nested of scanStatements(body).placeholders) add(nested, holders); diff --git a/packages/compiler/src/frontend/lowering/dispatch-sites.test.ts b/packages/compiler/src/frontend/lowering/dispatch-sites.test.ts new file mode 100644 index 000000000..1d027ac34 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/dispatch-sites.test.ts @@ -0,0 +1,60 @@ +import { expect, test, vi } from "vitest"; +import { DYN, F64, VOID, type IrFunction, type IrStmt, type SrcLoc } from "../../ir/ir.js"; +import * as traversal from "../../ir/traverse.js"; +import { DispatchSites } from "./dispatch-sites.js"; + +const loc: SrcLoc = { file: "sites.ts", start: 0, end: 1 }; +const keyRead = (key: string): IrStmt => ({ + kind: "exprStmt", + expr: { + kind: "dynKeyGet", + value: { kind: "varRef", localId: "value", type: DYN, loc }, + key: { kind: "strLit", value: key, type: DYN, loc }, + type: DYN, + loc, + }, + loc, +}); +const value = (n: number): IrStmt => ({ + kind: "exprStmt", + expr: { kind: "numLit", value: n, type: F64, loc }, + loc, +}); +const fn = (body: IrStmt[]): IrFunction => ({ + name: "f", + body, + params: [], + locals: [], + returnType: VOID, + loc, +}); + +test("reuses a function's sites while its statement list is unchanged", () => { + const sites = new DispatchSites(); + const f = fn([value(1)]); + const walk = vi.spyOn(traversal, "everyStmtList"); + try { + expect(sites.of(f).dispatches).toBe(false); + expect(sites.of(f).dispatches).toBe(false); + expect(walk).toHaveBeenCalledTimes(1); + // A statement added to the same list is found. + f.body.push(keyRead("next")); + expect(sites.of(f)).toMatchObject({ dispatches: true, generatorSteps: true }); + // So is a replaced list. + f.body = [value(2)]; + expect(sites.of(f)).toMatchObject({ dispatches: false, generatorSteps: false }); + expect(walk).toHaveBeenCalledTimes(3); + } finally { + walk.mockRestore(); + } +}); + +test("scans a body again once a change inside an unchanged list is reported", () => { + const sites = new DispatchSites(); + const placeholder: Extract = { kind: "block", body: [], loc }; + const f = fn([placeholder]); + expect(sites.of(f).dispatches).toBe(false); + placeholder.body.push(keyRead("map")); + sites.forget(f); + expect(sites.of(f)).toMatchObject({ dispatches: true, runtimeSteps: true }); +}); diff --git a/packages/compiler/src/frontend/lowering/dispatch-sites.ts b/packages/compiler/src/frontend/lowering/dispatch-sites.ts new file mode 100644 index 000000000..2618dacf9 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/dispatch-sites.ts @@ -0,0 +1,272 @@ +import { InternalCompilerError } from "../../errors.js"; +import { + isClassOwnEnumerableFieldName, + isDynTypedRefType, + typeKey, + type IrFunction, + type IrStmt, + type IrType, +} from "../../ir/ir.js"; +import { everyStmtList } from "../../ir/traverse.js"; +import { toPrimitiveHint } from "./class-dynamic-dispatch.js"; +import { GENERATOR_STEP_METHODS, HELPER_METHODS } from "./generator-dynamic-dispatch.js"; + +/** What one function's body holds for the class dispatch pass and its + * caught-value, generator and class construction sub-passes, collected in + * one walk. Lists keep the order the walk meets their entries. */ +export interface FunctionSites { + statements: IrStmt[]; + length: number; + first: IrStmt | undefined; + last: IrStmt | undefined; + /** The value types of `dynFrom` expressions, once per type key. */ + values: IrType[]; + /** The types the class dispatch scan boxes, once per type key and class: + * `dynFrom` values, the parameter types of dynamic functions checked into + * native function types, and the receivers of own enumerable field writes + * and updates. A receiver counts only while its class (`fieldClass`) + * tracks its own fields. */ + boxing: { type: IrType; fieldClass?: string }[]; + /** JSON serialization of dynamic values. */ + serializes: boolean; + /** ToPrimitive conversions, and `dyn.valueOf` and `dyn.toString` calls. */ + conversions: boolean; + /** Sites the class dispatch rewrite can change. */ + dispatches: boolean; + /** `dynHasKey` tests. */ + keyTests: boolean; + /** `instanceof` tests of caught values, and caught values narrowed or + * checked to class types. */ + caughtClassTests: boolean; + /** Calls and reads of generator steps, and reads of iterator helper + * methods. */ + generatorSteps: boolean; + /** Values the runtime steps: iterator helper calls and reads, `dynIterN`, + * and rest destructuring. */ + runtimeSteps: boolean; + /** `dyn.classBasePrototype` calls. */ + basePrototypes: boolean; + /** The classes of thrown class instances, once each. */ + thrown: string[]; + /** `caughtToDyn` conversions. */ + caughtValues: boolean; +} + +/** The `libCall`s the class dispatch rewrite can change. */ +const DISPATCH_LIB_CALLS = new Set([ + "bytes.construct", + "dyn.keySet", + "dyn.keySetComputed", + "dyn.iterator", + "dyn.arrayFromIterator", + "dyn.toString", + "dyn.stringConstructor", + "dyn.forInKeys", + "dyn.forInKeyStrings", + "dyn.hasKeyComputed", + "dyn.hasOwnComputed", + "dyn.propertyIsEnumerableComputed", + "dyn.defineProperty", + "dyn.getPrototype", + "dyn.reflectGet", + "dyn.reflectSet", +]); + +/** Prototype data helpers (classPrototypeData) gain method slots inside + * their initialization block, so their summaries are not reused. */ +const PROTOTYPE_DATA_PREFIX = "%prototype.data."; + +/** SCRIPTC_CHECK_DISPATCH_SCANS=1 checks each reused summary against a fresh walk. */ +const CHECK_SCANS = process.env["SCRIPTC_CHECK_DISPATCH_SCANS"] === "1"; + +/** The function summaries (FunctionSites) of one class dispatch pass, kept + * across its runs. */ +export class DispatchSites { + private readonly sites = new Map(); + + /** The sites of `fn`. A summary is reused while the function's statement + * list is the same array with the same length and the same first and last + * statements: the dispatch passes replace the lists they rewrite and add + * statements to the helpers they extend. Other changes inside an + * unchanged list are reported with `forget`, except in prototype data + * helpers, which are scanned on every use. */ + of(fn: IrFunction): FunctionSites { + const cached = this.sites.get(fn); + if ( + cached !== undefined && + !fn.name.startsWith(PROTOTYPE_DATA_PREFIX) && + cached.statements === fn.body && + cached.length === fn.body.length && + cached.first === fn.body[0] && + cached.last === fn.body[fn.body.length - 1] + ) { + if (CHECK_SCANS && !sameSites(cached, scanSites(fn.body))) + throw new InternalCompilerError( + `reused dispatch sites of ${fn.name} disagree with a fresh scan`, + ); + return cached; + } + const sites = scanSites(fn.body); + this.sites.set(fn, sites); + return sites; + } + + /** Drops the summary of a function whose body changed inside an unchanged + * statement list. */ + forget(fn: IrFunction): void { + this.sites.delete(fn); + } +} + +function scanSites(statements: IrStmt[]): FunctionSites { + const sites: FunctionSites = { + statements, + length: statements.length, + first: statements[0], + last: statements[statements.length - 1], + values: [], + boxing: [], + serializes: false, + conversions: false, + dispatches: false, + keyTests: false, + caughtClassTests: false, + generatorSteps: false, + runtimeSteps: false, + basePrototypes: false, + thrown: [], + caughtValues: false, + }; + const valueKeys = new Set(); + const boxingKeys = new Set(); + const thrown = new Set(); + const box = (type: IrType, fieldClass?: string): void => { + const key = `${fieldClass ?? ""}\0${typeKey(type)}`; + if (boxingKeys.has(key)) return; + boxingKeys.add(key); + sites.boxing.push(fieldClass === undefined ? { type } : { type, fieldClass }); + }; + everyStmtList(statements, { + stmt: (statement) => { + if (statement.kind === "throw" && statement.value.type.kind === "object") { + const className = statement.value.type.className; + if (!thrown.has(className)) { + thrown.add(className); + sites.thrown.push(className); + } + } + // Field-creation instrumentation can box a base-constructor receiver + // even when only its derived instances otherwise cross the boundary. + if (statement.kind === "fieldSet" && isClassOwnEnumerableFieldName(statement.field)) + box(statement.obj.type, statement.className); + return true; + }, + expr: (expr) => { + if ( + expr.kind === "jsonStringify" || + (expr.kind === "libCall" && + (expr.fn === "json.stringifyValue" || expr.fn === "json.stringifyReplacer")) + ) + sites.serializes = true; + if ( + toPrimitiveHint(expr) !== null || + (expr.kind === "libCall" && (expr.fn === "dyn.valueOf" || expr.fn === "dyn.toString")) + ) + sites.conversions = true; + if ( + (expr.kind === "dynInvoke" && GENERATOR_STEP_METHODS.has(expr.method)) || + (expr.kind === "dynKeyGet" && + expr.key.kind === "strLit" && + (GENERATOR_STEP_METHODS.has(expr.key.value) || HELPER_METHODS.has(expr.key.value))) + ) + sites.generatorSteps = true; + if ( + (expr.kind === "dynInvoke" && HELPER_METHODS.has(expr.method)) || + (expr.kind === "dynKeyGet" && + expr.key.kind === "strLit" && + HELPER_METHODS.has(expr.key.value)) || + expr.kind === "dynIterN" || + (expr.kind === "libCall" && expr.fn === "dyn.destructureRest") + ) + sites.runtimeSteps = true; + switch (expr.kind) { + case "dynFrom": { + const key = typeKey(expr.value.type); + if (!valueKeys.has(key)) { + valueKeys.add(key); + sites.values.push(expr.value.type); + } + box(expr.value.type); + break; + } + case "fieldIncDec": + if (isClassOwnEnumerableFieldName(expr.field)) box(expr.obj.type, expr.className); + break; + case "call": + if ( + (expr.callee === "%dyn.class.properties" || + expr.callee === "%dyn.class.readProperties") && + expr.args[0]?.kind === "dynFrom" && + isDynTypedRefType(expr.args[0].value.type) + ) + sites.dispatches = true; + break; + case "dynHasKey": + sites.keyTests = true; + break; + case "caughtTest": + if (expr.test === "instanceof") sites.caughtClassTests = true; + break; + case "caughtNarrow": + case "caughtCheck": + if (expr.type.kind === "object") sites.caughtClassTests = true; + break; + case "caughtToDyn": + sites.caughtValues = true; + break; + case "dynKeyGet": + case "dynInvoke": + sites.dispatches = true; + break; + case "dynCheck": + if (isDynTypedRefType(expr.type)) sites.dispatches = true; + // A dynamic function checked into a native function type + // receives its arguments as dynamic values: class instances + // passed to it cross the boundary when it is called (a JS + // `list.map((rec) => rec.match(x))` callback with an untyped + // parameter). + if (expr.type.kind === "func") expr.type.params.forEach((type) => box(type)); + break; + case "libCall": + if (expr.fn === "dyn.classBasePrototype") sites.basePrototypes = true; + if (DISPATCH_LIB_CALLS.has(expr.fn)) sites.dispatches = true; + break; + } + return true; + }, + }); + return sites; +} + +function sameSites(left: FunctionSites, right: FunctionSites): boolean { + const sameList = (a: readonly T[], b: readonly T[], same: (x: T, y: T) => boolean): boolean => + a.length === b.length && a.every((item, index) => same(item, b[index]!)); + return ( + sameList(left.values, right.values, (a, b) => a === b) && + sameList( + left.boxing, + right.boxing, + (a, b) => a.type === b.type && a.fieldClass === b.fieldClass, + ) && + sameList(left.thrown, right.thrown, (a, b) => a === b) && + left.serializes === right.serializes && + left.conversions === right.conversions && + left.dispatches === right.dispatches && + left.keyTests === right.keyTests && + left.caughtClassTests === right.caughtClassTests && + left.generatorSteps === right.generatorSteps && + left.runtimeSteps === right.runtimeSteps && + left.basePrototypes === right.basePrototypes && + left.caughtValues === right.caughtValues + ); +} diff --git a/packages/compiler/src/frontend/lowering/expressions/array-literals.ts b/packages/compiler/src/frontend/lowering/expressions/array-literals.ts index 8f119504c..cb2d86804 100644 --- a/packages/compiler/src/frontend/lowering/expressions/array-literals.ts +++ b/packages/compiler/src/frontend/lowering/expressions/array-literals.ts @@ -58,6 +58,22 @@ export function jsHoldsDynObject(lowerer: Lowerer, expr: ts.Expression): boolean return false; } +/** An empty TypeScript literal whose own type is `never[]`: tsc's type for + * `[]` with no contextual array type (`return []` in an async function, + * whose contextual type is `string[] | PromiseLike`; `{ files: [] }`). + * It holds no element, as the empty tuple does, so it builds as the + * unit-only element array, and an array slot receives it through the + * checked empty-array lift. never's f64 element mapping would make it a + * `number[]`, which no `string[]` slot accepts. */ +function emptyNeverLiteral(lowerer: Lowerer, expr: ts.ArrayLiteralExpression): boolean { + if (expr.elements.nodes.length > 0 || isJsSourceFile(ts.getSourceFileOfNode(expr))) return false; + const own = lowerer.typeOf(expr); + return ( + lowerer.checker.isArrayType(own) && + (lowerer.checker.getTypeArguments(own as ts.TypeReference)[0]!.flags & ts.TypeFlags.Never) !== 0 + ); +} + /** `[a, b, c]`. The element type comes from the contextual type when tsc * has one (`const a: number[] = []`, arguments, nested literals) and from * the literal's own inferred type otherwise. A bare `[]` with no context @@ -80,6 +96,8 @@ export function lowerArrayLiteral( ? lowerer.typeOf(expr) : (ctxType ?? lowerer.typeOf(expr)); let mapped = expected ?? lowerer.mapTypeOf(tsType); + if (expected === undefined && ctxType === undefined && emptyNeverLiteral(lowerer, expr)) + mapped = arrayOf(unitOnlyUnion(lowerer.unions)); // Inferred JavaScript arrays can contain unrelated constructors with // structurally equal checker types. Their native class ABIs still differ. if ( @@ -131,11 +149,14 @@ export function lowerArrayLiteral( // the literal's own inferred type is the real tuple. if (!mapped || mapped.kind === "union" || mapped.kind === "dyn") { const ctxUnion = mapped?.kind === "union" ? mapped : null; + const ctxUnmapped = mapped === null; mapped = lowerer.mapTypeOf(lowerer.typeOf(expr)); // The never-tainted own type carries no element information here // either (the JS story above) — only a contextual union's single // array arm below may still supply a static home. if (neverTaintedOwnJs) mapped = null; + if (ctxUnmapped && emptyNeverLiteral(lowerer, expr)) + mapped = arrayOf(unitOnlyUnion(lowerer.unions)); // An EMPTY literal in a union slot (`const t: string[] | undefined = // []`, the `[]` default argument against `string[] | undefined`) has // no useful own type — tsc infers `never[]`, whose f64-element diff --git a/packages/compiler/src/frontend/lowering/expressions/narrowing.ts b/packages/compiler/src/frontend/lowering/expressions/narrowing.ts index 2e3dd6140..786f3a337 100644 --- a/packages/compiler/src/frontend/lowering/expressions/narrowing.ts +++ b/packages/compiler/src/frontend/lowering/expressions/narrowing.ts @@ -985,11 +985,41 @@ export function runtimeOptionalReceiverRead( loc, }; } + // An array hole in a slot with more than two arms fails the member + // access with Node's TypeError before the checked extraction runs. + const holeGuarded = (extracted: IrExpr): IrExpr => { + if (local.type.kind !== "union") return extracted; + const holeTag = lowerer.armTag(local.type.unionId, UNDEFINED_T); + if (holeTag < 0) return extracted; + return { + kind: "ternary", + cond: { + kind: "unionIsTag", + unionId: local.type.unionId, + tag: holeTag, + negated: false, + value: varRef(local.id, local.type, loc), + type: BOOL, + loc, + }, + then: nodeThrowExpr( + 1, + "", + runtimeOptionalReceiverMessage(expr, property), + extracted.type, + loc, + ), + else_: extracted, + type: extracted.type, + loc, + }; + }; if (narrowed.kind === "object") { const view = checkedClassAssertion(lowerer, varRef(local.id, local.type, loc), narrowed, loc); // Preserve the member-specific missing-value error on the two-arm // path below; mixed storage also needs to validate its concrete class. - if (view && (lowerer.unions.get(local.type.unionId)?.arms.length ?? 0) > 2) return view; + if (view && (lowerer.unions.get(local.type.unionId)?.arms.length ?? 0) > 2) + return holeGuarded(view); } if (narrowed.kind === "record" && lowerer.armTag(local.type.unionId, narrowed) < 0) { const hosted = hostedStorageView(lowerer, varRef(local.id, local.type, loc), narrowed, loc); @@ -1014,7 +1044,9 @@ export function runtimeOptionalReceiverRead( "a direct read from a runtime-optional capture with multiple value arms", ); } - return lowerer.checkedNarrowCall(helper, varRef(local.id, local.type, loc), narrowed, loc); + return holeGuarded( + lowerer.checkedNarrowCall(helper, varRef(local.id, local.type, loc), narrowed, loc), + ); } // A predicate can strengthen a record's fields or narrow a base class // to a subclass without changing the slot's stored value arm. Extract @@ -1029,10 +1061,7 @@ export function runtimeOptionalReceiverRead( "a runtime-optional receiver without a representable value arm", ); } - const message = - property === null - ? `${expr.text} is not a function` - : `Cannot read properties of undefined (reading '${property}')`; + const message = runtimeOptionalReceiverMessage(expr, property); return lowerer.maybeNarrow( { kind: "ternary", @@ -1061,6 +1090,14 @@ export function runtimeOptionalReceiverRead( ); } +/** Node's TypeError message for a member read (`property`) or a call + * (`property` null) through an undefined runtime-optional receiver. */ +function runtimeOptionalReceiverMessage(expr: ts.Identifier, property: string | null): string { + return property === null + ? `${expr.text} is not a function` + : `Cannot read properties of undefined (reading '${property}')`; +} + export function runtimeOptionalTrueIds(lowerer: Lowerer, node: ts.Expression): IrLocal[] { return runtimeOptionalGuardIds(lowerer, node, true); } diff --git a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts index 015be7914..678b375fe 100644 --- a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts +++ b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts @@ -36,6 +36,7 @@ import { tryLowerExpression } from "./try-lower-expression.js"; import { fenceSymbolFieldCopy } from "../symbol-fields.js"; import { planUnionRetag } from "../union-retag.js"; import { jsHoldsDynObject, runtimeOptionalStorageOperand } from "../lower-exprs.js"; +import { builtinGlobalValue } from "./property-access.js"; /** `{ a: 1, b: "x" }` → recordLit. The record type comes from the * contextual type when tsc has one (annotated declarations, arguments, @@ -394,9 +395,11 @@ export function lowerDynObjectLiteral( const lowerValue = (): IrExpr => ts.isMethodDeclaration(prop) ? lowerer.lowerLambda(prop, true) - : !boxValue && ts.isObjectLiteralExpression(valueExpr) - ? lowerer.lowerExprExpecting(valueExpr, DYN) - : lowerer.lowerExpr(valueExpr as ts.Expression); + : ts.isShorthandPropertyAssignment(prop) && shorthandBuiltinValue(lowerer, prop) + ? shorthandBuiltinValue(lowerer, prop)! + : !boxValue && ts.isObjectLiteralExpression(valueExpr) + ? lowerer.lowerExprExpecting(valueExpr, DYN) + : lowerer.lowerExpr(valueExpr as ts.Expression); raw = fenceClosureProbe(lowerer, valueExpr, undefined, lowerValue) ?? lowerValue(); } catch (err) { // A PURE member read a JS file cannot lower (a namespace object @@ -3157,6 +3160,20 @@ function lowerDeclaredSpreadMerge( /** The value of a shorthand property (`{ x }`): the binding `x` refers to. * The property name's own symbol is the PROPERTY, so resolution goes * through getShorthandAssignmentValueSymbol. */ +/** `{ TypeError }`: a stdlib global in shorthand position reads as the value + * its name reads as elsewhere (the interned constructor or function value), + * or null when the global has no such value. The shorthand's name carries + * the property's symbol, so the identifier path cannot resolve it. */ +function shorthandBuiltinValue( + lowerer: Lowerer, + prop: ts.ShorthandPropertyAssignment, +): IrExpr | null { + const symbol = lowerer.checker.getShorthandAssignmentValueSymbol(prop); + if (!symbol || !lowerer.isStdlibSymbol(symbol)) return null; + const js = isJsSourceFile(ts.getSourceFileOfNode(prop)); + return builtinGlobalValue(ts.symbolName(symbol), locOf(prop.name), js); +} + export function lowerShorthandValue( lowerer: Lowerer, prop: ts.ShorthandPropertyAssignment, @@ -3173,6 +3190,8 @@ export function lowerShorthandValue( const callable = lowerer.lowerBuiltinCallableValue(builtin, loc); if (callable) return callable; } + const builtinValue = shorthandBuiltinValue(lowerer, prop); + if (builtinValue) return builtinValue; if (symbol) { if (lowerer.ctx.selfSymbol === symbol) { return { kind: "selfRef", type: lowerer.ctx.selfType!, loc }; diff --git a/packages/compiler/src/frontend/lowering/expressions/property-access.ts b/packages/compiler/src/frontend/lowering/expressions/property-access.ts index 957dd1ddf..5df3c7fe0 100644 --- a/packages/compiler/src/frontend/lowering/expressions/property-access.ts +++ b/packages/compiler/src/frontend/lowering/expressions/property-access.ts @@ -666,14 +666,20 @@ export function lowerPropertyAccessExpr( ); if (fixedArity) { // A `this` parameter is type-only: callers never pass it. - return { - kind: "numLit", - value: (fn as ts.FunctionLikeDeclaration).parameters.nodes.filter( - (p) => !isThisParameter(p), - ).length, - type: F64, - loc, - }; + const declared = (fn as ts.FunctionLikeDeclaration).parameters.nodes.filter( + (p) => !isThisParameter(p), + ).length; + const count = Math.max(declared, contextualArgumentCount(lowerer, fn) ?? 0); + // Calls through the expression's own name pass the declared count, + // calls through the wider slot pass more: no constant answers both. + if (count > declared && referencesOwnName(lowerer, fn)) { + lowerer.unsupported( + "SC1090", + expr, + "'arguments.length' in a function expression that is called both through its own name and through a function type with more parameters (the count varies per call)", + ); + } + return { kind: "numLit", value: count, type: F64, loc }; } lowerer.unsupported( "SC1090", @@ -4090,10 +4096,26 @@ function lowerStreamObjectProperty( return lowerStreamProperty(lowerer, expr, info); } +/** The error constructors among GLOBAL_CONSTRUCTOR_VALUES. Their statics + * (`Error.stackTraceLimit`, `Error.captureStackTrace`) keep their own + * lowerings, so fenceBuiltinConstructorStaticRead leaves them alone. */ +const ERROR_CONSTRUCTOR_VALUES: ReadonlySet = new Set([ + "Error", + "TypeError", + "RangeError", + "SyntaxError", + "ReferenceError", + "EvalError", + "URIError", + "AggregateError", +]); + /** The builtin constructors JavaScript sources read as real function * values rather than identity tokens: the runtime interns one function per * constructor, and dynamic values answer `.constructor` with the same one, - * so `o.constructor === Object` is an exact identity test. */ + * so `o.constructor === Object` and `err.constructor === TypeError` are + * exact identity tests, and `err.constructor.name` is the constructor's + * name. */ const GLOBAL_CONSTRUCTOR_VALUES: ReadonlySet = new Set([ "Object", "Function", @@ -4104,6 +4126,7 @@ const GLOBAL_CONSTRUCTOR_VALUES: ReadonlySet = new Set([ "Promise", "WeakMap", "WeakSet", + ...ERROR_CONSTRUCTOR_VALUES, ]); /** The builtin global functions JavaScript sources read as interned @@ -4154,8 +4177,18 @@ export function builtinGlobalValue(canonical: string, loc: SrcLoc, js = false): const BUILTIN_VALUE_MEMBERS: ReadonlySet = new Set(["name", "length", "constructor"]); /** The Object statics whose answers for an interned builtin function value - * match Node's (identity, and the empty enumerable own-key lists). */ -const BUILTIN_VALUE_REFLECTION: ReadonlySet = new Set(["is", "keys", "values", "entries"]); + * match Node's (identity, the empty enumerable own-key lists, and + * extensibility: freezing, sealing and preventExtensions are fenced, so the + * values stay extensible). */ +const BUILTIN_VALUE_REFLECTION: ReadonlySet = new Set([ + "is", + "keys", + "values", + "entries", + "isExtensible", + "isFrozen", + "isSealed", +]); /** The value `builtinGlobalValue` gives the stdlib global `expr` read at its * use site: null when the use reads the global's members (they keep their @@ -4246,6 +4279,7 @@ function fenceBuiltinConstructorStaticRead( symbol !== undefined && ts.symbolName(symbol).endsWith("Constructor") && GLOBAL_CONSTRUCTOR_VALUES.has(ts.symbolName(symbol).slice(0, -"Constructor".length)) && + !ERROR_CONSTRUCTOR_VALUES.has(ts.symbolName(symbol).slice(0, -"Constructor".length)) && lowerer.isStdlibSymbol(symbol) ) lowerer.stdlibMemberFence(access); @@ -4412,3 +4446,47 @@ function lowerPromiseConstructorRead( } return null; } + +/** Whether a named function expression refers to itself by name in its + * body (a recursive call, or the function escaping under its own name). */ +function referencesOwnName(lowerer: Lowerer, fn: ts.Node): boolean { + if (!ts.isFunctionExpression(fn) || fn.name === undefined || fn.body === undefined) return false; + const self = lowerer.checker.getSymbolAtLocation(fn.name); + if (self === undefined) return false; + const name = fn.name.text; + let found = false; + ts.walkPreorder(fn.body, (node) => { + if (found) return "stop"; + if ( + ts.isIdentifier(node) && + node.text === name && + lowerer.checker.getSymbolAtLocation(node) === self + ) + found = true; + return undefined; + }); + return found; +} + +/** The argument count every caller passes a function expression whose + * value flows into one fixed-arity function type, an array method's + * callback or an annotated slot: that type's parameter count. Null when + * there is no such type or its arity varies. */ +function contextualArgumentCount(lowerer: Lowerer, fn: ts.Node): number | null { + if (!ts.isFunctionExpression(fn)) return null; + const contextual = lowerer.checker.getContextualType(fn); + if (contextual === undefined) return null; + const signatures = lowerer.checker.getCallSignatures( + lowerer.checker.getNonNullableType(contextual), + ); + if (signatures.length !== 1) return null; + const declaration = signatures[0]!.declaration; + if (declaration === undefined) return null; + let count = 0; + for (const parameter of (declaration.parameters?.nodes ?? []) as ts.ParameterDeclaration[]) { + if (isThisParameter(parameter)) continue; + if (parameter.questionToken || parameter.initializer || parameter.dotDotDotToken) return null; + count++; + } + return count; +} diff --git a/packages/compiler/src/frontend/lowering/expressions/relational.ts b/packages/compiler/src/frontend/lowering/expressions/relational.ts index 61558f008..759687ff0 100644 --- a/packages/compiler/src/frontend/lowering/expressions/relational.ts +++ b/packages/compiler/src/frontend/lowering/expressions/relational.ts @@ -49,6 +49,7 @@ import { InternalCompilerError } from "../../../errors.js"; import { tryLowerExpression } from "./try-lower-expression.js"; import { isJsSourceFile, locOf } from "../../program.js"; import { checkedClassInstanceOf } from "../class-construction.js"; +import { unsupportedDiag } from "../../../diagnostics/diagnostic.js"; import { globalSymbolKey } from "./global-symbols.js"; import { foldedStringKeyOf } from "./object-literals.js"; import { NARROW_FIRST, sideEffectFreeOptionValue } from "../surfaces.js"; @@ -490,7 +491,13 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc const constructor = tryLowerExpression(lowerer, expr.right); if (constructor?.type.kind === "dyn" && isJsSourceFile(ts.getSourceFileOfNode(expr))) { const value = lowerer.coerceInto(expr.left, lowerer.lowerExpr(expr.left), DYN); - return checkedClassInstanceOf(lowerer, value, constructor, loc); + return checkedClassInstanceOf( + lowerer, + value, + constructor, + dynInstanceOfRefusal(lowerer, expr), + loc, + ); } // `this instanceof Glob` in a JavaScript constructor function: the // function's prototype against the value's prototype chain. @@ -2000,3 +2007,25 @@ function lowerRuntimeKeyIn( } return { kind: "call", callee: helper, args: [keyIr, recv], type: BOOL, loc }; } + +/** Standard-library constructors the untyped instanceof helper has no + * instance test for: an untyped `x instanceof Object` always refuses. */ +const DYN_INSTANCEOF_REFUSED: ReadonlySet = new Set(["Object", "Function"]); + +/** The SC1090 message `x instanceof C` throws, with the site's position, + * when `C` is a runtime constructor with no instance test. A right-hand + * side that names one of DYN_INSTANCEOF_REFUSED always reaches that + * refusal, so the site is listed with the runtime fences. */ +function dynInstanceOfRefusal(lowerer: Lowerer, expr: ts.BinaryExpression): string { + const diag = unsupportedDiag("SC1090", locOf(expr), "instanceof against this native constructor"); + const symbol = lowerer.checker.getSymbolAtLocation(expr.right); + if ( + symbol !== undefined && + lowerer.isStdlibSymbol(symbol) && + DYN_INSTANCEOF_REFUSED.has(ts.symbolName(symbol)) + ) + lowerer.runtimeFences.push(diag); + const sf = ts.getSourceFileOfNode(expr); + const line = ts.getLineAndCharacterOfPosition(sf, diag.loc.start).line + 1; + return `${diag.message} [${diag.code} at ${diag.loc.file}:${line}]`; +} diff --git a/packages/compiler/src/frontend/lowering/generator-dynamic-dispatch.ts b/packages/compiler/src/frontend/lowering/generator-dynamic-dispatch.ts index 3f43e5e05..db542412f 100644 --- a/packages/compiler/src/frontend/lowering/generator-dynamic-dispatch.ts +++ b/packages/compiler/src/frontend/lowering/generator-dynamic-dispatch.ts @@ -13,16 +13,20 @@ import { type IrStmt, type IrType, } from "../../ir/ir.js"; -import { everyStmtList, transformStmtList } from "../../ir/traverse.js"; +import { transformStmtList } from "../../ir/traverse.js"; import { genResultRecord } from "../type-mapper.js"; +import type { DispatchSites } from "./dispatch-sites.js"; import type { Lowerer } from "./lowerer.js"; type GeneratorType = Extract; type Invoke = Extract; type KeyGet = Extract; +/** The generator methods a dynamic call or read steps. */ +export const GENERATOR_STEP_METHODS = new Set(["next", "return", "throw"]); + /** The iterator helper methods the runtime answers on checked iterators. */ -const HELPER_METHODS = new Set([ +export const HELPER_METHODS = new Set([ "map", "filter", "take", @@ -101,6 +105,7 @@ export class GeneratorDynamicDispatch { lowerer: Lowerer, functions: readonly IrFunction[], classHelpers: ReadonlySet, + sites: DispatchSites, ): boolean { const seen = new Set(); const rewrite = new Set(); @@ -117,34 +122,14 @@ export class GeneratorDynamicDispatch { lowerer.shapes.get(type.shapeId)?.fields.forEach((field) => discover(field.type)); else if (type.kind === "union") lowerer.unions.get(type.unionId)?.arms.forEach(discover); }; - for (const fn of functions) - everyStmtList(fn.body, { - stmt: () => true, - expr: (expr) => { - if (expr.kind === "dynFrom") discover(expr.value.type); - if (expr.kind === "dynInvoke" && ["next", "return", "throw"].includes(expr.method)) - rewrite.add(fn); - // The runtime steps the values iterator helpers and destructuring - // receive. - if ( - (expr.kind === "dynInvoke" && HELPER_METHODS.has(expr.method)) || - (expr.kind === "dynKeyGet" && - expr.key.kind === "strLit" && - HELPER_METHODS.has(expr.key.value)) || - expr.kind === "dynIterN" || - (expr.kind === "libCall" && expr.fn === "dyn.destructureRest") - ) - runtimeSteps = true; - if ( - expr.kind === "dynKeyGet" && - expr.key.kind === "strLit" && - (["next", "return", "throw"].includes(expr.key.value) || - HELPER_METHODS.has(expr.key.value)) - ) - rewrite.add(fn); - return true; - }, - }); + for (const fn of functions) { + const found = sites.of(fn); + found.values.forEach(discover); + // The runtime steps the values iterator helpers and destructuring + // receive. + if (found.runtimeSteps) runtimeSteps = true; + if (found.generatorSteps) rewrite.add(fn); + } let changed = false; for (const fn of functions) { // The generator helper's fallback may itself dispatch native classes. @@ -159,7 +144,7 @@ export class GeneratorDynamicDispatch { if ( expr.kind === "dynKeyGet" && expr.key.kind === "strLit" && - (["next", "return", "throw"].includes(expr.key.value) || + (GENERATOR_STEP_METHODS.has(expr.key.value) || (HELPER_METHODS.has(expr.key.value) && this.generators.size > 0)) ) { const method = expr.key.value; @@ -173,8 +158,7 @@ export class GeneratorDynamicDispatch { loc: expr.loc, }; } - if (expr.kind !== "dynInvoke" || !["next", "return", "throw"].includes(expr.method)) - return expr; + if (expr.kind !== "dynInvoke" || !GENERATOR_STEP_METHODS.has(expr.method)) return expr; const key = JSON.stringify([expr.method, expr.args.length, expr.calleeName]); let dispatch = this.dispatches.get(key); if (!dispatch) { diff --git a/packages/compiler/src/frontend/lowering/generic-functions.ts b/packages/compiler/src/frontend/lowering/generic-functions.ts index 1fff26585..e27ab7f5f 100644 --- a/packages/compiler/src/frontend/lowering/generic-functions.ts +++ b/packages/compiler/src/frontend/lowering/generic-functions.ts @@ -5,15 +5,20 @@ import * as ts from "../ts7/adapter.js"; import { bodyReadsArguments } from "../arguments-usage.js"; import type { Lowerer } from "./lowerer.js"; import { + BOOL, DYN, type IrExpr, type IrFunction, type IrParam, type IrStmt, type IrType, + type SrcLoc, + STRING, + VOID, isUnitType, typeEquals, } from "../../ir/ir.js"; +import { helperLoc, nodeThrowExpr } from "../../ir/build.js"; import { isJsSourceFile, locOf } from "../program.js"; import { typeKey } from "../type-mapper.js"; import { functionCanReturnUndefined } from "../function-completion.js"; @@ -24,7 +29,7 @@ import { nsPathPrefix } from "./lower-namespaces.js"; import { declSymbolOf } from "./lower-modules.js"; import { npmStaticPackageOfPath } from "../npm-static.js"; import { rejectStaticThis } from "./static-this.js"; -import { jsBindingHasOpenWrites } from "./lower-stmts.js"; +import { isVarDeclared, jsBindingHasOpenWrites } from "./lower-stmts.js"; import { type ParamShape, paramShapes, @@ -93,6 +98,17 @@ export interface GenericFnInfo { * declaration makes a new function object in JavaScript, while the * compiled instances are shared module functions, so only calls lower. */ functionScoped?: true; + /** The declaration of a generic arrow/function-expression binding. */ + bindingDecl?: ts.VariableDeclaration; + /** A module-scope binding that code can use before its declaration + * runs: the BOOL global its declaration sets, and whether it is a + * `let`/`const` (a temporal dead zone) or a `var` (undefined). */ + presence?: { flagId: string; lexical: boolean }; + /** genericValueForwarder's lifted functions, by instance name. */ + valueForwarders?: Map; + /** runsAfterDeclaration's answers for the function declarations whose + * calls it placed. */ + ordering?: Map; /** IMPLICIT-ANY monomorphization (npm-static JS): parallel to * decl.parameters — the param's own symbol when the slot is a BINDABLE * implicit-any parameter (untyped, identifier-named, never written in @@ -266,7 +282,14 @@ export function lowerGenericCall( ? implicitCallInstance(lowerer, expr, info) : genericCallInstance(lowerer, expr, info); const args = lowerer.completeArgs(expr.arguments.nodes, instance.params, loc, expr); - return { kind: "call", callee: instance.name, args, type: instance.returnType, loc }; + const call: IrExpr & { kind: "call" } = { + kind: "call", + callee: instance.name, + args, + type: instance.returnType, + loc, + }; + return guardEarlyBindingCall(lowerer, info, expr, call); } /** The instance a CALL of a generic function-like names: resolved @@ -1361,6 +1384,110 @@ export function lowerGenericFnValue( } } if (!target) fenceUnpinned(); + return guardEarlyBindingValue( + lowerer, + info, + ref, + pinnedGenericClosure(lowerer, ref, info, target, fenceUnpinned), + ); +} + +/** The value a generic binding declared inside a function stores when a + * nested function read it before the declaration lowered (`const read = + * () => id; const id: (x: number) => number = (x: T): T => x`): such + * a read resolves to an ordinary TDZ local, so the declaration assigns it + * the instance its concrete declared signature pins. Each evaluation of + * the declaration makes a new function object, as in JavaScript: the + * value is a closure over a forwarder to the instance, not the instance's + * interned closure. */ +export function genericBindingStoredValue( + lowerer: Lowerer, + decl: ts.VariableDeclaration, + info: GenericFnInfo, +): IrExpr { + const name = decl.name as ts.Identifier; + const fenceUnpinned = (): never => + lowerer.unsupported( + "SC1090", + name, + `generic function bindings read before their declaration without a concrete declared signature (annotate '${info.baseName}' with one, or declare it before its uses)`, + ); + const sigs = lowerer.checker.getCallSignatures(lowerer.typeOf(name)); + if (sigs.length !== 1) fenceUnpinned(); + const pinned = pinnedGenericClosure(lowerer, name, info, sigs[0]!, fenceUnpinned); + const async = ts.getModifiers(info.decl)?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword); + if (pinned.kind !== "closure" || pinned.type.kind !== "func" || pinned.type.rest || async) + return lowerer.unsupported( + "SC1090", + name, + `async or rest-parameter generic function bindings read before their declaration (each evaluation of '${info.baseName}' makes a new function object)`, + ); + return { + ...pinned, + fnName: genericValueForwarder(lowerer, info, pinned.fnName, pinned.type, pinned.loc), + }; +} + +/** A lifted function that calls the instance `instance` (of type `type`) + * with its own arguments. Lifted functions get a new closure object per + * evaluation; module functions, which instances are, get one interned + * closure. */ +function genericValueForwarder( + lowerer: Lowerer, + info: GenericFnInfo, + instance: string, + type: IrType & { kind: "func" }, + at: SrcLoc, +): string { + info.valueForwarders ??= new Map(); + const existing = info.valueForwarders.get(instance); + if (existing !== undefined) return existing; + const name = `${instance}%value`; + const loc = helperLoc(at); + const params: IrParam[] = type.params.map((paramType, i) => ({ + localId: `%arg${i}.0`, + name: `%arg${i}`, + type: paramType, + })); + const call: IrExpr = { + kind: "call", + callee: instance, + args: params.map((p): IrExpr => ({ kind: "varRef", localId: p.localId, type: p.type, loc })), + type: type.ret, + loc, + }; + lowerer.liftedFns.push({ + name, + params, + returnType: type.ret, + locals: params.map((p) => ({ id: p.localId, name: p.name, type: p.type, mutable: false })), + captures: [], + ...(ts.isFunctionExpression(info.decl) ? { ownsPrototype: true } : {}), + body: + type.ret.kind === "void" + ? [ + { kind: "exprStmt", expr: call, loc }, + { kind: "return", value: null, loc }, + ] + : [{ kind: "return", value: call, loc }], + loc, + }); + lowerer.noteEdge(name); + info.valueForwarders.set(instance, name); + return name; +} + +/** The closure of the instance of `info` that `target` pins, for a value + * read at `ref`. `fenceUnpinned` reports a signature that leaves a type + * parameter unbound. */ +function pinnedGenericClosure( + lowerer: Lowerer, + ref: ts.Expression, + info: GenericFnInfo, + target: ts.Signature, + fenceUnpinned: () => never, +): IrExpr { + const loc = locOf(ref); const bindings = new Map(); unifySignatureBindings(lowerer, info, target, bindings); const hofBinding = runtimeOptionalHofGenericBinding(lowerer, ref, info); @@ -2693,9 +2820,18 @@ export function bindingGenericFnInfoOf( typeParams, instances: new Map(), ...(functionScoped ? { functionScoped: true as const } : {}), + bindingDecl: decl, }; lowerer.bindingGenericFns.set(fnNode, info); lowerer.genericFnsBySymbol.set(sym, info); + // Module-scope uses resolve to the instances from the start, so those + // that can run first need the flag. Inside a function the declaration + // registers before any later use lowers, and earlier uses read a TDZ + // box (genericBindingStoredValue). + if (!functionScoped) { + const presence = bindingPresenceOf(lowerer, info, decl, sym); + if (presence !== undefined) info.presence = presence; + } if (ts.isFunctionExpression(fnNode) && fnNode.name !== undefined) { const inner = lowerer.checker.getSymbolAtLocation(fnNode.name); if (inner) lowerer.genericFnsBySymbol.set(inner, info); @@ -2703,6 +2839,261 @@ export function bindingGenericFnInfoOf( return info; } +/** Whether `use`, a reference to the binding `info` declares, can only + * run after the declaration has run: it follows the declaration in its + * statement list (closures count where they are created), sits inside the + * initializer itself, or is inside a function declaration whose every + * reference does. References from other modules run after this module's + * body, and export specifiers only export. A use the check cannot place + * answers false. */ +export function runsAfterDeclaration(lowerer: Lowerer, info: GenericFnInfo, use: ts.Node): boolean { + const decl = info.bindingDecl; + if (decl === undefined) return true; + info.ordering ??= new Map(); + return useRunsAfter(lowerer, decl, use, info.ordering, new Set()); +} + +function useRunsAfter( + lowerer: Lowerer, + decl: ts.VariableDeclaration, + use: ts.Node, + memo: Map, + inProgress: Set, +): boolean { + if (ts.getSourceFileOfNode(use) !== ts.getSourceFileOfNode(decl)) return true; + const init = decl.initializer; + if (init !== undefined && use.pos >= init.pos && use.end <= init.end) return true; + const stmt = decl.parent?.parent; + const list = stmt?.parent; + if (stmt === undefined || list === undefined) return false; + for (let n: ts.Node = use; n.parent !== undefined; n = n.parent) { + if (n.parent === list) return n === stmt ? use.pos >= decl.end : n.pos >= stmt.end; + if (ts.isFunctionDeclaration(n)) return functionRunsAfter(lowerer, decl, n, memo, inProgress); + } + return false; +} + +/** Whether every call of the function declaration `fn` runs after `decl` + * has run. A function in progress counts as running after: its calls of + * itself, directly or through other functions, add no earlier caller. + * Only answers that depend on no function in progress are memoized. */ +function functionRunsAfter( + lowerer: Lowerer, + decl: ts.VariableDeclaration, + fn: ts.FunctionDeclaration, + memo: Map, + inProgress: Set, +): boolean { + const known = memo.get(fn); + if (known !== undefined) return known; + if (inProgress.has(fn)) return true; + // `export default function () {}`: only importers call it, after this + // module's body has run. + if (fn.name === undefined) return true; + inProgress.add(fn); + let result = true; + try { + const symbol = lowerer.checker.getSymbolAtLocation(fn.name); + if (symbol === undefined) result = false; + else { + for (const ref of bindingReferences(lowerer, ts.getSourceFileOfNode(fn), symbol, fn.name)) { + if (ref.pos >= fn.pos && ref.end <= fn.end) continue; + if (!useRunsAfter(lowerer, decl, ref, memo, inProgress)) { + result = false; + break; + } + } + } + } finally { + inProgress.delete(fn); + } + if (inProgress.size === 0) memo.set(fn, result); + return result; +} + +/** The value references of `symbol` in `file` other than `name` (its + * declaration name): identifiers spelled like it that resolve to it, + * outside type positions and export specifiers. */ +function bindingReferences( + lowerer: Lowerer, + file: ts.SourceFile, + symbol: ts.Symbol, + name: ts.Identifier, +): ts.Identifier[] { + let byName = lowerer.bindingIdentifiers.get(file); + if (byName === undefined) { + byName = new Map(); + lowerer.bindingIdentifiers.set(file, byName); + } + let idents = byName.get(name.text); + if (idents === undefined) { + const found: ts.Identifier[] = []; + ts.walkPreorder(file, (node) => { + if (ts.isIdentifier(node) && node.text === name.text) found.push(node); + return undefined; + }); + lowerer.checker.prefetchSymbolNodesExact(found); + byName.set(name.text, found); + idents = found; + } + return idents.filter((ident) => { + if (ident === name) return false; + const parent = ident.parent; + if (parent === undefined || ts.isExportSpecifier(parent) || ts.isExportAssignment(parent)) + return false; + let resolved = lowerer.checker.getSymbolAtLocation(ident); + if (ts.isShorthandPropertyAssignment(parent) && parent.name === ident) + resolved = lowerer.checker.getShorthandAssignmentValueSymbol(parent) ?? resolved; + if (resolved !== symbol) return false; + for (let p: ts.Node | undefined = parent; p !== undefined; p = p.parent) { + if (ts.isTypeNode(p) && p.kind !== ts.SyntaxKind.ExpressionWithTypeArguments) return false; + if (ts.isStatement(p) || ts.isFunctionLike(p)) break; + } + return true; + }); +} + +/** The presence flag of a module-scope generic binding that code can use + * before its declaration runs (a hoisted function or an earlier closure + * called during module evaluation), or undefined when every use runs + * after it. The declaration statement sets the flag. */ +function bindingPresenceOf( + lowerer: Lowerer, + info: GenericFnInfo, + decl: ts.VariableDeclaration, + symbol: ts.Symbol, +): GenericFnInfo["presence"] { + const name = decl.name as ts.Identifier; + const early = bindingReferences(lowerer, ts.getSourceFileOfNode(decl), symbol, name).some( + (ref) => !runsAfterDeclaration(lowerer, info, ref), + ); + if (!early) return undefined; + const flagId = `%g.${info.qualifiedName}%initialized`; + lowerer.globalsList.push({ id: flagId, name: "%initialized", type: BOOL, mutable: true }); + return { flagId, lexical: !isVarDeclared(decl) }; +} + +/** The statement that records a generic binding's declaration as run, + * for a binding with a presence flag. */ +export function genericBindingPresenceStore( + info: GenericFnInfo, + decl: ts.VariableDeclaration, +): IrStmt | null { + if (info.presence === undefined) return null; + const loc = locOf(decl); + return { + kind: "assign", + localId: info.presence.flagId, + value: { kind: "boolLit", value: true, type: BOOL, loc }, + loc, + }; +} + +/** A call of a generic binding where the binding may not be initialized + * yet. A `let`/`const` binding in its temporal dead zone throws Node's + * ReferenceError before the arguments run; an unassigned `var` is + * undefined, so the arguments run and the call throws "is not a + * function". Calls that run after the declaration stay direct. */ +function guardEarlyBindingCall( + lowerer: Lowerer, + info: GenericFnInfo, + expr: ts.CallExpression, + call: IrExpr & { kind: "call" }, +): IrExpr { + if (info.bindingDecl === undefined || runsAfterDeclaration(lowerer, info, expr)) return call; + const loc = call.loc; + const presence = requirePresence(lowerer, info, expr.expression); + const flag: IrExpr = { kind: "varRef", localId: presence.flagId, type: BOOL, loc }; + if (presence.lexical) + return { + kind: "seqExpr", + stmts: [tdzCheck(flag, info.baseName, loc)], + result: call, + type: call.type, + loc, + }; + // Arguments first, in order, then the check: Node evaluates them + // before it finds the callee is not callable. + const stmts: IrStmt[] = []; + const args = call.args.map((arg): IrExpr => { + const id = `%earlyCallArg.${lowerer.ctx.locals.length}`; + lowerer.ctx.locals.push({ id, name: "%earlyCallArg", type: arg.type, mutable: false }); + stmts.push({ kind: "varDecl", localId: id, init: arg, loc }); + return { kind: "varRef", localId: id, type: arg.type, loc }; + }); + return { + kind: "seqExpr", + stmts, + result: { + kind: "ternary", + cond: flag, + then: { ...call, args }, + else_: nodeThrowExpr(1, "", `${info.baseName} is not a function`, call.type, loc), + type: call.type, + loc, + }, + type: call.type, + loc, + }; +} + +/** A generic binding read as a value where it may not be initialized + * yet: a `let`/`const` binding checks its temporal dead zone first. */ +function guardEarlyBindingValue( + lowerer: Lowerer, + info: GenericFnInfo, + ref: ts.Expression, + value: IrExpr, +): IrExpr { + if (info.bindingDecl === undefined || runsAfterDeclaration(lowerer, info, ref)) return value; + const presence = requirePresence(lowerer, info, ref); + if (!presence.lexical) + lowerer.unsupported( + "SC1090", + ref, + `generic 'var' bindings read as values before their declaration runs (Node reads undefined there; declare '${info.baseName}' with const)`, + ); + const loc = value.loc; + const flag: IrExpr = { kind: "varRef", localId: presence.flagId, type: BOOL, loc }; + return { + kind: "seqExpr", + stmts: [tdzCheck(flag, info.baseName, loc)], + result: value, + type: value.type, + loc, + }; +} + +/** The presence flag a use that can run before the declaration checks. + * Only module-scope bindings have one: a binding inside a function would + * need a per-call flag that its closures share. */ +function requirePresence( + lowerer: Lowerer, + info: GenericFnInfo, + blame: ts.Node, +): NonNullable { + if (info.presence !== undefined) return info.presence; + return lowerer.unsupported( + "SC1090", + blame, + `generic arrow/function-expression bindings declared inside functions that code can call before their declaration runs (the binding '${info.baseName}' is a 'var' in a branch or block, or is called from a place that can run first — declare it with const before its uses)`, + ); +} + +function tdzCheck(flag: IrExpr, name: string, loc: SrcLoc): IrStmt { + return { + kind: "exprStmt", + expr: { + kind: "intrinsic", + name: "module.tdzCheck", + args: [flag, { kind: "strLit", value: name, type: STRING, loc }], + type: VOID, + loc, + }, + loc, + }; +} + /** `const h = id` — a binding ALIASING a generic function (a top-level * declaration, a registered generic binding, or another alias — resolved * left to right in declaration order). The alias registers the SAME info diff --git a/packages/compiler/src/frontend/lowering/lower-async-iteration.ts b/packages/compiler/src/frontend/lowering/lower-async-iteration.ts index 0e48f2040..5b427a750 100644 --- a/packages/compiler/src/frontend/lowering/lower-async-iteration.ts +++ b/packages/compiler/src/frontend/lowering/lower-async-iteration.ts @@ -428,7 +428,9 @@ function lowerNodeReadable( }, { kind: "if", - cond: { kind: "dynTest", test: "undefined", value: chunkRef, type: BOOL, loc }, + // the end is the done marker: an object-mode stream can yield + // undefined + cond: { kind: "libCall", fn: "stream.chunkDone", args: [chunkRef], type: BOOL, loc }, then: [{ kind: "break", loc }], else_: null, loc, diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index bd5068d05..39b7207c0 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -7439,6 +7439,35 @@ function lowerObjectStaticCall( if (call.questionDotToken || access.questionDotToken) return null; if (!lowerer.isStdlibGlobal(access.expression, "Object")) return null; const member = access.name.text; + // `Object.getOwnPropertyDescriptor(Error, "stackTraceLimit")` (Effect's + // isStackTraceLimitWritable): Error's own writable, enumerable, + // configurable data property holding the current limit. The interned + // Error value models no other statics (builtinGlobalValueAt). + const [first, second] = call.arguments.nodes; + if ( + member === "getOwnPropertyDescriptor" && + call.arguments.nodes.length === 2 && + lowerer.isStdlibGlobal(first!, "Error") && + ts.isStringLiteralLike(second!) && + second.text === "stackTraceLimit" + ) { + const loc = locOf(call); + const dyn = (value: IrExpr): IrExpr => ({ kind: "dynFrom", value, type: DYN, loc }); + const yes = dyn({ kind: "boolLit", value: true, type: BOOL, loc }); + const limit: IrExpr = { kind: "libCall", fn: "error.stackLimitGet", args: [], type: F64, loc }; + return { + kind: "dynObjLit", + fields: [ + { key: { kind: "strLit", value: "value", type: STRING, loc }, value: dyn(limit) }, + ...["writable", "enumerable", "configurable"].map((key) => ({ + key: { kind: "strLit" as const, value: key, type: STRING, loc }, + value: yes, + })), + ], + type: DYN, + loc, + }; + } if ( (member === "getOwnPropertyNames" || member === "getOwnPropertySymbols" || diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index 2a772ae25..d8ccc15b7 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -158,7 +158,7 @@ import { classSymbolKeyOfSymbol, symbolFieldDisplayName, } from "./symbol-fields.js"; -import { lowerHttpAgentNew, lowerHttpServerNew } from "./lower-server.js"; +import { lowerHttpAgentNew, lowerHttpServerNew, lowerTlsSocketNew } from "./lower-server.js"; import { ambientNsRootOf, ambientUndefReadType, @@ -9836,6 +9836,12 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { const httpServer = lowerHttpServerNew(lowerer, expr); if (httpServer) return httpServer; } + // `new tls.TLSSocket(socket, options)` — the server-mode wrap of an open + // socket (lower-server). + { + const tlsSocket = lowerTlsSocketNew(lowerer, expr); + if (tlsSocket) return tlsSocket; + } // `new http.Agent(opts?)` / `new https.Agent(opts?)` — the Agent // handle (lower-server): getName/destroy/counters through the dyn // handle ops, requests thread it via the agent option. diff --git a/packages/compiler/src/frontend/lowering/lower-event-emitter.ts b/packages/compiler/src/frontend/lowering/lower-event-emitter.ts index f105af786..f44a3d6d8 100644 --- a/packages/compiler/src/frontend/lowering/lower-event-emitter.ts +++ b/packages/compiler/src/frontend/lowering/lower-event-emitter.ts @@ -1458,10 +1458,11 @@ export function lowerEmitterMethodCall( const receiver = lowerReceiver(); const once = member === "once" || member === "prependOnceListener"; const prepend = member === "prependListener" || member === "prependOnceListener"; - // Stream 'data' rides its own two-slot payload ABI (bytes + string — - // encoded streams deliver strings; scr_stream_emit_data): listeners - // on readable-sided receivers register through DATA thunks that - // unwrap the declared side (typed) or box by tag (dyn). + // Stream 'data' rides its own three-slot payload ABI (bytes, string — + // encoded streams deliver strings — and an object-mode value; + // scr_stream_emit_data): listeners on readable-sided receivers + // register through DATA thunks that unwrap the declared side, convert + // to the declared type, or take the value (dyn). const sides = streamSidesOf(lowerer, info); if (name === "data" && (sides === "r" || sides === "rw")) { const dynFlavor = dynListenerFlavor(lowerer, args[1]!); @@ -1500,16 +1501,25 @@ export function lowerEmitterMethodCall( (lowerer.unions.get(p.unionId)?.arms ?? []).every( (arm) => (arm.kind === "bytes" && arm.elem === "u8") || arm.kind === "string", ); + // Any other chunk type converts from the chunk's value (an + // object-mode stream's value, or the Buffer or string) when the + // listener runs, or throws a TypeError there. if ( p !== undefined && !(p.kind === "bytes" && p.elem === "u8") && p.kind !== "string" && - !chunkUnion + !chunkUnion && + (p.kind === "func" || + !canConvertToDyn( + p, + (id) => lowerer.shapes.get(id), + (id) => lowerer.unions.get(id), + )) ) { lowerer.noLowering( `a 'data' listener whose chunk parameter is '${lowerer.fmt(p)}'`, args[1]!, - "chunks are Buffers — or strings once setEncoding/the encoding option applies", + "declare the chunk as a type that converts from a checked-dynamic value", ); } if (!registering) { @@ -1654,8 +1664,10 @@ export function lowerEmitterMethodCall( loc, ); } - // Stream 'data' rides the two-slot payload ABI: a user emit fills - // the chunk's slot (bytes or string), NULLs the other. + // Stream 'data' rides the three-slot payload ABI: a user emit fills + // the chunk's slot (bytes or string) and NULLs the others; any other + // value is emitted as a checked-dynamic value, filling the slots by + // its tag. const emitSides = streamSidesOf(lowerer, info); if (name === "data" && (emitSides === "r" || emitSides === "rw")) { if (args.length !== 2) { @@ -1665,16 +1677,27 @@ export function lowerEmitterMethodCall( "the event carries one chunk (a Buffer or string)", ); } - const chunk = lowerer.lowerExpr(args[1]!); + let chunk = lowerer.lowerExpr(args[1]!); if ( !(chunk.type.kind === "bytes" && chunk.type.elem === "u8") && chunk.type.kind !== "string" ) { - lowerer.noLowering( - `emit('data') with a '${lowerer.fmt(chunk.type)}' chunk`, - args[1]!, - "chunks are Buffers or strings", - ); + if ( + chunk.type.kind === "func" || + (chunk.type.kind !== "dyn" && + !canConvertToDyn( + chunk.type, + (id) => lowerer.shapes.get(id), + (id) => lowerer.unions.get(id), + )) + ) { + lowerer.noLowering( + `emit('data') with a '${lowerer.fmt(chunk.type)}' chunk`, + args[1]!, + "the chunk is a value that converts to a checked-dynamic value", + ); + } + chunk = lowerer.coerceToExpected(chunk, DYN); } return { kind: "libCall", diff --git a/packages/compiler/src/frontend/lowering/lower-functions.ts b/packages/compiler/src/frontend/lowering/lower-functions.ts index 653c0d837..c1cca9c64 100644 --- a/packages/compiler/src/frontend/lowering/lower-functions.ts +++ b/packages/compiler/src/frontend/lowering/lower-functions.ts @@ -1,3 +1,4 @@ +import type { ScrDiagnostic } from "../../diagnostics/diagnostic.js"; import { declareTypedArguments } from "./typed-arguments.js"; import { dynUndefinedExpr, varRef } from "../../ir/build.js"; import { everyStmtList, transformStmtList } from "../../ir/traverse.js"; @@ -17,6 +18,7 @@ import { type IrLocal, type IrParam, type IrStmt, + type IrType, type SrcLoc, VOID, canBoxFuncIntoDyn, @@ -858,3 +860,79 @@ function isBufferCheckOf(e: IrExpr, slotId: string): boolean { e.type.elem === "u8" ); } + +/** The diagnostics of a TypeScript function whose signature did not + * compile, for the coverage remainder: its statements lower for their + * diagnostics only (the remainder's output is discarded), so the report + * lists them with the signature's blocker instead of hiding them behind it. + * Each parameter whose type maps is declared, and a use of any other + * parameter reports SC2004 back to the declaration. The statements already + * count as failed (countSkippedBody), so the pass leaves the stats alone. + * The pass adds diagnostics only: an internal error while it lowers drops + * what it found, and the signature's blocker stands alone as before. */ +export function blockedBodyDiagnostics( + lowerer: Lowerer, + decl: ts.FunctionDeclaration, +): ScrDiagnostic[] { + const body = decl.body; + if (!body || decl.typeParameters || decl.asteriskToken) return []; + if (isJsSourceFile(ts.getSourceFileOfNode(decl))) return []; + const symbol = declSymbolOf(lowerer, decl); + if (!symbol || lowerer.fnSigsBySymbol.has(symbol)) return []; + const diagsBefore = lowerer.diags.length; + const fencesBefore = lowerer.runtimeFences.length; + const suppressStats = lowerer.suppressStats; + lowerer.suppressStats = true; + let declared: IrType = DYN; + lowersCleanly(lowerer, () => { + declared = lowerer.declaredReturnType(decl, decl); + }); + const isAsync = decl.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) ?? false; + const ctx = newFnCtx(false, null, null, lowerer.bodyReturnType(isAsync, declared)); + ctx.isAsync = isAsync; + lowerer.fnStack.push(ctx); + let internal = false; + try { + // A declared `this` parameter gives the body its receiver, as + // lowerFunction declares it. + if (decl.parameters.nodes.some(isThisParameter)) declareCallReceiver(lowerer, decl, false); + for (const param of decl.parameters.nodes) { + if (isThisParameter(param)) continue; + const declaredParam = lowersCleanly(lowerer, () => { + lowerer.declareParams([param], lowerer.paramShapes([param])); + }); + if (declaredParam) continue; + const symbol = ts.isIdentifier(param.name) + ? lowerer.checker.getSymbolAtLocation(param.name) + : undefined; + if (symbol) lowerer.blockedBindings.add(symbol); + } + lowersCleanly(lowerer, () => { + lowerer.lowerStmts(body.statements.nodes); + }); + } catch (error) { + if (!(error instanceof InternalCompilerError)) throw error; + internal = true; + } finally { + lowerer.fnStack.pop(); + lowerer.suppressStats = suppressStats; + } + lowerer.runtimeFences.splice(fencesBefore); + const diags = lowerer.diags.splice(diagsBefore); + return internal || diags.some((d) => d.code === "SC9001") ? [] : diags; +} + +/** Runs `lower`, for blockedBodyDiagnostics: false when it poisons, with the + * diagnostics it recorded dropped (a failed mapping reports what the + * signature's blocker already reports). */ +function lowersCleanly(lowerer: Lowerer, lower: () => void): boolean { + const before = lowerer.diags.length; + try { + lower(); + return true; + } catch (error) { + if (!(error instanceof PoisonError)) throw error; + lowerer.diags.splice(before); + return false; + } +} diff --git a/packages/compiler/src/frontend/lowering/lower-modules.ts b/packages/compiler/src/frontend/lowering/lower-modules.ts index 5b78c7278..5238333ad 100644 --- a/packages/compiler/src/frontend/lowering/lower-modules.ts +++ b/packages/compiler/src/frontend/lowering/lower-modules.ts @@ -2929,6 +2929,38 @@ export function lowerDefaultExport(lowerer: Lowerer, stmt: ts.ExportAssignment): return { kind: "assign", localId: g.id, value, loc: locOf(stmt) }; } +/** Marks the runtime fences recorded since `fencesBefore` that lie in + * `sf`'s top-level code: they run when the module initializes. A fence + * inside a function, or inside a class member that runs later (a method, + * accessor, constructor or instance field initializer), runs when that + * code is called. */ +function markModuleInitFences(lowerer: Lowerer, sf: ts.SourceFile, fencesBefore: number): void { + for (let i = fencesBefore; i < lowerer.runtimeFences.length; i++) { + const fence = lowerer.runtimeFences[i]!; + if (fence.loc.file !== sf.fileName || runsAfterModuleInit(sf, fence.loc.start)) continue; + lowerer.runtimeFences[i] = { ...fence, startup: true }; + } +} + +/** Whether the code at `pos` sits in a function or a later-running class + * member body. */ +function runsAfterModuleInit(sf: ts.SourceFile, pos: number): boolean { + let later = false; + ts.walkPreorder(sf, (node) => { + if (node.pos > pos || node.end <= pos) return "skip"; + if ( + ts.isFunctionLike(node) || + (ts.isPropertyDeclaration(node) && + !node.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.StaticKeyword)) + ) { + later = true; + return "stop"; + } + return undefined; + }); + return later; +} + export function lowerFileInit( lowerer: Lowerer, sf: ts.SourceFile, @@ -2938,6 +2970,7 @@ export function lowerFileInit( const isAsync = lowerer.asyncInitFiles.has(sf); const ctx = newFnCtx(false, null, null, VOID); ctx.isAsync = isAsync; + const fencesBefore = lowerer.runtimeFences.length; lowerer.fnStack.push(ctx); try { const loc0: SrcLoc = { file: sf.fileName, start: 0, end: 0 }; @@ -3288,6 +3321,7 @@ export function lowerFileInit( }; } finally { lowerer.fnStack.pop(); + markModuleInitFences(lowerer, sf, fencesBefore); } } diff --git a/packages/compiler/src/frontend/lowering/lower-server.ts b/packages/compiler/src/frontend/lowering/lower-server.ts index 94d49d8b3..d9746e1f6 100644 --- a/packages/compiler/src/frontend/lowering/lower-server.ts +++ b/packages/compiler/src/frontend/lowering/lower-server.ts @@ -2939,6 +2939,26 @@ function lowerNetSocketMethodCall( loc, }; } + if (event === "secure") { + // The handshake's completion on a TLS socket (after secureConnect on + // a client); plain sockets never fire it. + const { cb } = lowerCallbackArg( + lowerer, + args[1]!, + "secure listeners", + 0, + () => false, + "use ()", + [], + ); + return { + kind: "libCall", + fn: "tls.sockOnSecure", + args: [receiver, cb, once], + type: VOID, + loc, + }; + } if (event === "session") { // The received-ticket event: fires once per TLS client socket with // the serialized session (a Buffer); plain sockets never fire it. @@ -2962,7 +2982,7 @@ function lowerNetSocketMethodCall( lowerer.noLowering( `socket.${name}(${event === null ? "non-literal event" : `"${event}"`}, ...)`, args[0]!, - '"data", "end", "close", "error", "connect", "lookup", "ready", "timeout", "readable", "secureConnect", and "session" are the supported socket events (as literals)', + '"data", "end", "close", "error", "connect", "lookup", "ready", "timeout", "readable", "secureConnect", "secure", and "session" are the supported socket events (as literals)', ); } lowerer.noLowering( @@ -4676,6 +4696,125 @@ export function lowerHttpServerNew(lowerer: Lowerer, expr: ts.NewExpression): Ir return lowerHttpCreateServerForms(lowerer, expr, "new http.Server", locOf(expr)); } +/** `new tls.TLSSocket(socket, options)` in server mode: a new TLS socket + * takes over the socket's connection and runs the server handshake + * (scr_tls_wrap_socket). The options record reads at runtime, the + * tls.connect precedent. A literal's secureContext member is a + * SecureContext handle, which the checked-dynamic record cannot hold, so + * it passes as its own argument; members after it must be free of side + * effects, because it is read after them. Node never starts the handshake + * of a client-mode TLSSocket, so a literal without isServer: true fences + * here. */ +export function lowerTlsSocketNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr | null { + const callee = expr.expression; + const isTlsSocket = + (ts.isPropertyAccessExpression(callee) && + callee.name.text === "TLSSocket" && + lowerer.builtinMemberOf(callee)?.module === "tls") || + (ts.isIdentifier(callee) && + lowerer.builtinImportOf(callee)?.module === "tls" && + lowerer.builtinImportOf(callee)?.member === "TLSSocket"); + if (!isTlsSocket) return null; + const loc = locOf(expr); + const HINT = + "the supported forms are new tls.TLSSocket(socket, { isServer: true, cert, key }) and new tls.TLSSocket(socket, { isServer: true, secureContext })"; + const args = expr.arguments?.nodes ?? []; + if (args.length !== 2 || args.some(ts.isSpreadElement)) { + lowerer.noLowering(`new tls.TLSSocket with ${args.length} argument(s)`, expr, HINT); + } + const socket = lowerer.lowerExpr(args[0]!); + if (socket.type.kind !== "netSocket") { + lowerer.noLowering( + `new tls.TLSSocket over a '${lowerer.fmt(socket.type)}' value`, + args[0]!, + "the wrapped value must be a net.Socket (a server connection or an http CONNECT socket)", + ); + } + let optsNode: ts.Expression = args[1]!; + while (ts.isParenthesizedExpression(optsNode)) optsNode = optsNode.expression; + if (!ts.isObjectLiteralExpression(optsNode)) { + const o = lowerer.lowerExpr(optsNode); + const opts: IrExpr = + o.type.kind === "dyn" + ? o + : canConvertToDyn( + o.type, + (id) => lowerer.shapes.get(id), + (id) => lowerer.unions.get(id), + ) + ? { kind: "dynFrom", value: o, type: DYN, loc } + : lowerer.noLowering( + `new tls.TLSSocket with a '${lowerer.fmt(o.type)}' options record`, + optsNode, + `${HINT} — a secureContext member lowers in an options object literal`, + ); + return { kind: "libCall", fn: "tls.wrapSocket", args: [socket, opts], type: NETSOCKET_T, loc }; + } + const props = optsNode.properties.nodes; + const nameOf = (p: ts.ObjectLiteralElementLike): string | null => + (ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p)) && + (ts.isIdentifier(p.name) || ts.isStringLiteral(p.name)) + ? p.name.text + : null; + const isServer = props.find((p) => nameOf(p) === "isServer"); + if ( + isServer === undefined || + (ts.isPropertyAssignment(isServer) && isServer.initializer.kind === ts.SyntaxKind.FalseKeyword) + ) { + lowerer.noLowering( + "new tls.TLSSocket in client mode", + optsNode, + "Node starts no handshake for a client-mode TLSSocket — pass isServer: true for the server side; tls.connect makes client sockets", + ); + } + const ctxIndex = props.findIndex((p) => nameOf(p) === "secureContext"); + if (ctxIndex < 0) { + const opts = lowerDynObjectLiteral(lowerer, optsNode); + return { kind: "libCall", fn: "tls.wrapSocket", args: [socket, opts], type: NETSOCKET_T, loc }; + } + const ctxProp = props[ctxIndex]!; + const ctxNode = ts.isShorthandPropertyAssignment(ctxProp) + ? ctxProp.name + : (ctxProp as ts.PropertyAssignment).initializer; + for (const p of props.slice(ctxIndex + 1)) { + const pure = + ts.isShorthandPropertyAssignment(p) || + (ts.isPropertyAssignment(p) && sideEffectFreeOptionValue(p.initializer)); + if (!pure) { + lowerer.noLowering( + "a new tls.TLSSocket options literal with side effects after secureContext", + p, + "secureContext is read after the other members — move it to the end of the object", + ); + } + } + let ctx: IrExpr | null = null; + const opts = lowerDynObjectLiteral(lowerer, optsNode, (node, value) => { + if (node !== ctxNode) return lowerer.coerceToExpected(value, DYN); + if (value.type.kind !== "secureCtx") { + lowerer.noLowering( + `a '${lowerer.fmt(value.type)}' secureContext`, + node, + "pass a tls.createSecureContext({ cert, key }) result", + ); + } + ctx = value; + return { + kind: "dynFrom", + value: { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc }, + type: DYN, + loc, + }; + }); + return { + kind: "libCall", + fn: "tls.wrapSocketCtx", + args: [socket, opts, ctx!], + type: NETSOCKET_T, + loc, + }; +} + /** Module-function calls on http import bindings: createServer([options][, * handler]) and the call-form Server (Node's constructor works without * `new` — test/parallel's http.Server(fn) spelling). The handler takes @@ -4985,14 +5124,14 @@ function lowerTlsServerOptionsOrDyn( * connect(options[, cb]) and connect(port[, host][, options][, cb]) with * a RUNTIME options record (the checked-dynamic JS lane's shape — a dyn * value, or a record that converts): the runtime walk implements - * port/host/rejectUnauthorized/ca/servername/ALPNProtocols/minVersion/maxVersion/timeout and throws the catchable + * port/host/socket/rejectUnauthorized/ca/servername/ALPNProtocols/minVersion/maxVersion/timeout and throws the catchable * fence for other documented members (the divergence-66 stance). The * callback (and 'secureConnect'/'connect' listeners) fires * post-handshake — Node's secureConnect timing. */ function lowerTlsConnectCall(lowerer: Lowerer, expr: ts.CallExpression, loc: SrcLoc): IrExpr { const args = expr.arguments.nodes; const FORMS_HINT = - "the supported forms are connect(options[, cb]) and connect(port[, host][, options][, cb]) — options implement port/host/rejectUnauthorized/ca/servername/ALPNProtocols/minVersion/maxVersion/timeout"; + "the supported forms are connect(options[, cb]) and connect(port[, host][, options][, cb]) — options implement port/host/socket/rejectUnauthorized/ca/servername/ALPNProtocols/minVersion/maxVersion/timeout"; if (args.length < 1 || args.length > 4 || args.some(ts.isSpreadElement)) { lowerer.noLowering(`tls.connect with ${args.length} arguments`, expr, FORMS_HINT); } @@ -5043,7 +5182,22 @@ function lowerTlsConnectCall(lowerer: Lowerer, expr: ts.CallExpression, loc: Src // The options value rides whole to the runtime walk: dyn passes // directly; a typed record that converts boxes into the checked-dynamic tree. let opts: IrExpr; - if (optsNode !== null) { + let literal: ts.Expression | null = optsNode; + while (literal !== null && ts.isParenthesizedExpression(literal)) literal = literal.expression; + if ( + literal !== null && + ts.isObjectLiteralExpression(literal) && + literal.properties.nodes.some( + (p) => + (ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p)) && + ts.isIdentifier(p.name) && + p.name.text === "socket", + ) + ) { + // A socket member is a handle the record form cannot box: the literal + // builds the checked-dynamic object member by member. + opts = lowerDynObjectLiteral(lowerer, literal); + } else if (optsNode !== null) { const o = lowerer.lowerExpr(optsNode); if (o.type.kind === "dyn") { opts = o; diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index 687420baf..d49c7643e 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -139,6 +139,8 @@ import { bindingGenericFnAliasInfoOf, bindingGenericFnInfoOf, bindingGenericFnNodeOf, + genericBindingPresenceStore, + genericBindingStoredValue, implicitLocalFnInfoOf, implicitLocalFnNodeOf, implicitMethodCallInfersReturn, @@ -3889,6 +3891,9 @@ function stdlibGlobalTokenDestructure( if (decl.initializer === undefined || !isJsSourceFile(ts.getSourceFileOfNode(decl))) return null; const globalName = stdlibGlobalNameOf(lowerer, decl.initializer); if (globalName === null) return null; + // `workerData` is a value (the data the worker was started with), not an + // API surface: its members destructure as any untyped value's do. + if (globalName === "workerData") return null; if (!ts.isObjectBindingPattern(decl.name)) { lowerer.unsupported( "SC1031", @@ -5113,8 +5118,19 @@ export function lowerVarDecl( { const gfnNode = bindingGenericFnNodeOf(decl) ?? bindingContextualGenericFnNodeOf(lowerer, decl); if (gfnNode) { - bindingGenericFnInfoOf(lowerer, decl, gfnNode); - return null; + const info = bindingGenericFnInfoOf(lowerer, decl, gfnNode); + // A nested function that lowered before this declaration read the + // binding as an ordinary TDZ box (predeclareForwardCapture); the + // declaration initializes it, so those reads see the function once + // it runs and a ReferenceError before. + const symbol = lowerer.checker.getSymbolAtLocation(decl.name); + const pre = symbol ? lowerer.ctx.tdzPredeclared.get(symbol) : undefined; + if (pre) { + lowerer.ctx.tdzPredeclared.delete(symbol!); + const value = genericBindingStoredValue(lowerer, decl, info); + return { kind: "assign", localId: pre.id, value, initializes: true, loc: locOf(decl) }; + } + return genericBindingPresenceStore(info, decl); } } @@ -11468,11 +11484,13 @@ function lowerForIn(lowerer: Lowerer, stmt: ts.ForInStatement): IrStmt { keysT, loc, ); + // The overflow map's hashed key set answers presence directly; a + // fresh key list per visit would make the walk quadratic. const guard = (kRef: IrExpr): IrExpr => ({ - kind: "arrIntrinsic", - method: "includes", - receiver: { kind: "recordOvfKeys", obj: recvRef(), shapeId, type: keysT, loc }, - args: [kRef], + kind: "recordOvfHas", + obj: recvRef(), + shapeId, + key: kRef, type: BOOL, loc, }); diff --git a/packages/compiler/src/frontend/lowering/lower-stream.ts b/packages/compiler/src/frontend/lowering/lower-stream.ts index d6f89dc11..6540b3a99 100644 --- a/packages/compiler/src/frontend/lowering/lower-stream.ts +++ b/packages/compiler/src/frontend/lowering/lower-stream.ts @@ -289,6 +289,7 @@ function lowerStreamCallback( node: ts.MethodDeclaration | ts.FunctionExpression | ts.ArrowFunction, thisType: IrType, fullTuple: IrType[], + chunkValues = true, ): IrExpr { const loc = locOf(node); const { shapes, funcType } = lowerer.lambdaSignature(node); @@ -308,6 +309,25 @@ function lowerStreamCallback( // boxes each position by kind (the completion callback arrives as a // callable dyn whose glue dispatches to the runtime's *_done). if (funcType.params[i]!.kind === "dyn") continue; + // A write or transform chunk of another type converts from the + // chunk's value when the callback runs (an object-mode side's value), + // or throws a TypeError there — on a side known to be byte mode the + // chunk is a Buffer, so the declared type must be Node's or a + // supertype of it (`Buffer | string`); an encoding of another type + // (`string | null | undefined`) converts from the encoding argument's + // value. + if ( + ((i === 0 && (chunkValues || widensInto(lowerer, fullTuple[0]!, funcType.params[0]!, loc))) || + i === 1) && + (which === "write" || which === "transform") && + funcType.params[i]!.kind !== "func" && + canConvertToDyn( + funcType.params[i]!, + (id) => lowerer.shapes.get(id), + (id) => lowerer.unions.get(id), + ) + ) + continue; if (!typeEquals(funcType.params[i]!, fullTuple[i]!)) { lowerer.noLowering( `a '${which}' callback whose parameter ${i} is '${lowerer.fmt(funcType.params[i]!)}' where Node passes '${lowerer.fmt(fullTuple[i]!)}'`, @@ -404,11 +424,66 @@ interface StreamOptions { * right after construction (Node's constructors call addAbortSignal * once the state exists), or null when absent. */ signal: IrExpr | null; + /** The objectMode option's sides (a Duplex's readableObjectMode and + * writableObjectMode set one side): applied with stream.objectMode + * right after construction. */ + readableObject: boolean; + writableObject: boolean; /** Present callbacks in canonical order with their flag bits. */ flags: number; cbs: IrExpr[]; } +/** The objectMode, readableObjectMode and writableObjectMode members of a + * stream options literal (boolean literals; the sided keys count only on + * a Duplex, Transform or PassThrough, as in Node). */ +function streamObjectModes( + lowerer: Lowerer, + ctorName: string, + optsLit: ts.ObjectLiteralExpression | undefined, +): { readable: boolean; writable: boolean } { + const out = { readable: false, writable: false }; + const duplexish = ctorName !== "Readable" && ctorName !== "Writable"; + for (const prop of optsLit?.properties?.nodes ?? []) { + if (!ts.isPropertyAssignment(prop)) continue; + const name = + ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name) ? prop.name.text : null; + if (name !== "objectMode" && name !== "readableObjectMode" && name !== "writableObjectMode") + continue; + const value = prop.initializer; + if (value.kind === ts.SyntaxKind.FalseKeyword) continue; + if (value.kind !== ts.SyntaxKind.TrueKeyword) { + lowerer.noLowering( + `${ctorName} with a runtime '${name}' value`, + prop, + `write '${name}: true' or '${name}: false'`, + ); + } + if (name === "objectMode") { + out.readable = true; + out.writable = true; + } else if (duplexish) { + if (name === "readableObjectMode") out.readable = true; + else out.writable = true; + } + } + return out; +} + +/** Whether an options literal sets the writable side's object mode with a + * `true` literal (objectMode, or writableObjectMode on a duplex stream). + * Reports nothing: streamObjectModes fences the other value forms. */ +function literalWritesValues(opts: ts.ObjectLiteralExpression, duplexish: boolean): boolean { + for (const prop of opts.properties?.nodes ?? []) { + if (!ts.isPropertyAssignment(prop)) continue; + const name = + ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name) ? prop.name.text : null; + if (name !== "objectMode" && !(duplexish && name === "writableObjectMode")) continue; + if (prop.initializer.kind !== ts.SyntaxKind.FalseKeyword) return true; + } + return false; +} + /** The full Node parameter tuples of the option callbacks, given the * concrete stream's `this` type. The trailing completion callbacks keep * whatever func type the checker contextually gave the user's parameter @@ -657,6 +732,8 @@ function parseStreamOptions( badEncoding: null, pushEncoding: null, signal: null, + readableObject: false, + writableObject: false, flags: 0, cbs: [], }; @@ -669,6 +746,18 @@ function parseStreamOptions( "the options must be an object literal, inline or through const-initialized bindings (the callbacks lower as compile-time closures)", ); } + // Read before the callbacks lower: an object-mode readable side keeps a + // completion callback's `data: any` (its values are not only strings and + // Buffers). An object-mode side's high-water mark defaults to 16. + const objectModes = streamObjectModes( + lowerer, + ctorName, + optsLit !== undefined && ts.isObjectLiteralExpression(optsLit) ? optsLit : undefined, + ); + out.readableObject = objectModes.readable; + out.writableObject = objectModes.writable; + if (out.readableObject) out.hwmR = numLit(16, loc); + if (out.writableObject) out.hwmW = numLit(16, loc); const tuples = cbTuples(lowerer); const present = new Map(); for (const prop of optsLit?.properties?.nodes ?? []) { @@ -715,7 +804,7 @@ function parseStreamOptions( continue; } const full = tuples[name]!.map((t) => (t === "fn" ? null : t)); - const pinned = pinAnyDataCompletion(lowerer, name, fnNode); + const pinned = out.readableObject ? null : pinAnyDataCompletion(lowerer, name, fnNode); try { // The completion-callback tail: typed by the checker contextually; // accept the user's declared func type as the tuple's entry so the @@ -732,7 +821,10 @@ function parseStreamOptions( } return declared ?? funcOf([], VOID); }); - present.set(name, lowerStreamCallback(lowerer, name, fnNode, thisType, tuple)); + present.set( + name, + lowerStreamCallback(lowerer, name, fnNode, thisType, tuple, out.writableObject), + ); } finally { if (pinned) lowerer.stageSourceTypes.delete(pinned); } @@ -776,16 +868,9 @@ function parseStreamOptions( break; case "objectMode": case "readableObjectMode": - case "writableObjectMode": { - // `objectMode: false` is the byte default — admit the literal. - if (value.kind === ts.SyntaxKind.FalseKeyword) break; - lowerer.noLowering( - `${ctorName} in objectMode`, - prop, - "object-mode streams have no static chunk type yet — chunks are Buffers (utf8 strings convert at push/write)", - ); + case "writableObjectMode": + // read up front (streamObjectModes) break; - } case "decodeStrings": { if (value.kind === ts.SyntaxKind.TrueKeyword) break; // Node's default lowerer.noLowering( @@ -942,6 +1027,8 @@ function streamCtorArgs( badEncoding: string | null; pushEncoding: string | null; signal: IrExpr | null; + readableObject: boolean; + writableObject: boolean; } { const shape = streamCtorShape(cls); const o = parseStreamOptions( @@ -971,6 +1058,8 @@ function streamCtorArgs( badEncoding: o.badEncoding, pushEncoding: o.pushEncoding, signal: o.signal, + readableObject: o.readableObject, + writableObject: o.writableObject, }; } @@ -1015,6 +1104,17 @@ export function lowerStreamNew(lowerer: Lowerer, expr: ts.NewExpression, info: C ); } let built: IrExpr = { kind: "libCall", fn, args: parsed.args, type: thisType, loc }; + if (parsed.readableObject || parsed.writableObject) { + // { objectMode }: applied before any other follow-up (answers the + // receiver +1). + built = { + kind: "libCall", + fn: "stream.objectMode", + args: [built, boolLit(parsed.readableObject, loc), boolLit(parsed.writableObject, loc)], + type: thisType, + loc, + }; + } if (parsed.pushEncoding !== null) { // { defaultEncoding }: chain the push-side decode default (the // setEncoding chaining shape — answers the receiver +1). @@ -1073,6 +1173,7 @@ function streamMethodWrapper( thisType: IrType, loc: SrcLoc, blame: ts.Node, + chunkValues = true, ): IrExpr | null { const found = lowerer.findMethodOn(info, methodName); if (!found) { @@ -1133,6 +1234,22 @@ function streamMethodWrapper( ); } } else if (!typeEquals(got.type, want)) { + // A write or transform encoding of another type (`string | null | + // undefined`) converts from the encoding argument's value: an + // object-mode write passes undefined, null or the name. + if ( + i === 1 && + (which === "write" || which === "transform") && + got.type.kind !== "func" && + canConvertToDyn( + got.type, + (id) => lowerer.shapes.get(id), + (id) => lowerer.unions.get(id), + ) + ) { + paramTypes.push(got.type); + continue; + } // A declared supertype of what Node passes (`chunk: Buffer | string`) // is sound: the wrapper takes Node's value and widens it into the // declared parameter. @@ -1140,6 +1257,23 @@ function streamMethodWrapper( paramTypes.push(want); continue; } + // A write or transform chunk of another type converts from the + // chunk's value when the method runs (an object-mode side's value), + // or throws a TypeError there; a byte-mode side's chunk is a Buffer. + if ( + i === 0 && + chunkValues && + (which === "write" || which === "transform") && + got.type.kind !== "func" && + canConvertToDyn( + got.type, + (id) => lowerer.shapes.get(id), + (id) => lowerer.unions.get(id), + ) + ) { + paramTypes.push(got.type); + continue; + } lowerer.noLowering( `a '${methodName}' method whose parameter ${i} is '${lowerer.fmt(got.type)}' where Node passes '${lowerer.fmt(want)}'`, blame, @@ -1233,10 +1367,22 @@ export function lowerStreamSuperCall( ); } const shape = streamCtorShape(cls); + // Whether the writable side holds values: options read at run time may + // set objectMode, and a literal says so with objectMode (or a Duplex's + // writableObjectMode) true; otherwise _write and _transform receive + // Buffers. + // super(undefined) and super(null) pass no options (followOptionsArg + // answers null), so their sides are byte mode too. + const optsNode = argNodes[0] !== undefined ? followOptionsArg(lowerer, argNodes[0]) : null; + const chunkValues = + optsNode !== null && + optsNode !== undefined && + (!ts.isObjectLiteralExpression(optsNode) || + literalWritesValues(optsNode, cls !== "%Readable" && cls !== "%Writable")); const fallback = (name: string): IrExpr | null => { const methodName = UNDERSCORE_METHODS.get(name); return methodName - ? streamMethodWrapper(lowerer, info, name, methodName, thisType, loc, blame) + ? streamMethodWrapper(lowerer, info, name, methodName, thisType, loc, blame, chunkValues) : null; }; const thisRef = (): IrExpr => ({ kind: "varRef", localId: thisLocal.id, type: thisType, loc }); @@ -1301,6 +1447,19 @@ export function lowerStreamSuperCall( loc, }, ]; + if (parsed.readableObject || parsed.writableObject) { + out.push({ + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "stream.objectMode", + args: [thisRef(), boolLit(parsed.readableObject, loc), boolLit(parsed.writableObject, loc)], + type: thisType, + loc, + }, + loc, + }); + } if (parsed.pushEncoding !== null) { out.push({ kind: "exprStmt", @@ -1602,7 +1761,17 @@ export function lowerStreamStaticCall( } const anySrc = srcT?.kind === "dyn" || - (srcT?.kind === "set" && typedElem(srcT.elem) !== null) || + // a Set or Map (entry arrays) of other values: the checked-dynamic + // view iterates the collection itself + ((srcT?.kind === "set" || srcT?.kind === "map") && + canConvertToDyn( + srcT, + (id) => lowerer.shapes.get(id), + (id) => lowerer.unions.get(id), + )) || + // an array of other values: the object-mode entries come from its + // checked-dynamic copy + (srcT?.kind === "array" && fromGenChunk(lowerer, srcT.elem) && typedElem(srcT.elem) === null) || (srcNode !== undefined && srcT === null && (lowerer.typeOf(srcNode).flags & ts.TypeFlags.Any) !== 0); @@ -1623,20 +1792,24 @@ export function lowerStreamStaticCall( lowerer.noLowering( `Readable.from over a '${srcT ? lowerer.fmt(srcT) : "?"}' source`, srcNode ?? call, - "the supported sources are a string or Buffer, and arrays, Sets, generators and class iterables of strings or Buffers", + "the supported sources are a string or Buffer, and arrays, Sets, Maps, generators and class iterables of values that convert to checked-dynamic values", ); } -/** Whether a generator's yields are stream chunks: strings, Buffers, a - * union of them (null included: the runtime destroys the stream with - * ERR_STREAM_NULL_VALUES), or checked-dynamic values (checked when - * pushed). */ +/** Whether a generator's yields can be stream chunks: values that box + * into checked-dynamic values (null included: the runtime destroys the + * stream with ERR_STREAM_NULL_VALUES). An object-mode stream takes any of + * them; a byte-mode one checks them when they are pushed or written. */ function fromGenChunk(lowerer: Lowerer, t: IrType): boolean { - const chunk = (a: IrType): boolean => - a.kind === "string" || (a.kind === "bytes" && a.elem === "u8") || a.kind === "nullT"; - if (t.kind === "dyn" || (chunk(t) && t.kind !== "nullT")) return true; - if (t.kind !== "union") return false; - return lowerer.unions.get(t.unionId)?.arms.every(chunk) ?? false; + if (t.kind === "nullT" || t.kind === "void" || t.kind === "func") return false; + return ( + t.kind === "dyn" || + canConvertToDyn( + t, + (id) => lowerer.shapes.get(id), + (id) => lowerer.unions.get(id), + ) + ); } /** The helpers Readable.from pulls a compiled generator through, as @@ -3587,14 +3760,6 @@ function lowerStreamOperator( throw new InternalCompilerError(`unknown stream operator ${member}`); } -/** The literal 'buffer' encoding on write()/end(): Node 24 takes it for a - * Buffer chunk and throws ERR_UNKNOWN_ENCODING for a string chunk. */ -function isBufferEncoding(lowerer: Lowerer, arg: ts.Expression | undefined): boolean { - if (arg === undefined) return false; - const t = lowerer.typeOf(arg); - return t.isStringLiteralType() && t.value === "buffer"; -} - /** The one supported encoding argument: utf8 (Node's default), as a * compile-time literal or absent. */ function checkUtf8Encoding(lowerer: Lowerer, member: string, arg: ts.Expression | undefined): void { @@ -3925,10 +4090,24 @@ export function lowerStreamMethodCall( if (probed?.type.kind === "dyn") return "dyn"; if (probed?.type.kind === "nullT") return "null"; } + // Any other value is a checked-dynamic chunk: one entry on an + // object-mode side, Node's chunk type error on a byte-mode one + // (undefined, which a byte-mode push ignores, included). + if (t?.kind === "void" || t?.kind === "undefinedT") return "dyn"; + if ( + t !== null && + t.kind !== "func" && + canConvertToDyn( + t, + (id) => lowerer.shapes.get(id), + (id) => lowerer.unions.get(id), + ) + ) + return "dyn"; lowerer.noLowering( `${member} with a '${t ? lowerer.fmt(t) : "?"}' chunk`, node, - "chunks are Buffers or utf8 strings — or unions of those with null (objectMode is not lowered)", + "chunks are values that convert to checked-dynamic values: Buffers, strings, numbers, records, arrays and class instances", ); }; @@ -3973,11 +4152,17 @@ export function lowerStreamMethodCall( checkUtf8Encoding(lowerer, member, args[1]); const kind = chunkKind(args[0]!); if ((kind === "null" || kind === "union" || kind === "dyn") && member === "unshift") { - lowerer.noLowering( - kind === "null" ? "unshift(null)" : "unshift with a nullable or dynamic chunk", - call, - "EOF signals through push(null)", - ); + // unshift(value): an object-mode stream's value at the front, null + // the end of the stream, as Node's readableAddChunk takes them + const receiver = lowerer.lowerExpr(access.expression); + const chunk = lowerer.lowerExprExpecting(args[0]!, DYN); + return { + kind: "libCall", + fn: "readable.unshiftDyn", + args: [receiver, chunk], + type: VOID, + loc, + }; } const receiver = lowerer.lowerExpr(access.expression); if (kind === "null") { @@ -4237,9 +4422,54 @@ export function lowerStreamMethodCall( } const kind = chunkKind(args[0]!); if (kind === "null") lowerer.noLowering("write(null)", call, "end() finishes a writable"); + const lowerWriteCb = (cbArg: ts.Expression): IrExpr => { + const cb = callbackDeclaresParameters(cbArg) + ? null + : lowerer.lowerExprExpecting(cbArg, funcOf([], VOID)); + if (cb === null || cb.type.kind !== "func" || cb.type.params.length > 0) { + lowerer.noLowering( + "write completion callbacks taking the error argument", + cbArg, + "() => void callbacks are supported (listen for 'error' to observe failures)", + ); + } + return cb!; + }; + // write(chunk, encoding[, cb]), and write(value, cb): the encoding + // argument reaches an object-mode side's _write as passed, null when + // the callback took its place (a byte-mode side encodes a string + // chunk with it). + const writeEnc = (encArg: ts.Expression | undefined, cbArg: ts.Expression | undefined) => { + const receiver = lowerer.lowerExpr(access.expression); + const chunk = lowerer.lowerExprExpecting(args[0]!, DYN); + const tail: IrExpr[] = []; + if (encArg !== undefined) tail.push(lowerer.lowerExprExpecting(encArg, DYN)); + if (cbArg !== undefined) tail.push(lowerWriteCb(cbArg)); + const flags = (encArg !== undefined ? 1 : 0) | (cbArg !== undefined ? 2 : 0); + return { + kind: "libCall", + fn: "writable.writeEnc", + args: [receiver, numLit(flags, loc), chunk, ...tail], + type: BOOL, + loc, + } satisfies IrExpr; + }; if (kind === "dyn") { - if (args.length > 1) - lowerer.noLowering("write with a dynamic chunk and more arguments", call); + if (args.length === 3) return writeEnc(args[1], args[2]); + if (args.length === 2) { + // Node's write takes a function there as the callback, anything + // else as the encoding + const t = lowerer.mapTypeOf(lowerer.typeOf(args[1]!)); + if (t?.kind === "func") return writeEnc(undefined, args[1]); + if (t === null || t === undefined || t.kind === "dyn") { + lowerer.noLowering( + "write with a dynamic second argument", + args[1]!, + "pass an encoding string or a completion callback", + ); + } + return writeEnc(args[1], undefined); + } const receiver = lowerer.lowerExpr(access.expression); const chunk = lowerer.lowerExprExpecting(args[0]!, DYN); return { kind: "libCall", fn: "writable.writeDyn", args: [receiver, chunk], type: BOOL, loc }; @@ -4263,19 +4493,15 @@ export function lowerStreamMethodCall( return { kind: "libCall", fn, args: [receiver, chunk], type: BOOL, loc }; } // Disambiguate (chunk, cb?) vs (chunk, encoding, cb?) by the second - // argument's static type. + // argument's static type. A write with an encoding runs through + // writeEnc, whose runtime checks and applies it as Node's _write does. let cbArg: ts.Expression | undefined; - if (args.length >= 2 && isBufferEncoding(lowerer, args[1])) { - if (kind !== "bytes") - return nodeThrowExpr(1, "ERR_UNKNOWN_ENCODING", "Unknown encoding: buffer", BOOL, loc); - cbArg = args[2]; - } else if (args.length === 2) { + if (args.length === 3) return writeEnc(args[1], args[2]); + if (args.length === 2) { const t = lowerer.mapTypeOf(lowerer.typeOf(args[1]!)); - if (t?.kind === "string") checkUtf8Encoding(lowerer, "write", args[1]); - else cbArg = args[1]; - } else if (args.length === 3) { - checkUtf8Encoding(lowerer, "write", args[1]); - cbArg = args[2]; + if (t?.kind === "string" || t?.kind === "nullT" || t?.kind === "undefinedT") + return writeEnc(args[1], undefined); + cbArg = args[1]; } const receiver = lowerer.lowerExpr(access.expression); const chunk = @@ -4284,16 +4510,7 @@ export function lowerStreamMethodCall( : lowerer.lowerExprExpecting(args[0]!, STRING); const fn = kind === "bytes" ? "writable.write" : "writable.writeStr"; if (cbArg !== undefined) { - const cb = callbackDeclaresParameters(cbArg) - ? null - : lowerer.lowerExprExpecting(cbArg, funcOf([], VOID)); - if (cb === null || cb.type.kind !== "func" || cb.type.params.length > 0) { - lowerer.noLowering( - "write completion callbacks taking the error argument", - cbArg, - "() => void callbacks are supported (listen for 'error' to observe failures)", - ); - } + const cb = lowerWriteCb(cbArg); return { kind: "libCall", fn, args: [receiver, chunk, cb], type: BOOL, loc }; } return { kind: "libCall", fn, args: [receiver, chunk], type: BOOL, loc }; @@ -4305,34 +4522,29 @@ export function lowerStreamMethodCall( // Forms: (), (cb), (chunk), (chunk, cb), (chunk, encoding, cb). let chunkNode: ts.Expression | undefined; let cbArg: ts.Expression | undefined; + let encNode: ts.Expression | undefined; if (args.length === 1) { const t = lowerer.mapTypeOf(lowerer.typeOf(args[0]!)); if (t?.kind === "func") cbArg = args[0]; else chunkNode = args[0]; - } else if (args.length >= 2 && isBufferEncoding(lowerer, args[1])) { - chunkNode = args[0]; - const endKind = chunkKind(args[0]!); - if (endKind === "string") { - const endT = lowerer.mapTypeOf(lowerer.typeOf(call)) ?? DYN; - return nodeThrowExpr(1, "ERR_UNKNOWN_ENCODING", "Unknown encoding: buffer", endT, loc); - } - if (endKind !== "bytes") checkUtf8Encoding(lowerer, "end", args[1]); - cbArg = args[2]; } else if (args.length === 2) { chunkNode = args[0]; const t = lowerer.mapTypeOf(lowerer.typeOf(args[1]!)); - if (t?.kind === "string") checkUtf8Encoding(lowerer, "end", args[1]); + if (t?.kind === "string" || t?.kind === "nullT" || t?.kind === "undefinedT") + encNode = args[1]; else cbArg = args[1]; } else if (args.length === 3) { chunkNode = args[0]; - checkUtf8Encoding(lowerer, "end", args[1]); + encNode = args[1]; cbArg = args[2]; } const receiver = lowerer.lowerExpr(access.expression); let flags = 0; const tail: IrExpr[] = []; if (chunkNode !== undefined) { - const kind = chunkKind(chunkNode); + // end(chunk, encoding[, cb]) writes the chunk with the encoding + // argument as passed (flags 16), through the dyn write + const kind = encNode !== undefined ? "dyn" : chunkKind(chunkNode); if (kind === "null" || kind === "union") lowerer.noLowering("end(null)", call); flags |= kind === "bytes" ? 1 : kind === "dyn" ? 8 : 2; tail.push( @@ -4342,6 +4554,10 @@ export function lowerStreamMethodCall( ? lowerer.lowerExprExpecting(chunkNode, DYN) : lowerer.lowerExprExpecting(chunkNode, STRING), ); + if (encNode !== undefined) { + flags |= 16; + tail.push(lowerer.lowerExprExpecting(encNode, DYN)); + } } if (cbArg !== undefined) { const cb = callbackDeclaresParameters(cbArg) @@ -4604,8 +4820,7 @@ export function lowerStreamProperty( return propCall(BOOL); } if (name === "writableObjectMode" && writableSide) { - // objectMode construction fences, so the answer is compile-time false. - return boolLit(false, loc); + return propCall(BOOL); } if (name === "allowHalfOpen" && sides === "rw") { return propCall(BOOL); diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index edf0a694b..ae91a87d5 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -61,6 +61,7 @@ import { encodeBase64 } from "../../wasm/base64.js"; import { resolveRequireRuntime } from "../runtime-resolve.js"; import { lowerKeyObjectCall } from "./builtins/key-object.js"; import { isEcdhType, lowerEcdhCall } from "./builtins/ecdh.js"; +import { isDiffieHellmanType, lowerDiffieHellmanCall } from "./builtins/dh.js"; import { fenceWebCryptoMember, lowerWebCryptoCall } from "./builtins/webcrypto.js"; import { valueStreamOf } from "./builtins/hash-stream.js"; import { lowerNetAddressCall, lowerNetAddressNew } from "./builtins/net-address.js"; @@ -180,7 +181,7 @@ export { newFnCtx, type FnCtx } from "./function-context.js"; * - Lexical scoping is resolved here: locals get function-unique ids * ("x.0", "x.1" for shadowing); the IR is scope-flat. */ -import { dirname, resolve } from "node:path"; +import { dirname, relative, resolve } from "node:path"; import { bindingSource } from "../binding-source.js"; import { tsgoPath } from "../dts-paths.js"; import * as ts from "../ts7/adapter.js"; @@ -444,6 +445,7 @@ import { lowerNestedFunctionDecl, lowerLambda, lowerFunction, + blockedBodyDiagnostics, } from "./lower-functions.js"; import { lowerArrayMethodCall } from "./containers/array-methods.js"; import { type ArrayHofInfo, fuseArrayPipelines } from "./containers/array-pipelines.js"; @@ -1346,6 +1348,7 @@ export function lowerToIr( // travel with them: a failed statement is never left unexplained. const covered: LowerResult = { ...result, + diagnostics: [...result.diagnostics, ...remainder.reachedBlockedBodyDiagnostics], stats: { ...result.stats, statementsDynamic }, unreached: { diagnostics: rem.diagnostics, @@ -1914,6 +1917,9 @@ export class Lowerer { * pinned values resolve through genericFnOf exactly like top-level * generic function declarations (bindingGenericFnInfoOf). */ readonly bindingGenericFns = new Map(); + /** Identifiers by spelling, per file, for the generic bindings' use + * ordering (runsAfterDeclaration). */ + readonly bindingIdentifiers = new Map>(); /** Per-symbol result of the never-reassigned file scan * (bindingNeverReassigned — object-literal generic-method receivers). */ readonly neverReassignedCache = new Map(); @@ -3188,12 +3194,14 @@ export class Lowerer { errorPropertyDispatchRevision: string | null = null; /** Structural source positions (structural-positions.ts) for names that * stay the same when unrelated code moves. */ - /** Default library files are named by their base name: two checkers - * install them in different directories. */ + /** Files are named by their path relative to the entry's directory, so + * names do not depend on where the program sits, and default library + * files by their base name: two checkers install them in different + * directories. */ readonly positions = new StructuralPositions((sf) => this.program.isSourceFileDefaultLibrary(sf) ? `lib:${sf.fileName.slice(sf.fileName.lastIndexOf("/") + 1)}` - : sf.fileName, + : relative(dirname(this.entry.fileName), sf.fileName), ); /** The outermost node whose token span is exactly `loc`, or null. */ @@ -3740,9 +3748,9 @@ export class Lowerer { * is `unknown` or `object` (also with `null` or `undefined`, or an array * of those), names checked protocol identity, is an owned runtime * handle (a Worker or MessagePort, a fetch Headers/Request/Response, or - * a node:crypto ECDH), or is a WeakMap/WeakSet (native weak collections - * live in checked-dynamic storage, as locals of those types do); `any` - * fields stay refused. */ + * a node:crypto ECDH, DiffieHellman or DiffieHellmanGroup), or is a + * WeakMap/WeakSet (native weak collections live in checked-dynamic + * storage, as locals of those types do); `any` fields stay refused. */ classFieldStoresDyn(type: ts.Type): boolean { return ( this.opaqueValueType(type) || @@ -3750,7 +3758,10 @@ export class Lowerer { isWorkerHandleType(type, this.typeCtx) || isFetchHandleType(type, this.typeCtx) || this.weakCollectionType(type) || - this.stdlibHandleType(type, (part) => isEcdhType(this, part)) + this.stdlibHandleType( + type, + (part) => isEcdhType(this, part) || isDiffieHellmanType(this, part), + ) ); } @@ -6612,7 +6623,10 @@ export class Lowerer { node.expression.name.text, node.arguments.nodes.length >= 2, ); - const callback = node.arguments.nodes[0]; + // `xs.map(((x) => x))`: the parenthesized callback is the arrow. + let callback = node.arguments.nodes[0]; + while (callback !== undefined && ts.isParenthesizedExpression(callback)) + callback = callback.expression; if (callbackIndices !== null && callback !== undefined && !ts.isSpreadElement(callback)) { const receiverNode = node.expression.expression; const receiverTs = this.typeOf(receiverNode); @@ -6671,13 +6685,26 @@ export class Lowerer { presentSites.set(node, elementsPresent && !reduce); if (elementsPresent && reduce && retypeInlineCallback(callback, callbackIndices)) changed = true; - if ( - (method === "reduce" || method === "reduceRight") && - ((!elementsPresent && node.arguments.nodes.length < 2) || - callbackReturnsOptional(callback)) - ) { - if (promoteHofCallback(callback, [0, 1])) changed = true; - const result = this.runtimeOptionalType(this.irTypeOf(node)); + // The accumulator holds undefined when the callback can return + // it, when an initial value can be missing, or when elements can + // be absent and there is no initial value. It holds another + // primitive when the callback's result can be one at run time + // (`missing[0] ?? "fallback"`). + const initial = node.arguments.nodes[1]; + const optional = + (!elementsPresent && initial === undefined) || + (initial !== undefined && !ts.isSpreadElement(initial) && mayBeOptional(initial)) || + callbackReturnsOptional(callback); + const arithmetic = + reduce && (ts.isArrowFunction(callback) || ts.isFunctionExpression(callback)) + ? lambdaArithmeticReturn(callback) + : null; + if (reduce && (optional || arithmetic !== null)) { + if (optional && promoteHofCallback(callback, [0, 1])) changed = true; + let result = this.irTypeOf(node); + if (arithmetic !== null) + result = joinPrimitiveTypes([result, arithmetic]) ?? result; + if (optional) result = this.runtimeOptionalType(result); siteDependencies?.read(node); const previous = this.runtimeOptionalReduceTypes.get(node); if (!previous || !typeEquals(previous, result)) { @@ -7437,7 +7464,10 @@ export class Lowerer { if (sig && !this.wantBody(sig.name)) continue; const fn = this.lowerFunction(decl); if (fn) functions.push(fn); - else if (this.countsSkips()) this.countSkippedBody(decl); + else if (this.countsSkips()) { + this.countSkippedBody(decl); + if (this.remainder) this.reportBlockedBody(decl); + } } for (const decl of fp.classDecls) { const info = this.classes.get(this.classNamer(decl)); @@ -7768,6 +7798,19 @@ export class Lowerer { } } + /** Coverage remainder: the body diagnostics of a function whose + * signature blocked (blockedBodyDiagnostics). A function reachable emit + * reached (its signature's diagnostics flushed there) reports them with + * the reached diagnostics; any other reports them as unreached. */ + readonly reachedBlockedBodyDiagnostics: ScrDiagnostic[] = []; + private reportBlockedBody(decl: ts.FunctionDeclaration): void { + const diags = blockedBodyDiagnostics(this, decl); + const symbol = declSymbolOf(this, decl); + if (symbol && this.alreadyFlushed.has(symbol)) + this.reachedBlockedBodyDiagnostics.push(...diags); + else for (const d of diags) this.pushDiag(d); + } + /** Whether run() counts a signature-blocked declaration in * stats.functionsSkipped: whole-program passes and the coverage * remainder do; an externally-gated emit pass leaves the counting to the @@ -8224,13 +8267,17 @@ export class Lowerer { for (;;) { drainInstances(); const declared = unitsInOrder(); - const initializerChanged = this.deferredModuleInitializers.process(this, [ - ...declared, - ...initFunctions, - ...instanceFunctions, - ...byName(this.liftedFns), - ...byName(this.implicitFns), - ]); + const initializerChanged = this.deferredModuleInitializers.process( + this, + [ + ...declared, + ...initFunctions, + ...instanceFunctions, + ...byName(this.liftedFns), + ...byName(this.implicitFns), + ], + (fn) => classDispatch.sites.forget(fn), + ); const dispatchChanged = classDispatch.process(this, [ ...declared, ...initFunctions, @@ -12905,6 +12952,7 @@ export class Lowerer { lowerWebCryptoCall(this, expr) ?? lowerKeyObjectCall(this, expr) ?? lowerEcdhCall(this, expr) ?? + lowerDiffieHellmanCall(this, expr) ?? lowerNetAddressCall(this, expr) ?? lowerFetchElementMethodCall(this, expr) ?? lowerDynamicImportCall(this, expr) ?? diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts index bb9a6c062..cb16fd257 100644 --- a/packages/compiler/src/frontend/lowering/surfaces.ts +++ b/packages/compiler/src/frontend/lowering/surfaces.ts @@ -1553,6 +1553,16 @@ export const BUILTIN_MODULE_FNS: Record< createSecretKey: { fn: "crypto.createSecretKey", params: [DYN, DYN], result: DYN }, generateKeySync: { fn: "crypto.generateKeySync", params: [DYN, DYN], result: DYN }, generateKey: { fn: "crypto.generateKeyCb", params: [], result: VOID }, + // Asymmetric keys and signatures (lowerPkiCall): checked-dynamic + // arguments, results converted to the call site's type. + createPrivateKey: { fn: "crypto.createPrivateKey", params: [DYN], result: DYN }, + createPublicKey: { fn: "crypto.createPublicKey", params: [DYN], result: DYN }, + createSign: { fn: "crypto.createSign", params: [DYN], result: DYN }, + createVerify: { fn: "crypto.createVerify", params: [DYN], result: DYN }, + generateKeyPairSync: { fn: "crypto.generateKeyPairSync", params: [], result: DYN }, + generateKeyPair: { fn: "crypto.generateKeyPairCb", params: [], result: VOID }, + sign: { fn: "crypto.signOneshot", params: [], result: DYN }, + verify: { fn: "crypto.verifyOneshot", params: [], result: DYN }, pbkdf2Sync: { fn: "crypto.pbkdf2", params: [BYTES_U8, BYTES_U8, F64, F64, STRING], @@ -1574,6 +1584,18 @@ export const BUILTIN_MODULE_FNS: Record< // A checked-dynamic curve name; the result is a native ECDH handle // (lowerCreateEcdh). createECDH: { fn: "crypto.ecdhNew", params: [DYN], result: DYN }, + // Checked-dynamic arguments; the result is a native DiffieHellman or + // DiffieHellmanGroup handle (lowerCreateDiffieHellman and + // lowerGetDiffieHellman). + createDiffieHellman: { fn: "crypto.dhNew", params: [DYN, DYN, DYN, DYN], result: DYN }, + getDiffieHellman: { fn: "crypto.dhGroupNew", params: [DYN], result: DYN }, + createDiffieHellmanGroup: { fn: "crypto.dhGroupNew", params: [DYN], result: DYN }, + // Prime checks and generation (lowerPrimeCall): checked-dynamic + // arguments, results converted to the call site's type. + checkPrimeSync: { fn: "crypto.checkPrimeSync", params: [], result: DYN }, + checkPrime: { fn: "crypto.checkPrimeCb", params: [], result: VOID }, + generatePrimeSync: { fn: "crypto.generatePrimeSync", params: [], result: DYN }, + generatePrime: { fn: "crypto.generatePrimeCb", params: [], result: VOID }, }, zlib: { // The default-options convenience family is special-cased in @@ -2211,18 +2233,20 @@ export const AMBIENT_SURFACE_FNS: readonly AmbientSurfaceRow[] = [ kind: "stdlib", name: "globalThis.crypto.subtle", fns: [ - "webcrypto.importKey", "webcrypto.generateKey", + "webcrypto.importKey", "webcrypto.exportKey", "webcrypto.encrypt", "webcrypto.decrypt", + "webcrypto.wrapKey", + "webcrypto.unwrapKey", "webcrypto.deriveBits", "webcrypto.deriveKey", "webcrypto.sign", "webcrypto.verify", "webcrypto.digest", ], - note: "importKey, generateKey and exportKey (raw and jwk) for HMAC and AES keys, importKey for PBKDF2 and HKDF keys, AES-GCM, AES-CBC and AES-CTR encrypt and decrypt, PBKDF2 and HKDF deriveBits and deriveKey, HMAC sign and verify, and SHA-1/SHA-2 digest; other methods, algorithms and key formats are refused", + note: "importKey, generateKey and exportKey (raw and jwk) for HMAC and AES keys, importKey for PBKDF2 and HKDF keys, AES-GCM, AES-CBC and AES-CTR encrypt and decrypt, wrapKey and unwrapKey of secret keys with AES-KW or those ciphers, PBKDF2 and HKDF deriveBits and deriveKey, HMAC sign and verify, generateKey, importKey, exportKey, sign and verify for ECDSA, Ed25519, RSASSA-PKCS1-v1_5 and RSA-PSS, and SHA-1/SHA-2 digest; other methods, algorithms and key formats are refused", }, { id: "stdlib.webcrypto.CryptoKey", @@ -3067,35 +3091,22 @@ export const BUILTIN_MODULE_FENCE_HINTS: Record< }, crypto: { ...Object.fromEntries( - [ - "generateKeyPair", - "generateKeyPairSync", - "createPrivateKey", - "createPublicKey", - "createSign", - "createVerify", - "sign", - "verify", - "createDiffieHellman", - "createDiffieHellmanGroup", - "getDiffieHellman", - "diffieHellman", - "publicEncrypt", - "publicDecrypt", - "privateEncrypt", - "privateDecrypt", - ].map((m) => [ + ["publicEncrypt", "publicDecrypt", "privateEncrypt", "privateDecrypt"].map((m) => [ m, - "asymmetric-key operations need a public-key stack (bignum, RSA/EC/EdDSA math) and " + - "public and private KeyObjects — neither exists in the static runtime, so no faithful lowering " + - "can be small; the lowered crypto surface is hashing, randomness, secret KeyObjects, and the introspection statics", + "RSA encryption has no lowering — the asymmetric crypto surface is " + + "RSA and EC KeyObjects (generateKeyPair, createPrivateKey, createPublicKey), sign, verify, " + + "createSign, createVerify, createECDH, and createDiffieHellman", ]), ), + diffieHellman: + "crypto.diffieHellman takes DH, EC or X25519 KeyObjects and has no lowering; " + + "the objects of createDiffieHellman, getDiffieHellman and createECDH compute shared secrets", ...Object.fromEntries( ["getCipherInfo"].map((m) => [ m, "cipher metadata has no lowering — createCipheriv and createDecipheriv support " + - "aes-128/192/256 in cbc, ctr, and gcm modes and chacha20-poly1305", + "aes-128/192/256 in cbc, ctr, gcm, ecb, cfb, cfb8, cfb1, and ofb modes, des-ede3 and " + + "des-ede, chacha20, and chacha20-poly1305", ]), ), setFips: diff --git a/packages/compiler/src/frontend/program.ts b/packages/compiler/src/frontend/program.ts index e398e59df..507d3392d 100644 --- a/packages/compiler/src/frontend/program.ts +++ b/packages/compiler/src/frontend/program.ts @@ -140,6 +140,15 @@ import { } from "./type-only-imports.js"; import { moduleSourceCandidates } from "./module-source-candidates.js"; import { inferredJsDiagnosticSuppressed } from "./inferred-js-diagnostics.js"; +import { + containsJsx, + firstJsx, + jsxEmitName, + jsxEmitPlan, + jsxPragmas, + lowerJsx, + type LoweredJsx, +} from "./jsx-source.js"; const BASE_OPTIONS: ts.Ts7CompilerOptions = { strict: true, @@ -377,6 +386,18 @@ export interface LoadResult { * checkPreflight. */ startupCrash?: StartupCrash | null; configDiags: ScrDiagnostic[]; + /** The files whose JSX the program lowers (jsx-source.ts), by file name. + * The program holds their rewritten text; positions in diagnostics refer + * to it. */ + jsxSources?: ReadonlyMap; + /** With JSX sources: the program over the original text, whose TypeScript + * errors are the program's, and its project world when it has errors. */ + tscWorld?: { + program: ts.Program; + /** The program's pre-emit diagnostics, computed once. */ + diagnostics: readonly ts.Diagnostic[]; + projectWorld: ts.Program | null; + }; /** How scriptc's type world differs from the project's own tsc setup — * used only to label the TypeScript errors those differences cause. */ typeWorld?: TypeWorldDifferences; @@ -778,6 +799,47 @@ function moduleSpecifierKind7(specifier: ts.StringLiteralLike): "import" | "requ : "import"; } +/** The program's files with JSX, lowered with the project's jsx settings + * and each file's pragmas, or the refusal of a setting that emits no calls. + * TypeScript files (.ts, .mts, .cts) cannot hold JSX; without a jsx setting + * the checker already rejects any JSX (TS17004). */ +function lowerProgramJsx7( + program: ts.Program, + options: ts.Ts7CompilerOptions, +): { sources: Map; refusal: ScrDiagnostic | null } { + const sources = new Map(); + const mode = jsxEmitName(options.jsx as string | number | undefined); + if (mode === undefined) return { sources, refusal: null }; + for (const sf of program.getImplementationSourceFiles()) { + if (sf.isDeclarationFile || /\.[mc]?ts$/.test(sf.fileName) || !containsJsx(sf)) continue; + const at = firstJsx(sf)!; + const loc = { file: sf.fileName, start: ts.tokenStartOfNode(at), end: at.end }; + if (mode === "preserve" || mode === "react-native") { + return { + sources, + refusal: { + code: "SC1090", + message: `JSX with "jsx": "${mode}" is not supported: that setting keeps JSX in the output instead of lowering it to calls`, + loc, + hint: 'set "jsx" to "react", "react-jsx" or "react-jsxdev" in the project tsconfig', + }, + }; + } + const plan = jsxEmitPlan( + { + jsx: mode, + jsxFactory: options.jsxFactory as string | undefined, + jsxFragmentFactory: options.jsxFragmentFactory as string | undefined, + jsxImportSource: options.jsxImportSource as string | undefined, + reactNamespace: options.reactNamespace as string | undefined, + }, + jsxPragmas(sf.text), + ); + sources.set(sf.fileName, lowerJsx(sf, plan)); + } + return { sources, refusal: null }; +} + function loadProgram7( host: ts.Ts7Host, entryPath: string, @@ -859,6 +921,26 @@ function loadProgram7( }; program = host.createProgram([...programRoots, overridesDtsPath()], options); } + // JSX: the program above checks the files as written; the program the + // lowering compiles holds them with their JSX lowered. + const jsx = lowerProgramJsx7(program, options); + let tscWorld: LoadResult["tscWorld"]; + if (jsx.refusal !== null) { + config.diags.push(jsx.refusal); + } else if (jsx.sources.size > 0) { + const original = program; + // The project world must read the original text too, so it is built + // before the rewritten files are served, and only when it is needed. + const diagnostics = ts.getPreEmitDiagnostics(original); + const errors = diagnostics.some((d) => d.category === ts.DiagnosticCategory.Error); + tscWorld = { + program: original, + diagnostics, + projectWorld: errors ? host.createProgram(programRoots, options) : null, + }; + for (const [file, lowered] of jsx.sources) host.addVirtualFile(file, lowered.text); + program = host.createProgram([...programRoots, overridesDtsPath()], options); + } const entry = program.getSourceFile(entryPath); if (!entry) throw new Error(`could not load ${entryPath}`); const externalTypeSpecifiersByFile = externalTypeFileClosure7(program, externalTypes); @@ -876,10 +958,14 @@ function loadProgram7( }, externalTypes, externalTypeSpecifiersByFile, + ...(tscWorld === undefined ? {} : { jsxSources: jsx.sources }), + ...(tscWorld === undefined ? {} : { tscWorld }), projectWorld: () => (projectWorld ??= host.createProgram(programRoots, options)), disposeAll: () => { try { projectWorld?.dispose(); + tscWorld?.projectWorld?.dispose(); + tscWorld?.program.dispose(); } finally { program.dispose(); } @@ -4665,8 +4751,8 @@ function preflight7(load: LoadResult): { const sf = p.getSourceFile(d.fileName); return sf !== undefined && isCheckedJsSourceFile(sf, projectCheckJs); }; - const errorsOf = (p: ts.Program): ts.Diagnostic[] => { - const all = ts.getPreEmitDiagnostics(p); + const errorsOf = (p: ts.Program): ts.Diagnostic[] => keptErrors(p, ts.getPreEmitDiagnostics(p)); + const keptErrors = (p: ts.Program, all: readonly ts.Diagnostic[]): ts.Diagnostic[] => { // First pass: every comment-side 2300's (file, name) — the partners // the second pass forgives. const commentDup = new Set(); @@ -4706,9 +4792,39 @@ function preflight7(load: LoadResult): { // instead (the SC1100 checked-cast family). Dirty there → the project- // world errors are the ones reported: they are reproducible with the // project's own tsc, which is what "fix type errors first" asks for. - const tscErrors = errorsOf(program); + // With JSX sources, the program over the original text answers the tsc + // gate; its positions move into the rewritten text the program holds. + // Errors only the rewritten text has come from the calls the JSX lowers + // to (a factory whose signature does not accept them). + const tscWorld = load.tscWorld; + const tscErrors = + tscWorld === undefined ? errorsOf(program) : keptErrors(tscWorld.program, tscWorld.diagnostics); if (tscErrors.length > 0) { - for (const d of errorsOf(load.projectWorld())) diags.push(toPassthrough(d)); + const world = + tscWorld === undefined ? load.projectWorld() : (tscWorld.projectWorld ?? tscWorld.program); + for (const d of errorsOf(world)) { + const diag = toPassthrough(d); + const lowered = tscWorld === undefined ? undefined : load.jsxSources?.get(diag.loc.file); + if (lowered !== undefined) { + diag.loc = { + ...diag.loc, + start: lowered.rewrittenPosition(diag.loc.start, false), + end: lowered.rewrittenPosition(diag.loc.end, true), + }; + } + diags.push(diag); + } + } else if (load.jsxSources !== undefined) { + for (const file of load.jsxSources.keys()) { + const sf = program.getSourceFile(file); + if (sf === undefined) continue; + const own = [...program.getSyntacticDiagnostics(sf), ...program.getSemanticDiagnostics(sf)]; + for (const d of keptErrors(program, own)) { + const diag = toPassthrough(d); + diag.message = `${diag.message} (in the call this JSX lowers to)`; + diags.push(diag); + } + } } // Preflight and lowering share this CheckerFacade. Claim executable diff --git a/packages/compiler/src/frontend/token-equivalence.test.ts b/packages/compiler/src/frontend/token-equivalence.test.ts index 18bfb32e6..880a07221 100644 --- a/packages/compiler/src/frontend/token-equivalence.test.ts +++ b/packages/compiler/src/frontend/token-equivalence.test.ts @@ -37,8 +37,10 @@ test("comment-like text inside regular expressions is code", () => { ).toBe(false); }); -test("a regular expression can start after a statement that ends at its keyword or label", () => { +test("a / after a line break is never equivalent", () => { const loop = (body: string) => `a: for (;;) {\n${body}\n}\n`; + // Automatic semicolon insertion decides whether these start a regular + // expression. expect( equivalent(loop("break\n/[//]/.test(x); c = 1;"), loop("break\n/[//]/.test(x); c = 2;")), ).toBe(false); @@ -46,12 +48,34 @@ test("a regular expression can start after a statement that ends at its keyword equivalent(loop("break a\n/[//]/.test(x); c = 1;"), loop("break a\n/[//]/.test(x); c = 2;")), ).toBe(false); expect( - equivalent("debugger\n/[//]/.test(x); c = 1;\n", "debugger\n/[//]/.test(x); c = 2;\n"), + equivalent("type A = B\n/[//]/.test(x); c = 1;\n", "type A = B\n/[//]/.test(x); c = 2;\n"), ).toBe(false); - // The next line's `x` is no label: it starts a new statement, and `/` divides. + // A comment spanning lines separates them too; JavaScript keeps its comments as tokens. + expect( + equivalent( + "x\n/* a\n */ /[//]/.test(y); c = 1;\n", + "x\n/* a\n */ /[//]/.test(y); c = 2;\n", + "/src/a.js", + ), + ).toBe(false); + // A / on the line of the token before it still divides. expect(equivalent(loop("continue\nx / 2; // one"), loop("continue\nx / 2; // two"))).toBe(true); }); +test("code units outside ASCII are equivalent only inside strings, templates and comments", () => { + expect(equivalent('const s = "é"; // one\n', 'const s = "é"; // two\n')).toBe(true); + expect(equivalent("const s = `é${a}`; // one\n", "const s = `é${a}`; // two\n")).toBe(true); + expect(equivalent("const a = 1; // é\n", "const a = 1; // è\n")).toBe(true); + expect(equivalent("const café = 1; // one\n", "const café = 1; // two\n")).toBe(false); + expect( + equivalent( + "ä: for (;;) { break ä\n/[//]/.test(x); c = 1; }\n", + "ä: for (;;) { break ä\n/[//]/.test(x); c = 2; }\n", + ), + ).toBe(false); + expect(equivalent("const r = /é/; // one\n", "const r = /é/; // two\n")).toBe(false); +}); + test("whitespace outside ASCII is never equivalent", () => { expect( equivalent("return\u3000/[//]/.test(x); c = 1;\n", "return\u3000/[//]/.test(x); c = 2;\n"), diff --git a/packages/compiler/src/frontend/token-equivalence.ts b/packages/compiler/src/frontend/token-equivalence.ts index b13b67498..7a0f6cc79 100644 --- a/packages/compiler/src/frontend/token-equivalence.ts +++ b/packages/compiler/src/frontend/token-equivalence.ts @@ -15,22 +15,21 @@ * annotations lowering reads). * - A `/` starts a regular expression or divides according to the token * before it. After `)` it divides unless the parenthesis closes an `if`, - * `while`, `for`, `for await`, or `with` header. After `}`, `++` or `--` the scanner - * cannot tell which, so it gives up, as it does for escaped identifiers, - * which can spell keywords. + * `while`, `for`, `for await`, or `with` header. After `}`, `++` or + * `--` the scanner cannot tell which, so it gives up, as it does for + * escaped identifiers, which can spell keywords. + * - A `/` with a line break (or a comment that spans lines) since the + * previous token gives up: whether automatic semicolon insertion ends the + * statement before it decides how it reads. * - Template literals track their `${` substitutions on a brace stack. - * - After `break`, `continue` and `debugger`, and after the label of a - * `break` or `continue`, a `/` starts a regular expression: it can only - * follow there once automatic semicolon insertion ends the statement. - * - JSX files, files that contain whitespace or a line terminator outside - * ASCII anywhere (the identifier scan would take it as part of a word, and - * U+2028 and U+2029 end a line comment), and unterminated strings, - * comments, templates and regular expressions give up. + * - Any code unit outside ASCII gives up, except inside strings, templates + * and comments. So do files that contain whitespace or a line terminator + * outside ASCII anywhere (U+2028 and U+2029 end a line comment), JSX + * files, and unterminated strings, comments, templates and regular + * expressions. * - * Plain character scans keep the scanner free of Unicode property escapes: - * any non-ASCII code unit outside strings, templates, regular expressions - * and comments belongs to the token around it, so an edit there compares - * literally. */ + * The scanner models only what these rules name; anything else makes the + * versions not equivalent, which rebuilds. */ interface Token { text: string; @@ -58,9 +57,6 @@ const EXPRESSION_KEYWORDS = new Set([ "extends", ]); -/** Statements that end at their keyword, or at the label after it. */ -const JUMP_KEYWORDS = new Set(["break", "continue", "debugger"]); - /** Whitespace and line terminators outside ASCII. */ const NON_ASCII_SPACE = /[\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]/; @@ -113,8 +109,7 @@ function isIdentifierCode(code: number): boolean { (code >= 48 && code <= 57) || code === 36 || code === 95 || - code === 92 || - code > 127 + code === 92 ); } @@ -137,23 +132,18 @@ function scan(path: string, source: string): Token[] | null { // Paren kinds: true for a statement header. const parens: boolean[] = []; let lineBreakBefore = false; - // The previous token and the one before it, whether the previous token is - // the label of a `break` or `continue` on its line, and whether a `)` - // closed a statement header. - const last: { token: string | null; before: string | null; label: boolean } = { - token: null, - before: null, - label: false, - }; + // A line break, or a comment spanning lines, since the last token other + // than a comment. + let breakSinceCode = false; + // The previous token and the one before it, and whether a `)` closed a + // statement header. + const last: { token: string | null; before: string | null } = { token: null, before: null }; let closedHeader = false; const push = (text: string, comment = false): void => { tokens.push({ text, comment, lineBreakBefore }); lineBreakBefore = false; if (!comment) { - last.label = - (last.token === "break" || last.token === "continue") && - !tokens[tokens.length - 1]!.lineBreakBefore && - /^[A-Za-z_$]/.test(text); + breakSinceCode = false; last.before = last.token; last.token = text; } @@ -198,10 +188,12 @@ function scan(path: string, source: string): Token[] | null { while (index < length) { const code = source.charCodeAt(index); if (isSpace(code)) { - if (isLineBreak(code)) lineBreakBefore = true; + if (isLineBreak(code)) lineBreakBefore = breakSinceCode = true; index++; continue; } + // Outside strings, templates and comments, the scanner models ASCII only. + if (code > 127) return null; const next = source.charCodeAt(index + 1); // Comments. if (code === 47 && (next === 47 || next === 42)) { @@ -216,8 +208,10 @@ function scan(path: string, source: string): Token[] | null { const text = source.slice(start, index); const semantic = !typeScript || text.startsWith("///") || text.includes("@") || text.includes("#"); + const multiline = /[\r\n]/.test(text); + if (multiline) breakSinceCode = true; if (semantic) push(text, true); - else if (/[\r\n]/.test(text)) lineBreakBefore = true; + else if (multiline) lineBreakBefore = true; continue; } // Strings. @@ -284,6 +278,7 @@ function scan(path: string, source: string): Token[] | null { } // Division or a regular expression. if (code === 47) { + if (breakSinceCode) return null; const before = last.token; let regex: boolean; if (before === null) regex = true; @@ -292,8 +287,7 @@ function scan(path: string, source: string): Token[] | null { // `++/re/.lastIndex` is valid, `x++ / 2` too. else if (before === "++" || before === "--") return null; else if (before === "]") regex = false; - else if (/^[A-Za-z0-9_$\\]/.test(before) || before.charCodeAt(0) > 127) - regex = EXPRESSION_KEYWORDS.has(before) || JUMP_KEYWORDS.has(before) || last.label; + else if (/^[A-Za-z0-9_$\\]/.test(before)) regex = EXPRESSION_KEYWORDS.has(before); else if (before.startsWith("'") || before.startsWith('"') || before.endsWith("`")) regex = false; else if (before.startsWith("/") && before.length > 1 && before !== "/=") regex = false; @@ -304,7 +298,7 @@ function scan(path: string, source: string): Token[] | null { let inClass = false; while (index < length) { const inner = source.charCodeAt(index); - if (isLineBreak(inner)) return null; + if (isLineBreak(inner) || inner > 127) return null; if (inner === 92) { index += 2; continue; diff --git a/packages/compiler/src/frontend/ts7/checker.ts b/packages/compiler/src/frontend/ts7/checker.ts index 4f2d60294..9550d1dc2 100644 --- a/packages/compiler/src/frontend/ts7/checker.ts +++ b/packages/compiler/src/frontend/ts7/checker.ts @@ -45,7 +45,7 @@ import type { TypePredicate, TypeReference, } from "./semantic-types.js"; -import type { SemanticChecker as Checker } from "./semantic-checker.js"; +import { NODE_BATCH_QUERIES, type SemanticChecker as Checker } from "./semantic-checker.js"; import { symbolNameOf, type SemanticProject as Project } from "./semantic-model.js"; import { isTypeNode, walkPreorder } from "./ast.js"; import { SignatureKind, SyntaxKind, TypeFlags, type SymbolFlags } from "./enums.js"; @@ -79,6 +79,44 @@ function withPanicFence( } } +/** Answers for `items` in chunks, each answer given to `store`. A chunk + * that throws is split until the throwing items are alone; those get no + * answer, so their direct query throws again where it is made. */ +function batchAnswers( + items: readonly I[], + fetch: (chunk: readonly I[]) => readonly (O | undefined)[], + store: (item: I, answer: O | undefined) => void, + unknown: (error: unknown) => boolean = () => false, +): boolean { + let known = true; + const answer = (chunk: readonly I[]): void => { + if (!known) return; + let answers: readonly (O | undefined)[]; + try { + answers = fetch(chunk); + } catch (error) { + if (unknown(error)) { + known = false; + return; + } + if (chunk.length === 1) return; + const mid = chunk.length >> 1; + answer(chunk.slice(0, mid)); + answer(chunk.slice(mid)); + return; + } + chunk.forEach((item, index) => store(item, answers[index])); + }; + for (let i = 0; i < items.length; i += BATCH_CHUNK) answer(items.slice(i, i + BATCH_CHUNK)); + return known; +} + +/** Whether a request failed because the server has no such method: tsgo, + * and tsc-ts before 0.0.7, answer the batch methods this way. */ +function unknownMethod(error: unknown, method: string): boolean { + return error instanceof Error && error.message.includes(`unknown API method "${method}"`); +} + function chunked(items: readonly T[], fetch: (chunk: readonly T[]) => readonly R[]): R[] { const out: R[] = []; for (let i = 0; i < items.length; i += BATCH_CHUNK) { @@ -215,6 +253,9 @@ function collectNodes(roots: readonly Node[], walk: PrefetchWalk = "all"): Node[ export class CheckerFacade { private readonly cache = new CheckerCache(); + /** Batch methods the server answered as unknown. Lowering asks those + * node by node from then on, without a failed request per prefetch. */ + private readonly unknownBatchMethods = new Set(); constructor( /** The underlying 7.0.2 sync checker — exposed for methods the facade @@ -322,7 +363,66 @@ export class CheckerFacade { prefetchRoots(roots: readonly Node[]): void { this.ensureActive(); this.markManaged(roots); - this.prefetchNodes(collectNodes(roots, "reachable")); + const nodes = collectNodes(roots, "reachable"); + this.prefetchNodes(nodes); + if (!NODE_BATCH_QUERIES) return; + // Lowering a call resolves its signature (reconcileOverloadReturn). + this.prefetchResolvedSignatures( + nodes.filter( + (node) => + node.kind === SyntaxKind.CallExpression || + node.kind === SyntaxKind.TaggedTemplateExpression, + ), + ); + // Lowering an array literal or a conditional asks its contextual type. + this.prefetchContextualTypes( + nodes.filter( + (node) => + node.kind === SyntaxKind.ArrayLiteralExpression || + node.kind === SyntaxKind.ConditionalExpression, + ), + ); + } + + /** Batches the contextual type of each node, for an analysis that asks it + * of many nodes (class-protocols.ts). A query that fails stays out of the + * memo, so a later getContextualType asks again as it would have. */ + prefetchContextualTypes(nodes: readonly Node[]): void { + if (!NODE_BATCH_QUERIES) return; + this.ensureActive(); + const missing = [...new Set(nodes)].filter((node) => !this.cache.contextualType.has(node)); + this.batchNodes( + "getContextualTypes", + missing, + (chunk) => this.raw.getContextualTypes(chunk), + (node, type) => this.cache.contextualType.set(node, type), + ); + } + + /** Batches the resolved signature of each call (getResolvedSignature). */ + prefetchResolvedSignatures(nodes: readonly Node[]): void { + if (!NODE_BATCH_QUERIES) return; + this.ensureActive(); + const missing = [...new Set(nodes)].filter((node) => !this.cache.resolvedSignature.has(node)); + this.batchNodes( + "getResolvedSignatures", + missing, + (chunk) => this.raw.getResolvedSignatures(chunk), + (node, signature) => this.cache.resolvedSignature.set(node, signature), + ); + } + + /** Answers a node batch method's chunks (batchAnswers), unless the server + * has answered the method as unknown. */ + private batchNodes( + method: string, + nodes: readonly Node[], + fetch: (chunk: readonly Node[]) => readonly (O | undefined)[], + store: (node: Node, answer: O | undefined) => void, + ): void { + if (nodes.length === 0 || this.unknownBatchMethods.has(method)) return; + if (!batchAnswers(nodes, fetch, store, (error) => unknownMethod(error, method))) + this.unknownBatchMethods.add(method); } /** Batches symbol queries for every identifier under roots without the diff --git a/packages/compiler/src/frontend/ts7/enums.generated.ts b/packages/compiler/src/frontend/ts7/enums.generated.ts index f21134bb6..4551ab428 100644 --- a/packages/compiler/src/frontend/ts7/enums.generated.ts +++ b/packages/compiler/src/frontend/ts7/enums.generated.ts @@ -1315,3 +1315,25 @@ export enum LanguageVariant { Standard = 0, JSX = 1 } + +export enum JsxEmit { + None = 0, + Preserve = 1, + ReactNative = 2, + React = 3, + ReactJSX = 4, + ReactJSXDev = 5 +} + +/** Numeric lookup with the SDK's aliases and undefined for unknown values. */ +export function jsxEmitName(value: number): string | undefined { + switch (value) { + case 0: return "None"; + case 1: return "Preserve"; + case 2: return "ReactNative"; + case 3: return "React"; + case 4: return "ReactJSX"; + case 5: return "ReactJSXDev"; + default: return undefined; + } +} diff --git a/packages/compiler/src/frontend/ts7/enums.ts b/packages/compiler/src/frontend/ts7/enums.ts index 6eba358f2..844643e80 100644 --- a/packages/compiler/src/frontend/ts7/enums.ts +++ b/packages/compiler/src/frontend/ts7/enums.ts @@ -1,4 +1,25 @@ /** Runtime enums generated from the pinned TypeScript 7 declarations. * Their nominal names and values match the SDK; TypeScript 5 remains a * separate world. No package loader runs on the compiler's native path. */ +import { jsxEmitName } from "./enums.generated.js"; + export * from "./enums.generated.js"; + +/** The tsconfig spelling of a `jsx` option value: "preserve", "react-native", + * "react", "react-jsx" or "react-jsxdev". */ +export function jsxOptionName(value: number): string | undefined { + switch (jsxEmitName(value)) { + case "Preserve": + return "preserve"; + case "ReactNative": + return "react-native"; + case "React": + return "react"; + case "ReactJSX": + return "react-jsx"; + case "ReactJSXDev": + return "react-jsxdev"; + default: + return undefined; + } +} diff --git a/packages/compiler/src/frontend/ts7/program-host.ts b/packages/compiler/src/frontend/ts7/program-host.ts index cc867d208..639086e94 100644 --- a/packages/compiler/src/frontend/ts7/program-host.ts +++ b/packages/compiler/src/frontend/ts7/program-host.ts @@ -30,6 +30,7 @@ import type { } from "./session.js"; import { CheckerFacade } from "./checker.js"; import { + jsxOptionName, moduleDetectionKindName, moduleKindName, moduleResolutionKindName, @@ -82,6 +83,9 @@ function serializeOptions(options: Ts7CompilerOptions): Record case "moduleDetection": out[key] = moduleDetectionKindName(value as number)?.toLowerCase() ?? value; break; + case "jsx": + out[key] = typeof value === "number" ? (jsxOptionName(value) ?? value) : value; + break; case "lib": // 5.9.3 spells lib entries "lib.es2025.d.ts"; tsconfig wants "es2025". out[key] = (value as string[]).map((lib) => @@ -188,7 +192,10 @@ export class Ts7Host { const key = tsgoPath(path); if (this.virtualFiles.get(key) === content) return; if (!this.virtualChanges.has(key)) - this.virtualChanges.set(key, this.virtualFiles.has(key) ? "changed" : "created"); + this.virtualChanges.set( + key, + this.virtualFiles.has(key) || trackedFileExists(path) ? "changed" : "created", + ); this.virtualFiles.set(key, content); } diff --git a/packages/compiler/src/frontend/ts7/semantic-checker.ts b/packages/compiler/src/frontend/ts7/semantic-checker.ts index 5e7f8339a..c19f93598 100644 --- a/packages/compiler/src/frontend/ts7/semantic-checker.ts +++ b/packages/compiler/src/frontend/ts7/semantic-checker.ts @@ -96,6 +96,13 @@ export interface SemanticCompletion { symbol: SemanticSymbol | undefined; } +/** Whether lowering asks the checker for contextual types and resolved + * signatures in batches (`getContextualTypes`, `getResolvedSignatures`, + * answered from tsc-ts 0.0.7 on). Off, the batch prefetches (checker.ts) do + * nothing and lowering asks node by node. A server without a batch method + * (tsgo) answers it as unknown once, and the facade stops asking. */ +export const NODE_BATCH_QUERIES = true; + /** The pinned checker's request surface over native objects. Every query * converges on the same snapshot/project registries, including batch and * lazy-object queries. No SDK object is constructed or consulted here. */ @@ -201,6 +208,10 @@ export class SemanticChecker { getContextualType(node: AstNode): SemanticType | undefined { return this.type("getContextualType", { location: node.id }); } + /** getContextualType for each node. */ + getContextualTypes(nodes: readonly AstNode[]): (SemanticType | undefined)[] { + return this.optionalTypes("getContextualTypes", { locations: nodes.map((node) => node.id) }); + } getTypeFromTypeNode(node: AstNode): SemanticType | undefined { return this.type("getTypeFromTypeNode", { location: node.id }); } @@ -213,6 +224,12 @@ export class SemanticChecker { getResolvedSignature(node: AstNode): SemanticSignature | undefined { return this.signature("getResolvedSignature", { location: node.id }); } + /** getResolvedSignature for each node. */ + getResolvedSignatures(nodes: readonly AstNode[]): (SemanticSignature | undefined)[] { + return this.project.requestSignatures("getResolvedSignatures", { + locations: nodes.map((node) => node.id), + }); + } getSignatureFromDeclaration(node: AstNode): SemanticSignature | undefined { return this.signature("getSignatureFromDeclaration", { location: node.id }); } diff --git a/packages/compiler/src/frontend/tsc-codes.ts b/packages/compiler/src/frontend/tsc-codes.ts index fe48c8855..e6c0be7cd 100644 --- a/packages/compiler/src/frontend/tsc-codes.ts +++ b/packages/compiler/src/frontend/tsc-codes.ts @@ -27,6 +27,13 @@ export const ADOPTED_OPTIONS = [ // SC1012 "not supported yet" beats a raw TS1259 at the same site. "esModuleInterop", "allowSyntheticDefaultImports", + // JSX: the checker types JSX as the project's tsc does, and the JSX + // lowering (jsx-source.ts) emits the calls that setting selects. + "jsx", + "jsxFactory", + "jsxFragmentFactory", + "jsxImportSource", + "reactNamespace", ] as const; export const JS_RELAXED_TSC_CODES: ReadonlySet = new Set([ diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index 9abd5a0b0..14379f30f 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -147,6 +147,15 @@ export const WEBCRYPTO_DYN_TYPES: ReadonlySet = new Set([ "KeyAlgorithm", "HmacImportParams", "HmacKeyAlgorithm", + "CryptoKeyPair", + "EcKeyAlgorithm", + "RsaHashedKeyAlgorithm", + "EcKeyGenParams", + "EcKeyImportParams", + "EcdsaParams", + "RsaHashedKeyGenParams", + "RsaHashedImportParams", + "RsaPssParams", "HmacKeyGenParams", "AesKeyGenParams", "AesKeyAlgorithm", @@ -159,6 +168,28 @@ export const WEBCRYPTO_DYN_TYPES: ReadonlySet = new Set([ "AesDerivedKeyParams", ]); +/** node:crypto's key and signature option interfaces: the key functions, + * KeyObject.export and sign/verify take them as checked-dynamic arguments + * (scr_lib.c, scr_pki.c). */ +export const CRYPTO_KEY_OPTION_DYN_TYPES: ReadonlySet = new Set([ + "AsymmetricKeyExportOptions", + "ECKeyPairKeyObjectOptions", + "ECKeyPairOptions", + "ED25519KeyPairOptions", + "JsonWebKeyInput", + "JwkKeyExportOptions", + "KeyExportOptions", + "PrivateKeyInput", + "PublicKeyInput", + "RSAKeyPairKeyObjectOptions", + "RSAKeyPairOptions", + "SignKeyObjectInput", + "SignPrivateKeyInput", + "SigningOptions", + "VerifyKeyObjectInput", + "VerifyPublicKeyInput", +]); + export const FETCH_AMBIENT_TYPES = [ "Response", "Request", @@ -2987,9 +3018,10 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { ) { return DYN; } - // node:crypto's KeyObject: secret keys are native handles (scr_lib.c) - // whose getters and methods dispatch through the handle. The JsonWebKey - // that export() returns is a plain checked-dynamic object. + // node:crypto's KeyObject: secret keys (scr_lib.c) and private and + // public keys (scr_pki.c) are native handles whose getters and methods + // dispatch through the handle. The JsonWebKey that export() returns is a + // plain checked-dynamic object. if ( ((psym !== undefined && ts.symbolName(psym) === "KeyObject") || (psym !== undefined && ts.symbolName(psym) === "JsonWebKey")) && @@ -3001,6 +3033,35 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { ) { return DYN; } + // node:crypto's key and signature options are plain checked-dynamic + // objects, so a value asserted to one (`opts as + // crypto.AsymmetricKeyExportOptions<"pem">`) reaches the call unchanged. + if ( + psym !== undefined && + CRYPTO_KEY_OPTION_DYN_TYPES.has(ts.symbolName(psym)) && + psym.declarations.some( + (d) => + ts.isInterfaceDeclaration(d) && + ctx.isStdlibFile(ts.getSourceFileOfNode(d)) && + isDeclaredInAmbientModule(d, "crypto"), + ) + ) { + return DYN; + } + // node:crypto's Sign and Verify (createSign/createVerify): native + // handles over the digest state (scr_pki.c). + if ( + psym !== undefined && + (ts.symbolName(psym) === "Sign" || ts.symbolName(psym) === "Verify") && + psym.declarations.some( + (d) => + ts.isClassDeclaration(d) && + ctx.isStdlibFile(ts.getSourceFileOfNode(d)) && + isDeclaredInAmbientModule(d, "crypto"), + ) + ) { + return DYN; + } if ( psym && (FETCH_AMBIENT_TYPES as readonly string[]).includes(ts.symbolName(psym)) && @@ -3057,6 +3118,23 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { ) { return DYN; } + // node:crypto DiffieHellman values, and @types/node's DiffieHellmanGroup + // alias over them, are native handles carried by the checked-dynamic + // representation; lowerDiffieHellmanCall dispatches their methods. + { + const dhSym = widened.getAliasSymbol() ?? psym; + if ( + dhSym !== undefined && + (ts.symbolName(dhSym) === "DiffieHellman" || ts.symbolName(dhSym) === "DiffieHellmanGroup") && + dhSym.declarations.some( + (d) => + (ts.isClassDeclaration(d) || ts.isTypeAliasDeclaration(d)) && + ctx.isStdlibFile(ts.getSourceFileOfNode(d)), + ) + ) { + return DYN; + } + } // ReadonlyMap/ReadonlySet are the SAME runtime values as Map/Set — the // readonly-ness is a checker-only view (no mutating members on the // interface), so both map to the identical IR kinds and the read-side @@ -4267,6 +4345,41 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { }; } } + // crypto.generateKeyPairSync's results: KeyPairKeyObjectResult and + // KeyPairSyncResult as { publicKey, privateKey } records over the + // mapped property types (KeyObjects, PEM strings or DER Buffers), in + // Node's key order. + if ( + psym !== undefined && + (ts.symbolName(psym) === "KeyPairKeyObjectResult" || + ts.symbolName(psym) === "KeyPairSyncResult") && + psym.declarations.some( + (d) => + ts.isInterfaceDeclaration(d) && + ctx.isStdlibFile(ts.getSourceFileOfNode(d)) && + isDeclaredInAmbientModule(d, "crypto"), + ) + ) { + const field = (name: string): IrType | null => { + const prop = ctx.checker.getPropertyOfType(type, name); + return prop ? mapType(ctx.checker.getTypeOfSymbol(prop), ctx) : null; + }; + const publicKey = field("publicKey"); + const privateKey = field("privateKey"); + if (!publicKey || !privateKey) return null; + return { + kind: "record", + shapeId: ctx.shapes.intern( + [ + { name: "privateKey", type: privateKey }, + { name: "publicKey", type: publicKey }, + ], + false, + undefined, + ["publicKey", "privateKey"], + ), + }; + } // crypto.X509Certificate — the Dirent-style data record: THREE visible // fields (fingerprint and the validFrom/validTo validity window, all // computed at construction by the new-expression lowering). Module- @@ -5731,6 +5844,41 @@ function constAssertedEmptyArrayProp(p: ts.Symbol): boolean { return underConstAssertion(d); } +/** A TypeScript property assignment `name: []` whose type is `never[]`. */ +function emptyNeverArrayProp(p: ts.Symbol, fieldTs: ts.Type, checker: ts.TypeChecker): boolean { + const d = p.valueDeclaration; + if (d === undefined || !ts.isPropertyAssignment(d)) return false; + if (isJsSourceFile(ts.getSourceFileOfNode(d))) return false; + if (!ts.isArrayLiteralExpression(d.initializer) || d.initializer.elements.nodes.length > 0) + return false; + return ( + checker.isArrayType(fieldTs) && + (checker.getTypeArguments(fieldTs as ts.TypeReference)[0]!.flags & ts.TypeFlags.Never) !== 0 + ); +} + +/** A JavaScript object literal's property (`{ lastComponent: undefined }`, + * `{ user: null }`). Nothing checks a JavaScript file's later writes, which + * can store any value in it (`path.lastComponent = { ... }`), so a + * property whose only initializer is a unit holds the checked-dynamic value + * rather than the unit-only union. */ +function isUnitOnlyIr(t: IrType, ctx: TypeMapperCtx): boolean { + if (t.kind !== "union") return false; + const def = ctx.unions.get(t.unionId); + return def !== undefined && def.arms.every((arm) => isUnitType(arm)); +} + +function jsObjectLiteralProperty(p: ts.Symbol): boolean { + const d = p.valueDeclaration; + // `__proto__: null` sets the literal's prototype; it declares no property. + return ( + d !== undefined && + (ts.isPropertyAssignment(d) || ts.isShorthandPropertyAssignment(d)) && + ts.symbolName(p) !== "__proto__" && + isJsSourceFile(ts.getSourceFileOfNode(d)) + ); +} + function mapGenericAwaitedAlias(type: ts.Type, ctx: TypeMapperCtx): IrType | null { const { resolveTypeParam } = ctx; if (!resolveTypeParam) return null; @@ -7767,6 +7915,14 @@ function mapRecordTypeInner( ) { pt = arrayOf(unitOnlyUnion(ctx.unions)); } + // A TypeScript property initialized with an empty literal and typed + // `never[]` (`const out = { files: [] }`) holds no element, so it maps + // like the empty tuple: the unit-only element array, which a + // destination's typed array field (`files: string[]`) receives through + // the checked empty-array lift. never's f64 element would make it a + // `number[]` field. + if (pt?.kind === "array" && emptyNeverArrayProp(p, fieldTs, checker)) + pt = arrayOf(unitOnlyUnion(ctx.unions)); if (pt === null && (fieldTs.flags & ts.TypeFlags.Any) !== 0) pt = DYN; // An `any[]` field (`list: any[]` in an SDK's message type, also // optional or nullable) is the checked-dynamic array, exactly as @@ -7790,6 +7946,7 @@ function mapRecordTypeInner( // omits the undefined arm exactly like an omitted optional. if (pt?.kind === "void" && isUnitOnlyTsType(fieldTs, ctx.resolveTypeParam)) pt = unitOnlyUnion(ctx.unions); + if (pt !== null && isUnitOnlyIr(pt, ctx) && jsObjectLiteralProperty(p)) pt = DYN; // dyn FIELDS map now (`{ v: unknown }`, `[string, unknown]` tuples): // the slot carries a dyn value exactly like an `unknown`-valued // overflow entry — same RC adapters, same dynFrom conversion on the diff --git a/packages/compiler/src/index.ts b/packages/compiler/src/index.ts index 2b216c77d..f0f19a408 100644 --- a/packages/compiler/src/index.ts +++ b/packages/compiler/src/index.ts @@ -49,7 +49,8 @@ import { frontendProcessExecArgv } from "./frontend-process-args.js"; import { type ChildProcess, spawn } from "node:child_process"; import { createHash } from "node:crypto"; import { existsSync } from "node:fs"; -import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises"; +import { copyFile, mkdir, mkdtemp, readFile, rename, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; import { basename, dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { @@ -524,6 +525,9 @@ async function compileExecutableNative( onArtifactReady?: NonNullable[0]["onArtifactReady"]>, programPartitions: readonly string[] = [], profileGenerate = false, + /** The kept LLVM text's shared name, when `llvmPath` is the build's private copy of it + * (BuildSources): the spelling clang sees and native cache keys name. */ + llvmVisiblePath?: string, ): Promise { const programIsObject = /\.(?:o|obj)$/.test(llvmPath); const runtimePackTarget = programIsObject && !sanitize ? nativeCodegenTarget() : null; @@ -563,7 +567,9 @@ async function compileExecutableNative( if (programIsObject) throw new InternalCompilerError("program objects must link against a precompiled runtime pack"); await compileExternalC({ - cPath: llvmPath, + ...(llvmVisiblePath === undefined || llvmVisiblePath === llvmPath + ? { cPath: llvmPath } + : { cPath: llvmVisiblePath, cSourcePath: llvmPath }), outPath, cacheIdentity: "scriptc-generated-v1", ...optimizationField(features.optimization), @@ -624,6 +630,9 @@ async function compileExecutableNative( tlsCa: features.tlsCa, cipher: features.cipher, ecdh: features.ecdh, + dh: features.dh, + pki: features.pki, + ed25519: features.ed25519, webcrypto: features.webcrypto, perfLoop: features.perfLoop, ...(onArtifactReady === undefined ? {} : { onArtifactReady }), @@ -729,8 +738,12 @@ function runtimePackDiagnostic(error: RuntimePackError, entryPath: string): ScrD interface PreparedExecutable { llvmSource: LlvmInput; + /** The kept LLVM text beside the executable, installed when the build ends. */ llvmPath: string; + /** This build's private copy of the LLVM text (BuildSources). */ + llvmSourcePath: string; irPath: string | undefined; + irSourcePath: string | undefined; nativeFeatures: EarlyExecutableNativeFeatures; programSplit: ReturnType; sourceTexts: Map; @@ -841,6 +854,8 @@ async function prepareExecutableInput( await mkdir(opts.outDir, { recursive: true }); const llvmPath = defaultSourcePaths.llvm; + // The extension keeps clang reading the copy as LLVM IR rather than as a linker input. + const llvmSourcePath = `${privateSiblingPath(llvmPath, "llvm")}.ll`; const backend = "llvm" as const; const useRuntimePack = opts.nativeProgramObject === true || usesPrecompiledRuntimePack(opts, backend); @@ -863,12 +878,14 @@ async function prepareExecutableInput( return { ok: false, diagnostics: [llvmRefusalDiag(err, entryPath)], sourceTexts }; } timing("llvm-emit"); - await writeLlvmSource(llvmPath, llvmSource); + await writeLlvmSource(llvmSourcePath, llvmSource); timing("llvm-write"); let irPath: string | undefined; + let irSourcePath: string | undefined; if (opts.emitIr) { irPath = defaultSourcePaths.ir; - await writeTextParts(irPath, serializeModuleParts(mod)); + irSourcePath = privateSiblingPath(irPath, "ir"); + await writeTextParts(irSourcePath, serializeModuleParts(mod)); } const nativeFeatures = executableNativeFeatures(mod, backend, opts.optimization ?? "release"); @@ -882,7 +899,87 @@ async function prepareExecutableInput( ? splitLlvmProgram(llvmSource) : null; timing("llvm-split"); - return { llvmSource, llvmPath, irPath, nativeFeatures, programSplit, sourceTexts }; + return { + llvmSource, + llvmPath, + llvmSourcePath, + irPath, + irSourcePath, + nativeFeatures, + programSplit, + sourceTexts, + }; +} + +/** + * The kept LLVM text (`.ll`) and IR (`.ir.json`) sit beside the executable, + * so programs whose entries share a name and are built into one output directory share them. Each + * build therefore writes, compiles and caches private copies (privateSiblingPath) and moves them to + * the shared names only when it is done (rename replaces a file in one step): a concurrent build of + * another program cannot replace the text a build compiles or the translation unit it caches. + * Builds that compile with the C driver install them before the native build instead + * (stageDriverBuildSources). + */ +interface BuildSources { + llvmPath: string; + llvmSourcePath: string; + irPath?: string | undefined; + irSourcePath?: string | undefined; + /** The shared name already holds the private copy's bytes (a cache hit's restored files): it is + * left as it is, with its time stamps. */ + llvmPathCurrent?: boolean | undefined; + irPathCurrent?: boolean | undefined; +} + +/** Moves a build's private copies to the shared names, or drops them when that fails. */ +async function installBuildSources(sources: BuildSources): Promise { + const install = async ( + source: string | undefined, + destination: string | undefined, + current: boolean | undefined, + ) => { + if (source === undefined || destination === undefined || source === destination) return; + if (current === true) await rm(source, { force: true }); + else await rename(source, destination).catch(() => rm(source, { force: true })); + }; + await Promise.all([ + install(sources.llvmSourcePath, sources.llvmPath, sources.llvmPathCurrent), + install(sources.irSourcePath, sources.irPath, sources.irPathCurrent), + ]); +} + +/** + * A build that compiles the LLVM text with the C driver (no precompiled runtime pack) records the + * output directory in its native cache proofs, so that directory has to be in its final state before + * the native build starts. The build therefore installs the shared names first and compiles and + * caches copies in a private directory outside the output directory. Returns that directory and the + * copies. + */ +async function stageDriverBuildSources( + sources: BuildSources, +): Promise<{ dir: string; sources: BuildSources }> { + const dir = await mkdtemp(join(tmpdir(), "scriptc-program-")); + const llvmSourcePath = join(dir, basename(sources.llvmPath)); + const irSourcePath = + sources.irPath === undefined || sources.irSourcePath === undefined + ? undefined + : join(dir, basename(sources.irPath)); + try { + await Promise.all([ + copyFile(sources.llvmSourcePath, llvmSourcePath), + irSourcePath === undefined || sources.irSourcePath === undefined + ? undefined + : copyFile(sources.irSourcePath, irSourcePath), + ]); + } catch (err) { + await rm(dir, { recursive: true, force: true }); + throw err; + } + await installBuildSources(sources); + return { + dir, + sources: { llvmPath: sources.llvmPath, llvmSourcePath, irPath: sources.irPath, irSourcePath }, + }; } /** @@ -976,10 +1073,11 @@ type FrontendProcessResult = | { kind: "prepared"; llvmPath: string; - /** A private copy of the LLVM text, which the native helper reads (`nativeInputPath`). */ + /** The build's private copy of the LLVM text (BuildSources), which the native helper reads. */ nativeInputPath: string; llvmBytes: number; irPath: string | undefined; + irSourcePath: string | undefined; nativeFeatures: EarlyExecutableNativeFeatures; sourceTexts: [string, string][]; warnings: ScrDiagnostic[]; @@ -1047,7 +1145,9 @@ async function prepareExecutableInputInChild( return { llvmSource: { file: result.nativeInputPath, bytes: result.llvmBytes }, llvmPath: result.llvmPath, + llvmSourcePath: result.nativeInputPath, irPath: result.irPath, + irSourcePath: result.irSourcePath, nativeFeatures: result.nativeFeatures, programSplit: null, sourceTexts, @@ -1085,18 +1185,15 @@ export async function runFrontendProcess(requestPath: string, resultPath: string ); const frontend = frontendInputs.snapshot(); if (!("ok" in prepared)) { - // The public .ll file may be rewritten by a concurrent build of another entry with the - // same output stem before the native helper reads it (see emitNativeArtifact). if (isLlvmFile(prepared.llvmSource)) throw new InternalCompilerError("in-process preparation returned LLVM text as a file"); - const nativeInputPath = privateSiblingPath(prepared.llvmPath, "frontend-llvm"); - await writeLlvmSource(nativeInputPath, prepared.llvmSource, 0o600); result = { kind: "prepared", llvmPath: prepared.llvmPath, - nativeInputPath, + nativeInputPath: prepared.llvmSourcePath, llvmBytes: llvmInputBytes(prepared.llvmSource), irPath: prepared.irPath, + irSourcePath: prepared.irSourcePath, nativeFeatures: prepared.nativeFeatures, sourceTexts: [...prepared.sourceTexts], warnings: warnings.warnings, @@ -1450,37 +1547,54 @@ async function compileTracked( ...(earlyHit.irPath === undefined ? {} : { irPath: earlyHit.irPath }), }; } - let nativeInputPath = earlyHit.llvmPath; + // The build's private copies of the cached LLVM text and IR (BuildSources). + const hitSources: BuildSources = { + llvmPath: earlyHit.llvmPath, + llvmSourcePath: earlyHit.llvmSourcePath ?? earlyHit.llvmPath, + irPath: earlyHit.irPath, + irSourcePath: earlyHit.irSourcePath, + llvmPathCurrent: earlyHit.llvmPathCurrent, + irPathCurrent: earlyHit.irPathCurrent, + }; + // What the native build compiles and the cache publishes: hitSources, or their staged copies. + let nativeSources = hitSources; + let stagedDir: string | null = null; + let nativeInputPath = hitSources.llvmSourcePath; let nativeProgramObject: NativeProgramObject | null = null; const useRuntimePack = opts.nativeProgramObject === true || usesPrecompiledRuntimePack(opts, earlyHit.native.backend); - if (useRuntimePack) { - if (earlyHit.native.backend !== "llvm") { - throw new InternalCompilerError( - "native program-object cache hit restored a non-LLVM translation unit", - ); - } - try { - nativeProgramObject = await emitNativeProgramObject( - entryPath, - opts, - // A large program's LLVM exceeds the engine's string limit as one - // string; the emitter takes it in parts, as on a cold build. - await readTextParts(earlyHit.llvmPath), - profile, - ); - nativeInputPath = nativeProgramObject.linkPath; - } catch (err) { - if (!(err instanceof NativeCodegenError)) throw err; - return { - ok: false, - diagnostics: [nativeCodegenDiag(err.diagnosticCode, err.message, entryPath)], - sourceTexts: new Map(), - }; - } - } try { + if (useRuntimePack) { + if (earlyHit.native.backend !== "llvm") { + throw new InternalCompilerError( + "native program-object cache hit restored a non-LLVM translation unit", + ); + } + try { + nativeProgramObject = await emitNativeProgramObject( + entryPath, + opts, + // A large program's LLVM exceeds the engine's string limit as one + // string; the emitter takes it in parts, as on a cold build. + await readTextParts(hitSources.llvmSourcePath), + profile, + ); + nativeInputPath = nativeProgramObject.linkPath; + } catch (err) { + if (!(err instanceof NativeCodegenError)) throw err; + return { + ok: false, + diagnostics: [nativeCodegenDiag(err.diagnosticCode, err.message, entryPath)], + sourceTexts: new Map(), + }; + } + } else { + const staged = await stageDriverBuildSources(hitSources); + stagedDir = staged.dir; + nativeSources = staged.sources; + nativeInputPath = nativeSources.llvmSourcePath; + } await compileExecutableNative( earlyHit.native, nativeInputPath, @@ -1496,6 +1610,10 @@ async function compileTracked( : async ({ dependencies }) => { await publishEarlyExecutableCache(cacheRoot, executableCacheOptions, { ...earlyHit, + llvmPath: nativeSources.llvmSourcePath, + ...(nativeSources.irSourcePath === undefined + ? {} + : { irPath: nativeSources.irSourcePath }), executableRestored: true, nativeDependencies: dependencies, frontend: earlyHit.frontend, @@ -1503,6 +1621,7 @@ async function compileTracked( }, nativeProgramObject?.partitionPaths, profile?.generate !== undefined, + hitSources.llvmPath, ); if (nativeProgramObject !== null && opts.nativeProgramObject === true) { await rename(nativeProgramObject.linkPath, nativeProgramObject.artifactPath); @@ -1532,6 +1651,8 @@ async function compileTracked( nativeProgramObject.partitionPaths, ); } + if (stagedDir === null) await installBuildSources(hitSources); + else await rm(stagedDir, { recursive: true, force: true }); } await pruneBuildCache(cacheRoot); return { @@ -1564,7 +1685,17 @@ async function compileTracked( ) : await prepareExecutableInput(entryPath, opts, ffi, buildPlatform, timing, warningSink); if ("ok" in prepared) return prepared; - const { llvmSource, llvmPath, irPath, nativeFeatures, programSplit, sourceTexts } = prepared; + const buildSources: BuildSources = { + llvmPath: prepared.llvmPath, + llvmSourcePath: prepared.llvmSourcePath, + irPath: prepared.irPath, + irSourcePath: prepared.irSourcePath, + }; + const { llvmSource, llvmPath, irPath } = prepared; + const { nativeFeatures, programSplit, sourceTexts } = prepared; + // What the native build compiles and the cache publishes: buildSources, or their staged copies. + let { llvmSourcePath, irSourcePath } = buildSources; + let stagedDir: string | null = null; const backend = "llvm" as const; await mkdir(dirname(opts.outPath), { recursive: true }); if (earlyCacheOptions === null) { @@ -1596,10 +1727,14 @@ async function compileTracked( sourceTexts, }; } + } else { + const staged = await stageDriverBuildSources(buildSources); + stagedDir = staged.dir; + ({ llvmSourcePath, irSourcePath } = staged.sources); } await compileExecutableNative( nativeFeatures, - nativeProgramObject?.linkPath ?? llvmPath, + nativeProgramObject?.linkPath ?? llvmSourcePath, opts.outPath, opts.sanitize ?? false, ffi, @@ -1611,23 +1746,34 @@ async function compileTracked( ? undefined : async ({ dependencies }) => { await publishEarlyExecutableCache(cacheRoot, executableCacheOptions, { - llvmPath, + llvmPath: llvmSourcePath, native: nativeFeatures, executableRestored: true, nativeDependencies: dependencies, frontend: frontendInputs.snapshot(), - ...(irPath === undefined ? {} : { irPath }), + ...(irSourcePath === undefined ? {} : { irPath: irSourcePath }), ...performanceWarnings(warningSink), }); publishedExecutable = true; }, nativeProgramObject?.partitionPaths, profile?.generate !== undefined, + llvmPath, ); timing("native-link"); if (nativeProgramObject !== null && opts.nativeProgramObject === true) { await rename(nativeProgramObject.linkPath, nativeProgramObject.artifactPath); } + if (!publishedExecutable) { + await publishEarlyExecutableCache(cacheRoot, executableCacheOptions, { + llvmPath: llvmSourcePath, + native: nativeFeatures, + executableRestored: false, + frontend: frontendInputs.snapshot(), + ...(irSourcePath === undefined ? {} : { irPath: irSourcePath }), + ...performanceWarnings(warningSink), + }).catch(() => undefined); + } } catch (err) { if (err instanceof RuntimePackError) { return { ok: false, diagnostics: [runtimePackDiagnostic(err, entryPath)], sourceTexts }; @@ -1649,18 +1795,8 @@ async function compileTracked( nativeProgramObject.partitionPaths, ); } - // The frontend process's private copy of the LLVM text. - if (isLlvmFile(llvmSource)) await rm(llvmSource.file, { force: true }); - } - if (!publishedExecutable) { - await publishEarlyExecutableCache(cacheRoot, executableCacheOptions, { - llvmPath, - native: nativeFeatures, - executableRestored: false, - frontend: frontendInputs.snapshot(), - ...(irPath === undefined ? {} : { irPath }), - ...performanceWarnings(warningSink), - }).catch(() => undefined); + if (stagedDir === null) await installBuildSources(buildSources); + else await rm(stagedDir, { recursive: true, force: true }); } await pruneBuildCache(cacheRoot); timing("complete"); diff --git a/packages/compiler/src/ir/array-length-stability.test.ts b/packages/compiler/src/ir/array-length-stability.test.ts index a0d0e49a7..f9b3cc9c2 100644 --- a/packages/compiler/src/ir/array-length-stability.test.ts +++ b/packages/compiler/src/ir/array-length-stability.test.ts @@ -167,3 +167,29 @@ test("fresh local arrays read only as receivers have no alias", () => { const rebound = [...body, { kind: "assign", localId: "out", value: arr("a"), loc } as IrStmt]; expect(findUnaliasedLocalArrays(fn(rebound)).has("out")).toBe(false); }); + +test("number and boolean writes to class fields run no user code", () => { + const obj: IrExpr = { + kind: "varRef", + localId: "o", + type: { kind: "object", className: "P" }, + loc, + }; + const scope = new Map(locals).set("o", { + id: "o", + name: "o", + type: { kind: "object", className: "P" }, + mutable: false, + }); + const fieldSet = (value: IrExpr): IrStmt => ({ + kind: "fieldSet", + obj, + className: "P", + field: "x", + value, + loc, + }); + expect(preserves([fieldSet(read("a", ref("i")))], "i", new Set(), scope)).toBe(true); + // A reference stored into a field releases the old one. + expect(preserves([fieldSet(arr("b"))], "i", new Set(), scope)).toBe(false); +}); diff --git a/packages/compiler/src/ir/array-length-stability.ts b/packages/compiler/src/ir/array-length-stability.ts index 3f9c3b9ca..ed57972b6 100644 --- a/packages/compiler/src/ir/array-length-stability.ts +++ b/packages/compiler/src/ir/array-length-stability.ts @@ -264,6 +264,15 @@ export function preservesArrayLength( ); case "return": return s.value === null || expr(s.value); + case "fieldSet": + // A number or boolean stored into a class field: accessors are + // calls, not field writes, and the old value holds no reference to + // release, so the write runs no user code. + return ( + (s.value.type.kind === "f64" || s.value.type.kind === "bool") && + expr(s.obj) && + expr(s.value) + ); default: if (!PLAIN_STATEMENTS.has(s.kind)) return false; } diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index dd82d06c2..e98ae0bd9 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -13,6 +13,15 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ // Node's curve, key, point, encoding and format errors. "crypto.ecdhNew", "crypto.ecdhConvertKey", + // Node's prime, generator, size, encoding and group-name errors. + "crypto.dhNew", + "crypto.dhGroupNew", + // Node's candidate, size, options, add and rem errors, and OpenSSL's + // generation errors. + "crypto.checkPrimeSync", + "crypto.checkPrimeCb", + "crypto.generatePrimeSync", + "crypto.generatePrimeCb", // node:test's { timeout } and { concurrency } range checks. "test.register", "test.registerEmpty", @@ -230,6 +239,8 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "http2.createSecureServerDynCb", "tls.connect", "tls.connectCb", + "tls.wrapSocket", + "tls.wrapSocketCtx", // The CA-store surface: an unknown getCACertificates type throws // Node's ERR_INVALID_ARG_VALUE TypeError; setDefaultCACertificates // throws Node's ERR_CRYPTO_OPERATION_FAILED on a certificate-free @@ -358,6 +369,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "readable.pushU", "readable.pushDyn", "readable.unshift", + "readable.unshiftDyn", "readable.unshiftStr", "readable.read", "readable.readDyn", @@ -370,6 +382,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "writable.writeStr", "writable.writeU", "writable.writeDyn", + "writable.writeEnc", "writable.end", "writable.uncork", "stream.destroy", @@ -985,4 +998,12 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "crypto.x509ValidFromStr", "crypto.x509ValidTo", "crypto.x509ValidToStr", + "crypto.createPrivateKey", + "crypto.createPublicKey", + "crypto.generateKeyPairSync", + "crypto.generateKeyPairCb", + "crypto.signOneshot", + "crypto.verifyOneshot", + "crypto.createSign", + "crypto.createVerify", ]); diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index 8f5928d12..b26b5d1d8 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -2182,6 +2182,20 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ result: VOID, }, "tls.sockOnSession": { argTypes: [NETSOCKET_T, null, BOOL], result: VOID }, + /** The 'secure' event: the handshake completed (after secureConnect on + * a client). Plain sockets never fire it. */ + "tls.sockOnSecure": { + argTypes: [NETSOCKET_T, { kind: "func", params: [], ret: VOID }, BOOL], + result: VOID, + }, + /** new tls.TLSSocket(socket, options) in server mode: a new TLS socket + * takes over the socket's connection (wrapSocketCtx passes a literal's + * secureContext member apart from the runtime options record). The + * record reads at runtime: isServer, cert, key, ALPNProtocols, + * minVersion and maxVersion are implemented, the client mode and other + * documented members throw the runtime fence. MAY THROW. */ + "tls.wrapSocket": { argTypes: [NETSOCKET_T, DYN], result: NETSOCKET_T }, + "tls.wrapSocketCtx": { argTypes: [NETSOCKET_T, DYN, SECURECTX_T], result: NETSOCKET_T }, // createSecureContext({ cert, key }) mints the opaque SNI-answer handle. /** tls.createSecureContext({ cert, key }) — parses the PEM pair into an * opaque SecureContext handle (secureCtx kind) for SNI callbacks to @@ -2799,6 +2813,23 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ /** ECDH.convertKey(key, curve, inputEncoding, outputEncoding, format): * the converted point as a Buffer or string. */ "crypto.ecdhConvertKey": { argTypes: [DYN, DYN, DYN, DYN, DYN], result: DYN }, + /** createDiffieHellman(sizeOrKey, keyEncoding, generator, + * generatorEncoding) over checked-dynamic arguments (scr_dh.c): a native + * DiffieHellman handle whose methods dispatch through dynInvoke. */ + "crypto.dhNew": { argTypes: [DYN, DYN, DYN, DYN], result: DYN }, + /** getDiffieHellman(groupName) and createDiffieHellmanGroup(name): a + * native DiffieHellmanGroup handle. */ + "crypto.dhGroupNew": { argTypes: [DYN], result: DYN }, + /** checkPrimeSync(candidate, options) and generatePrimeSync(size, + * options) over checked-dynamic arguments (scr_dh.c): a boolean, and an + * ArrayBuffer or bigint, as checked-dynamic results. The Cb forms take + * the callback and whether its error parameter takes null for Node's + * undefined, and call it in a later macrotask; Node's argument errors + * throw (may-throw). */ + "crypto.checkPrimeSync": { argTypes: [DYN, DYN], result: DYN }, + "crypto.checkPrimeCb": { argTypes: [DYN, DYN, DYN, BOOL], result: VOID }, + "crypto.generatePrimeSync": { argTypes: [DYN, DYN], result: DYN }, + "crypto.generatePrimeCb": { argTypes: [DYN, DYN, DYN, BOOL], result: VOID }, "crypto.hmacNewStr": { argTypes: [STRING, STRING], result: CRYPTOHMAC_T }, "crypto.hmacNewBytes": { argTypes: [STRING, BYTES_U8], result: CRYPTOHMAC_T }, "crypto.hashUpdateStr": { argTypes: [CRYPTOHASH_T, STRING], result: CRYPTOHASH_T }, @@ -2857,11 +2888,13 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ /** WebCrypto (scr_webcrypto.c): each method takes its argument vector as * one dyn array and answers a promise that argument and validation * failures reject. CryptoKey values are native handles. */ - "webcrypto.importKey": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, "webcrypto.generateKey": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, + "webcrypto.importKey": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, "webcrypto.exportKey": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, "webcrypto.encrypt": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, "webcrypto.decrypt": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, + "webcrypto.wrapKey": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, + "webcrypto.unwrapKey": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, "webcrypto.deriveBits": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, "webcrypto.deriveKey": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, "webcrypto.sign": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, @@ -3570,7 +3603,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "weakSet.new": { argTypes: [DYN], result: DYN }, "dyn.fromEntries": { argTypes: [DYN], result: DYN }, "bytes.constructor": { argTypes: [STRING], result: DYN }, - "bytes.instanceOf": { argTypes: [DYN, DYN], result: BOOL }, + "bytes.instanceOf": { argTypes: [DYN, DYN, STRING], result: BOOL }, "bytes.construct": { argTypes: [DYN, DYN, STRING], result: DYN }, /** The ambient receiver — JS `this` in a plain (non-method) function * body: the innermost binding the current firing/dispatch window @@ -3875,7 +3908,11 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ * readable.unpipe (recv[, dst]) → recv +1. * writable.write/writeStr: (recv, chunk[, cb]) → below-hwm bool (cb * MOVES when present, called after the user write completes). - * writable.end: (recv, flags[, chunk][, cb]) → recv +1. cork/uncork are + * writable.writeEnc: (recv, flags, chunk[, encoding][, cb]) — flags 1 + * an encoding argument, 2 a callback (the encoding is then null, as + * Node's write shifts it). + * writable.end: (recv, flags[, chunk][, encoding][, cb]) → recv +1; + * flags 16: an encoding argument follows the dyn chunk. cork/uncork are * void. stream.destroy/destroyErr: (recv[, err]) → recv +1. * stream.prop: (recv, name-literal) → the flag/number the name asks * for; stream.errored → Error|null union; readable.flowing → @@ -4014,6 +4051,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "readable.pushU": { argTypes: [null, null], result: BOOL }, "readable.pushDyn": { argTypes: [null, DYN], result: BOOL }, "readable.unshift": { argTypes: [null, BYTES_U8], result: VOID }, + "readable.unshiftDyn": { argTypes: [null, DYN], result: VOID }, "readable.unshiftStr": { argTypes: [null, STRING], result: VOID }, "stream.onDyn": { argTypes: [null, STRING, DYN, BOOL, BOOL], result: VOID }, "readable.readDyn": { argTypes: [null, DYN], result: DYN }, @@ -4025,6 +4063,12 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ * Buffer.from(chunk, enc)); both carry the CANONICAL literal. */ "readable.pushStrEnc": { argTypes: [null, STRING, STRING], result: BOOL }, "readable.pushEncoding": { argTypes: [null, STRING], result: VOID }, + /** The objectMode option's sides (readable, writable), applied right + * after construction; answers the receiver (+1, the chaining shape). */ + "stream.objectMode": { argTypes: [null, BOOL, BOOL], result: VOID }, + /** Whether a readable.nextChunkDyn step answered the end (the done + * marker; an object-mode stream can yield undefined). */ + "stream.chunkDone": { argTypes: [DYN], result: BOOL }, /** for-await over a readable (the desugared loop's per-pass promise): * +1 promise of the next chunk — buffered content, the EOF sentinel * (empty Buffer / dyn undefined), or a rejection with the stream's @@ -4078,6 +4122,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "writable.writeStr": { argTypes: [null, STRING], result: BOOL }, "writable.writeU": { argTypes: [null, null], result: BOOL }, "writable.writeDyn": { argTypes: [null, DYN], result: BOOL }, + "writable.writeEnc": { argTypes: [null, F64, DYN], result: BOOL }, "writable.end": { argTypes: [null, F64], result: VOID }, "writable.cork": { argTypes: [null], result: VOID }, "writable.uncork": { argTypes: [null], result: VOID }, @@ -4663,6 +4708,19 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ * body passes its undefined result on into a Buffer slot. Owned (+1); * never throws. */ "bytes.absent": { argTypes: [], result: BYTES_U8 }, + /** node:crypto asymmetric keys and signatures (scr_pki.c): checked- + * dynamic arguments, borrowed; KeyObject, Sign and Verify handles, + * Buffers and booleans as checked-dynamic results (+1). Node's argument + * and decoder errors throw (may-throw); generateKeyPairCb's callback + * receives (null, publicKey, privateKey) in a later macrotask. */ + "crypto.createPrivateKey": { argTypes: [DYN], result: DYN }, + "crypto.createPublicKey": { argTypes: [DYN], result: DYN }, + "crypto.generateKeyPairSync": { argTypes: [DYN, DYN], result: DYN }, + "crypto.generateKeyPairCb": { argTypes: [DYN, DYN, DYN], result: VOID }, + "crypto.signOneshot": { argTypes: [DYN, DYN, DYN], result: DYN }, + "crypto.verifyOneshot": { argTypes: [DYN, DYN, DYN, DYN], result: DYN }, + "crypto.createSign": { argTypes: [DYN], result: DYN }, + "crypto.createVerify": { argTypes: [DYN], result: DYN }, /** The Buffer brand of a Uint8Array source onto a fresh Uint8Array * result (map and filter of a Buffer return a Buffer). */ "bytes.copyBrand": { argTypes: [BYTES_U8, BYTES_U8], result: VOID }, diff --git a/packages/compiler/src/ir/integer-loops.test.ts b/packages/compiler/src/ir/integer-loops.test.ts index 24bbf5be0..b6ac27e3a 100644 --- a/packages/compiler/src/ir/integer-loops.test.ts +++ b/packages/compiler/src/ir/integer-loops.test.ts @@ -426,3 +426,119 @@ test("paired counters closing in on each other run as integers", () => { unused.body = []; expect(matchIntegerPairedForLoop(unused, vars)).toBeNull(); }); + +test("counters stepped beside the tested one run as integers", () => { + const vars = new Map(locals); + for (const id of ["j", "k", "limit", "from"]) + vars.set(id, { id, name: id, type: F64, mutable: true }); + vars.set("s", { id: "s", name: "s", type: STRING, mutable: false }); + const step = (id: string, op: "+" | "-", value = 1): IrStmt => ({ + kind: "assign", + localId: id, + value: { kind: "bin", op, left: ref(id), right: number(value), type: F64, loc }, + loc, + }); + const read = (index: IrExpr): IrStmt => ({ + kind: "exprStmt", + expr: { + kind: "strIntrinsic", + method: "charCodeAt", + receiver: { kind: "varRef", localId: "s", type: STRING, loc }, + args: [index], + type: F64, + loc, + }, + loc, + }); + const comoving = ( + limit: IrExpr, + followerStart: IrExpr = number(0), + update: IrStmt = { kind: "block", body: [step("i", "+"), step("j", "+", 4)], loc }, + ) => + ({ + kind: "for", + loc, + init: { + kind: "block", + body: [ + { kind: "varDecl", localId: "i", init: number(0), loc }, + { kind: "varDecl", localId: "j", init: followerStart, loc }, + ], + loc, + }, + cond: { kind: "bin", op: "<", left: ref("i"), right: limit, type: BOOL, loc }, + update, + body: [read(ref("j"))], + }) as IrStmt & { kind: "for" }; + // A literal limit bounds the follower: at most 9 updates of 4 before the + // last body entry. + expect(matchIntegerCountedForLoop(comoving(number(10)), vars)).toMatchObject({ + localId: "i", + guarded: false, + range: { min: 0, max: 9 }, + followers: [ + { + localId: "j", + step: 4, + startGuarded: false, + tripGuarded: false, + range: { min: 0, max: 36 }, + }, + ], + }); + // An unknown limit or start is tested at run time. + expect(matchIntegerCountedForLoop(comoving(ref("limit")), vars)).toMatchObject({ + guarded: true, + followers: [{ tripGuarded: true, range: { min: 0, max: 2 ** 52 + 2 } }], + }); + expect(matchIntegerCountedForLoop(comoving(number(10), ref("from")), vars)).toMatchObject({ + guarded: true, + followers: [{ startGuarded: true, tripGuarded: true }], + }); + // A literal limit with known starts that can pass 2^53 is refused. + expect(matchIntegerCountedForLoop(comoving(number(10), number(2 ** 53 - 9)), vars)).toBeNull(); + // Nested update blocks (`i++, j += 4, k--`) step three counters. + const three = comoving(number(10), number(0), { + kind: "block", + body: [{ kind: "block", body: [step("i", "+"), step("j", "+", 4)], loc }, step("k", "-")], + loc, + }); + if (three.init?.kind === "block") + three.init.body.push({ kind: "varDecl", localId: "k", init: number(5), loc }); + expect(matchIntegerCountedForLoop(three, vars)?.followers).toMatchObject([ + { localId: "j", range: { min: 0, max: 36 } }, + { localId: "k", step: -1, range: { min: -4, max: 5 } }, + ]); + // A follower without a step, one the body writes, one the limit reads, a + // captured one, and a start that reads another counter are refused. + expect( + matchIntegerCountedForLoop( + comoving(number(10), number(0), { kind: "block", body: [step("i", "+")], loc }), + vars, + ), + ).toBeNull(); + const writing = comoving(number(10)); + writing.body.push(step("j", "+")); + expect(matchIntegerCountedForLoop(writing, vars)).toBeNull(); + expect(matchIntegerCountedForLoop(comoving(ref("j")), vars)).toBeNull(); + const boxed = new Map(vars); + boxed.set("j", { ...vars.get("j")!, boxed: true }); + expect(matchIntegerCountedForLoop(comoving(number(10)), boxed)).toBeNull(); + expect(matchIntegerCountedForLoop(comoving(number(10), ref("i")), vars)).toBeNull(); + // A driver span above 2^53 - 1 bounds no trip count. From -(2^53 - 1) to + // below 2^52 - 1 the span 3 * 2^52 - 3 rounds down to 3 * 2^52 - 4, which + // would put this follower's last value at 2^53 - 1 instead of 2^53. + const wide = comoving(number(2 ** 52 - 1), number(-(2 ** 52) + 3), { + kind: "block", + body: [step("i", "+"), step("j", "+")], + loc, + }); + if (wide.init?.kind === "block" && wide.init.body[0]?.kind === "varDecl") + wide.init.body[0].init = number(-Number.MAX_SAFE_INTEGER); + expect(matchIntegerCountedForLoop(wide, vars)).toBeNull(); + if (wide.init?.kind === "block" && wide.init.body[1]?.kind === "varDecl") + wide.init.body[1].init = ref("from"); + expect(matchIntegerCountedForLoop(wide, vars)?.followers).toMatchObject([ + { startGuarded: true, tripGuarded: true }, + ]); +}); diff --git a/packages/compiler/src/ir/integer-loops.ts b/packages/compiler/src/ir/integer-loops.ts index e80680d6e..c451c0852 100644 --- a/packages/compiler/src/ir/integer-loops.ts +++ b/packages/compiler/src/ir/integer-loops.ts @@ -28,8 +28,36 @@ export interface IntegerCountedForLoop { startGuarded: boolean; guardLimit: number; range: IntegerRange; + /** Counters declared and stepped beside this one that the condition does + * not test, such as `j` in `for (let i = 0, j = 0; i < n; i++, j += 4)`. */ + followers: IntegerLoopFollower[]; } +/** A counter that steps by a constant on every update of a counted loop. + * After `k` updates it holds `start + step * k`. */ +export interface IntegerLoopFollower { + localId: string; + start: IrExpr; + step: number; + /** As the driver's `startGuarded` (part of the loop's `guarded`). */ + startGuarded: boolean; + /** The follower's range depends on the run-time trip count: the versioned + * loop also tests `|start| + |step| * span / |driver step|` against + * `FOLLOWER_GUARD_LIMIT`, where `span` is the distance from the driver's + * start to the limit (part of the loop's `guarded`). */ + tripGuarded: boolean; + /** The follower's values on every body entry. */ + range: IntegerRange; +} + +/** The bound of the run-time follower test. */ +export const FOLLOWER_GUARD_LIMIT = 2 ** 52; +/** The magnitude a follower that passed the test can reach. The test's four + * f64 operations (the step ratio, the span, its product and the sum) each + * round down by at most a factor of 1 - 2^-53, so the exact sum is below + * `2^52 / (1 - 2^-53)^4`, which is less than 2^52 + 3. */ +const FOLLOWER_GUARD_RANGE = FOLLOWER_GUARD_LIMIT + 2; + function constantStep(update: IrStmt | null, id: string): number | null { if (update?.kind === "exprStmt" && update.expr.kind === "incDec" && update.expr.localId === id) return update.expr.op === "+" ? 1 : -1; @@ -115,33 +143,61 @@ export function matchIntegerCountedForLoop( ranges: IntegerRanges = new Map(), unaliased: ReadonlySet | null = null, ): IntegerCountedForLoop | null { - const init = stmt.init; - if (init?.kind !== "varDecl" || !init.init) return null; - const local = locals.get(init.localId); - if (local?.type.kind !== "f64" || !local.mutable || local.boxed || local.tdz) return null; - const start = init.init; - let startRange = - start.kind === "numLit" && Number.isSafeInteger(start.value) && !Object.is(start.value, -0) - ? { min: start.value, max: start.value } - : ranges.get(start); - // An unproven start is tested at run time, on a versioned loop. - const startGuarded = !startRange && start.kind !== "numLit" && start.type.kind === "f64"; - if (startGuarded) startRange = { min: -Number.MAX_SAFE_INTEGER, max: Number.MAX_SAFE_INTEGER }; - if ( - !startRange || - startRange.min < -Number.MAX_SAFE_INTEGER || - startRange.max > Number.MAX_SAFE_INTEGER || - !stableLoopValue(start, locals, stmt.body, false, local.id) - ) - return null; - const step = constantStep(stmt.update, local.id); - if (step === null || writesLocal(stmt.body, local.id)) return null; const cond = stmt.cond; if (cond?.kind !== "bin") return null; + const decls = stmt.init?.kind === "block" ? stmt.init.body : stmt.init ? [stmt.init] : []; + // `i++, j += 2, k--` lowers to nested blocks of steps. + const flat = (u: IrStmt | null): (IrStmt | null)[] => + u?.kind === "block" ? u.body.flatMap(flat) : [u]; + const updates = flat(stmt.update); + if (decls.length === 0 || updates.length !== decls.length) return null; + const declared = (e: IrExpr): string | null => + e.kind === "varRef" && decls.some((d) => d.kind === "varDecl" && d.localId === e.localId) + ? e.localId + : null; + // The condition tests the driving counter; every other declaration + // follows it. + const driver = declared(cond.left) ?? declared(cond.right); + const counterIds = decls.map((d) => (d.kind === "varDecl" ? d.localId : "")); + const stepOf = (id: string): number | null => { + const steps = updates.map((u) => constantStep(u, id)).filter((v) => v !== null); + return steps.length === 1 ? steps[0]! : null; + }; + // A counter's start is a stable integer that refers to no counter. An + // unproven start is tested at run time, on a versioned loop. + const counterStart = ( + id: string, + ): { start: IrExpr; range: IntegerRange; guarded: boolean; step: number } | null => { + const decl = decls.find((d) => d.kind === "varDecl" && d.localId === id); + if (decl?.kind !== "varDecl" || !decl.init) return null; + const local = locals.get(id); + if (local?.type.kind !== "f64" || !local.mutable || local.boxed || local.tdz) return null; + const start = decl.init; + const known = + start.kind === "numLit" && Number.isSafeInteger(start.value) && !Object.is(start.value, -0) + ? { min: start.value, max: start.value } + : ranges.get(start); + const guarded = !known && start.kind !== "numLit" && start.type.kind === "f64"; + const range = guarded ? { min: -Number.MAX_SAFE_INTEGER, max: Number.MAX_SAFE_INTEGER } : known; + const step = stepOf(id); + if ( + !range || + range.min < -Number.MAX_SAFE_INTEGER || + range.max > Number.MAX_SAFE_INTEGER || + !counterIds.every((counter) => stableLoopValue(start, locals, stmt.body, false, counter)) || + step === null || + writesLocal(stmt.body, id) + ) + return null; + return { start, range, guarded, step }; + }; + const head = driver === null ? null : counterStart(driver); + if (!head) return null; + const local = locals.get(driver!)!; + const { start, step, guarded: startGuarded, range: startRange } = head; let limit = cond.right, op: string = cond.op; if (cond.left.kind !== "varRef" || cond.left.localId !== local.id) { - if (cond.right.kind !== "varRef" || cond.right.localId !== local.id) return null; limit = cond.left; op = ({ "<": ">", "<=": ">=", ">": "<", ">=": "<=" } as Record)[op] ?? ""; } @@ -185,11 +241,70 @@ export function matchIntegerCountedForLoop( const limitGuarded = !boundRange || (step > 0 ? !(boundRange.max <= guardLimit) : !(boundRange.min >= guardLimit)); if (limitGuarded && limit.kind === "numLit") return null; - const guarded = limitGuarded || startGuarded; + const range = + step > 0 + ? { + min: startRange.min, + max: Math.max( + startRange.min, + Math.min( + Number.MAX_SAFE_INTEGER, + boundRange + ? Math.ceil(boundRange.max) - (inclusive ? 0 : 1) + : Number.MAX_SAFE_INTEGER, + ), + ), + } + : { + min: Math.min( + startRange.max, + Math.max( + -Number.MAX_SAFE_INTEGER, + boundRange + ? Math.floor(boundRange.min) + (inclusive ? 0 : 1) + : -Number.MAX_SAFE_INTEGER, + ), + ), + max: startRange.max, + }; + // On body entry k the driver holds `start + step * k`, so k is at most the + // driver's span over |step|, and a follower holds its start plus its own + // step times k. A span above 2^53 - 1 can round down, so it bounds no + // trip count. Below it the quotient rounds to at least its floor, and the + // follower's products and sums below are exact while they stay within + // ±(2^53 - 1) and round outside it otherwise. + const span = step > 0 ? range.max - startRange.min : startRange.max - range.min; + const trips = span <= Number.MAX_SAFE_INTEGER ? Math.floor(span / Math.abs(step)) : Infinity; + const followers: IntegerLoopFollower[] = []; + for (const id of counterIds) { + if (id === local.id) continue; + const follower = counterStart(id); + // The limit is evaluated once; a follower changes on every update. + if (!follower || !stableLoopValue(limit, locals, stmt.body, false, id)) return null; + const reach = follower.step * trips; + const min = follower.range.min + Math.min(0, reach), + max = follower.range.max + Math.max(0, reach); + const tripGuarded = min < -Number.MAX_SAFE_INTEGER || max > Number.MAX_SAFE_INTEGER; + // Known starts and a literal limit are not tested at run time. + if (tripGuarded && !follower.guarded && !startGuarded && limit.kind === "numLit") return null; + followers.push({ + localId: id, + start: follower.start, + step: follower.step, + startGuarded: follower.guarded, + tripGuarded, + range: tripGuarded + ? { min: Math.max(-FOLLOWER_GUARD_RANGE, min), max: Math.min(FOLLOWER_GUARD_RANGE, max) } + : { min, max }, + }); + } + const guarded = + limitGuarded || startGuarded || followers.some((f) => f.startGuarded || f.tripGuarded); let useful = false; let cost = 0; const counter = (e: IrExpr): boolean => - (e.kind === "varRef" && e.localId === local.id) || + (e.kind === "varRef" && + (e.localId === local.id || followers.some((f) => f.localId === e.localId))) || (e.kind === "bin" && (e.op === "+" || e.op === "-" || e.op === "*") && (counter(e.left) || counter(e.right))); @@ -241,32 +356,6 @@ export function matchIntegerCountedForLoop( return (!guarded || ++cost <= 80) && everyStmtChild(s, expr, bodyStmt); }; if (!stmt.body.every(bodyStmt) || !useful) return null; - const range = - step > 0 - ? { - min: startRange.min, - max: Math.max( - startRange.min, - Math.min( - Number.MAX_SAFE_INTEGER, - boundRange - ? Math.ceil(boundRange.max) - (inclusive ? 0 : 1) - : Number.MAX_SAFE_INTEGER, - ), - ), - } - : { - min: Math.min( - startRange.max, - Math.max( - -Number.MAX_SAFE_INTEGER, - boundRange - ? Math.floor(boundRange.min) + (inclusive ? 0 : 1) - : -Number.MAX_SAFE_INTEGER, - ), - ), - max: startRange.max, - }; return { localId: local.id, start, @@ -278,6 +367,7 @@ export function matchIntegerCountedForLoop( startGuarded, guardLimit, range, + followers, }; } diff --git a/packages/compiler/src/ir/parameter-versions.ts b/packages/compiler/src/ir/parameter-versions.ts new file mode 100644 index 000000000..88232251c --- /dev/null +++ b/packages/compiler/src/ir/parameter-versions.ts @@ -0,0 +1,307 @@ +import { analyzeInt32Slots } from "./int32-slots.js"; +import { BOOL, F64, type IrExpr, type IrFunction, type IrModule, type IrStmt } from "./ir.js"; +import { reparseStmt } from "./serialize.js"; +import { everyExprChild, everyStmtChild, everyStmtList } from "./traverse.js"; + +const MAX_FUNCTION_NODES = 2048; +const MAX_VERSIONS = 16; + +function size(fn: IrFunction): number { + let nodes = 0; + everyStmtList(fn.body, { + stmt: () => ++nodes <= MAX_FUNCTION_NODES, + expr: () => ++nodes <= MAX_FUNCTION_NODES, + }); + return nodes; +} + +/** The function a direct call or construction runs, if any. */ +function callee(e: IrExpr): string | null { + if (e.kind === "call") return e.callee; + if (e.kind === "new") return `%${e.className}.constructor`; + return null; +} + +/** Direct call and construction sites of each function in the module. */ +function callSites(mod: IrModule): Map { + const sites = new Map(); + for (const fn of mod.functions) + everyStmtList(fn.body, { + stmt: () => true, + expr: (e) => { + const name = callee(e); + if (name !== null) sites.set(name, (sites.get(name) ?? 0) + 1); + return true; + }, + }); + return sites; +} + +/** Expressions and statements of a callee that LLVM inlines at any number + * of call sites: scalar arithmetic and reads, no calls or allocations. */ +const SIMPLE_EXPRESSIONS = new Set([ + "varRef", + "numLit", + "boolLit", + "bin", + "unary", + "logical", + "ternary", + "toBool", + "incDec", + "assignExpr", + "arrayGet", + "fieldGet", + "recordGet", + "unionNarrow", + "unionIsTag", +]); +const SIMPLE_STATEMENTS = new Set([ + "varDecl", + "assign", + "exprStmt", + "if", + "block", + "return", +]); + +function simple(fn: IrFunction): boolean { + return ( + (fn.returnType.kind === "f64" || fn.returnType.kind === "bool") && + everyStmtList(fn.body, { + stmt: (s) => SIMPLE_STATEMENTS.has(s.kind), + expr: (e) => SIMPLE_EXPRESSIONS.has(e.kind), + }) + ); +} + +/** Whether a copy of `fn` would cost a callee its only call site. LLVM + * inlines a function called once wherever its size allows; with a second + * site, both calls can stay out of line, with the records and arguments + * they pass built in memory. A callee that only computes a number or a + * boolean from scalar reads inlines either way. A function calling itself + * would also dispatch on every call. */ +function keepsSingleBody( + fn: IrFunction, + sites: ReadonlyMap, + functions: ReadonlyMap, +): boolean { + return !everyStmtList(fn.body, { + stmt: () => true, + expr: (e) => { + const name = callee(e); + if (name === null) return true; + if (name === fn.name) return false; + if (sites.get(name) !== 1) return true; + const target = functions.get(name); + return target !== undefined && simple(target); + }, + }); +} + +/** Statements and expressions of a loop whose work is numeric: arithmetic, + * element and field reads and writes, `Math` calls and calls of simple + * functions. Loops that allocate, build strings or call other code gain + * little from an integer bound and would only grow the copy. */ +const NUMERIC_STATEMENTS = new Set([ + ...SIMPLE_STATEMENTS, + "for", + "while", + "doWhile", + "break", + "continue", + "arraySet", + "fieldSet", +]); +const NUMERIC_EXPRESSIONS = new Set([...SIMPLE_EXPRESSIONS, "arrayHas", "arrayState"]); + +function numericLoop(loop: IrStmt, functions: ReadonlyMap): boolean { + const expr = (e: IrExpr): boolean => { + let allowed = NUMERIC_EXPRESSIONS.has(e.kind); + if (e.kind === "call") { + const target = functions.get(e.callee); + allowed = target !== undefined && simple(target); + } else if (e.kind === "libCall") allowed = e.fn.startsWith("math."); + else if (e.kind === "arrIntrinsic") allowed = e.method === "length"; + return allowed && everyExprChild(e, expr, stmt); + }; + const stmt = (s: IrStmt): boolean => + NUMERIC_STATEMENTS.has(s.kind) && everyStmtChild(s, expr, stmt); + return stmt(loop); +} + +/** Number parameters of `fn` that bound one of its numeric loops: read in + * the loop's condition or in its `for` initializer. */ +function loopBounds(fn: IrFunction, functions: ReadonlyMap): Set { + const params = new Set( + fn.params.filter((param) => param.type.kind === "f64").map((param) => param.localId), + ); + const result = new Set(); + if (params.size === 0) return result; + const reads = (e: IrExpr): boolean => { + if (e.kind === "varRef" && params.has(e.localId)) result.add(e.localId); + return everyExprChild(e, reads, readsStmt); + }; + const readsStmt = (s: IrStmt): boolean => everyStmtChild(s, reads, readsStmt); + everyStmtList(fn.body, { + stmt: (s) => { + if (s.kind !== "for" && s.kind !== "while" && s.kind !== "doWhile") return true; + if (!numericLoop(s, functions)) return true; + if (s.cond) reads(s.cond); + if (s.kind === "for" && s.init) readsStmt(s.init); + return true; + }, + expr: () => true, + }); + return result; +} + +/** Versions of functions whose number parameters bound their loops. + * + * A parameter that every call site proves an int32 already reaches the + * function's i32 entry (int32-abi.ts). One that some caller passes as an + * unproven number (a value read from a message, a field, a parse) keeps + * its double, so the loops it bounds compare a counter with a double and + * the values derived from it stay doubles. + * + * A parameter qualifies when it bounds a numeric loop (`numericLoop`). + * Such a function gets a copy of its body under a new name, and its own + * body starts by calling the copy when every such parameter holds an + * int32: `(p | 0) === p` (an integer in the int32 range, not NaN) and not + * -0 (`1 / p` is not -Infinity). The copy then receives `p | 0`, which is + * the same double, so it computes what the body would; its only caller + * proves the parameter an int32, so it gets the i32 entry. Other values + * run the body as before. + * + * Only plain synchronous functions qualify: not class members, no + * captures, not the module entry, not dynamic-tier, at most + * MAX_FUNCTION_NODES nodes, not calling themselves, and not holding the + * only call site of another function (`keepsSingleBody`). The caller + * skips modules that observe call stacks, where the copy's frame could be + * seen. */ +export function versionLoopBoundParameters( + mod: IrModule, + symbolName: (name: string) => string = (name) => name, +): IrModule { + const candidates: { fn: IrFunction; params: Set }[] = []; + const sites = callSites(mod); + const functions = new Map(mod.functions.map((fn) => [fn.name, fn])); + const members = new Set((mod.classes ?? []).flatMap((cls) => cls.methods ?? [])); + for (const fn of mod.functions) { + if ( + // Constructors, methods and accessors (`%C.m`) and generated helper + // families (`%arr.map_…`) keep their single body. + fn.name.includes(".") || + members.has(fn.name) || + fn.async || + fn.generator || + fn.captures !== undefined || + fn.classCaptures?.length || + fn.tier === "dynamic" || + fn.name === mod.entry + ) + continue; + const params = loopBounds(fn, functions); + const locals = new Map(fn.locals.map((local) => [local.id, local])); + for (const id of params) { + const local = locals.get(id); + if (!local || local.boxed || local.tdz) params.delete(id); + } + if (params.size === 0 || size(fn) > MAX_FUNCTION_NODES || keepsSingleBody(fn, sites, functions)) + continue; + candidates.push({ fn, params }); + } + if (candidates.length === 0) return mod; + const slots = analyzeInt32Slots(mod); + const names = new Set(mod.functions.map((fn) => symbolName(fn.name))); + const replaced = new Map(); + const added: IrFunction[] = []; + for (const { fn, params } of candidates) { + if (added.length >= MAX_VERSIONS) break; + const unproven = fn.params.filter( + (param) => params.has(param.localId) && !slots.hasParam(fn.name, param.localId), + ); + if (unproven.length === 0) continue; + let name = `${fn.name}.int32Params`; + while (names.has(symbolName(name))) name += "_"; + names.add(symbolName(name)); + const { ownsPrototype: _ownsPrototype, ...implementation } = fn; + added.push({ ...implementation, name, body: fn.body.map(reparseStmt) }); + const loc = fn.body[0]?.loc ?? { file: mod.sourceFile, start: 0, end: 0 }; + const read = (localId: string): IrExpr => ({ kind: "varRef", localId, type: F64, loc }); + const int32 = (localId: string): IrExpr => ({ + kind: "bin", + op: "|", + left: read(localId), + right: { kind: "numLit", value: 0, type: F64, loc }, + type: F64, + loc, + }); + let test: IrExpr | null = null; + for (const param of unproven) { + const exact: IrExpr = { + kind: "bin", + op: "===", + left: int32(param.localId), + right: read(param.localId), + type: BOOL, + loc, + }; + const notNegativeZero: IrExpr = { + kind: "bin", + op: "!==", + left: { + kind: "bin", + op: "/", + left: { kind: "numLit", value: 1, type: F64, loc }, + right: read(param.localId), + type: F64, + loc, + }, + right: { kind: "numLit", value: -Infinity, type: F64, loc }, + type: BOOL, + loc, + }; + const both: IrExpr = { + kind: "logical", + op: "&&", + left: exact, + right: notNegativeZero, + type: BOOL, + loc, + }; + test = + test === null + ? both + : { kind: "logical", op: "&&", left: test, right: both, type: BOOL, loc }; + } + const versioned = new Set(unproven.map((param) => param.localId)); + const call: IrExpr = { + kind: "call", + callee: name, + args: fn.params.map((param) => + versioned.has(param.localId) + ? int32(param.localId) + : { kind: "varRef", localId: param.localId, type: param.type, loc }, + ), + type: fn.returnType, + loc, + }; + const run: IrStmt[] = + fn.returnType.kind === "void" + ? [ + { kind: "exprStmt", expr: call, loc }, + { kind: "return", value: null, loc }, + ] + : [{ kind: "return", value: call, loc }]; + replaced.set(fn, { + ...fn, + body: [{ kind: "if", cond: test!, then: run, else_: null, loc }, ...fn.body], + }); + } + if (added.length === 0) return mod; + return { + ...mod, + functions: [...mod.functions.map((fn) => replaced.get(fn) ?? fn), ...added], + }; +} diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts index 9531d7845..ec2edb931 100644 --- a/packages/compiler/src/ir/runtime-features.ts +++ b/packages/compiler/src/ir/runtime-features.ts @@ -216,6 +216,8 @@ const ABORT_SIGNAL_LIB_FNS: ReadonlySet = new Set([ /** Entry points that link the stream unit (with STREAM_LIB_PREFIXES). */ const STREAM_LIB_FNS: ReadonlySet = new Set([ "child.stdioStream", + "stream.chunkDone", + "stream.objectMode", "console.bind", "crypto.hashStream", "crypto.hashStreamPiped", @@ -624,6 +626,73 @@ const rule = (r: RuntimeFeatureRule): RuntimeFeatureRule => r; /* The HTTP, HTTPS, HTTP/2 and TLS surfaces, which link the net unit. */ const NET_PREFIXES: readonly string[] = ["net.", "http.", "tls.", "https.", "http2."]; +/** The scr_dh.c entry points (the dh unit). */ +export const DH_LIB_FNS: ReadonlySet = new Set([ + "crypto.dhNew", + "crypto.dhGroupNew", + "crypto.checkPrimeSync", + "crypto.checkPrimeCb", + "crypto.generatePrimeSync", + "crypto.generatePrimeCb", +]); + +/** The scr_pki.c entry points (the pki unit). */ +export const PKI_LIB_FNS: ReadonlySet = new Set([ + "crypto.createPrivateKey", + "crypto.createPublicKey", + "crypto.createSign", + "crypto.createVerify", + "crypto.generateKeyPairCb", + "crypto.generateKeyPairSync", + "crypto.signOneshot", + "crypto.verifyOneshot", +]); + +/** The WebCrypto algorithms scr_webcrypto_pki.c implements over the pki + * unit (lowercased). */ +const WEBCRYPTO_ASYMMETRIC_NAMES: ReadonlySet = new Set([ + "ecdsa", + "ed25519", + "rsa-pss", + "rsassa-pkcs1-v1_5", +]); + +/** Whether a string literal in the module names Ed25519, without regard + * to case ("ed25519" for node:crypto, "Ed25519" for WebCrypto): the + * Ed25519 unit links then, alongside the pki unit. */ +export function moduleNamesEd25519(mod: IrModule): boolean { + let named = false; + everyModuleNode(mod, { + expr: (node) => { + if (node.kind === "strLit" && node.value.toLowerCase() === "ed25519") named = true; + return !named; + }, + stmt: () => !named, + type: () => true, + }); + return named; +} + +/** Whether the module calls WebCrypto and a string literal in it names one + * of the asymmetric algorithms (matched without regard to case, as + * normalizeAlgorithm matches names): scr_webcrypto_pki.c and the pki unit + * link then. */ +export function moduleUsesWebCryptoAsymmetric(mod: IrModule): boolean { + let webcrypto = false; + let named = false; + everyModuleNode(mod, { + expr: (node) => { + if (node.kind === "libCall" && node.fn.startsWith("webcrypto.")) webcrypto = true; + if (node.kind === "strLit" && WEBCRYPTO_ASYMMETRIC_NAMES.has(node.value.toLowerCase())) + named = true; + return !(webcrypto && named); + }, + stmt: () => !(webcrypto && named), + type: () => true, + }); + return webcrypto && named; +} + /** One rule per runtime feature, sorted by name. A new feature is an * entry here and its line in noRuntimeFeatures; RuntimeFeatures, the scan * and its early exit derive from this table. The lists above keep one @@ -650,9 +719,17 @@ const RUNTIME_FEATURE_RULES = { }), /** scr_cipher.c over the mbedTLS archive; DEP0182 reports through the * process-warning unit, and unknown output encodings render through the - * inspect unit. WebCrypto's encrypt and decrypt (scr_webcrypto_aes.c) - * link the same archive. */ - cipher: rule({ fns: ["crypto.cipherNew", "webcrypto.encrypt", "webcrypto.decrypt"] }), + * inspect unit. WebCrypto's encrypt, decrypt, wrapKey and unwrapKey + * (scr_webcrypto_aes.c) link the same archive. */ + cipher: rule({ + fns: [ + "crypto.cipherNew", + "webcrypto.encrypt", + "webcrypto.decrypt", + "webcrypto.wrapKey", + "webcrypto.unwrapKey", + ], + }), copying: rule({ expr: (node, _tables) => (node.kind === "arrIntrinsic" && COPYING_ARR_METHODS.has(node.method)) || @@ -688,6 +765,8 @@ const RUNTIME_FEATURE_RULES = { "crypto.cipherNew", "webcrypto.encrypt", "webcrypto.decrypt", + "webcrypto.wrapKey", + "webcrypto.unwrapKey", "crypto.ecdhNew", ], prefixes: [...READABLE_DYN_ASYNC_PREFIXES, "wasm."], @@ -751,6 +830,10 @@ const RUNTIME_FEATURE_RULES = { * process-warning unit, and unknown encodings and formats render through * the inspect unit. */ ecdh: rule({ fns: ["crypto.ecdhNew", "crypto.ecdhConvertKey"] }), + /** scr_dh.c over the mbedTLS archive, also for checkPrime and + * generatePrime; unknown encodings and argument values render through + * the inspect unit. */ + dh: rule({ fns: [...DH_LIB_FNS] }), emitter: rule({ initial: (mod) => mod.workers === true || (mod.classes ?? []).some((c) => c.name === RUNTIME_EMITTER_CLASS), @@ -796,8 +879,11 @@ const RUNTIME_FEATURE_RULES = { "crypto.cipherNew", "webcrypto.encrypt", "webcrypto.decrypt", + "webcrypto.wrapKey", + "webcrypto.unwrapKey", "crypto.ecdhNew", "crypto.ecdhConvertKey", + ...DH_LIB_FNS, ...BUILTIN_CLASS_LIB_FNS, ...UTIL_CALLABLE_LIB_FNS, ], @@ -828,6 +914,12 @@ const RUNTIME_FEATURE_RULES = { /** perf_hooks event-loop metrics (eventLoopUtilization, * monitorEventLoopDelay): scr_perf_loop.c. */ perfLoop: rule({ fns: ["perf.idleTime"], prefixes: ["perf.loop", "perf.eld"] }), + /** scr_pki.c over the mbedTLS archive: asymmetric KeyObjects and + * signatures. */ + pki: rule({ initial: moduleUsesWebCryptoAsymmetric, fns: [...PKI_LIB_FNS] }), + /** scr_ed25519.c (the vendored libsodium Ed25519), linked with pki: a + * string literal in the module names Ed25519. */ + ed25519: rule({ initial: moduleNamesEd25519 }), processEvents: rule({ fns: [...PROCESS_EVENT_LIB_FNS, "util.styleText"], types: ["procStream"] }), /** scr_url_legacy.c parses and stringifies queries. */ qs: rule({ fns: [...QS_LIB_FNS, ...LEGACY_URL_LIB_FNS] }), @@ -919,6 +1011,7 @@ function noRuntimeFeatures(): RuntimeFeatures { dynInvoke: false, dynTier: false, ecdh: false, + dh: false, emitter: false, eventTarget: false, eventsOnceSignal: false, @@ -935,6 +1028,8 @@ function noRuntimeFeatures(): RuntimeFeatures { nodeTest: false, parseArgs: false, perfLoop: false, + pki: false, + ed25519: false, processEvents: false, qs: false, regex: false, diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts index 9de1b5b62..bb36c7007 100644 --- a/packages/compiler/src/ir/validate.ts +++ b/packages/compiler/src/ir/validate.ts @@ -4109,6 +4109,7 @@ function validateFunction( e.fn === "writable.write" || e.fn === "writable.writeStr" || e.fn === "writable.writeU" || + e.fn === "writable.writeEnc" || e.fn === "writable.end" || e.fn === "readable.unpipe" || e.fn === "rl.create" || @@ -5492,6 +5493,7 @@ function validateFunction( e.fn.startsWith("passthrough.") || e.fn === "stream.destroy" || e.fn === "stream.destroyErr" || + e.fn === "stream.objectMode" || e.fn === "stream.iteratorClose" || e.fn === "stream.prop" || e.fn === "stream.errored" || @@ -5782,6 +5784,7 @@ function validateFunction( e.fn === "writable.end" || e.fn === "readable.setEncoding" || e.fn === "readable.pushEncoding" || + e.fn === "stream.objectMode" || e.fn === "stream.destroy" || e.fn === "stream.destroyErr" || e.fn === "stream.iteratorClose" @@ -5898,8 +5901,10 @@ function validateFunction( const chunkT = e.args[2]?.type; const ok = chunkT !== undefined && - ((chunkT.kind === "bytes" && chunkT.elem === "u8") || chunkT.kind === "string"); - if (!ok) err(`libCall emitter.emitData chunk must be bytes or string`, e.loc); + ((chunkT.kind === "bytes" && chunkT.elem === "u8") || + chunkT.kind === "string" || + chunkT.kind === "dyn"); + if (!ok) err(`libCall emitter.emitData chunk must be bytes, string or dyn`, e.loc); if (e.type.kind !== "bool") err(`libCall emitter.emitData must be bool`, e.loc); return true; } diff --git a/packages/compiler/src/library/ir-policy.ts b/packages/compiler/src/library/ir-policy.ts index 51aeb6146..e8fad6dd1 100644 --- a/packages/compiler/src/library/ir-policy.ts +++ b/packages/compiler/src/library/ir-policy.ts @@ -52,6 +52,17 @@ const LIB_MODE_REFUSED_PREFIXES: readonly [string, string][] = [ ["tls.", "the node:tls surface"], ["crypto.cipherNew", "crypto.createCipheriv and createDecipheriv"], ["crypto.ecdh", "crypto.createECDH and ECDH.convertKey"], + ["crypto.dhNew", "crypto.createDiffieHellman"], + ["crypto.dhGroupNew", "crypto.getDiffieHellman and createDiffieHellmanGroup"], + ["crypto.checkPrime", "crypto.checkPrime and checkPrimeSync"], + ["crypto.generatePrime", "crypto.generatePrime and generatePrimeSync"], + ["crypto.createPrivateKey", "asymmetric KeyObjects, generateKeyPair, sign and verify"], + ["crypto.createPublicKey", "asymmetric KeyObjects, generateKeyPair, sign and verify"], + ["crypto.createSign", "asymmetric KeyObjects, generateKeyPair, sign and verify"], + ["crypto.createVerify", "asymmetric KeyObjects, generateKeyPair, sign and verify"], + ["crypto.generateKeyPair", "asymmetric KeyObjects, generateKeyPair, sign and verify"], + ["crypto.signOneshot", "asymmetric KeyObjects, generateKeyPair, sign and verify"], + ["crypto.verifyOneshot", "asymmetric KeyObjects, generateKeyPair, sign and verify"], ["webcrypto.", "WebCrypto (crypto.subtle)"], ["fs.watch", "fs.watch"], ["watcher.", "fs.watch"], @@ -159,6 +170,7 @@ export function moduleLibAsyncSurface(mod: IrModule): { surface: string; loc: Sr [features.fsWatch, "fs.watch"], [features.stream, "the node:stream surface"], [features.tls, "the node:tls surface"], + [features.pki, "asymmetric KeyObjects, generateKeyPair, sign and verify"], [features.fetch, "fetch"], [features.eventTarget, "the EventTarget surface"], [features.nodeTest, "the node:test surface"], diff --git a/packages/compiler/src/native/cli.ts b/packages/compiler/src/native/cli.ts index ed8d52293..e2c3a895b 100644 --- a/packages/compiler/src/native/cli.ts +++ b/packages/compiler/src/native/cli.ts @@ -110,6 +110,9 @@ async function main(): Promise { tls: false, cipher: false, ecdh: false, + dh: false, + pki: false, + ed25519: false, webcrypto: false, perfLoop: false, tlsCa: false, diff --git a/packages/compiler/src/native/prepare.ts b/packages/compiler/src/native/prepare.ts index 261879194..9a60f300c 100644 --- a/packages/compiler/src/native/prepare.ts +++ b/packages/compiler/src/native/prepare.ts @@ -217,6 +217,9 @@ function validFeatures(value: unknown): value is NativeLinkFeatures { "tlsCa", "cipher", "ecdh", + "dh", + "pki", + "ed25519", "webcrypto", "perfLoop", ].every((name) => typeof features[name] === "boolean"); diff --git a/packages/compiler/src/native/sanitizer.test.ts b/packages/compiler/src/native/sanitizer.test.ts index e6beb24cc..49867f0ad 100644 --- a/packages/compiler/src/native/sanitizer.test.ts +++ b/packages/compiler/src/native/sanitizer.test.ts @@ -120,6 +120,9 @@ test.skipIf(process.platform !== "linux" && process.platform !== "darwin")( tlsCa: false, cipher: false, ecdh: false, + dh: false, + pki: false, + ed25519: false, webcrypto: false, perfLoop: false, nativeFetch: false, diff --git a/packages/compiler/surface-manifest.json b/packages/compiler/surface-manifest.json index 3e8b9df73..81fb23ae2 100644 --- a/packages/compiler/surface-manifest.json +++ b/packages/compiler/surface-manifest.json @@ -1,6 +1,6 @@ { "schemaVersion": 2, - "compilerVersion": "0.4.1", + "compilerVersion": "0.4.2", "coverage": [ "Entries are projected mechanically from the compiler's own decision tables: the diagnostics registry, the unsupported-syntax dispatch tables, the stdlib and node-builtin lowering tables, and the supported-builtin-module list. Nothing is hand-maintained; the manifest regenerates byte-identically from the source tree at this version.", "Absence from this manifest means 'not projected', never 'unsupported'. Surfaces lowered through dedicated code paths rather than tables are not yet projected: console, JSON, Promise/async and the timer surface, the net/http/tls/https/dgram/dns/assert/test/stream/readline module member surfaces, template literals, the regex slice, global functions (parseInt, parseFloat, isNaN, isFinite), and the process surface outside its ambient slice.", @@ -480,6 +480,18 @@ "status": "static", "note": "recognized module (bare and node:-prefixed specifiers)" }, + { + "id": "node-builtin.crypto.checkPrime", + "kind": "node-builtin", + "name": "crypto.checkPrime", + "status": "static" + }, + { + "id": "node-builtin.crypto.checkPrimeSync", + "kind": "node-builtin", + "name": "crypto.checkPrimeSync", + "status": "static" + }, { "id": "node-builtin.crypto.createCipheriv", "kind": "node-builtin", @@ -496,17 +508,13 @@ "id": "node-builtin.crypto.createDiffieHellman", "kind": "node-builtin", "name": "crypto.createDiffieHellman", - "status": "unsupported", - "code": "SC2020", - "note": "asymmetric-key operations need a public-key stack (bignum, RSA/EC/EdDSA math) and public and private KeyObjects — neither exists in the static runtime, so no faithful lowering can be small; the lowered crypto surface is hashing, randomness, secret KeyObjects, and the introspection statics" + "status": "static" }, { "id": "node-builtin.crypto.createDiffieHellmanGroup", "kind": "node-builtin", "name": "crypto.createDiffieHellmanGroup", - "status": "unsupported", - "code": "SC2020", - "note": "asymmetric-key operations need a public-key stack (bignum, RSA/EC/EdDSA math) and public and private KeyObjects — neither exists in the static runtime, so no faithful lowering can be small; the lowered crypto surface is hashing, randomness, secret KeyObjects, and the introspection statics" + "status": "static" }, { "id": "node-builtin.crypto.createECDH", @@ -530,17 +538,13 @@ "id": "node-builtin.crypto.createPrivateKey", "kind": "node-builtin", "name": "crypto.createPrivateKey", - "status": "unsupported", - "code": "SC2020", - "note": "asymmetric-key operations need a public-key stack (bignum, RSA/EC/EdDSA math) and public and private KeyObjects — neither exists in the static runtime, so no faithful lowering can be small; the lowered crypto surface is hashing, randomness, secret KeyObjects, and the introspection statics" + "status": "static" }, { "id": "node-builtin.crypto.createPublicKey", "kind": "node-builtin", "name": "crypto.createPublicKey", - "status": "unsupported", - "code": "SC2020", - "note": "asymmetric-key operations need a public-key stack (bignum, RSA/EC/EdDSA math) and public and private KeyObjects — neither exists in the static runtime, so no faithful lowering can be small; the lowered crypto surface is hashing, randomness, secret KeyObjects, and the introspection statics" + "status": "static" }, { "id": "node-builtin.crypto.createSecretKey", @@ -552,17 +556,13 @@ "id": "node-builtin.crypto.createSign", "kind": "node-builtin", "name": "crypto.createSign", - "status": "unsupported", - "code": "SC2020", - "note": "asymmetric-key operations need a public-key stack (bignum, RSA/EC/EdDSA math) and public and private KeyObjects — neither exists in the static runtime, so no faithful lowering can be small; the lowered crypto surface is hashing, randomness, secret KeyObjects, and the introspection statics" + "status": "static" }, { "id": "node-builtin.crypto.createVerify", "kind": "node-builtin", "name": "crypto.createVerify", - "status": "unsupported", - "code": "SC2020", - "note": "asymmetric-key operations need a public-key stack (bignum, RSA/EC/EdDSA math) and public and private KeyObjects — neither exists in the static runtime, so no faithful lowering can be small; the lowered crypto surface is hashing, randomness, secret KeyObjects, and the introspection statics" + "status": "static" }, { "id": "node-builtin.crypto.diffieHellman", @@ -570,7 +570,7 @@ "name": "crypto.diffieHellman", "status": "unsupported", "code": "SC2020", - "note": "asymmetric-key operations need a public-key stack (bignum, RSA/EC/EdDSA math) and public and private KeyObjects — neither exists in the static runtime, so no faithful lowering can be small; the lowered crypto surface is hashing, randomness, secret KeyObjects, and the introspection statics" + "note": "crypto.diffieHellman takes DH, EC or X25519 KeyObjects and has no lowering; the objects of createDiffieHellman, getDiffieHellman and createECDH compute shared secrets" }, { "id": "node-builtin.crypto.generateKey", @@ -582,17 +582,13 @@ "id": "node-builtin.crypto.generateKeyPair", "kind": "node-builtin", "name": "crypto.generateKeyPair", - "status": "unsupported", - "code": "SC2020", - "note": "asymmetric-key operations need a public-key stack (bignum, RSA/EC/EdDSA math) and public and private KeyObjects — neither exists in the static runtime, so no faithful lowering can be small; the lowered crypto surface is hashing, randomness, secret KeyObjects, and the introspection statics" + "status": "static" }, { "id": "node-builtin.crypto.generateKeyPairSync", "kind": "node-builtin", "name": "crypto.generateKeyPairSync", - "status": "unsupported", - "code": "SC2020", - "note": "asymmetric-key operations need a public-key stack (bignum, RSA/EC/EdDSA math) and public and private KeyObjects — neither exists in the static runtime, so no faithful lowering can be small; the lowered crypto surface is hashing, randomness, secret KeyObjects, and the introspection statics" + "status": "static" }, { "id": "node-builtin.crypto.generateKeySync", @@ -600,21 +596,31 @@ "name": "crypto.generateKeySync", "status": "static" }, + { + "id": "node-builtin.crypto.generatePrime", + "kind": "node-builtin", + "name": "crypto.generatePrime", + "status": "static" + }, + { + "id": "node-builtin.crypto.generatePrimeSync", + "kind": "node-builtin", + "name": "crypto.generatePrimeSync", + "status": "static" + }, { "id": "node-builtin.crypto.getCipherInfo", "kind": "node-builtin", "name": "crypto.getCipherInfo", "status": "unsupported", "code": "SC2020", - "note": "cipher metadata has no lowering — createCipheriv and createDecipheriv support aes-128/192/256 in cbc, ctr, and gcm modes and chacha20-poly1305" + "note": "cipher metadata has no lowering — createCipheriv and createDecipheriv support aes-128/192/256 in cbc, ctr, gcm, ecb, cfb, cfb8, cfb1, and ofb modes, des-ede3 and des-ede, chacha20, and chacha20-poly1305" }, { "id": "node-builtin.crypto.getDiffieHellman", "kind": "node-builtin", "name": "crypto.getDiffieHellman", - "status": "unsupported", - "code": "SC2020", - "note": "asymmetric-key operations need a public-key stack (bignum, RSA/EC/EdDSA math) and public and private KeyObjects — neither exists in the static runtime, so no faithful lowering can be small; the lowered crypto surface is hashing, randomness, secret KeyObjects, and the introspection statics" + "status": "static" }, { "id": "node-builtin.crypto.getRandomValues", @@ -658,7 +664,7 @@ "name": "crypto.privateDecrypt", "status": "unsupported", "code": "SC2020", - "note": "asymmetric-key operations need a public-key stack (bignum, RSA/EC/EdDSA math) and public and private KeyObjects — neither exists in the static runtime, so no faithful lowering can be small; the lowered crypto surface is hashing, randomness, secret KeyObjects, and the introspection statics" + "note": "RSA encryption has no lowering — the asymmetric crypto surface is RSA and EC KeyObjects (generateKeyPair, createPrivateKey, createPublicKey), sign, verify, createSign, createVerify, createECDH, and createDiffieHellman" }, { "id": "node-builtin.crypto.privateEncrypt", @@ -666,7 +672,7 @@ "name": "crypto.privateEncrypt", "status": "unsupported", "code": "SC2020", - "note": "asymmetric-key operations need a public-key stack (bignum, RSA/EC/EdDSA math) and public and private KeyObjects — neither exists in the static runtime, so no faithful lowering can be small; the lowered crypto surface is hashing, randomness, secret KeyObjects, and the introspection statics" + "note": "RSA encryption has no lowering — the asymmetric crypto surface is RSA and EC KeyObjects (generateKeyPair, createPrivateKey, createPublicKey), sign, verify, createSign, createVerify, createECDH, and createDiffieHellman" }, { "id": "node-builtin.crypto.publicDecrypt", @@ -674,7 +680,7 @@ "name": "crypto.publicDecrypt", "status": "unsupported", "code": "SC2020", - "note": "asymmetric-key operations need a public-key stack (bignum, RSA/EC/EdDSA math) and public and private KeyObjects — neither exists in the static runtime, so no faithful lowering can be small; the lowered crypto surface is hashing, randomness, secret KeyObjects, and the introspection statics" + "note": "RSA encryption has no lowering — the asymmetric crypto surface is RSA and EC KeyObjects (generateKeyPair, createPrivateKey, createPublicKey), sign, verify, createSign, createVerify, createECDH, and createDiffieHellman" }, { "id": "node-builtin.crypto.publicEncrypt", @@ -682,7 +688,7 @@ "name": "crypto.publicEncrypt", "status": "unsupported", "code": "SC2020", - "note": "asymmetric-key operations need a public-key stack (bignum, RSA/EC/EdDSA math) and public and private KeyObjects — neither exists in the static runtime, so no faithful lowering can be small; the lowered crypto surface is hashing, randomness, secret KeyObjects, and the introspection statics" + "note": "RSA encryption has no lowering — the asymmetric crypto surface is RSA and EC KeyObjects (generateKeyPair, createPrivateKey, createPublicKey), sign, verify, createSign, createVerify, createECDH, and createDiffieHellman" }, { "id": "node-builtin.crypto.randomBytes", @@ -744,9 +750,7 @@ "id": "node-builtin.crypto.sign", "kind": "node-builtin", "name": "crypto.sign", - "status": "unsupported", - "code": "SC2020", - "note": "asymmetric-key operations need a public-key stack (bignum, RSA/EC/EdDSA math) and public and private KeyObjects — neither exists in the static runtime, so no faithful lowering can be small; the lowered crypto surface is hashing, randomness, secret KeyObjects, and the introspection statics" + "status": "static" }, { "id": "node-builtin.crypto.timingSafeEqual", @@ -758,9 +762,7 @@ "id": "node-builtin.crypto.verify", "kind": "node-builtin", "name": "crypto.verify", - "status": "unsupported", - "code": "SC2020", - "note": "asymmetric-key operations need a public-key stack (bignum, RSA/EC/EdDSA math) and public and private KeyObjects — neither exists in the static runtime, so no faithful lowering can be small; the lowered crypto surface is hashing, randomness, secret KeyObjects, and the introspection statics" + "status": "static" }, { "id": "node-builtin.dgram", @@ -5718,7 +5720,7 @@ "kind": "stdlib", "name": "globalThis.crypto.subtle", "status": "static", - "note": "importKey, generateKey and exportKey (raw and jwk) for HMAC and AES keys, importKey for PBKDF2 and HKDF keys, AES-GCM, AES-CBC and AES-CTR encrypt and decrypt, PBKDF2 and HKDF deriveBits and deriveKey, HMAC sign and verify, and SHA-1/SHA-2 digest; other methods, algorithms and key formats are refused" + "note": "importKey, generateKey and exportKey (raw and jwk) for HMAC and AES keys, importKey for PBKDF2 and HKDF keys, AES-GCM, AES-CBC and AES-CTR encrypt and decrypt, wrapKey and unwrapKey of secret keys with AES-KW or those ciphers, PBKDF2 and HKDF deriveBits and deriveKey, HMAC sign and verify, generateKey, importKey, exportKey, sign and verify for ECDSA, Ed25519, RSASSA-PKCS1-v1_5 and RSA-PSS, and SHA-1/SHA-2 digest; other methods, algorithms and key formats are refused" }, { "id": "syntax.async-generators", diff --git a/packages/compiler/test/await-call-emission.test.ts b/packages/compiler/test/await-call-emission.test.ts index 96c039155..5a967cc5f 100644 --- a/packages/compiler/test/await-call-emission.test.ts +++ b/packages/compiler/test/await-call-emission.test.ts @@ -222,7 +222,8 @@ test("bodies that suspend only at their own awaits compile to stackless coroutin }); const framedSources = { - // Entering a store makes the frame isolate the AsyncLocalStorage context. + // Entering a store needs no frame: the body runs in its caller's context, + // as Node runs an async body's synchronous prefix. context: ` import { AsyncLocalStorage } from "node:async_hooks"; const store = new AsyncLocalStorage(); @@ -238,7 +239,7 @@ async function leaf(x: number): Promise { }`, }; -test("fiberless bodies keep their frame where it isolates a context or a stack", async () => { +test("fiberless bodies keep their frame only where it isolates a stack", async () => { for (const [name, prefix] of Object.entries(framedSources)) { const llvm = await emit(`${prefix} async function slow(x: number): Promise { @@ -262,9 +263,12 @@ async function main(): Promise { void main(); `); for (const symbol of ["sc_ai_leaf", "sc_aw_leaf", "sc_as_leaf"]) { - expect(definition(llvm, symbol), `${name} ${symbol}`).toContain( - "call void @scr_async_inline_enter(ptr %frame)", - ); + const body = definition(llvm, symbol); + if (name === "stack") + expect(body, `${name} ${symbol}`).toContain( + "call void @scr_async_inline_enter(ptr %frame)", + ); + else expect(body, `${name} ${symbol}`).not.toContain("@scr_async_inline_enter"); } } }); diff --git a/packages/compiler/test/callback-loop-versions.test.ts b/packages/compiler/test/callback-loop-versions.test.ts new file mode 100644 index 000000000..63a669fd4 --- /dev/null +++ b/packages/compiler/test/callback-loop-versions.test.ts @@ -0,0 +1,114 @@ +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { compile } from "../src/index.js"; + +async function emitLlvm(source: string): Promise { + const dir = await mkdtemp(join(tmpdir(), "scriptc-callback-versions-")); + try { + const entry = join(dir, "main.ts"); + const outPath = join(dir, "main.ll"); + await writeFile(entry, source); + const result = await compile(entry, { outDir: dir, outPath, outputKind: "llvm" }); + if (!result.ok) throw new Error(result.diagnostics.map((d) => d.message).join("\n")); + return await readFile(outPath, "utf8"); + } finally { + await rm(dir, { recursive: true, force: true }); + } +} + +const body = (llvm: string, marker: string): string => { + const match = new RegExp(`^define internal [^\\n]*${marker}[^]*?^}`, "m").exec(llvm); + if (!match) throw new Error(`no function ${marker}`); + return match[0]; +}; + +test("loops that call a known callback test its closure once and call it directly", async () => { + const llvm = await emitLlvm(` + function each(values: number[], visit: (v: number) => void): void { + for (let i = 0; i < values.length; i++) visit(values[i]!); + } + let sum = 0; + each([1, 2, 3], (v) => { sum += v; }); + console.log(sum); + `); + const fn = body(llvm, "@sc_bf_each"); + const fast = /^callback\.version\.fast\d+:$[^]*?^callback\.version\.done\d+:$/m.exec(fn)![0]; + // The guard compares the closure's code pointer once, before the loop. + expect(fn.slice(0, fn.indexOf(fast))).toMatch(/icmp eq ptr %\S+, @/); + expect(fast).not.toContain("callback.indirect"); + expect(fast).not.toContain("scr_dyn_this_push_dyn"); + // The ordinary copy keeps the fallback through the closure's pointer. + expect(fn).toContain("callback.indirect"); +}); + +test("captured number payloads carry their own alias tag", async () => { + const llvm = await emitLlvm(` + function total(values: number[]): number { + let sum = 0; + values.forEach((v) => { sum += v; }); + return sum; + } + console.log(total([1, 2, 3])); + `); + // Every read and write through a box's payload slot (field 5) is tagged. + const slots = [ + ...llvm.matchAll(/(%\S+) = getelementptr inbounds %ScrBox, ptr %\S+, i32 0, i32 5/g), + ]; + expect(slots.length).toBeGreaterThan(0); + for (const [, slot] of slots) { + for (const access of llvm.matchAll( + new RegExp(`(load double, ptr|store double %\\S+, ptr) \\${slot}\\b[^\\n]*`, "g"), + )) + expect(access[0]).toMatch(/!tbaa !\d+$/); + } + expect(llvm).toMatch(/load double, ptr %\S+, !tbaa !\d+/); +}); + +test("a callback declared in the loop body is not read before the loop", async () => { + const llvm = await emitLlvm(` + function strides(): number { + let calls = 0; + for (let i = 0; i < 10; i++) { + const bump = (): void => { i += 2; }; + bump(); + calls++; + } + return calls; + } + console.log(strides()); + `); + expect(llvm).not.toContain("callback.version"); +}); + +test("loops inside a callback version are not versioned again", async () => { + // Each inner loop calls a callback a factory builds just before it; without + // the limit, every copy of a version would version the next loop again. + const llvm = await emitLlvm(` + function adder(k: number): (v: number) => number { + return (v: number) => v + k; + } + function nest(n: number, f: (v: number) => number): number { + let s = 0; + for (let i = 0; i < n; i++) { + s += f(i); + const g = adder(i); + for (let j = 0; j < n; j++) { + s += g(j); + const h = adder(j); + for (let k = 0; k < n; k++) s += h(k); + } + } + return s; + } + console.log(nest(3, (v) => v - 1), nest(2, (v) => v * 3)); + `); + // One version for the outer loop of each compiled `nest`; its two copies + // keep the inner loops whole. + const versions = [...llvm.matchAll(/^define [^\n]*@sc_bf_nest[^]*?^}/gm)].map( + ([body]) => body.match(/^callback\.version\.fast\d+:$/gm)?.length ?? 0, + ); + expect(versions.every((count) => count <= 1)).toBe(true); + expect(versions.some((count) => count === 1)).toBe(true); +}); diff --git a/packages/compiler/test/cc-driver.test.ts b/packages/compiler/test/cc-driver.test.ts index b9f0403c6..5ddae6534 100644 --- a/packages/compiler/test/cc-driver.test.ts +++ b/packages/compiler/test/cc-driver.test.ts @@ -291,6 +291,11 @@ test.skipIf(!zigOnPath())( options: { ecdh: true, dynAsync: true, dynInvoke: true, inspect: true }, call: "scr_dyn_release(scr_crypto_ecdh_new(scr_dyn_undefined()))", }, + { + name: "dh", + options: { dh: true, dynInvoke: true, inspect: true }, + call: "scr_dyn_release(scr_crypto_dh_group_new(scr_dyn_undefined()))", + }, ]; try { await withCcEnv("zigcc", "x86_64-windows-gnu", async () => { diff --git a/packages/compiler/test/native-codegen-integration.test.ts b/packages/compiler/test/native-codegen-integration.test.ts index c05e7f994..35a747d60 100644 --- a/packages/compiler/test/native-codegen-integration.test.ts +++ b/packages/compiler/test/native-codegen-integration.test.ts @@ -378,7 +378,7 @@ done: expect(await symbols(helperObject, ["-u"])).toEqual(await symbols(clangObject, ["-u"])); expect(await symbols(helperObject, ["-gU"])).toEqual(await symbols(clangObject, ["-gU"])); expect(await symbols(helperObject, ["-gU"])).toEqual(["0000000000000000 T _main"]); - expect(await symbols(helperObject, ["-u"])).toContain("_scr_runtime_abi_v25"); + expect(await symbols(helperObject, ["-u"])).toContain("_scr_runtime_abi_v27"); const clangExe = join(dir, "clang-program"); const linkDriver = join(dir, "clang-driver.c"); @@ -712,7 +712,7 @@ done: .split("\n"); expect(undefinedSymbols).toContain("_sf_scale"); expect(undefinedSymbols).toContain("_sf_callback_mix"); - expect(undefinedSymbols).toContain("_scr_runtime_abi_v25"); + expect(undefinedSymbols).toContain("_scr_runtime_abi_v27"); }); test("helper and clang object paths retain the same outbound FFI C ABI", async () => { diff --git a/packages/compiler/test/parameter-version-emission.test.ts b/packages/compiler/test/parameter-version-emission.test.ts new file mode 100644 index 000000000..eb6daff56 --- /dev/null +++ b/packages/compiler/test/parameter-version-emission.test.ts @@ -0,0 +1,71 @@ +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { compile } from "../src/index.js"; + +async function emitLlvm(source: string): Promise { + const dir = await mkdtemp(join(tmpdir(), "scriptc-parameter-version-")); + try { + const entry = join(dir, "main.ts"); + const outPath = join(dir, "main.ll"); + await writeFile(entry, source); + const result = await compile(entry, { outDir: dir, outPath, outputKind: "llvm" }); + expect(result.ok, JSON.stringify(result.diagnostics)).toBe(true); + return await readFile(outPath, "utf8"); + } finally { + await rm(dir, { recursive: true, force: true }); + } +} + +const versions = (llvm: string): string[] => [ + ...new Set([...llvm.matchAll(/^define [^@]*@sc_\w+?_(\w+?)_int32Params\b/gm)].map((m) => m[1]!)), +]; + +const loops = ` +function sumTo(n: number): number { + let s = 0; + for (let i = 0; i < n; i++) s += i; + return s; +} +function literal(n: number): number { + let s = 0; + for (let i = 0; i < n; i++) s += i; + return s; +} +function recursive(n: number): number { + let s = 0; + for (let i = 0; i < n; i++) s += i; + return n > 0 ? s + recursive(n - 1) : s; +} +function unbounded(n: number): number { + let s = 0; + for (let i = 0; i < 10; i++) s += n; + return s; +} +class Counter { + total(n: number): number { + let s = 0; + for (let i = 0; i < n; i++) s += i; + return s; + } +} +const n = Number(process.argv.length > 5 ? "7.5" : "6"); +console.log(sumTo(n), literal(4), literal(5), recursive(n), unbounded(n), new Counter().total(n)); +`; + +test("a loop-bounding parameter some caller passes unproven gets an int32 version", async () => { + const llvm = await emitLlvm(loops); + expect(versions(llvm)).toEqual(["sumTo"]); + // The version takes the parameter as an i32, and the body dispatches to it + // after testing for an int32 that is not -0. + expect(llvm).toMatch(/^define [^@]*@sc_\w+_sumTo_int32Params\.i32\(i32 /m); + const body = /^define [^@]*@sc_\w+?_sumTo\([^]*?^}/m.exec(llvm)?.[0] ?? ""; + expect(body).toMatch(/call [^@]*@sc_\w+_sumTo_int32Params/); + expect(body).toMatch(/fdiv double 0x3FF0000000000000, %\S+/); +}); + +test("a module that reads stacks keeps a single body", async () => { + const llvm = await emitLlvm(`${loops}\nconsole.log(typeof new Error("x").stack);\n`); + expect(versions(llvm)).toEqual([]); +}); diff --git a/packages/compiler/test/regex-matcher-bounds.test.ts b/packages/compiler/test/regex-matcher-bounds.test.ts new file mode 100644 index 000000000..3e0ec7f1b --- /dev/null +++ b/packages/compiler/test/regex-matcher-bounds.test.ts @@ -0,0 +1,91 @@ +/* Compiled regex matchers bound every read by the characters left in the + * subject, so subjects near the runtime's 2^31 - 2 character limit are + * read only inside the subject. Each matcher runs on a subject of exactly + * that length whose last byte sits just before an inaccessible page: a + * bounds check that wraps past INT32_MAX reads that page and faults. The + * subject is a lazily mapped zero region (no memory is committed for it) + * with the characters each case needs written near its end. */ +import { execFileSync } from "node:child_process"; +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { compileRegexMatcher, REGEX_MATCHER_DECLS } from "../src/backend/llvm/regex-matchers.js"; + +const LIMIT = 2 ** 31 - 2; + +/** [symbol, pattern, flags, start offset from the end, tail text]. */ +const CASES: [string, string, string, number, string][] = [ + // The first-character vector search, 10 characters before the end. + ["seek", "[b-d]", "", 10, "c"], + // The start filter (two 8-character alternatives), 20 before the end. + ["filter", "agggtaaa|tttaccct", "i", 20, "TTTACCCT"], + // A run of 8 literal characters beginning 2 before the end. + ["run", "xy234567", "", 30, "xy"], + // A greedy class over the whole subject, 16 characters per step. + ["greedy", "[^x]+", "", LIMIT, ""], +]; + +const DRIVER = ` +#include +#include +#include +#include +#include +void scr_regex_stack_grow(void *a, void *b, void *c, int32_t d) { (void)a; (void)b; (void)c; (void)d; _exit(3); } +void scr_regex_stack_free(void *p) { (void)p; } +${CASES.map(([sym]) => `int32_t ${sym}(const char *, int32_t, int32_t, const char **);`).join("\n")} +static void check(const char *name, int32_t (*m)(const char *, int32_t, int32_t, const char **), + int32_t start, const char *tail) { + const int32_t len = ${LIMIT}; + size_t page = (size_t)sysconf(_SC_PAGESIZE); + size_t body = ((size_t)len + page - 1) / page * page; + char *base = mmap(NULL, body + page, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE, -1, 0); + if (base == MAP_FAILED || mprotect(base + body, page, PROT_NONE) != 0) _exit(4); + char *s = base + (body - (size_t)len); + memcpy(s + len - strlen(tail), tail, strlen(tail)); + const char *out[16] = {0}; + int32_t found = m(s, len, start, out); + if (found) printf("%s %ld %ld\\n", name, (long)(out[0] - s), (long)(out[1] - s)); + else printf("%s none\\n", name); + fflush(stdout); + munmap(base, body + page); +} +int main(void) { +${CASES.map(([sym, , , back, tail]) => ` check("${sym}", ${sym}, ${LIMIT - back}, ${JSON.stringify(tail)});`).join("\n")} + return 0; +} +`; + +test.skipIf(process.platform === "win32")( + "compiled regex matchers stay inside subjects near 2^31 characters", + { timeout: 120_000 }, + () => { + const lines: string[] = []; + const decls = new Set(REGEX_MATCHER_DECLS); + for (const [sym, pattern, flags] of CASES) { + const code = compileRegexMatcher(sym, pattern, flags, "i64", true); + if (code === null) throw new Error(`/${pattern}/${flags} did not compile`); + lines.push(...code.lines.map((line) => line.replace(/^define internal /, "define "))); + for (const decl of code.decls) decls.add(decl); + } + const dir = mkdtempSync(join(tmpdir(), "scriptc-regex-bounds-")); + try { + const ir = join(dir, "matchers.ll"); + const driver = join(dir, "driver.c"); + const binary = join(dir, "bounds"); + writeFileSync(ir, [...decls, ...lines, "attributes #0 = { nounwind }", ""].join("\n")); + writeFileSync(driver, DRIVER); + execFileSync("clang", ["-O2", "-Wno-override-module", ir, driver, "-o", binary]); + const output = execFileSync(binary, { encoding: "utf8" }); + expect(output.trim().split("\n")).toEqual([ + `seek ${LIMIT - 1} ${LIMIT}`, + `filter ${LIMIT - 8} ${LIMIT}`, + "run none", + `greedy 0 ${LIMIT}`, + ]); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }, +); diff --git a/packages/compiler/test/running-extrema-branches.test.ts b/packages/compiler/test/running-extrema-branches.test.ts new file mode 100644 index 000000000..e1f5a7837 --- /dev/null +++ b/packages/compiler/test/running-extrema-branches.test.ts @@ -0,0 +1,75 @@ +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { compile } from "../src/index.js"; + +async function emitLlvm(source: string): Promise { + const dir = await mkdtemp(join(tmpdir(), "scriptc-running-extrema-")); + try { + const entry = join(dir, "main.ts"); + const outPath = join(dir, "main.ll"); + await writeFile(entry, source); + const result = await compile(entry, { outDir: dir, outPath, outputKind: "llvm" }); + if (!result.ok) throw new Error(result.diagnostics.map((d) => d.message).join("\n")); + return await readFile(outPath, "utf8"); + } finally { + await rm(dir, { recursive: true, force: true }); + } +} + +const RARE = + /br i1 %\S+, label %if\.t\d+, label %if\.j\d+, !prof !\{!"branch_weights", i32 1, i32 1000\}/; + +test("a running maximum's update is weighted as rarely taken", async () => { + const llvm = await emitLlvm(` + function argmax(a: Float64Array): number { + let max = -Infinity; + let at = -1; + for (let i = 0; i < a.length; i++) { + const v = a[i]!; + if (v > max) { max = v; at = i; } + } + return at; + } + console.log(argmax(new Float64Array([1, 3, 2]))); + `); + expect(llvm).toMatch(RARE); +}); + +test("a callback's update of a captured minimum is weighted", async () => { + const llvm = await emitLlvm(` + function least(values: number[]): number { + let min = Infinity; + values.forEach((v) => { if (min > v) min = v; }); + return min; + } + console.log(least([3, 1, 2])); + `); + expect(llvm).toMatch(RARE); +}); + +test("clamps and saturating sums keep their selects", async () => { + const llvm = await emitLlvm(` + function clampSum(a: Float64Array, hi: number): number { + let s = 0; + for (let i = 0; i < a.length; i++) { + let v = a[i]!; + if (hi < v) v = hi; + s += v; + } + return s; + } + function saturate(a: Float64Array, hi: number): number { + let v = 0; + for (let i = 0; i < a.length; i++) { + v += a[i]!; + if (hi < v) v = hi; + } + return v; + } + const a = new Float64Array([0.5, 2, 0.25]); + console.log(clampSum(a, 1), saturate(a, 1)); + `); + expect(llvm).not.toContain("branch_weights"); +}); diff --git a/packages/compiler/test/speculative-loop-emission.test.ts b/packages/compiler/test/speculative-loop-emission.test.ts new file mode 100644 index 000000000..a691d856d --- /dev/null +++ b/packages/compiler/test/speculative-loop-emission.test.ts @@ -0,0 +1,94 @@ +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { compile } from "../src/index.js"; + +async function emitLlvm(source: string): Promise { + const dir = await mkdtemp(join(tmpdir(), "scriptc-speculative-loop-")); + try { + const entry = join(dir, "main.ts"); + const outPath = join(dir, "main.ll"); + await writeFile(entry, source); + const result = await compile(entry, { outDir: dir, outPath, outputKind: "llvm" }); + expect(result.ok, JSON.stringify(result.diagnostics)).toBe(true); + return await readFile(outPath, "utf8"); + } finally { + await rm(dir, { recursive: true, force: true }); + } +} + +const source = ` +class Point { + x: number; + w = 0; + constructor(x: number) { + this.x = x; + } +} +function scaled(points: Point[], scale: number): number { + let s = 0; + for (let i = 0; i < points.length; i++) { + const p = points[i]!; + p.w += 1; + const k = p.x * scale; + s += k * k; + } + return s; +} +console.log(scaled([new Point(1), new Point(2)], 2), scaled([new Point(1)], 0.5)); +`; + +function labels(fn: string): string[] { + return [...fn.matchAll(/^([\w.]+):/gm)].map((m) => m[1]!); +} + +test("a failed check finishes its iteration outside the ordinary copy", async () => { + const llvm = await emitLlvm(source); + const fn = /^define internal double @sc_bf_scaled\([^]*?^}/m.exec(llvm)?.[0] ?? ""; + const order = labels(fn); + // The loop may have several speculative sites (one in each copy of a + // dense-array loop). Each site emits its ordinary copy, its third copy + // and its fast copy in that order. + const slows = order.flatMap((l, i) => (l.startsWith("spec.slow") ? [i] : [])); + expect(slows.length).toBeGreaterThan(0); + let resumes = 0; + for (const slow of slows) { + const next = (prefix: string): number => + order.findIndex((l, i) => i > slow && l.startsWith(prefix)); + const tail = next("spec.tail"); + const fast = next("spec.fast"); + expect(tail).toBeGreaterThan(slow); + expect(fast).toBeGreaterThan(tail); + // Every resume point of the site is in its third copy, between that + // copy's start and the fast copy, so the ordinary copy has one entry. + for (let i = slow + 1; i < fast; i++) { + if (!order[i]!.startsWith("spec.resume")) continue; + expect(i).toBeGreaterThan(tail); + resumes++; + } + // The third copy's header leaves for the ordinary copy's header. + const header = order[slow + 1]!; + const third = fn.slice(fn.indexOf(`\n${order[tail]}:`), fn.indexOf(`\n${order[fast]}:`)); + expect(third).toContain(`br label %${header}`); + } + expect(resumes).toBe(order.filter((l) => l.startsWith("spec.resume")).length); + expect(resumes).toBeGreaterThan(0); +}); + +test("a loop site stops entering its fast copy after repeated bails", async () => { + const llvm = await emitLlvm(source); + const site = /^(@scr\.spec\.bails\.\d+) = internal global i8 0$/m.exec(llvm)?.[1]; + expect(site).toBeDefined(); + const fn = /^define internal double @sc_bf_scaled\([^]*?^}/m.exec(llvm)?.[0] ?? ""; + const escaped = site!.replace(/\./g, "\\."); + // The entry test reads the count, a bail adds one, and a completed fast + // copy clears it. + expect(fn).toMatch( + new RegExp( + `(%\\S+) = load atomic i8, ptr ${escaped} unordered, align 1\\n\\s*%\\S+ = icmp ult i8 \\1, 8`, + ), + ); + expect(fn).toMatch(new RegExp(`call i8 @llvm\\.uadd\\.sat\\.i8\\(i8 %\\S+, i8 1\\)`)); + expect(fn).toContain(`store atomic i8 0, ptr ${site} unordered, align 1`); +}); diff --git a/packages/compiler/test/stable-names.test.ts b/packages/compiler/test/stable-names.test.ts index 720472556..c82db039d 100644 --- a/packages/compiler/test/stable-names.test.ts +++ b/packages/compiler/test/stable-names.test.ts @@ -110,3 +110,10 @@ console.log(box.value); expect(locals).toHaveLength(2); expect(locals[0]!.name).not.toBe(locals[1]!.name); }); + +test("names do not depend on the directory the program sits in", async () => { + // Each call lowers in its own temporary directory. + const [first] = await lower(recursive); + const [second] = await lower(recursive); + expect(names(second!)).toEqual(names(first!)); +}); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/4638-contract-program-lifetime-arrays.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/4638-contract-program-lifetime-arrays.ts.json new file mode 100644 index 000000000..a89ae97b8 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/4638-contract-program-lifetime-arrays.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/4638-contract-program-lifetime-arrays.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/4639-contract-program-lifetime-maps.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/4639-contract-program-lifetime-maps.ts.json new file mode 100644 index 000000000..5f7afb8e7 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/4639-contract-program-lifetime-maps.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/4639-contract-program-lifetime-maps.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6706-dh-groups.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6706-dh-groups.ts.json new file mode 100644 index 000000000..d8f5b48c3 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6706-dh-groups.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6706-dh-groups.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6707-dh-explicit-params.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6707-dh-explicit-params.ts.json new file mode 100644 index 000000000..4cb3c882c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6707-dh-explicit-params.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6707-dh-explicit-params.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6708-dh-errors.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6708-dh-errors.js.json new file mode 100644 index 000000000..c252657a8 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6708-dh-errors.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6708-dh-errors.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6709-dh-generate.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6709-dh-generate.ts.json new file mode 100644 index 000000000..5c50c8ec1 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6709-dh-generate.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6709-dh-generate.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6710-dh-encodings.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6710-dh-encodings.cjs.json new file mode 100644 index 000000000..86abbb6ba --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6710-dh-encodings.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6710-dh-encodings.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6711-dh-handles.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6711-dh-handles.ts.json new file mode 100644 index 000000000..ae8daf9f1 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6711-dh-handles.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6711-dh-handles.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6712-dh-peer-keys.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6712-dh-peer-keys.js.json new file mode 100644 index 000000000..101260f54 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6712-dh-peer-keys.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6712-dh-peer-keys.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6822-jsx-classic-runtime/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6822-jsx-classic-runtime/main.ts.json new file mode 100644 index 000000000..9d97dc765 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6822-jsx-classic-runtime/main.ts.json @@ -0,0 +1,11 @@ +{ + "order": [ + "/tests/corpus/6822-jsx-classic-runtime/html.ts", + "/tests/corpus/6822-jsx-classic-runtime/list.jsx", + "/tests/corpus/6822-jsx-classic-runtime/page.tsx", + "/tests/corpus/6822-jsx-classic-runtime/factory.ts", + "/tests/corpus/6822-jsx-classic-runtime/pragma.tsx", + "/tests/corpus/6822-jsx-classic-runtime/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6824-jsx-automatic-runtime/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6824-jsx-automatic-runtime/main.ts.json new file mode 100644 index 000000000..95ba5cd15 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6824-jsx-automatic-runtime/main.ts.json @@ -0,0 +1,12 @@ +{ + "order": [ + "/tests/corpus/6824-jsx-automatic-runtime/runtime/jsx-runtime.ts", + "/tests/corpus/6824-jsx-automatic-runtime/alt/jsx-runtime.ts", + "/tests/corpus/6824-jsx-automatic-runtime/alt.tsx", + "/tests/corpus/6824-jsx-automatic-runtime/runtime/index.ts", + "/tests/corpus/6824-jsx-automatic-runtime/page.tsx", + "/tests/corpus/6824-jsx-automatic-runtime/render.ts", + "/tests/corpus/6824-jsx-automatic-runtime/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6825-jsx-automatic-development/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6825-jsx-automatic-development/main.ts.json new file mode 100644 index 000000000..649f0bdff --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6825-jsx-automatic-development/main.ts.json @@ -0,0 +1,8 @@ +{ + "order": [ + "/tests/corpus/6825-jsx-automatic-development/runtime/jsx-dev-runtime.ts", + "/tests/corpus/6825-jsx-automatic-development/view.tsx", + "/tests/corpus/6825-jsx-automatic-development/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6870-error-constructor-values.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6870-error-constructor-values.js.json new file mode 100644 index 000000000..a2f0e02ea --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6870-error-constructor-values.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6870-error-constructor-values.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6871-worker-data-destructuring.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6871-worker-data-destructuring.cjs.json new file mode 100644 index 000000000..b610a5878 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6871-worker-data-destructuring.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6871-worker-data-destructuring.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6872-regex-start-filter.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6872-regex-start-filter.ts.json new file mode 100644 index 000000000..e2c97c71f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6872-regex-start-filter.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6872-regex-start-filter.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6997-typed-array-invariant-index-terms.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6997-typed-array-invariant-index-terms.ts.json new file mode 100644 index 000000000..6d60d2635 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6997-typed-array-invariant-index-terms.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6997-typed-array-invariant-index-terms.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6998-typed-array-while-loop-versions.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6998-typed-array-while-loop-versions.ts.json new file mode 100644 index 000000000..f5cfba0cc --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6998-typed-array-while-loop-versions.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6998-typed-array-while-loop-versions.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7002-empty-array-contextual-slots/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7002-empty-array-contextual-slots/main.ts.json new file mode 100644 index 000000000..d22715215 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7002-empty-array-contextual-slots/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7002-empty-array-contextual-slots/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7005-dyn-constructor-instanceof/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7005-dyn-constructor-instanceof/main.ts.json new file mode 100644 index 000000000..313888b49 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7005-dyn-constructor-instanceof/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7005-dyn-constructor-instanceof/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7008-js-unit-initialized-properties/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7008-js-unit-initialized-properties/main.ts.json new file mode 100644 index 000000000..85f5a6e12 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7008-js-unit-initialized-properties/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7008-js-unit-initialized-properties/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7011-for-in-dictionary-deletes.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7011-for-in-dictionary-deletes.ts.json new file mode 100644 index 000000000..3b2b79c3b --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7011-for-in-dictionary-deletes.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7011-for-in-dictionary-deletes.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7080-crypto-ed25519.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7080-crypto-ed25519.mjs.json new file mode 100644 index 000000000..035d9d765 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7080-crypto-ed25519.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7080-crypto-ed25519.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7081-webcrypto-ed25519.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7081-webcrypto-ed25519.mjs.json new file mode 100644 index 000000000..bbd2d1ebc --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7081-webcrypto-ed25519.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7081-webcrypto-ed25519.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7082-crypto-key-objects.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7082-crypto-key-objects.ts.json new file mode 100644 index 000000000..c2e50cc97 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7082-crypto-key-objects.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7082-crypto-key-objects.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7083-crypto-sign-verify.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7083-crypto-sign-verify.ts.json new file mode 100644 index 000000000..385c9347c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7083-crypto-sign-verify.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7083-crypto-sign-verify.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7084-crypto-key-pairs.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7084-crypto-key-pairs.cjs.json new file mode 100644 index 000000000..e1ac90536 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7084-crypto-key-pairs.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7084-crypto-key-pairs.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7085-webcrypto-ecdsa-rsa.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7085-webcrypto-ecdsa-rsa.ts.json new file mode 100644 index 000000000..3fc5406e8 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7085-webcrypto-ecdsa-rsa.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7085-webcrypto-ecdsa-rsa.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7086-webcrypto-ecdsa-rsa-errors.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7086-webcrypto-ecdsa-rsa-errors.mjs.json new file mode 100644 index 000000000..ef44f9180 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7086-webcrypto-ecdsa-rsa-errors.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7086-webcrypto-ecdsa-rsa-errors.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7087-tls-socket-wrap.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7087-tls-socket-wrap.ts.json new file mode 100644 index 000000000..37009eaea --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7087-tls-socket-wrap.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7087-tls-socket-wrap.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7088-tls-socket-connect-tunnel.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7088-tls-socket-connect-tunnel.ts.json new file mode 100644 index 000000000..71947ecdd --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7088-tls-socket-connect-tunnel.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7088-tls-socket-connect-tunnel.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7089-http-host-header-tunnels.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7089-http-host-header-tunnels.ts.json new file mode 100644 index 000000000..9b3002173 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7089-http-host-header-tunnels.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7089-http-host-header-tunnels.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7094-tls-socket-failed-handshakes.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7094-tls-socket-failed-handshakes.ts.json new file mode 100644 index 000000000..8fa524155 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7094-tls-socket-failed-handshakes.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7094-tls-socket-failed-handshakes.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7106-async-prefix-enter-with.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7106-async-prefix-enter-with.ts.json new file mode 100644 index 000000000..2945faca5 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7106-async-prefix-enter-with.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7106-async-prefix-enter-with.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7107-for-await-context.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7107-for-await-context.ts.json new file mode 100644 index 000000000..331f64937 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7107-for-await-context.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7107-for-await-context.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7108-generator-step-context.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7108-generator-step-context.ts.json new file mode 100644 index 000000000..e7c5876ac --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7108-generator-step-context.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7108-generator-step-context.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7130-typed-message-listeners.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7130-typed-message-listeners.ts.json new file mode 100644 index 000000000..f02244324 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7130-typed-message-listeners.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7130-typed-message-listeners.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7130-webcrypto-wrap-key.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7130-webcrypto-wrap-key.ts.json new file mode 100644 index 000000000..fa5b5529f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7130-webcrypto-wrap-key.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7130-webcrypto-wrap-key.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7131-typed-worker-messages.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7131-typed-worker-messages.ts.json new file mode 100644 index 000000000..89a6c125a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7131-typed-worker-messages.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7131-typed-worker-messages.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7131-webcrypto-wrap-key-errors.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7131-webcrypto-wrap-key-errors.mjs.json new file mode 100644 index 000000000..275ffa418 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7131-webcrypto-wrap-key-errors.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7131-webcrypto-wrap-key-errors.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7132-jose-pbes2/main.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7132-jose-pbes2/main.mjs.json new file mode 100644 index 000000000..9fbd24555 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7132-jose-pbes2/main.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7132-jose-pbes2/main.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7139-message-typed-records.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7139-message-typed-records.ts.json new file mode 100644 index 000000000..5e50dcd13 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7139-message-typed-records.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7139-message-typed-records.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7141-comoving-loop-counters.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7141-comoving-loop-counters.ts.json new file mode 100644 index 000000000..392c9f10d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7141-comoving-loop-counters.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7141-comoving-loop-counters.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7143-optional-read-integer-index.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7143-optional-read-integer-index.ts.json new file mode 100644 index 000000000..ae0d1f610 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7143-optional-read-integer-index.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7143-optional-read-integer-index.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7150-cipher-legacy-modes.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7150-cipher-legacy-modes.ts.json new file mode 100644 index 000000000..c0e5f9812 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7150-cipher-legacy-modes.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7150-cipher-legacy-modes.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7151-cipher-legacy-errors.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7151-cipher-legacy-errors.cjs.json new file mode 100644 index 000000000..1f37c6b64 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7151-cipher-legacy-errors.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7151-cipher-legacy-errors.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7152-crypto-prime.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7152-crypto-prime.ts.json new file mode 100644 index 000000000..722ebaacb --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7152-crypto-prime.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7152-crypto-prime.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7153-crypto-prime-errors.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7153-crypto-prime-errors.cjs.json new file mode 100644 index 000000000..9d121895a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7153-crypto-prime-errors.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7153-crypto-prime-errors.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7180-generic-local-binding-early-uses.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7180-generic-local-binding-early-uses.ts.json new file mode 100644 index 000000000..5fb971826 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7180-generic-local-binding-early-uses.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7180-generic-local-binding-early-uses.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7181-generic-module-binding-tdz.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7181-generic-module-binding-tdz.ts.json new file mode 100644 index 000000000..a67424e94 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7181-generic-module-binding-tdz.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7181-generic-module-binding-tdz.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7182-generic-binding-direct-calls.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7182-generic-binding-direct-calls.ts.json new file mode 100644 index 000000000..b641615f8 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7182-generic-binding-direct-calls.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7182-generic-binding-direct-calls.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7184-reduce-runtime-accumulator.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7184-reduce-runtime-accumulator.ts.json new file mode 100644 index 000000000..b93eb61b6 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7184-reduce-runtime-accumulator.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7184-reduce-runtime-accumulator.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7185-array-callback-arguments-length.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7185-array-callback-arguments-length.ts.json new file mode 100644 index 000000000..2514bcaa8 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7185-array-callback-arguments-length.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7185-array-callback-arguments-length.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7240-objectmode-streams.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7240-objectmode-streams.ts.json new file mode 100644 index 000000000..0b212fb07 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7240-objectmode-streams.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7240-objectmode-streams.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7241-objectmode-streams.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7241-objectmode-streams.mjs.json new file mode 100644 index 000000000..22dc5abc4 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7241-objectmode-streams.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7241-objectmode-streams.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7242-objectmode-dynamic.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7242-objectmode-dynamic.cjs.json new file mode 100644 index 000000000..1b932b82b --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7242-objectmode-dynamic.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7242-objectmode-dynamic.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7243-for-await-close-order.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7243-for-await-close-order.mjs.json new file mode 100644 index 000000000..ec479cf59 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7243-for-await-close-order.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7243-for-await-close-order.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7244-objectmode-subclasses.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7244-objectmode-subclasses.ts.json new file mode 100644 index 000000000..827bba4c0 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7244-objectmode-subclasses.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7244-objectmode-subclasses.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7245-writable-callback-order.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7245-writable-callback-order.mjs.json new file mode 100644 index 000000000..9485562b3 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7245-writable-callback-order.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7245-writable-callback-order.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7280-named-reads-hole-receivers.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7280-named-reads-hole-receivers.ts.json new file mode 100644 index 000000000..febb008f7 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7280-named-reads-hole-receivers.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7280-named-reads-hole-receivers.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7300-callback-captured-scalars.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7300-callback-captured-scalars.ts.json new file mode 100644 index 000000000..dc1158934 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7300-callback-captured-scalars.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7300-callback-captured-scalars.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7301-running-extrema-branches.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7301-running-extrema-branches.ts.json new file mode 100644 index 000000000..6dcb4b57d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7301-running-extrema-branches.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7301-running-extrema-branches.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7320-array-dense-loop-invariant-limits.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7320-array-dense-loop-invariant-limits.ts.json new file mode 100644 index 000000000..785bf9d82 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7320-array-dense-loop-invariant-limits.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7320-array-dense-loop-invariant-limits.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7321-speculative-loop-bail-sites.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7321-speculative-loop-bail-sites.ts.json new file mode 100644 index 000000000..113eeedb7 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7321-speculative-loop-bail-sites.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7321-speculative-loop-bail-sites.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7322-union-scalar-equality.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7322-union-scalar-equality.ts.json new file mode 100644 index 000000000..73257a3f4 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7322-union-scalar-equality.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7322-union-scalar-equality.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7323-loop-bound-parameter-versions.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7323-loop-bound-parameter-versions.ts.json new file mode 100644 index 000000000..0a79c8cdb --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7323-loop-bound-parameter-versions.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7323-loop-bound-parameter-versions.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7324-loop-bound-parameter-window-sums.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7324-loop-bound-parameter-window-sums.ts.json new file mode 100644 index 000000000..aedd853fb --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7324-loop-bound-parameter-window-sums.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7324-loop-bound-parameter-window-sums.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7360-worker-typed-records.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7360-worker-typed-records.ts.json new file mode 100644 index 000000000..4925143ad --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7360-worker-typed-records.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7360-worker-typed-records.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7361-message-listeners-share-value.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7361-message-listeners-share-value.ts.json new file mode 100644 index 000000000..f6a78ee9a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7361-message-listeners-share-value.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7361-message-listeners-share-value.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/class-quoted-accessor-keys.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/class-quoted-accessor-keys.mjs.json new file mode 100644 index 000000000..234634ebd --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/class-quoted-accessor-keys.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/class-quoted-accessor-keys.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/arguments-length-self-call.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/arguments-length-self-call.ts.json new file mode 100644 index 000000000..8ed5027e5 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/arguments-length-self-call.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/arguments-length-self-call.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/jsx-factory-mismatch/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/jsx-factory-mismatch/main.ts.json new file mode 100644 index 000000000..9552906dd --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/jsx-factory-mismatch/main.ts.json @@ -0,0 +1,17 @@ +{ + "order": [ + "/tests/diagnostics/jsx-factory-mismatch/view.tsx", + "/tests/diagnostics/jsx-factory-mismatch/main.ts" + ], + "diags": [ + { + "code": "SC0001", + "message": "Argument of type 'number' is not assignable to parameter of type 'string'. (in the call this JSX lowers to)", + "loc": { + "file": "/tests/diagnostics/jsx-factory-mismatch/view.tsx", + "start": 337, + "end": 342 + } + } + ] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/jsx-preserve/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/jsx-preserve/main.ts.json new file mode 100644 index 000000000..eb5e492f4 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/jsx-preserve/main.ts.json @@ -0,0 +1,18 @@ +{ + "order": [ + "/tests/diagnostics/jsx-preserve/view.tsx", + "/tests/diagnostics/jsx-preserve/main.ts" + ], + "diags": [ + { + "code": "SC1090", + "message": "JSX with \"jsx\": \"preserve\" is not supported: that setting keeps JSX in the output instead of lowering it to calls", + "loc": { + "file": "/tests/diagnostics/jsx-preserve/view.tsx", + "start": 158, + "end": 176 + }, + "hint": "set \"jsx\" to \"react\", \"react-jsx\" or \"react-jsxdev\" in the project tsconfig" + } + ] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/jsx-type-errors/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/jsx-type-errors/main.ts.json new file mode 100644 index 000000000..62a162ee2 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/jsx-type-errors/main.ts.json @@ -0,0 +1,26 @@ +{ + "order": [ + "/tests/diagnostics/jsx-type-errors/view.tsx", + "/tests/diagnostics/jsx-type-errors/main.ts" + ], + "diags": [ + { + "code": "SC0001", + "message": "Type 'number' is not assignable to type 'string'.", + "loc": { + "file": "/tests/diagnostics/jsx-type-errors/view.tsx", + "start": 488, + "end": 495 + } + }, + { + "code": "SC0001", + "message": "Type 'string' is not assignable to type 'number'.", + "loc": { + "file": "/tests/diagnostics/jsx-type-errors/view.tsx", + "start": 520, + "end": 527 + } + } + ] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/stream-byte-mode-chunk-fences.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/stream-byte-mode-chunk-fences.ts.json new file mode 100644 index 000000000..8a02db2be --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/stream-byte-mode-chunk-fences.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/stream-byte-mode-chunk-fences.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/tls-socket-fences.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/tls-socket-fences.ts.json new file mode 100644 index 000000000..409766067 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/tls-socket-fences.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/tls-socket-fences.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/dh.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/dh.ts.json new file mode 100644 index 000000000..db078292a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/dh.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/node-types/dh.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/keys.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/keys.ts.json new file mode 100644 index 000000000..b0b79568d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/keys.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/node-types/keys.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/tls-socket-wrap.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/tls-socket-wrap.ts.json new file mode 100644 index 000000000..68346a115 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/tls-socket-wrap.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/node-types/tls-socket-wrap.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/webcrypto-ecdsa.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/webcrypto-ecdsa.ts.json new file mode 100644 index 000000000..2c4a3b15b --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/webcrypto-ecdsa.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/node-types/webcrypto-ecdsa.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/facade.test.ts b/packages/compiler/test/ts7/facade.test.ts index 94bab8b31..8e03400a2 100644 --- a/packages/compiler/test/ts7/facade.test.ts +++ b/packages/compiler/test/ts7/facade.test.ts @@ -835,6 +835,37 @@ export function f(input: number): number { expect(panics).toBe(warmPanics); }); +test("a batch method the server answers as unknown is asked once, then node by node", () => { + const w = buildTwoWorlds(RICH_TS, host); + worlds.push(w); + const sf = w.p7.getSourceFile(w.files[0]!)!; + const raw = w.p7.project.checker; + const requests: Record = {}; + // A server without the batch methods, as tsgo is. + const older = new Proxy(raw, { + get(target, prop, receiver) { + const value = Reflect.get(target, prop, receiver); + if (prop === "getContextualTypes" || prop === "getResolvedSignatures") { + return () => { + requests[prop] = (requests[prop] ?? 0) + 1; + throw new Error(`unknown API method "${prop}"`); + }; + } + return typeof value === "function" ? value.bind(target) : value; + }, + }) as Checker; + const facade = new CheckerFacade(older); + + facade.prefetchRoots([sf]); + expect(requests).toEqual({ getContextualTypes: 1, getResolvedSignatures: 1 }); + facade.prefetchRoots(sf.statements.nodes); + facade.prefetchContextualTypes(collectNodes(w)); + expect(requests).toEqual({ getContextualTypes: 1, getResolvedSignatures: 1 }); + // The node-by-node queries still answer. + const call = collectNodes(w).find(ad.isCallExpression)!; + expect(facade.getResolvedSignature(call)).toBe(raw.getResolvedSignature(call)); +}); + test("autoPrefetch: false degrades to per-call queries (the escape hatch works)", () => { const w = buildTwoWorlds(RICH_TS, host); worlds.push(w); diff --git a/packages/compiler/test/union-scalar-equality-emission.test.ts b/packages/compiler/test/union-scalar-equality-emission.test.ts new file mode 100644 index 000000000..62d59e258 --- /dev/null +++ b/packages/compiler/test/union-scalar-equality-emission.test.ts @@ -0,0 +1,45 @@ +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { compile } from "../src/index.js"; + +async function emitLlvm(source: string): Promise { + const dir = await mkdtemp(join(tmpdir(), "scriptc-union-scalar-eq-")); + try { + const entry = join(dir, "main.ts"); + const outPath = join(dir, "main.ll"); + await writeFile(entry, source); + const result = await compile(entry, { outDir: dir, outPath, outputKind: "llvm" }); + expect(result.ok, JSON.stringify(result.diagnostics)).toBe(true); + return await readFile(outPath, "utf8"); + } finally { + await rm(dir, { recursive: true, force: true }); + } +} + +const source = ` +function zeros(a: number[], at: number[]): number { + let n = 0; + for (const i of at) if (a[i] === 0) n++; + return n; +} +function flags(a: (boolean | undefined)[]): number { + let n = 0; + for (let i = 0; i < a.length; i++) if (a[i] === true) n++; + return n; +} +console.log(zeros([0, 1, 0], [0, 1, 2, 5]), flags([true, undefined, false])); +`; + +test("comparing a union with a number or boolean reads its payload inline", async () => { + const llvm = await emitLlvm(source); + for (const name of ["zeros", "flags"]) { + const fn = new RegExp(`^define internal double @sc_bf_${name}\\([^]*?^}`, "m").exec(llvm)?.[0]; + expect(fn, name).toBeDefined(); + // The tag test and the payload load stay in the function, so the + // union's stack box has no escaping use. + expect(fn).not.toMatch(/@scr_union_get_(f64|bool)\(/); + expect(fn).toMatch(/getelementptr inbounds %ScrUnion, ptr %\S+, i64 0, i32 5/); + } +}); diff --git a/packages/experimental/package.json b/packages/experimental/package.json index 5b6906de6..cfb4151a4 100644 --- a/packages/experimental/package.json +++ b/packages/experimental/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/experimental", - "version": "0.4.1", + "version": "0.4.2", "description": "Experimental scriptc APIs, with Node.js implementations: lifetime regions for ProgramLifetime objects, intern tables shared between threads, and per-thread slots on shared objects", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/llvm-darwin-arm64/package.json b/packages/llvm-darwin-arm64/package.json index 472056c39..615fe9d81 100644 --- a/packages/llvm-darwin-arm64/package.json +++ b/packages/llvm-darwin-arm64/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/llvm-darwin-arm64", - "version": "0.4.1", + "version": "0.4.2", "description": "Pinned LLVM code-generation helper for scriptc on macOS arm64", "license": "Apache-2.0 WITH LLVM-exception", "homepage": "https://scriptc.dev", diff --git a/packages/llvm-darwin-x64/package.json b/packages/llvm-darwin-x64/package.json index 7f55cb6b5..fd6caf976 100644 --- a/packages/llvm-darwin-x64/package.json +++ b/packages/llvm-darwin-x64/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/llvm-darwin-x64", - "version": "0.4.1", + "version": "0.4.2", "description": "Pinned LLVM code-generation helper for scriptc on macOS x64", "license": "Apache-2.0 WITH LLVM-exception", "homepage": "https://scriptc.dev", diff --git a/packages/llvm-linux-arm64-gnu/package.json b/packages/llvm-linux-arm64-gnu/package.json index 0e1a55b22..7d66257f4 100644 --- a/packages/llvm-linux-arm64-gnu/package.json +++ b/packages/llvm-linux-arm64-gnu/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/llvm-linux-arm64-gnu", - "version": "0.4.1", + "version": "0.4.2", "description": "Pinned LLVM code-generation helper for scriptc on Linux arm64", "license": "Apache-2.0 WITH LLVM-exception", "homepage": "https://scriptc.dev", diff --git a/packages/llvm-linux-arm64-musl/package.json b/packages/llvm-linux-arm64-musl/package.json index efdd97444..b3a01089d 100644 --- a/packages/llvm-linux-arm64-musl/package.json +++ b/packages/llvm-linux-arm64-musl/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/llvm-linux-arm64-musl", - "version": "0.4.1", + "version": "0.4.2", "description": "Statically linked LLVM code-generation helper for scriptc on Linux arm64 musl", "license": "Apache-2.0 WITH LLVM-exception", "homepage": "https://scriptc.dev", diff --git a/packages/llvm-linux-x64-gnu/package.json b/packages/llvm-linux-x64-gnu/package.json index 39667d535..0b3264ec7 100644 --- a/packages/llvm-linux-x64-gnu/package.json +++ b/packages/llvm-linux-x64-gnu/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/llvm-linux-x64-gnu", - "version": "0.4.1", + "version": "0.4.2", "description": "Pinned LLVM code-generation helper for scriptc on Linux x64", "license": "Apache-2.0 WITH LLVM-exception", "homepage": "https://scriptc.dev", diff --git a/packages/llvm-linux-x64-musl/package.json b/packages/llvm-linux-x64-musl/package.json index 9efd41845..6c83d1cb9 100644 --- a/packages/llvm-linux-x64-musl/package.json +++ b/packages/llvm-linux-x64-musl/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/llvm-linux-x64-musl", - "version": "0.4.1", + "version": "0.4.2", "description": "Statically linked LLVM code-generation helper for scriptc on Linux x64 musl", "license": "Apache-2.0 WITH LLVM-exception", "homepage": "https://scriptc.dev", diff --git a/packages/llvm-win32-x64-msvc/package.json b/packages/llvm-win32-x64-msvc/package.json index 329fa308c..9fb682f0f 100644 --- a/packages/llvm-win32-x64-msvc/package.json +++ b/packages/llvm-win32-x64-msvc/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/llvm-win32-x64-msvc", - "version": "0.4.1", + "version": "0.4.2", "description": "Pinned LLVM code-generation helper for scriptc on Windows x64", "license": "Apache-2.0 WITH LLVM-exception", "homepage": "https://scriptc.dev", diff --git a/packages/runtime-android-arm64/package.json b/packages/runtime-android-arm64/package.json index 75c54e9c3..362fbc572 100644 --- a/packages/runtime-android-arm64/package.json +++ b/packages/runtime-android-arm64/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/runtime-android-arm64", - "version": "0.4.1", + "version": "0.4.2", "description": "Precompiled scriptc library runtime pack for Android arm64", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/runtime-darwin-arm64/package.json b/packages/runtime-darwin-arm64/package.json index 7473b05f3..a4176ed60 100644 --- a/packages/runtime-darwin-arm64/package.json +++ b/packages/runtime-darwin-arm64/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/runtime-darwin-arm64", - "version": "0.4.1", + "version": "0.4.2", "description": "Precompiled scriptc runtime pack for macOS arm64", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/runtime-darwin-arm64/runtime-pack-matrix.mjs b/packages/runtime-darwin-arm64/runtime-pack-matrix.mjs index db72284f6..7b2d3e115 100644 --- a/packages/runtime-darwin-arm64/runtime-pack-matrix.mjs +++ b/packages/runtime-darwin-arm64/runtime-pack-matrix.mjs @@ -43,7 +43,7 @@ export const RUNTIME_PACK_MATRIX = withLibraryRuntimeFlavors( { id: "quickjs", predicate: false }, { id: "libregexp", predicate: "regex" }, { id: "zlib", predicate: "zlibEffective" }, - { id: "mbedtls", predicate: any("tlsEffective", "cipher", "ecdh") }, + { id: "mbedtls", predicate: any("tlsEffective", "cipher", "ecdh", "dh", "pki") }, ], system_libraries: [{ name: "System", predicate: true }], }), diff --git a/packages/runtime-darwin-arm64/scripts/build.mjs b/packages/runtime-darwin-arm64/scripts/build.mjs index 6cc32d7fa..631750cdf 100644 --- a/packages/runtime-darwin-arm64/scripts/build.mjs +++ b/packages/runtime-darwin-arm64/scripts/build.mjs @@ -173,6 +173,8 @@ async function build() { ...(unit.source === "scr_tls.c" || unit.source === "scr_cipher.c" || unit.source === "scr_ecdh.c" || + unit.source === "scr_dh.c" || + unit.source === "scr_pki.c" || unit.source === "scr_webcrypto_aes.c" ? ["-I", join(mbedtls, "include")] : []), @@ -338,7 +340,7 @@ async function build() { package: packageManifest.name, version: packageManifest.version, target: RUNTIME_PACK_MATRIX.target, - runtime_abi: { version: 25, marker: "scr_runtime_abi_v25" }, + runtime_abi: { version: 27, marker: "scr_runtime_abi_v27" }, compiler: { command: compiler, identity: compilerVersion, diff --git a/packages/runtime-darwin-x64/package.json b/packages/runtime-darwin-x64/package.json index ab8666c88..91bc4182b 100644 --- a/packages/runtime-darwin-x64/package.json +++ b/packages/runtime-darwin-x64/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/runtime-darwin-x64", - "version": "0.4.1", + "version": "0.4.2", "description": "Precompiled scriptc runtime pack for macOS x64", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/runtime-ios-arm64/package.json b/packages/runtime-ios-arm64/package.json index a3a254685..2fdd68897 100644 --- a/packages/runtime-ios-arm64/package.json +++ b/packages/runtime-ios-arm64/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/runtime-ios-arm64", - "version": "0.4.1", + "version": "0.4.2", "description": "Precompiled scriptc library runtime pack for iOS arm64", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/runtime-ios-simulator-arm64/package.json b/packages/runtime-ios-simulator-arm64/package.json index 7981422b8..cbdf96d1d 100644 --- a/packages/runtime-ios-simulator-arm64/package.json +++ b/packages/runtime-ios-simulator-arm64/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/runtime-ios-simulator-arm64", - "version": "0.4.1", + "version": "0.4.2", "description": "Precompiled scriptc library runtime pack for iOS simulator arm64", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/runtime-linux-arm64-gnu/package.json b/packages/runtime-linux-arm64-gnu/package.json index d633ac303..e0d228925 100644 --- a/packages/runtime-linux-arm64-gnu/package.json +++ b/packages/runtime-linux-arm64-gnu/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/runtime-linux-arm64-gnu", - "version": "0.4.1", + "version": "0.4.2", "description": "Precompiled scriptc runtime pack for Linux arm64 glibc", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/runtime-linux-arm64-musl/package.json b/packages/runtime-linux-arm64-musl/package.json index 460ca25fa..70eee223f 100644 --- a/packages/runtime-linux-arm64-musl/package.json +++ b/packages/runtime-linux-arm64-musl/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/runtime-linux-arm64-musl", - "version": "0.4.1", + "version": "0.4.2", "description": "Precompiled scriptc runtime pack for Linux arm64 musl", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/runtime-linux-x64-gnu/package.json b/packages/runtime-linux-x64-gnu/package.json index 9c6336a04..5822fd21d 100644 --- a/packages/runtime-linux-x64-gnu/package.json +++ b/packages/runtime-linux-x64-gnu/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/runtime-linux-x64-gnu", - "version": "0.4.1", + "version": "0.4.2", "description": "Precompiled scriptc runtime pack for Linux x64 glibc", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/runtime-linux-x64-musl/package.json b/packages/runtime-linux-x64-musl/package.json index e9f885a5c..1db34d4a7 100644 --- a/packages/runtime-linux-x64-musl/package.json +++ b/packages/runtime-linux-x64-musl/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/runtime-linux-x64-musl", - "version": "0.4.1", + "version": "0.4.2", "description": "Precompiled scriptc runtime pack for Linux x64 musl", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/runtime-pack-common/runtime-pack-matrix.mjs b/packages/runtime-pack-common/runtime-pack-matrix.mjs index 3529df137..322832f9f 100644 --- a/packages/runtime-pack-common/runtime-pack-matrix.mjs +++ b/packages/runtime-pack-common/runtime-pack-matrix.mjs @@ -151,8 +151,12 @@ export const OPTIONAL_RUNTIME_SOURCES = [ ["scr_tls.c", "tlsEffective"], ["scr_cipher.c", "cipher"], ["scr_ecdh.c", "ecdh"], + ["scr_dh.c", "dh"], + ["scr_pki.c", "pki"], ["scr_wasm.c", "wasm"], ["scr_webcrypto.c", "webcrypto"], + ["scr_webcrypto_pki.c", all("webcrypto", "pki")], + ["scr_ed25519.c", all("pki", "ed25519")], // WebCrypto's AES ciphers run over the cipher feature's mbedTLS archive. ["scr_webcrypto_aes.c", all("cipher", "webcrypto")], ["scr_fetch.c", "nativeFetch"], @@ -216,7 +220,7 @@ export function createRuntimePackMatrix({ { id: "quickjs", predicate: false }, { id: "libregexp", predicate: "regex" }, { id: "zlib", predicate: "zlibEffective" }, - { id: "mbedtls", predicate: any("tlsEffective", "cipher", "ecdh") }, + { id: "mbedtls", predicate: any("tlsEffective", "cipher", "ecdh", "dh", "pki") }, ]; const matrix = withLibraryRuntimeFlavors( withWorkerRuntimeVariants({ diff --git a/packages/runtime-pack-common/scripts/build.mjs b/packages/runtime-pack-common/scripts/build.mjs index a2c987b97..0e81033ad 100644 --- a/packages/runtime-pack-common/scripts/build.mjs +++ b/packages/runtime-pack-common/scripts/build.mjs @@ -224,6 +224,8 @@ async function build() { ...(unit.source === "scr_tls.c" || unit.source === "scr_cipher.c" || unit.source === "scr_ecdh.c" || + unit.source === "scr_dh.c" || + unit.source === "scr_pki.c" || unit.source === "scr_webcrypto_aes.c" ? ["-I", join(mbedtls, "include")] : []), @@ -435,7 +437,7 @@ async function build() { package: packageManifest.name, version: packageManifest.version, target: matrix.target, - runtime_abi: { version: 25, marker: "scr_runtime_abi_v25" }, + runtime_abi: { version: 27, marker: "scr_runtime_abi_v27" }, compiler: { command: compiler, identity: compilerVersion, diff --git a/packages/runtime-wasm32-wasi/package.json b/packages/runtime-wasm32-wasi/package.json index c9a40c2c0..5bbd23755 100644 --- a/packages/runtime-wasm32-wasi/package.json +++ b/packages/runtime-wasm32-wasi/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/runtime-wasm32-wasi", - "version": "0.4.1", + "version": "0.4.2", "description": "Precompiled scriptc runtime pack for WASI Preview 1", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/runtime-wasm32-wasi/scripts/build.mjs b/packages/runtime-wasm32-wasi/scripts/build.mjs index 0a8699b23..cbe744e4e 100644 --- a/packages/runtime-wasm32-wasi/scripts/build.mjs +++ b/packages/runtime-wasm32-wasi/scripts/build.mjs @@ -22,6 +22,10 @@ process.env.SCRIPTC_RUNTIME_PACK_CONFIG = JSON.stringify({ "scr_tls.c", "scr_cipher.c", "scr_ecdh.c", + "scr_dh.c", + "scr_pki.c", + "scr_webcrypto_pki.c", + "scr_ed25519.c", "scr_webcrypto_aes.c", "scr_wasm.c", "scr_fetch.c", diff --git a/packages/runtime-win32-x64-msvc/package.json b/packages/runtime-win32-x64-msvc/package.json index f7e4c9860..df4a9a109 100644 --- a/packages/runtime-win32-x64-msvc/package.json +++ b/packages/runtime-win32-x64-msvc/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/runtime-win32-x64-msvc", - "version": "0.4.1", + "version": "0.4.2", "description": "Precompiled scriptc runtime pack for Windows x64 MSVC", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 1af249b9d..9232266af 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/runtime", - "version": "0.4.1", + "version": "0.4.2", "description": "scriptc native runtime sources and vendored dependencies", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/runtime/src/scr_array.c b/packages/runtime/src/scr_array.c index c00d1b4f6..d4cdf0810 100644 --- a/packages/runtime/src/scr_array.c +++ b/packages/runtime/src/scr_array.c @@ -60,9 +60,10 @@ static void *scr_arr_alloc_start(const ScrArr *a) { return (char *)scr_arr_block(a) - scr_arr_prefix(a); } -/* Bytes per dense slot with its state: an 8-byte value, or a boolean byte. */ +/* Bytes per dense slot with its state: an 8-byte value, a boolean byte or + * a 4-byte compact reference. */ static size_t scr_arr_slot_unit(const ScrArr *a) { - return a->elem == SCR_ELEM_BOOL ? 2 * sizeof(uint8_t) : sizeof(uint64_t) + sizeof(uint8_t); + return scr_arr_slot_width(a) + sizeof(uint8_t); } /* Bytes of dense storage (head room included) after the prefix. */ @@ -212,6 +213,11 @@ static bool scr_elem_is_ref(ScrElemKind k) { k == SCR_ELEM_REF; } +/* Whether `a`'s element type comes with element ops (scr_arr_ops). */ +static bool scr_arr_has_ops(const ScrArr *a) { + return a->elem == SCR_ELEM_REF || a->elem == SCR_ELEM_CREF; +} + /* Releases an element slot of kind `elem`, with element ops `ops` for * SCR_ELEM_REF. An array's kind and ops never change, so loops look them up * once (element releases may run teardown, which reloads anything read @@ -266,6 +272,7 @@ typedef struct { static uint64_t scr_arr_storage_get(const ScrArrStorage *s, size_t index) { if (s->elem == SCR_ELEM_BOOL) return ((const uint8_t *)s->data)[index]; + if (s->elem == SCR_ELEM_CREF) return scr_cref_decode(((const uint32_t *)s->data)[index]); return s->data[index]; } @@ -388,9 +395,17 @@ static void scr_arr_sparse_drop_prefix(ScrArr *a, size_t count) { } /* Bytes per dense slot: the value and its state byte. Boolean arrays - * store a one-byte value. */ + * store a one-byte value, compact reference arrays a 4-byte one. */ #define SCR_ARR_SLOT_BYTES (sizeof(uint64_t) + sizeof(uint8_t)) #define SCR_ARR_BOOL_SLOT_BYTES (2 * sizeof(uint8_t)) +#define SCR_ARR_CREF_SLOT_BYTES (sizeof(uint32_t) + sizeof(uint8_t)) + +/* SCR_ARR_*_SLOT_BYTES for an element kind. */ +static size_t scr_elem_slot_unit(uint8_t elem) { + return elem == SCR_ELEM_BOOL ? SCR_ARR_BOOL_SLOT_BYTES + : elem == SCR_ELEM_CREF ? SCR_ARR_CREF_SLOT_BYTES + : SCR_ARR_SLOT_BYTES; +} /* Appends to an array smaller than this grow straight to it. */ #define SCR_ARR_MIN_GROWTH ((size_t)4) @@ -469,7 +484,7 @@ static void scr_arr_drop_head(ScrArr *a) { * for storage without head room (booleans, inline slots); the caller then * moves the tail down itself. */ static bool scr_arr_drop_front(ScrArr *a, size_t count) { - if (a->elem == SCR_ELEM_BOOL || scr_arr_inline(a)) return false; + if (scr_arr_slot_width(a) != sizeof(uint64_t) || scr_arr_inline(a)) return false; if (count == 0) return true; size_t head = scr_arr_head(a); memset(scr_arr_state_run(a, 0, count), SCR_ARR_HOLE, count); @@ -504,18 +519,21 @@ static bool scr_arr_drop_front(ScrArr *a, size_t count) { #endif /* Inline capacity of a small construction with `prefix` bytes of block - * before the slots. */ -static size_t scr_arr_small_cap(size_t prefix, bool bools) { + * before the slots of `unit` bytes (SCR_ARR_*_SLOT_BYTES). */ +static size_t scr_arr_small_cap(size_t prefix, size_t unit) { #if SCR_SMALL_ALLOC /* One return per slot unit, so each folds to a constant at its callers. */ - if (bools) + if (unit == SCR_ARR_BOOL_SLOT_BYTES) return (scr_sa_class_size(scr_sa_class(prefix + SCR_ARR_SMALL_SLOTS * SCR_ARR_BOOL_SLOT_BYTES)) - prefix) / SCR_ARR_BOOL_SLOT_BYTES; + if (unit == SCR_ARR_CREF_SLOT_BYTES) + return (scr_sa_class_size(scr_sa_class(prefix + SCR_ARR_SMALL_SLOTS * SCR_ARR_CREF_SLOT_BYTES)) - prefix) / + SCR_ARR_CREF_SLOT_BYTES; return (scr_sa_class_size(scr_sa_class(prefix + SCR_ARR_SMALL_SLOTS * SCR_ARR_SLOT_BYTES)) - prefix) / SCR_ARR_SLOT_BYTES; #else (void)prefix; - (void)bools; + (void)unit; return SCR_ARR_SMALL_SLOTS; #endif } @@ -528,26 +546,27 @@ static size_t scr_arr_small_cap(size_t prefix, bool bools) { #define SCR_ARR_KNOWN_INLINE_MAX ((size_t)16) /* Inline capacity of a construction of `need` elements with `prefix` bytes - * of block before the slots, rounded up to fill the block; 0 when its slots - * stay separate. */ -static size_t scr_arr_known_cap(size_t prefix, size_t need, bool bools) { + * of block before the slots of `unit` bytes (SCR_ARR_*_SLOT_BYTES), rounded + * up to fill the block; 0 when its slots stay separate. */ +static size_t scr_arr_known_cap(size_t prefix, size_t need, size_t unit) { if (need == 0 || need > SCR_ARR_KNOWN_INLINE_MAX) return 0; #if SCR_SMALL_ALLOC - size_t unit = bools ? SCR_ARR_BOOL_SLOT_BYTES : SCR_ARR_SLOT_BYTES; size_t bytes = prefix + need * unit; if (bytes > SCR_SA_MAX) return 0; return (scr_sa_class_size(scr_sa_class(bytes)) - prefix) / unit; #else (void)prefix; - (void)bools; + (void)unit; return need; #endif } -static void scr_arr_use_inline(ScrArr *a, size_t cap) { +/* Points `a` at `cap` inline slots of `unit` bytes (SCR_ARR_*_SLOT_BYTES), + * all holes. */ +static void scr_arr_use_inline(ScrArr *a, size_t cap, size_t unit) { a->data = (uint64_t *)((char *)a + SCR_ARR_INLINE_OFFSET); a->cap = (uint32_t)cap; - memset(scr_arr_state_run(a, 0, cap), SCR_ARR_HOLE, cap); + memset((char *)a->data + cap * (unit - 1), SCR_ARR_HOLE, cap); a->spare |= SCR_ARR_INLINE_SLOTS; } @@ -582,15 +601,15 @@ typedef enum { * reallocation. Growth keeps its own sequence (4, 8, 16, ...): rounding it * as well moves array storage into other size classes and measured * slower. */ -static size_t scr_arr_block_cap(size_t cap, bool bools) { +static size_t scr_arr_block_cap(size_t cap, size_t unit) { #if SCR_SMALL_ALLOC - size_t bytes = cap * (bools ? SCR_ARR_BOOL_SLOT_BYTES : SCR_ARR_SLOT_BYTES); + size_t bytes = cap * unit; if (bytes <= SCR_SA_MAX) { size_t block = scr_sa_class_size(scr_sa_class((bytes + 15) & ~(size_t)15)); - return bools ? block / SCR_ARR_BOOL_SLOT_BYTES : block / SCR_ARR_SLOT_BYTES; + return block / unit; } #else - (void)bools; + (void)unit; #endif return cap; } @@ -609,14 +628,14 @@ static void scr_arr_grow_dense_slow(ScrArr *a, size_t need, ScrArrGrow mode); * paths pick the same capacity. */ static inline void scr_arr_grow_dense_to(ScrArr *a, size_t need, ScrArrGrow mode) { if (need <= a->cap) return; - if (a->elem == SCR_ELEM_BOOL || (a->spare & (SCR_ARR_INLINE_SLOTS | SCR_ARR_HEAD_ROOM | SCR_ARR_HAS_EXT)) || - need > SCR_ARR_DENSE_LIMIT) { + if (a->elem == SCR_ELEM_BOOL || a->elem == SCR_ELEM_CREF || + (a->spare & (SCR_ARR_INLINE_SLOTS | SCR_ARR_HEAD_ROOM | SCR_ARR_HAS_EXT)) || need > SCR_ARR_DENSE_LIMIT) { scr_arr_grow_dense_slow(a, need, mode); return; } size_t cap; if (mode == SCR_ARR_GROW_EXACT) { - cap = scr_arr_block_cap(need, false); + cap = scr_arr_block_cap(need, SCR_ARR_SLOT_BYTES); } else { cap = a->cap < SCR_ARR_MIN_GROWTH ? SCR_ARR_MIN_GROWTH : a->cap; while (cap < need) cap += cap; @@ -660,12 +679,11 @@ static void scr_arr_grow_dense_slow(ScrArr *a, size_t need, ScrArrGrow mode) { mode = SCR_ARR_GROW_APPEND; } } - bool bools = a->elem == SCR_ELEM_BOOL; size_t width = scr_arr_slot_width(a); - size_t unit = bools ? SCR_ARR_BOOL_SLOT_BYTES : SCR_ARR_SLOT_BYTES; + size_t unit = scr_arr_slot_unit(a); size_t cap; if (mode == SCR_ARR_GROW_EXACT) { - cap = scr_arr_block_cap(need, bools); + cap = scr_arr_block_cap(need, unit); } else { /* Inline slots overflow into the ordinary sequence (4, 8, 16, ...). */ size_t from = scr_arr_inline(a) ? 0 : a->cap; @@ -723,7 +741,7 @@ static void scr_arr_grow_dense(ScrArr *a, size_t need) { * array's allocation. Returns false when the caller must move the elements * itself. */ static bool scr_arr_open_head(ScrArr *a, size_t count) { - if (a->elem == SCR_ELEM_BOOL || scr_arr_inline(a)) return false; + if (scr_arr_slot_width(a) != sizeof(uint64_t) || scr_arr_inline(a)) return false; size_t head = scr_arr_head(a); if (head < count) { size_t len = a->len; @@ -851,6 +869,20 @@ static inline uint8_t scr_arr_state_at(const ScrArr *a, size_t index, uint64_t * return sparse.state; } +/* scr_arr_state_at for a number[] (SCR_ELEM_F64): 8-byte slots, with no + * element-kind test. */ +static inline uint8_t scr_arr_f64_state_at(const ScrArr *a, size_t index, uint64_t *out) { + if (index < a->cap) { + uint8_t state = *scr_arr_state_in(scr_arr_states_end_w(a, 8), index); + if (state == SCR_ARR_HOLE) return SCR_ARR_HOLE; + *out = a->data[index]; + return state; + } + ScrArrStateSlot sparse = scr_arr_sparse_state_at(a, index); + if (sparse.state != SCR_ARR_HOLE) *out = sparse.slot; + return sparse.state; +} + static inline bool scr_arr_slot_at(const ScrArr *a, size_t index, uint64_t *out) { return scr_arr_state_at(a, index, out) != SCR_ARR_HOLE; } @@ -1148,9 +1180,10 @@ static void scr_arr_copy_dense(ScrArr *dst, size_t to, const ScrArr *src, if (count == 0) return; scr_arr_cut(dst, to); /* 8-byte slots: a constant slot size, and constant-width state ends. */ - bool wide = dst->elem != SCR_ELEM_BOOL; - uint8_t *dst_states = wide ? scr_arr_states_end_w(dst, 8) : scr_arr_states_end_w(dst, 1); - const uint8_t *src_states = wide ? scr_arr_states_end_w(src, 8) : scr_arr_states_end_w(src, 1); + size_t width = scr_arr_slot_width(dst); + bool wide = width == sizeof(uint64_t); + uint8_t *dst_states = wide ? scr_arr_states_end_w(dst, 8) : scr_arr_states_end_w(dst, width); + const uint8_t *src_states = wide ? scr_arr_states_end_w(src, 8) : scr_arr_states_end_w(src, width); if (!reverse && wide) { scr_arr_copy_slots(dst->data + to, src->data + from, count); /* A range below the source's hole-free prefix holds only values. */ @@ -1185,21 +1218,28 @@ static void scr_arr_copy_dense(ScrArr *dst, size_t to, const ScrArr *src, /* Store `count` 8-byte value slots into dense storage starting at `to`. */ static void scr_arr_put_slots(ScrArr *a, size_t to, size_t count, const uint64_t *slots) { - if (a->elem != SCR_ELEM_BOOL) { + if (scr_arr_slot_width(a) == sizeof(uint64_t)) { scr_arr_copy_slots(a->data + to, slots, count); return; } for (size_t i = 0; i < count; i++) scr_arr_dense_put(a, to + i, slots[i]); } +/* The slot unit of an array that scr_arr_new, scr_arr_new_small or + * scr_arr_new_inline makes. Their kinds have no element ops, so they are + * never SCR_ELEM_CREF (scr_arr_new_ops makes those). */ +static size_t scr_arr_plain_slot_unit(ScrElemKind elem) { + return elem == SCR_ELEM_BOOL ? SCR_ARR_BOOL_SLOT_BYTES : SCR_ARR_SLOT_BYTES; +} + /* Inlined into each constructor, so `small` and the slot unit fold to * constants there. */ static inline __attribute__((always_inline)) ScrArr *scr_arr_make(ScrElemKind elem, size_t initial_cap, bool small) { - size_t unit = elem == SCR_ELEM_BOOL ? SCR_ARR_BOOL_SLOT_BYTES : SCR_ARR_SLOT_BYTES; - size_t inline_cap = small ? scr_arr_small_cap(SCR_ARR_INLINE_OFFSET, elem == SCR_ELEM_BOOL) - : scr_arr_known_cap(SCR_ARR_INLINE_OFFSET, initial_cap, elem == SCR_ELEM_BOOL); + size_t unit = scr_arr_plain_slot_unit(elem); + size_t inline_cap = small ? scr_arr_small_cap(SCR_ARR_INLINE_OFFSET, unit) + : scr_arr_known_cap(SCR_ARR_INLINE_OFFSET, initial_cap, unit); ScrArr *a = scr_mem_alloc(inline_cap ? SCR_ARR_INLINE_OFFSET + inline_cap * unit : sizeof(ScrArr)); if (!a) scr_arr_oom(); a->rc = 1; @@ -1210,7 +1250,7 @@ static inline __attribute__((always_inline)) ScrArr *scr_arr_make(ScrElemKind el a->spare = 0; a->filled = 0; a->data = NULL; - if (inline_cap) scr_arr_use_inline(a, inline_cap); + if (inline_cap) scr_arr_use_inline(a, inline_cap, unit); else if (initial_cap > 0) scr_arr_grow_dense_to(a, initial_cap, SCR_ARR_GROW_EXACT); #ifdef SCR_RC_AUDIT scr_live_arrays++; @@ -1231,7 +1271,7 @@ ScrArr *scr_arr_new_small(ScrElemKind elem) { return scr_arr_make(elem, 0, true) ScrArr *scr_arr_new_inline(ScrElemKind elem, size_t cap) { if (cap == 0) return scr_arr_make(elem, 0, false); if (cap > SCR_ARR_MAX_LENGTH) scr_arr_oom(); - size_t unit = elem == SCR_ELEM_BOOL ? SCR_ARR_BOOL_SLOT_BYTES : SCR_ARR_SLOT_BYTES; + size_t unit = scr_arr_plain_slot_unit(elem); ScrArr *a = scr_mem_alloc(SCR_ARR_INLINE_OFFSET + cap * unit); if (!a) scr_arr_oom(); a->rc = 1; @@ -1242,7 +1282,7 @@ ScrArr *scr_arr_new_inline(ScrElemKind elem, size_t cap) { a->spare = 0; a->filled = 0; a->data = NULL; - scr_arr_use_inline(a, cap); + scr_arr_use_inline(a, cap, unit); #ifdef SCR_RC_AUDIT scr_live_arrays++; #endif @@ -1250,9 +1290,7 @@ ScrArr *scr_arr_new_inline(ScrElemKind elem, size_t cap) { } void scr_arr_outline(ScrArr *a) { - if (scr_arr_inline(a)) - scr_arr_move_inline(a, a->cap, - a->elem == SCR_ELEM_BOOL ? SCR_ARR_BOOL_SLOT_BYTES : SCR_ARR_SLOT_BYTES); + if (scr_arr_inline(a)) scr_arr_move_inline(a, a->cap, scr_arr_slot_unit(a)); /* The swapped fields do not include `spare`: no head room either. */ if (a->spare & SCR_ARR_HEAD_ROOM) { scr_arr_drop_head(a); @@ -1329,18 +1367,20 @@ static void scr_arr_gc_free(void *a0) { scr_cyc_free_inline(a); } -/* Inlined into each constructor like scr_arr_make: with `small` and the - * count constant, scr_arr_new_ops_small's size class folds to a table - * index. */ -static inline __attribute__((always_inline)) ScrArr *scr_arr_make_ops(const ScrArrOps *ops, - size_t initial_cap, - bool small) { +/* Inlined into each constructor like scr_arr_make: with `small`, the count + * and `elem` constant, scr_arr_new_ops_small's size class folds to a table + * index and the slot sizes to constants. */ +static inline __attribute__((always_inline)) ScrArr *scr_arr_make_ops_kind(const ScrArrOps *ops, + size_t initial_cap, + bool small, + uint8_t elem) { ScrArr *a; bool cyc = ops->trace != NULL; + size_t unit = scr_elem_slot_unit(elem); size_t prefix = SCR_ARR_INLINE_OFFSET + (cyc ? sizeof(ScrCycHdr) : 0); size_t inline_cap = - small ? scr_arr_small_cap(prefix, false) : scr_arr_known_cap(prefix, initial_cap, false); - size_t size = inline_cap ? SCR_ARR_INLINE_OFFSET + inline_cap * SCR_ARR_SLOT_BYTES : sizeof(ScrArr); + small ? scr_arr_small_cap(prefix, unit) : scr_arr_known_cap(prefix, initial_cap, unit); + size_t size = inline_cap ? SCR_ARR_INLINE_OFFSET + inline_cap * unit : sizeof(ScrArr); if (cyc) { static ScrCycType cyc_type = { &scr_arr_trace_v, &scr_arr_gc_free, SCR_CYC_KIND_ARRAY }; a = scr_cyc_alloc_inline(size, &cyc_type); @@ -1351,7 +1391,7 @@ static inline __attribute__((always_inline)) ScrArr *scr_arr_make_ops(const ScrA a->rc = 1; a->len = 0; a->cap = 0; - a->elem = SCR_ELEM_REF; + a->elem = elem; a->cyc = cyc; /* The ops id is written with the other header fields: reading `spare` * back after scr_arr_use_inline sets its inline flag stalls the @@ -1360,7 +1400,7 @@ static inline __attribute__((always_inline)) ScrArr *scr_arr_make_ops(const ScrA a->spare = (uint16_t)(id << SCR_ARR_OPS_SHIFT); a->filled = 0; a->data = NULL; - if (inline_cap) scr_arr_use_inline(a, inline_cap); + if (inline_cap) scr_arr_use_inline(a, inline_cap, unit); else if (initial_cap > 0) scr_arr_grow_dense_to(a, initial_cap, SCR_ARR_GROW_EXACT); /* After the storage: ops past the registry live in the ext, whose * pointer precedes the dense allocation, so inline slots move out first @@ -1372,6 +1412,16 @@ static inline __attribute__((always_inline)) ScrArr *scr_arr_make_ops(const ScrA return a; } +/* SCR_ELEM_CREF for compact element types, SCR_ELEM_REF otherwise. Each + * branch inlines scr_arr_make_ops_kind with a constant kind. */ +static inline __attribute__((always_inline)) ScrArr *scr_arr_make_ops(const ScrArrOps *ops, + size_t initial_cap, + bool small) { + if (__builtin_expect(scr_arr_ops_compact(ops), 0)) + return scr_arr_make_ops_kind(ops, initial_cap, small, SCR_ELEM_CREF); + return scr_arr_make_ops_kind(ops, initial_cap, small, SCR_ELEM_REF); +} + ScrArr *scr_arr_new_ops(const ScrArrOps *ops, size_t initial_cap) { return scr_arr_make_ops(ops, initial_cap, false); } @@ -1676,6 +1726,7 @@ static ScrArr *scr_arr_fill_slot(ScrArr *a, uint64_t slot, uint8_t state, if (!refs && until <= a->cap) { if (state != SCR_ARR_VALUE) scr_arr_cut(a, from); if (a->elem == SCR_ELEM_BOOL) memset(scr_arr_dense_at(a, from), (int)slot, until - from); + else if (a->elem == SCR_ELEM_CREF) for (size_t i = from; i < until; i++) scr_arr_dense_put(a, i, slot); else for (size_t i = from; i < until; i++) a->data[i] = slot; memset(scr_arr_state_run(a, from, until - from), state, until - from); } else { @@ -1712,7 +1763,7 @@ double scr_math_max_arr(ScrArr *a) { double best = -INFINITY; for (size_t i = 0; i < a->len; i++) { uint64_t slot; - if (scr_arr_state_at(a, i, &slot) != SCR_ARR_VALUE) return NAN; + if (scr_arr_f64_state_at(a, i, &slot) != SCR_ARR_VALUE) return NAN; double v = scr_slot_to_f64(slot); if (isnan(v)) return v; if (v > best || (v == 0.0 && best == 0.0 && !signbit(v))) best = v; @@ -1724,7 +1775,7 @@ double scr_math_min_arr(ScrArr *a) { double best = INFINITY; for (size_t i = 0; i < a->len; i++) { uint64_t slot; - if (scr_arr_state_at(a, i, &slot) != SCR_ARR_VALUE) return NAN; + if (scr_arr_f64_state_at(a, i, &slot) != SCR_ARR_VALUE) return NAN; double v = scr_slot_to_f64(slot); if (isnan(v)) return v; if (v < best || (v == 0.0 && best == 0.0 && signbit(v))) best = v; @@ -1739,7 +1790,7 @@ double scr_math_hypot_arr(ScrArr *a) { bool has_nan = false; for (size_t i = 0; i < a->len; i++) { uint64_t slot; - if (scr_arr_state_at(a, i, &slot) != SCR_ARR_VALUE) { + if (scr_arr_f64_state_at(a, i, &slot) != SCR_ARR_VALUE) { has_nan = true; continue; } @@ -1754,7 +1805,7 @@ double scr_math_hypot_arr(ScrArr *a) { double correction = 0.0; for (size_t i = 0; i < a->len; i++) { uint64_t slot; - (void)scr_arr_state_at(a, i, &slot); + (void)scr_arr_f64_state_at(a, i, &slot); double v = scr_slot_to_f64(slot) / scale; double term = v * v - correction; double next = sum + term; @@ -1830,9 +1881,9 @@ void *scr_arr_peek_ref(const ScrArr *a, double i) { if (i >= 0 && i < (double)a->cap) { size_t idx = (size_t)i; if ((double)idx == i) { - if (idx < a->filled) return scr_slot_to_ptr(a->data[idx]); + if (idx < a->filled) return scr_slot_to_ptr(scr_arr_dense_get(a, idx)); if (idx >= a->len || *scr_arr_state_ptr(a, idx) != SCR_ARR_VALUE) return NULL; - return scr_slot_to_ptr(a->data[idx]); + return scr_slot_to_ptr(scr_arr_dense_get(a, idx)); } } uint64_t slot; @@ -1886,7 +1937,7 @@ void *scr_arr_get_ref(ScrArr *a, double i) { if (a->elem == SCR_ELEM_STR) scr_str_retain((ScrStr *)p); else if (a->elem == SCR_ELEM_BYTES) scr_bytes_retain((ScrBytes *)p); else if (a->elem == SCR_ELEM_REF) p = scr_arr_ops(a)->retain(p); - else scr_arr_retain((ScrArr *)p); + else if (a->elem == SCR_ELEM_ARR) scr_arr_retain((ScrArr *)p); return p; } @@ -2395,7 +2446,7 @@ ScrArr *scr_arr_splice(ScrArr *a, double start, double deleteCount) { * otherwise the range may be mostly holes, so the capacity is a hint. */ size_t hint = scr_arr_is_dense(a) || n <= SCR_ARR_DENSE_LIMIT ? n : SCR_ARR_DENSE_LIMIT; ScrArr *out = - a->elem == SCR_ELEM_REF + scr_arr_has_ops(a) ? scr_arr_new_ops(scr_arr_ops(a), hint ? hint : 1) : scr_arr_new(a->elem, hint ? hint : 1); out->len = n; @@ -2475,7 +2526,7 @@ ScrArr *scr_arr_splice_insert(ScrArr *a, double start, double deleteCount, * is needed only by direct self-spread runtime callers. */ ScrArr *snapshot = items == a ? scr_arr_slice(a, 0, INFINITY) : NULL; if (snapshot) items = snapshot; - ScrArr *removed = a->elem == SCR_ELEM_REF + ScrArr *removed = scr_arr_has_ops(a) ? scr_arr_new_ops(scr_arr_ops(a), n) : scr_arr_new(a->elem, n); if (n) { @@ -2521,7 +2572,7 @@ void scr_arr_splice_drop(ScrArr *a, double start, double deleteCount, size_t cou size_t next_len = a->len - n + count; bool refs = scr_elem_is_ref(a->elem); if (!scr_arr_is_dense(a) || !scr_arr_dense_ok(a, next_len, count)) { - ScrArr *items = a->elem == SCR_ELEM_REF + ScrArr *items = scr_arr_has_ops(a) ? scr_arr_new_ops(scr_arr_ops(a), count) : scr_arr_new(a->elem, count); scr_arr_push_many(items, count, slots); @@ -2636,7 +2687,8 @@ double scr_arr_index_of_bool(ScrArr *a, bool v) { double scr_arr_index_of_ref(ScrArr *a, void *v) { const uint8_t *states = scr_arr_states_end(a); for (size_t i = 0; i < a->cap && i < a->len; i++) { - if (*scr_arr_state_in(states, i) == SCR_ARR_VALUE && scr_arr_ref_eq(a, a->data[i], v)) return (double)i; + if (*scr_arr_state_in(states, i) == SCR_ARR_VALUE && scr_arr_ref_eq(a, scr_arr_dense_get(a, i), v)) + return (double)i; } for (size_t i = 0; i < SCR_AX(a)->sparse_len; i++) { if (SCR_AX(a)->sparse[i].state == SCR_ARR_VALUE && scr_arr_ref_eq(a, SCR_AX(a)->sparse[i].slot, v)) return (double)SCR_AX(a)->sparse[i].index; @@ -2727,11 +2779,12 @@ ScrArr *scr_arr_slice(ScrArr *a, double start, double end) { * sparse side store may be mostly holes, so its capacity is a hint. */ size_t hint = to <= a->cap || n <= SCR_ARR_DENSE_LIMIT ? n : SCR_ARR_DENSE_LIMIT; ScrArr *out = - a->elem == SCR_ELEM_REF + scr_arr_has_ops(a) ? scr_arr_new_ops(scr_arr_ops(a), hint ? hint : 1) : scr_arr_new(a->elem, hint ? hint : 1); out->len = n; - if (to <= a->cap && to <= a->filled && a->elem != SCR_ELEM_BOOL && !scr_elem_is_ref(a->elem)) { + if (to <= a->cap && to <= a->filled && scr_arr_slot_width(a) == sizeof(uint64_t) && + !scr_elem_is_ref(a->elem)) { /* Numbers below the source's hole-free prefix: values only. */ scr_arr_copy_slots(out->data, a->data + from, n); scr_arr_set_states(scr_arr_states_end_w(out, 8) - n, SCR_ARR_VALUE, n); @@ -3024,14 +3077,14 @@ static ScrSortCompare scr_sort_compare_for(const ScrArr *a, ScrClosure *f, uint3 bool multi = order >= 2; if (order != 0 && (arity != 2 || (order != 1 && order != -1 && !scr_sort_keys_valid(order)) || - (c.numbers ? key != NULL || multi : a->elem != SCR_ELEM_REF || key == NULL))) + (c.numbers ? key != NULL || multi : !scr_arr_has_ops(a) || key == NULL))) scr_trap("scriptc: invalid keyed sort\n"); if (!c.numbers) { c.borrowing = f->fn_borrowed != NULL; if (a->elem == SCR_ELEM_STR) c.retain = scr_sort_retain_str; else if (a->elem == SCR_ELEM_ARR) c.retain = scr_sort_retain_arr; else if (a->elem == SCR_ELEM_BYTES) c.retain = scr_sort_retain_bytes; - else if (a->elem == SCR_ELEM_REF) c.retain = scr_arr_ops(a)->retain; + else if (scr_arr_has_ops(a)) c.retain = scr_arr_ops(a)->retain; else scr_trap("scriptc: invalid comparator sort element kind\n"); } return c; @@ -4619,7 +4672,7 @@ static bool scr_arr_dense_values(const ScrArr *a) { * elements (stored in 1-byte slots). The slots are borrowed and stay valid * while nothing writes the array. */ const uint64_t *scr_arr_dense_slots(const ScrArr *a, size_t *len) { - if (a->len == 0 || a->elem == SCR_ELEM_BOOL || !scr_arr_dense_values(a)) return NULL; + if (a->len == 0 || scr_arr_slot_width(a) != sizeof(uint64_t) || !scr_arr_dense_values(a)) return NULL; *len = a->len; return a->data; } @@ -4646,7 +4699,9 @@ bool scr_arr_sort_dense(ScrArr *a, ScrClosure *f, uint32_t arity, bool owned, in ScrClosure *key) { uint8_t elem = a->elem; const ScrArrOps *ops = scr_arr_ops(a); - if (a->rc == SIZE_MAX || !scr_arr_dense_values(a)) return false; + /* Compact references sort through the snapshot path, which reads and + * writes 8-byte slots. */ + if (a->rc == SIZE_MAX || elem == SCR_ELEM_CREF || !scr_arr_dense_values(a)) return false; size_t n = a->len; if (n < 2) return true; if (n > SIZE_MAX / 2 / sizeof(uint64_t)) scr_arr_oom(); @@ -4687,12 +4742,14 @@ bool scr_arr_sort_dense(ScrArr *a, ScrClosure *f, uint32_t arity, bool owned, in } else { /* Publish each new edge before releasing the value it replaces: a * release can collect cycles. The buffer still holds the slots not - * yet written. */ + * yet written. The kind, and so the slot width, never changes. */ + size_t width = scr_arr_slot_width(a); for (size_t i = 0; i < n; i++) { - bool held = *scr_arr_state_ptr(a, i) == SCR_ARR_VALUE; + uint8_t *state = scr_arr_state_in(scr_arr_states_end_w(a, width), i); + bool held = *state == SCR_ARR_VALUE; uint64_t old = held ? a->data[i] : 0; a->data[i] = src[i]; - *scr_arr_state_ptr(a, i) = SCR_ARR_VALUE; + *state = SCR_ARR_VALUE; if (held) scr_elem_release(a, old); } } @@ -4787,6 +4844,7 @@ ScrStr *scr_arr_join(ScrArr *a, ScrStr *sep) { case SCR_ELEM_ARR: case SCR_ELEM_BYTES: case SCR_ELEM_REF: + case SCR_ELEM_CREF: /* The compiler rejects join on ref-element arrays (SC1090). */ scr_trap("scriptc: internal error: join on a ref-element array\n"); } diff --git a/packages/runtime/src/scr_array.h b/packages/runtime/src/scr_array.h index 6c41b3dbd..67ff7d528 100644 --- a/packages/runtime/src/scr_array.h +++ b/packages/runtime/src/scr_array.h @@ -43,6 +43,11 @@ typedef enum { * constant descriptor per element type, stored once per array; every * array is still monomorphic (one element type, one set of entry points). */ SCR_ELEM_REF, + /* An SCR_ELEM_REF type whose elements are compact class instances in the + * program-lifetime region (SCR_ARR_OPS_COMPACT): 4-byte slots holding the + * address over 8, 0 for NULL. Compact instances are never counted or + * traced, so these arrays retain, release and trace nothing. */ + SCR_ELEM_CREF, } ScrElemKind; typedef enum { @@ -63,8 +68,27 @@ typedef struct ScrArrOps { /* Registry id (scr_arr_ops_table), 0 until scr_arr_ops_register assigns * it on the first array of the type. Descriptors are writable for it. */ uint32_t id; + /* SCR_ARR_OPS_*. On 64-bit targets the field takes the struct's former + * tail padding, so descriptors emitted without it read 0 here; on 32-bit + * targets it lies past their end and is never read (scr_arr_ops_compact). */ + uint32_t flags; } ScrArrOps; +/* The elements are compact class instances in the program-lifetime region: + * arrays of the type are SCR_ELEM_CREF (llvm/shapes.ts arrOps). */ +#define SCR_ARR_OPS_COMPACT 1u + +/* Whether `ops` describes compact class elements. Compact instances exist + * only on 64-bit targets. */ +static inline bool scr_arr_ops_compact(const ScrArrOps *ops) { +#if UINTPTR_MAX > 0xffffffffu + return (ops->flags & SCR_ARR_OPS_COMPACT) != 0; +#else + (void)ops; + return false; +#endif +} + typedef struct ScrArr { size_t rc; /* SIZE_MAX = immortal (unused for arrays; kept per convention) */ /* Length and dense capacity are at most SCR_ARR_MAX_LENGTH (2^32 - 1); @@ -84,7 +108,7 @@ typedef struct ScrArr { /* The low SCR_ARR_OPS_SHIFT bits: storage flags (SCR_ARR_INLINE_SLOTS, * SCR_ARR_HEAD_ROOM, SCR_ARR_UNSHIFTED, SCR_ARR_HAS_EXT). The rest: the * registry id of the element type's ops (scr_arr_ops), 0 for every kind - * but SCR_ELEM_REF. */ + * but SCR_ELEM_REF and SCR_ELEM_CREF. */ uint16_t spare; /* A lower bound on the hole-free prefix: filled <= len, filled <= cap, * and every dense state below it is SCR_ARR_VALUE. Element reads below @@ -93,10 +117,12 @@ typedef struct ScrArr { * undefined) or move states below it lowers it first (scr_arr_cut); * 0 is always valid. */ uint32_t filled; - /* Dense values: `cap` 8-byte slots, or `cap` bytes for SCR_ELEM_BOOL - * (scr_arr_slot_width), followed in the same allocation by one state byte - * per slot in reverse slot order (scr_arr_states_end). Sparse entries and properties keep 8-byte - * slots for every kind; a bool slot is 0 or 1 wherever it lives. Rarely + /* Dense values: `cap` 8-byte slots, `cap` bytes for SCR_ELEM_BOOL or + * `cap` 4-byte slots for SCR_ELEM_CREF (scr_arr_slot_width), followed in + * the same allocation by one state byte per slot in reverse slot order + * (scr_arr_states_end). Sparse entries and properties keep 8-byte slots + * for every kind; a bool slot is 0 or 1 wherever it lives, and a CREF + * slot's 8-byte form is the pointer (scr_arr_dense_get). Rarely * used storage (sparse indices, noncanonical numeric properties, * match-result metadata) lives out of line, and an array that has it * (SCR_ARR_HAS_EXT) keeps a pointer to it in the word before the dense @@ -190,16 +216,24 @@ static inline uint32_t scr_arr_ops_id(const ScrArrOps *ops) { /* The element ops of an array whose ops have no registry id. */ const ScrArrOps *scr_arr_ops_overflow(const ScrArr *a); -/* The element ops of `a`: NULL for every kind but SCR_ELEM_REF. */ +/* The element ops of `a`: NULL for every kind but SCR_ELEM_REF and + * SCR_ELEM_CREF. */ static inline const ScrArrOps *scr_arr_ops(const ScrArr *a) { uint32_t id = (uint32_t)a->spare >> SCR_ARR_OPS_SHIFT; if (__builtin_expect(id == SCR_ARR_OPS_OVERFLOW, 0)) return scr_arr_ops_overflow(a); return scr_arr_ops_table[id]; } +/* Bytes per dense value slot of element kind `elem`. */ +static inline size_t scr_elem_slot_width(uint8_t elem) { + if (elem == SCR_ELEM_BOOL) return 1; + if (__builtin_expect(elem == SCR_ELEM_CREF, 0)) return sizeof(uint32_t); + return sizeof(uint64_t); +} + /* Bytes per dense value slot. */ static inline size_t scr_arr_slot_width(const ScrArr *a) { - return a->elem == SCR_ELEM_BOOL ? 1 : sizeof(uint64_t); + return scr_elem_slot_width(a->elem); } /* The dense states, one byte per slot, share the values' allocation (inline @@ -233,14 +267,23 @@ static inline uint8_t *scr_arr_state_run(const ScrArr *a, size_t from, size_t co return scr_arr_states_end(a) - from - count; } +/* A CREF slot's pointer, and the slot of a pointer (NULL or a compact + * instance below 32 GiB). */ +static inline uint64_t scr_cref_decode(uint32_t ref) { return (uint64_t)ref << 3; } +static inline uint32_t scr_cref_encode(uint64_t slot) { return (uint32_t)(slot >> 3); } + /* The dense value at `index` (< cap) as an 8-byte slot, and its store. */ static inline uint64_t scr_arr_dense_get(const ScrArr *a, size_t index) { if (a->elem == SCR_ELEM_BOOL) return ((const uint8_t *)a->data)[index]; + if (__builtin_expect(a->elem == SCR_ELEM_CREF, 0)) + return scr_cref_decode(((const uint32_t *)a->data)[index]); return a->data[index]; } static inline void scr_arr_dense_put(ScrArr *a, size_t index, uint64_t slot) { if (a->elem == SCR_ELEM_BOOL) ((uint8_t *)a->data)[index] = (uint8_t)slot; + else if (__builtin_expect(a->elem == SCR_ELEM_CREF, 0)) + ((uint32_t *)a->data)[index] = scr_cref_encode(slot); else a->data[index] = slot; } diff --git a/packages/runtime/src/scr_async.c b/packages/runtime/src/scr_async.c index db1e54295..9e3365fbe 100644 --- a/packages/runtime/src/scr_async.c +++ b/packages/runtime/src/scr_async.c @@ -623,6 +623,10 @@ struct ScrFiber { * awaiter destroys the record when it takes the result. */ bool direct; struct ScrFiber *direct_awaiter; + /* While an async body runs its synchronous prefix (until its first await + * or its end), the AsyncLocalStorage slot of the execution that spawned + * it; NULL afterwards (scr_als_prefix_end). */ + ScrAlsCtx **prefix_slot; #ifdef SCR_ASAN_FIBERS void *fake_stack; /* Live stackless async fibers (scr_async_spawn_stackless) form a list @@ -670,6 +674,61 @@ void scr_als_ctx_release(ScrAlsCtx *c) { free(c); } +/* enterWith on the main stack lasts until that code returns to the loop, + * so the store does not reach callbacks scheduled without it: the loop + * clears the main slot when the main module's body returns, and an + * enterWith inside a callback the loop runs puts a tick that clears it at + * the front of the tick queue, so it runs as soon as that callback + * returns. A store that an async callee's prefix or a generator step + * enters becomes the caller's at scr_als_prefix_end and is cleared the + * same way. */ +static SCR_TL bool scr_loop_started = false; + +static void scr_als_main_clear(void) { + scr_als_ctx_release(scr_als_main_slot); + scr_als_main_slot = NULL; +} + +static void scr_next_tick_raw_first(void (*fn)(void)); + +/* A new store on the main slot: one a loop callback enters ends with it. */ +static void scr_als_main_entered(void) { + if (scr_loop_started) scr_next_tick_raw_first(scr_als_main_clear); +} + +void scr_als_entered_with(void) { + if (SCR_ALS_SLOT() == &scr_als_main_slot) scr_als_main_entered(); +} + +/* The end of an async body's synchronous prefix: its first await, or its + * end. Node runs that prefix inside the caller's execution, so a store it + * entered (enterWith) is still the caller's store after the call; the body + * keeps its own copy of the context for what runs after its await. */ +static void scr_als_prefix_end(ScrFiber *f) { + ScrAlsCtx **slot = f->prefix_slot; + if (slot == NULL) return; + f->prefix_slot = NULL; + if (*slot != f->als) { + scr_als_ctx_release(*slot); + *slot = scr_als_ctx_retain(f->als); + if (f->als != NULL && slot == &scr_als_main_slot) scr_als_main_entered(); + } +} + +/* A synchronous generator has no context of its own: each step runs in its + * resumer's execution, as Node runs it. A fiber-backed one switches to its + * own slot, so its step starts from the resumer's store and a store it + * enters is the resumer's after the step (scr_als_prefix_end on the way + * back); a stackless one runs in the resumer's slot (scr_gen_coro_hop). */ +static void scr_als_step_begin(ScrFiber *f) { + ScrAlsCtx **slot = SCR_ALS_SLOT(); + if (f->als != *slot) { + scr_als_ctx_release(f->als); + f->als = scr_als_ctx_retain(*slot); + } + f->prefix_slot = slot; +} + /* The lifetime region a new async fiber carries (ScrFiber.region). */ static inline void *scr_fiber_region_capture(void) { const ScrRgnFiberHooks *regions = atomic_load_explicit(&scr_region_fiber_hooks, memory_order_relaxed); @@ -726,26 +785,6 @@ ScrDyn *scr_als_peek(double id) { return NULL; } -/* enterWith on the main stack lasts until that code returns to the loop, - * so the store does not reach callbacks scheduled without it: the loop - * clears the main slot when the main module's body returns, and an - * enterWith inside a callback the loop runs puts a tick that clears it at - * the front of the tick queue, so it runs as soon as that callback - * returns. */ -static SCR_TL bool scr_loop_started = false; - -static void scr_als_main_clear(void) { - scr_als_ctx_release(scr_als_main_slot); - scr_als_main_slot = NULL; -} - -static void scr_next_tick_raw_first(void (*fn)(void)); - -void scr_als_entered_with(void) { - if (scr_loop_started && SCR_ALS_SLOT() == &scr_als_main_slot) - scr_next_tick_raw_first(scr_als_main_clear); -} - static SCR_TL ScrCtx scr_loop_ctx; /* the main stack (scheduler home) */ static SCR_TL ScrFiber *scr_current = NULL; /* A finished fiber whose last switch went to the loop (scr_resume_fiber). */ @@ -1889,6 +1928,7 @@ static void scr_trampoline(void) { ScrFiber *self = scr_current; self->entry(self, self->argpack); scr_fiber_finish(self); + scr_als_prefix_end(self); /* The loop reaps the fiber that comes back to it finished, which need * not be the one it resumed: a fiber that continued in place for the * awaiters it parked (scr_await_yield) returns to the loop too. */ @@ -2010,6 +2050,7 @@ static ScrPromise *scr_async_spawn_impl(void (*entry)(ScrFiber *, void *), void /* AsyncLocalStorage: the child runs in the SPAWNER's context (Node's * init-time capture); snapshots are immutable, so a retain suffices. */ f->als = scr_als_ctx_retain(*SCR_ALS_SLOT()); + f->prefix_slot = SCR_ALS_SLOT(); f->region = scr_fiber_region_capture(); scr_fibers_live++; @@ -2023,13 +2064,15 @@ static ScrPromise *scr_async_spawn_impl(void (*entry)(ScrFiber *, void *), void #elif defined(__wasi__) (void)awaiter; ScrFiber *spawner = scr_current; + ScrAlsCtx **spawner_slot = scr_als_active; scr_current = f; scr_exc_swap_cell(&f->exc); scr_als_active = &f->als; entry(f, argpack); /* eager prefix; returns at suspend/final suspend */ + scr_als_prefix_end(f); scr_current = spawner; scr_exc_swap_cell(spawner ? &spawner->exc : NULL); - scr_als_active = spawner ? &spawner->als : &scr_als_main_slot; + scr_als_active = spawner_slot; ScrPromise *result = scr_promise_retain(f->promise); if (f->done) { scr_promise_release(f->promise); @@ -2057,6 +2100,7 @@ static ScrPromise *scr_async_spawn_impl(void (*entry)(ScrFiber *, void *), void #endif f->return_to = home; ScrFiber *spawner = scr_current; + ScrAlsCtx **spawner_slot = scr_als_active; scr_switch(home, &f->ctx, f); /* back: the fiber suspended or finished. The switch back targeted NULL * (the child doesn't know who spawned it), which pointed both the current @@ -2066,7 +2110,7 @@ static ScrPromise *scr_async_spawn_impl(void (*entry)(ScrFiber *, void *), void * `undefined` rejection while the real payload leaks). */ scr_current = spawner; scr_exc_swap_cell(spawner ? &spawner->exc : NULL); - scr_als_active = spawner ? &spawner->als : &scr_als_main_slot; + scr_als_active = spawner_slot; if (awaited != NULL) { /* Parked by the child: the loop owns (and may have reaped) it. */ if (awaiter->await_parked) return awaited; @@ -2172,6 +2216,7 @@ static ScrFiber *scr_spawn_stackless_fiber(void (*entry)(ScrFiber *, void *), vo scr_fiber_track(f); f->stackless = true; f->als = scr_als_ctx_retain(*SCR_ALS_SLOT()); + f->prefix_slot = SCR_ALS_SLOT(); /* The lifetime region the eager prefix runs in, which every later step * resumes in (scr_resume_fiber), as a fiber's body does. */ f->region = scr_fiber_region_capture(); @@ -2183,6 +2228,7 @@ static ScrFiber *scr_spawn_stackless_fiber(void (*entry)(ScrFiber *, void *), vo scr_stackless_fibers = f; #endif ScrFiber *spawner = scr_current; + ScrAlsCtx **spawner_slot = scr_als_active; #ifndef _WIN32 uintptr_t guard = scr_stack_guard; if (guard != 0 && (uintptr_t)__builtin_frame_address(0) < guard + SCR_STACKLESS_PREFIX_ROOM) { @@ -2195,9 +2241,10 @@ static ScrFiber *scr_spawn_stackless_fiber(void (*entry)(ScrFiber *, void *), vo scr_als_active = &f->als; entry(f, argpack); /* the eager prefix: returns at a suspension or the end */ } + scr_als_prefix_end(f); scr_current = spawner; scr_exc_swap_cell(spawner ? &spawner->exc : NULL); - scr_als_active = spawner ? &spawner->als : &scr_als_main_slot; + scr_als_active = spawner_slot; return f; } @@ -2303,8 +2350,10 @@ void *scr_async_spawn_await_ref(void (*entry)(ScrFiber *, void *), void *argpack * settled promise is created and settled in the same order, rejections * enter the unhandled ledger through the same settle path, and the frame * isolates what a fresh fiber would — the Error.stack frame chain - * (cell->stack) and the AsyncLocalStorage context (enterWith inside the - * body stays inside it). The caller's exception cell is clean at any call + * (cell->stack). The AsyncLocalStorage context is the caller's throughout: + * the whole body is the synchronous prefix Node runs in the caller's + * execution, so a store it enters stays entered after the call + * (scr_als_prefix_end). The caller's exception cell is clean at any call * (try/finally stashes in-flight exceptions), so a pending exception * after the body is the body's own throw. */ static void scr_inline_park_trap(void) { @@ -2321,21 +2370,12 @@ void scr_async_inline_enter(ScrAsyncInline *frame) { ScrExcCell *cell = scr_exc_current_cell(); frame->stack = cell->stack; cell->stack = NULL; - frame->als = scr_als_ctx_retain(*SCR_ALS_SLOT()); if (scr_current != NULL) scr_current->inline_depth++; } void scr_async_inline_exit(ScrAsyncInline *frame) { if (scr_current != NULL) scr_current->inline_depth--; scr_exc_current_cell()->stack = (ScrStackFrame *)frame->stack; - ScrAlsCtx *saved = (ScrAlsCtx *)frame->als; - ScrAlsCtx **slot = SCR_ALS_SLOT(); - if (*slot != saved) { - scr_als_ctx_release(*slot); - *slot = saved; /* the frame's reference moves back in */ - } else { - scr_als_ctx_release(saved); - } } ScrPromise *scr_async_inline_leave(ScrAsyncInline *frame) { @@ -2379,11 +2419,14 @@ ScrPromise *scr_async_spawn_after(ScrPromise *dependency, * switch to its own resumer: park it here, for each such awaiter up the * chain, and switch there directly. Nothing runs in between either way. */ static ScrCtx *scr_fiber_park_awaiters(ScrFiber *self) { + scr_als_prefix_end(self); ScrCtx *to = self->return_to; #ifdef SCR_SPAWN_AWAIT for (ScrFiber *cur = self; cur->awaiter != NULL;) { ScrFiber *aw = cur->awaiter; cur->awaiter = NULL; + /* The awaiter's await is its own suspension: its prefix ends here. */ + scr_als_prefix_end(aw); scr_promise_add_waiter(cur->promise, aw); aw->await_parked = true; to = aw->return_to; @@ -5675,9 +5718,13 @@ __attribute__((always_inline)) static inline void scr_gen_coro_hop(ScrGen *g, bo if (retain) scr_gen_retain(g); g->state = SCR_GEN_RUNNING; f->resumer = scr_current; + ScrAlsCtx **resumer_slot = scr_als_active; scr_current = f; scr_exc_swap_cell(&f->exc); - scr_als_active = &f->als; + /* A synchronous generator has no AsyncLocalStorage context of its own: + * its step runs in the resumer's slot, as Node runs it in the execution + * that called next(). An async generator's step runs in its own. */ + if (g->is_async) scr_als_active = &f->als; if (f->coro == NULL) f->entry(f, f->argpack); else scr_coro_resume(f->coro); /* A running generator cannot be resumed again, so neither its fiber nor @@ -5686,7 +5733,7 @@ __attribute__((always_inline)) static inline void scr_gen_coro_hop(ScrGen *g, bo ScrFiber *me = f->resumer; scr_current = me; scr_exc_swap_cell(me != NULL ? &me->exc : NULL); - scr_als_active = me != NULL ? &me->als : &scr_als_main_slot; + scr_als_active = resumer_slot; scr_gen_switched_back(g, f, retain, true); } @@ -5708,6 +5755,7 @@ static void scr_gen_switch_in(ScrGen *g) { #endif scr_gen_retain(g); g->state = SCR_GEN_RUNNING; + if (!g->is_async) scr_als_step_begin(f); #ifdef _WIN32 ScrCtx here = scr_win_self(); #elif defined(__wasi__) @@ -5717,6 +5765,7 @@ static void scr_gen_switch_in(ScrGen *g) { #endif f->return_to = &here; ScrFiber *me = scr_current; + ScrAlsCtx **resumer_slot = scr_als_active; #ifdef __wasi__ scr_current = f; scr_exc_swap_cell(&f->exc); @@ -5725,14 +5774,17 @@ static void scr_gen_switch_in(ScrGen *g) { else scr_wasi_coro_resume(f->coro); scr_current = me; scr_exc_swap_cell(me != NULL ? &me->exc : NULL); - scr_als_active = me != NULL ? &me->als : &scr_als_main_slot; + scr_als_active = resumer_slot; #else scr_switch(&here, &f->ctx, f); - /* Back on the consumer: restore identity + the consumer's cell (the - * yield/finish switch targeted NULL — main's cell). */ + /* Back on the consumer: restore identity, the consumer's cell and its + * AsyncLocalStorage slot (the yield/finish switch targeted NULL — main's + * cell and slot). */ scr_current = me; scr_exc_swap_cell(me != NULL ? &me->exc : NULL); + scr_als_active = resumer_slot; #endif + scr_als_prefix_end(f); scr_gen_switched_back(g, f, true, false); } diff --git a/packages/runtime/src/scr_async.h b/packages/runtime/src/scr_async.h index 827c303f9..fc0128294 100644 --- a/packages/runtime/src/scr_async.h +++ b/packages/runtime/src/scr_async.h @@ -62,12 +62,14 @@ ScrPromise *scr_async_spawn_after(ScrPromise *dependency, /* Fiberless async call (native targets): the compiler proves an async body * can never park its execution context and runs it directly on the * caller's stack between enter and leave. The frame saves what a fresh - * fiber would isolate — the Error.stack frame chain and the - * AsyncLocalStorage context — and leave restores both, returning a fresh - * pending promise (+1) the caller settles from the body's outcome. */ + * fiber would isolate, the Error.stack frame chain, and leave restores it, + * returning a fresh pending promise (+1) the caller settles from the body's + * outcome. The body runs in the caller's AsyncLocalStorage context, as + * Node runs an async body's synchronous prefix. The second word is unused; + * emitted frames keep its size. */ typedef struct ScrAsyncInline { void *stack; - void *als; + void *reserved; } ScrAsyncInline; void scr_async_inline_enter(ScrAsyncInline *frame); ScrPromise *scr_async_inline_leave(ScrAsyncInline *frame); diff --git a/packages/runtime/src/scr_cipher.c b/packages/runtime/src/scr_cipher.c index c8a10a731..00d229bad 100644 --- a/packages/runtime/src/scr_cipher.c +++ b/packages/runtime/src/scr_cipher.c @@ -20,16 +20,30 @@ #include "mbedtls/aes.h" #include "mbedtls/chacha20.h" +#include "mbedtls/des.h" #include "mbedtls/gcm.h" #include "mbedtls/poly1305.h" #include "mbedtls/platform_util.h" -typedef enum { SC_CBC, SC_CTR, SC_GCM, SC_CHACHA } ScCipherMode; +/* SC_CHACHA is chacha20-poly1305; SC_CHACHA20 the bare stream cipher. */ +typedef enum { + SC_CBC, + SC_CTR, + SC_GCM, + SC_CHACHA, + SC_ECB, + SC_CFB, + SC_CFB8, + SC_CFB1, + SC_OFB, + SC_CHACHA20, +} ScCipherMode; typedef struct { const char *name; ScCipherMode mode; unsigned key_len; + bool des; /* triple DES (two- or three-key) rather than AES */ } ScCipherSpec; /* Node's names for the implemented ciphers, including OpenSSL's aliases. @@ -41,6 +55,35 @@ static const ScCipherSpec sc_specs[] = { {"aes-128-gcm", SC_GCM, 16}, {"aes-192-gcm", SC_GCM, 24}, {"aes-256-gcm", SC_GCM, 32}, {"id-aes128-gcm", SC_GCM, 16}, {"id-aes192-gcm", SC_GCM, 24}, {"id-aes256-gcm", SC_GCM, 32}, {"chacha20-poly1305", SC_CHACHA, 32}, + {"aes-128-ecb", SC_ECB, 16}, {"aes-192-ecb", SC_ECB, 24}, {"aes-256-ecb", SC_ECB, 32}, + {"aes-128-cfb", SC_CFB, 16}, {"aes-192-cfb", SC_CFB, 24}, {"aes-256-cfb", SC_CFB, 32}, + {"aes-128-cfb8", SC_CFB8, 16}, {"aes-192-cfb8", SC_CFB8, 24}, {"aes-256-cfb8", SC_CFB8, 32}, + {"aes-128-cfb1", SC_CFB1, 16}, {"aes-192-cfb1", SC_CFB1, 24}, {"aes-256-cfb1", SC_CFB1, 32}, + {"aes-128-ofb", SC_OFB, 16}, {"aes-192-ofb", SC_OFB, 24}, {"aes-256-ofb", SC_OFB, 32}, + {"des-ede3-cbc", SC_CBC, 24, true}, {"des3", SC_CBC, 24, true}, + {"des-ede3", SC_ECB, 24, true}, {"des-ede3-ecb", SC_ECB, 24, true}, + {"des-ede3-cfb", SC_CFB, 24, true}, {"des-ede3-cfb8", SC_CFB8, 24, true}, + {"des-ede3-cfb1", SC_CFB1, 24, true}, {"des-ede3-ofb", SC_OFB, 24, true}, + {"des-ede-cbc", SC_CBC, 16, true}, {"des-ede", SC_ECB, 16, true}, + {"des-ede-ecb", SC_ECB, 16, true}, {"des-ede-cfb", SC_CFB, 16, true}, + {"des-ede-ofb", SC_OFB, 16, true}, + {"chacha20", SC_CHACHA20, 32}, +}; + +/* Ciphers OpenSSL 3 keeps in its legacy provider, which Node 24 does not + * load: crypto.getCipherInfo() knows them, and createCipheriv checks their + * IV length, then fails with OpenSSL's "unsupported" error. */ +static const struct { + const char *name; + unsigned iv_len; +} sc_legacy[] = { + {"des", 8}, {"des-cbc", 8}, {"des-ecb", 0}, {"des-cfb", 8}, {"des-cfb1", 8}, {"des-cfb8", 8}, + {"des-ofb", 8}, {"desx", 8}, {"desx-cbc", 8}, {"bf", 8}, {"bf-cbc", 8}, {"bf-ecb", 0}, + {"bf-cfb", 8}, {"bf-ofb", 8}, {"blowfish", 8}, {"cast", 8}, {"cast-cbc", 8}, {"cast5-cbc", 8}, + {"cast5-ecb", 0}, {"cast5-cfb", 8}, {"cast5-ofb", 8}, {"rc2", 8}, {"rc2-cbc", 8}, {"rc2-ecb", 0}, + {"rc2-cfb", 8}, {"rc2-ofb", 8}, {"rc2-40-cbc", 8}, {"rc2-64-cbc", 8}, {"rc4", 0}, {"rc4-40", 0}, + {"rc4-hmac-md5", 0}, {"seed", 16}, {"seed-cbc", 16}, {"seed-ecb", 0}, {"seed-cfb", 16}, + {"seed-ofb", 16}, {"idea", 8}, {"idea-cbc", 8}, {"idea-ecb", 0}, {"idea-cfb", 8}, {"idea-ofb", 8}, }; /* crypto.getCiphers() on Node 24.22 (OpenSSL 3.5). Names in this list that @@ -88,15 +131,21 @@ typedef struct ScCipher { ScCipherMode mode; bool decrypt; bool active; /* false after final() or a failed backend call */ - bool pad; /* CBC automatic padding */ + bool pad; /* CBC and ECB automatic padding */ + bool des; /* triple DES rather than AES */ + unsigned bs; /* the block size: 16 for AES, 8 for triple DES */ bool data_started; /* a non-empty update() ran, so non-empty AAD is refused */ int tag_len; /* SC_NO_TAG until known */ int tag_state; unsigned char tag[16]; - unsigned char block[16]; /* CBC: chaining value; CTR: counter block */ - unsigned char buf[16]; /* CBC: buffered input; CTR: keystream block */ - size_t bufsz; /* CBC: buffered bytes; CTR: keystream offset */ + unsigned char block[16]; /* CBC: chaining value; CTR: counter block; CFB, OFB: + shift register; chacha20: the IV */ + unsigned char buf[16]; /* CBC, ECB: buffered input; CTR: keystream block */ + size_t bufsz; /* CBC, ECB: buffered bytes; CTR, CFB, OFB: keystream + offset */ + uint64_t chacha_left; /* chacha20: bytes before the low counter word wraps */ mbedtls_aes_context aes; + mbedtls_des3_context des3; mbedtls_gcm_context gcm; mbedtls_chacha20_context chacha; mbedtls_poly1305_context poly; @@ -127,6 +176,7 @@ static void sc_trace(void *h, ScrTraceVisit visit, void *ctx) { static void sc_gcfree(void *h) { ScCipher *c = h; mbedtls_aes_free(&c->aes); + mbedtls_des3_free(&c->des3); mbedtls_gcm_free(&c->gcm); mbedtls_chacha20_free(&c->chacha); mbedtls_poly1305_free(&c->poly); @@ -184,18 +234,21 @@ static void sc_invalid_tag(unsigned long length) { /* An OpenSSL provider error as Node reports it: the formatted message plus * own opensslErrorStack, library, reason and code properties, in Node's * key order. */ -static void sc_throw_openssl(const char *packed, const char *reason, const char *code) { +/* OpenSSL's error with its library, reason and error stack; Node's + * code. */ +static void sc_throw_openssl_in(const char *packed, const char *library, const char *reason, + const char *code) { char message[128]; - snprintf(message, sizeof message, "error:%s:Provider routines::%s", packed, reason); + snprintf(message, sizeof message, "error:%s:%s::%s", packed, library, reason); ScrStr *text = scr_str_new(message, strlen(message)); ScrError *e = scr_error_new(SCR_ERR_ERROR, text); ScrDyn *stack = scr_dyn_new_arr(); scr_dyn_arr_push(stack, scr_dyn_new_str(text)); scr_str_release(text); scr_error_set_prop(e, "opensslErrorStack", 17, stack); - ScrStr *library = scr_str_new("Provider routines", 17); - scr_error_set_prop(e, "library", 7, scr_dyn_new_str(library)); - scr_str_release(library); + ScrStr *library_text = scr_str_new(library, strlen(library)); + scr_error_set_prop(e, "library", 7, scr_dyn_new_str(library_text)); + scr_str_release(library_text); ScrStr *reason_text = scr_str_new(reason, strlen(reason)); scr_error_set_prop(e, "reason", 6, scr_dyn_new_str(reason_text)); scr_str_release(reason_text); @@ -206,6 +259,10 @@ static void sc_throw_openssl(const char *packed, const char *reason, const char scr_throw_obj(e, &scr_error_retain_v, &scr_error_release_v, scr_error_trace_arg()); } +static void sc_throw_openssl(const char *packed, const char *reason, const char *code) { + sc_throw_openssl_in(packed, "Provider routines", reason, code); +} + /* ERR_INVALID_ARG_VALUE's property flavor: "The property 'options.x' is * invalid. Received ". */ static void sc_invalid_property(const char *name, const ScrDyn *value) { @@ -440,6 +497,27 @@ static const ScCipherSpec *sc_lookup(const ScrStr *name) { return NULL; } +/* The legacy-provider IV length of `name`, or -1 for other names. */ +static int sc_legacy_iv(const ScrStr *name) { + for (size_t i = 0; i < sizeof sc_legacy / sizeof sc_legacy[0]; i++) + if (sc_ascii_eq_ci(name->data, name->len, sc_legacy[i].name)) return (int)sc_legacy[i].iv_len; + return -1; +} + +/* The IV length a mode takes; SC_GCM takes any nonzero length. */ +static size_t sc_iv_length(ScCipherMode mode, bool des) { + switch (mode) { + case SC_ECB: + return 0; + case SC_CHACHA: + return 12; + case SC_CHACHA20: + return 16; + default: + return des ? 8 : 16; + } +} + static bool sc_known_name(const ScrStr *name) { for (size_t i = 0; i < sizeof sc_known / sizeof sc_known[0]; i++) if (sc_ascii_eq_ci(name->data, name->len, sc_known[i])) return true; @@ -451,9 +529,10 @@ static void sc_unsupported_cipher(const ScrStr *name) { scr_jb_init(&b); scr_jb_puts(&b, "The cipher '"); scr_jb_put_str(&b, name); - scr_jb_puts(&b, "' has no native implementation; the supported ciphers are aes-128-cbc, aes-192-cbc, " - "aes-256-cbc, aes-128-ctr, aes-192-ctr, aes-256-ctr, aes-128-gcm, aes-192-gcm, " - "aes-256-gcm and chacha20-poly1305"); + scr_jb_puts(&b, "' has no native implementation; the supported ciphers are aes-128, aes-192 and " + "aes-256 in cbc, ctr, gcm, ecb, cfb, cfb8, cfb1 and ofb modes, des-ede3 in cbc, ecb, " + "cfb, cfb8, cfb1 and ofb modes, des-ede in cbc, ecb, cfb and ofb modes, chacha20 and " + "chacha20-poly1305"); ScrStr *message = scr_jb_finish(&b); scr_throw_error_msg_code(SCR_ERR_ERROR, message->data, message->len, "SC2020"); scr_str_release(message); @@ -522,14 +601,24 @@ static ScrDyn *sc_cipher_new(bool decrypt, ScrDyn *cipher, ScrDyn *key, ScrDyn * goto done; } const ScCipherSpec *spec = sc_lookup(cipher->v.str); + size_t iv_len = iv_bytes ? iv_bytes->len : 0; if (!spec) { - if (sc_known_name(cipher->v.str)) sc_unsupported_cipher(cipher->v.str); - else sc_throw(SCR_ERR_ERROR, "ERR_CRYPTO_UNKNOWN_CIPHER", "Unknown cipher"); + int legacy_iv = sc_legacy_iv(cipher->v.str); + if (legacy_iv >= 0) { + if (iv_len != (size_t)legacy_iv || (legacy_iv > 0 && !iv_bytes)) + sc_throw(SCR_ERR_TYPE, "ERR_CRYPTO_INVALID_IV", "Invalid initialization vector"); + else + sc_throw_openssl_in("0308010C", "digital envelope routines", "unsupported", + "ERR_OSSL_EVP_UNSUPPORTED"); + } else if (sc_known_name(cipher->v.str)) { + sc_unsupported_cipher(cipher->v.str); + } else { + sc_throw(SCR_ERR_ERROR, "ERR_CRYPTO_UNKNOWN_CIPHER", "Unknown cipher"); + } goto done; } - size_t iv_len = iv_bytes ? iv_bytes->len : 0; - bool aead = spec->mode == SC_GCM || spec->mode == SC_CHACHA; - if (iv_len == 0 || (!aead && iv_len != 16) || (spec->mode == SC_CHACHA && iv_len != 12)) { + size_t expected_iv = sc_iv_length(spec->mode, spec->des); + if (spec->mode == SC_GCM ? iv_len == 0 : iv_len != expected_iv || (expected_iv > 0 && !iv_bytes)) { sc_throw(SCR_ERR_TYPE, "ERR_CRYPTO_INVALID_IV", "Invalid initialization vector"); goto done; } @@ -570,8 +659,11 @@ static ScrDyn *sc_cipher_new(bool decrypt, ScrDyn *cipher, ScrDyn *key, ScrDyn * c->decrypt = decrypt; c->active = true; c->pad = true; + c->des = spec->des; + c->bs = spec->des ? 8 : 16; c->tag_len = tag_len; mbedtls_aes_init(&c->aes); + mbedtls_des3_init(&c->des3); mbedtls_gcm_init(&c->gcm); mbedtls_chacha20_init(&c->chacha); mbedtls_poly1305_init(&c->poly); @@ -579,9 +671,39 @@ static ScrDyn *sc_cipher_new(bool decrypt, ScrDyn *cipher, ScrDyn *key, ScrDyn * unsigned bits = spec->key_len * 8; switch (spec->mode) { case SC_CBC: - rc = decrypt ? mbedtls_aes_setkey_dec(&c->aes, key_bytes->data, bits) - : mbedtls_aes_setkey_enc(&c->aes, key_bytes->data, bits); + case SC_ECB: + case SC_CFB: + case SC_CFB8: + case SC_CFB1: + case SC_OFB: { + /* CBC and ECB decrypt with the inverse cipher; the feedback modes run + * the forward cipher both ways. */ + bool inverse = decrypt && (spec->mode == SC_CBC || spec->mode == SC_ECB); + if (spec->des && spec->key_len == 24) + rc = inverse ? mbedtls_des3_set3key_dec(&c->des3, key_bytes->data) + : mbedtls_des3_set3key_enc(&c->des3, key_bytes->data); + else if (spec->des) + rc = inverse ? mbedtls_des3_set2key_dec(&c->des3, key_bytes->data) + : mbedtls_des3_set2key_enc(&c->des3, key_bytes->data); + else + rc = inverse ? mbedtls_aes_setkey_dec(&c->aes, key_bytes->data, bits) + : mbedtls_aes_setkey_enc(&c->aes, key_bytes->data, bits); + if (expected_iv) memcpy(c->block, iv_bytes->data, expected_iv); + break; + } + case SC_CHACHA20: + /* OpenSSL's IV is a 64-bit little-endian block counter and an 8-byte + * nonce; mbedTLS takes a 32-bit counter and a 12-byte nonce, so the + * stream restarts each time the low counter word wraps. */ memcpy(c->block, iv_bytes->data, 16); + rc = mbedtls_chacha20_setkey(&c->chacha, key_bytes->data); + if (rc == 0) { + uint32_t low = (uint32_t)c->block[0] | (uint32_t)c->block[1] << 8 | + (uint32_t)c->block[2] << 16 | (uint32_t)c->block[3] << 24; + c->chacha_left = ((uint64_t)1 << 32) - low; + c->chacha_left *= 64; + rc = mbedtls_chacha20_starts(&c->chacha, c->block + 4, low); + } break; case SC_CTR: rc = mbedtls_aes_setkey_enc(&c->aes, key_bytes->data, bits); @@ -635,39 +757,123 @@ done: /* ── the cipher transforms ────────────────────────────────────────────── */ -/* Raw CBC over whole blocks. */ +/* Raw CBC or ECB over whole blocks. */ static bool sc_cbc_blocks(ScCipher *c, const unsigned char *in, size_t len, unsigned char *out) { + if (c->mode == SC_ECB) { + for (size_t off = 0; off < len; off += c->bs) { + int rc = c->des ? mbedtls_des3_crypt_ecb(&c->des3, in + off, out + off) + : mbedtls_aes_crypt_ecb(&c->aes, c->decrypt ? MBEDTLS_AES_DECRYPT : MBEDTLS_AES_ENCRYPT, + in + off, out + off); + if (rc != 0) return false; + } + return true; + } + if (c->des) + return mbedtls_des3_crypt_cbc(&c->des3, c->decrypt ? MBEDTLS_DES_DECRYPT : MBEDTLS_DES_ENCRYPT, len, c->block, + in, out) == 0; return mbedtls_aes_crypt_cbc(&c->aes, c->decrypt ? MBEDTLS_AES_DECRYPT : MBEDTLS_AES_ENCRYPT, len, c->block, in, out) == 0; } -/* OpenSSL's ossl_cipher_generic_block_update for CBC: complete blocks - * are processed, the remainder is buffered, and a decryption that ends on - * a block boundary holds the last block back for padding removal. Returns - * the output length, or -1 when the backend fails; out has room for - * len + 16 bytes. */ +/* One forward block of the feedback modes' cipher. */ +static bool sc_forward(ScCipher *c, const unsigned char *in, unsigned char *out) { + return (c->des ? mbedtls_des3_crypt_ecb(&c->des3, in, out) + : mbedtls_aes_crypt_ecb(&c->aes, MBEDTLS_AES_ENCRYPT, in, out)) == 0; +} + +/* CFB (full-block feedback), CFB8, CFB1 and OFB, as OpenSSL's + * CRYPTO_cfb128_encrypt, CRYPTO_cfb128_8_encrypt, CRYPTO_cfb128_1_encrypt + * and CRYPTO_ofb128_encrypt: CFB and OFB keep their keystream offset across + * updates; CFB1 feeds back one bit at a time, most significant first. */ +static bool sc_feedback(ScCipher *c, const unsigned char *in, size_t len, unsigned char *out) { + unsigned char stream[16]; + size_t bs = c->bs; + for (size_t i = 0; i < len; i++) { + switch (c->mode) { + case SC_CFB: + case SC_OFB: + if (c->bufsz == 0 && !sc_forward(c, c->block, c->block)) return false; + out[i] = (unsigned char)(in[i] ^ c->block[c->bufsz]); + if (c->mode == SC_CFB) c->block[c->bufsz] = c->decrypt ? in[i] : out[i]; + c->bufsz = (c->bufsz + 1) % bs; + break; + case SC_CFB8: { + if (!sc_forward(c, c->block, stream)) return false; + unsigned char byte = (unsigned char)(in[i] ^ stream[0]); + memmove(c->block, c->block + 1, bs - 1); + c->block[bs - 1] = c->decrypt ? in[i] : byte; + out[i] = byte; + break; + } + default: { + unsigned char result = 0; + for (int bit = 7; bit >= 0; bit--) { + if (!sc_forward(c, c->block, stream)) return false; + unsigned in_bit = (in[i] >> bit) & 1u; + unsigned out_bit = in_bit ^ (stream[0] >> 7); + unsigned feed = c->decrypt ? in_bit : out_bit; + for (size_t k = 0; k + 1 < bs; k++) + c->block[k] = (unsigned char)(c->block[k] << 1 | c->block[k + 1] >> 7); + c->block[bs - 1] = (unsigned char)(c->block[bs - 1] << 1 | feed); + result |= (unsigned char)(out_bit << bit); + } + out[i] = result; + break; + } + } + } + mbedtls_platform_zeroize(stream, sizeof stream); + return true; +} + +/* chacha20 over len bytes: when the 64-bit counter's low word wraps, the + * high word steps (and wraps itself, as OpenSSL's does) and the stream + * restarts from block 0 of the next word. */ +static bool sc_chacha20_update(ScCipher *c, const unsigned char *in, size_t len, unsigned char *out) { + while (len > 0) { + size_t n = len < c->chacha_left ? len : (size_t)c->chacha_left; + if (mbedtls_chacha20_update(&c->chacha, n, in, out) != 0) return false; + in += n; + out += n; + len -= n; + c->chacha_left -= n; + if (c->chacha_left == 0) { + for (int k = 4; k < 8 && ++c->block[k] == 0; k++) { + } + memset(c->block, 0, 4); + c->chacha_left = (uint64_t)1 << 38; + if (mbedtls_chacha20_starts(&c->chacha, c->block + 4, 0) != 0) return false; + } + } + return true; +} + +/* OpenSSL's ossl_cipher_generic_block_update for CBC and ECB: complete + * blocks are processed, the remainder is buffered, and a decryption that + * ends on a block boundary holds the last block back for padding removal. + * Returns the output length, or -1 when the backend fails; out has room + * for len + 16 bytes. */ static long sc_cbc_update(ScCipher *c, const unsigned char *in, size_t inl, unsigned char *out) { + size_t bs = c->bs; size_t outl = 0; size_t nextblocks; if (c->bufsz != 0) { - size_t remain = 16 - c->bufsz; + size_t remain = bs - c->bufsz; if (inl < remain) remain = inl; memcpy(c->buf + c->bufsz, in, remain); in += remain; inl -= remain; c->bufsz += remain; - nextblocks = inl & ~(size_t)15; - } else { - nextblocks = inl & ~(size_t)15; } - if (c->bufsz == 16 && (!c->decrypt || inl > 0 || !c->pad)) { - if (!sc_cbc_blocks(c, c->buf, 16, out)) return -1; + nextblocks = inl - inl % bs; + if (c->bufsz == bs && (!c->decrypt || inl > 0 || !c->pad)) { + if (!sc_cbc_blocks(c, c->buf, bs, out)) return -1; c->bufsz = 0; - outl = 16; - out += 16; + outl = bs; + out += bs; } if (nextblocks > 0) { - if (c->decrypt && c->pad && nextblocks == inl) nextblocks -= 16; + if (c->decrypt && c->pad && nextblocks == inl) nextblocks -= bs; if (nextblocks > 0) { if (!sc_cbc_blocks(c, in, nextblocks, out)) return -1; outl += nextblocks; @@ -682,33 +888,35 @@ static long sc_cbc_update(ScCipher *c, const unsigned char *in, size_t inl, unsi return (long)outl; } -/* ossl_cipher_generic_block_final for CBC. Returns the output length, -1 - * after throwing OpenSSL's error, or -2 when the backend fails. */ +/* ossl_cipher_generic_block_final for CBC and ECB. Returns the output + * length, -1 after throwing OpenSSL's error, or -2 when the backend + * fails. */ static long sc_cbc_final(ScCipher *c, unsigned char out[16]) { + size_t bs = c->bs; if (!c->decrypt) { if (c->pad) { - unsigned char fill = (unsigned char)(16 - c->bufsz); - for (size_t i = c->bufsz; i < 16; i++) c->buf[i] = fill; + unsigned char fill = (unsigned char)(bs - c->bufsz); + for (size_t i = c->bufsz; i < bs; i++) c->buf[i] = fill; } else if (c->bufsz == 0) { return 0; - } else if (c->bufsz != 16) { + } else if (c->bufsz != bs) { sc_throw_openssl("1C80006B", "wrong final block length", "ERR_OSSL_WRONG_FINAL_BLOCK_LENGTH"); return -1; } - if (!sc_cbc_blocks(c, c->buf, 16, out)) return -2; + if (!sc_cbc_blocks(c, c->buf, bs, out)) return -2; c->bufsz = 0; - return 16; + return (long)bs; } - if (c->bufsz != 16) { + if (c->bufsz != bs) { if (c->bufsz == 0 && !c->pad) return 0; sc_throw_openssl("1C80006B", "wrong final block length", "ERR_OSSL_WRONG_FINAL_BLOCK_LENGTH"); return -1; } - if (!sc_cbc_blocks(c, c->buf, 16, c->buf)) return -2; - size_t len = 16; + if (!sc_cbc_blocks(c, c->buf, bs, c->buf)) return -2; + size_t len = bs; if (c->pad) { - unsigned char fill = c->buf[15]; - bool bad = fill == 0 || fill > 16; + unsigned char fill = c->buf[bs - 1]; + bool bad = fill == 0 || fill > bs; for (size_t i = 0; !bad && i < fill; i++) bad = c->buf[--len] != fill; if (bad) { sc_throw_openssl("1C800064", "bad decrypt", "ERR_OSSL_BAD_DECRYPT"); @@ -769,7 +977,8 @@ static ScrBytes *sc_transform(ScCipher *c, const unsigned char *in, size_t len) size_t produced = 0; bool ok = true; switch (c->mode) { - case SC_CBC: { + case SC_CBC: + case SC_ECB: { long n = sc_cbc_update(c, in, len, out->data); ok = n >= 0; produced = ok ? (size_t)n : 0; @@ -786,6 +995,17 @@ static ScrBytes *sc_transform(ScCipher *c, const unsigned char *in, size_t len) ok = len == 0 || sc_chacha_update(c, in, len, out->data); produced = len; break; + case SC_CFB: + case SC_CFB8: + case SC_CFB1: + case SC_OFB: + ok = sc_feedback(c, in, len, out->data); + produced = len; + break; + case SC_CHACHA20: + ok = len == 0 || sc_chacha20_update(c, in, len, out->data); + produced = len; + break; } if (!ok) { mbedtls_platform_zeroize(out->data, room); @@ -926,7 +1146,8 @@ static ScrBytes *sc_final_bytes(ScCipher *c) { out->len = 0; bool ok = true, backend = true; switch (c->mode) { - case SC_CBC: { + case SC_CBC: + case SC_ECB: { long produced = sc_cbc_final(c, out->data); if (produced == -1) { scr_bytes_release(out); @@ -937,6 +1158,11 @@ static ScrBytes *sc_final_bytes(ScCipher *c) { break; } case SC_CTR: + case SC_CFB: + case SC_CFB8: + case SC_CFB1: + case SC_OFB: + case SC_CHACHA20: break; case SC_GCM: { unsigned char full[16]; diff --git a/packages/runtime/src/scr_console.c b/packages/runtime/src/scr_console.c index c8fa40b38..a3ac43cc0 100644 --- a/packages/runtime/src/scr_console.c +++ b/packages/runtime/src/scr_console.c @@ -148,7 +148,7 @@ static void scr_enable_utf8_console_output(void) { } #endif -void scr_runtime_abi_v25(void) {} +void scr_runtime_abi_v27(void) {} void scr_init(void) { #ifdef SCR_WORKERS diff --git a/packages/runtime/src/scr_copying.c b/packages/runtime/src/scr_copying.c index df4da1738..ee0bb055a 100644 --- a/packages/runtime/src/scr_copying.c +++ b/packages/runtime/src/scr_copying.c @@ -22,7 +22,7 @@ static bool copying_elem_is_ref(ScrElemKind kind) { } static ScrArr *copying_arr_new_like(const ScrArr *a, size_t cap) { - ScrArr *out = a->elem == SCR_ELEM_REF + ScrArr *out = a->elem == SCR_ELEM_REF || a->elem == SCR_ELEM_CREF ? scr_arr_new_ops(scr_arr_ops(a), cap ? cap : 1) : scr_arr_new(a->elem, cap ? cap : 1); out->len = cap; diff --git a/packages/runtime/src/scr_core.h b/packages/runtime/src/scr_core.h index 5e5278588..fea020d16 100644 --- a/packages/runtime/src/scr_core.h +++ b/packages/runtime/src/scr_core.h @@ -81,7 +81,7 @@ void scr_init(void); /* Program objects emitted by the bundled LLVM helper reference this symbol. * Its versioned spelling makes a mismatched manual runtime link fail before * the program can start. */ -void scr_runtime_abi_v25(void); +void scr_runtime_abi_v27(void); void scr_runtime_workers_v8(void); /* ── the trap funnel (scr_console.c; scr_library.c under -DSCR_LIB) ────── diff --git a/packages/runtime/src/scr_crypto.h b/packages/runtime/src/scr_crypto.h index ddc306598..0e6c6e07f 100644 --- a/packages/runtime/src/scr_crypto.h +++ b/packages/runtime/src/scr_crypto.h @@ -85,6 +85,10 @@ ScrDyn *scr_crypto_create_secret_key(ScrDyn *key, ScrDyn *encoding); ScrDyn *scr_crypto_generate_key_sync(ScrDyn *type, ScrDyn *options); ScrBytes *scr_crypto_key_object_input(ScrDyn *key); bool scr_crypto_is_key_object(const ScrDyn *value); +/* Throws Node's ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE for a private or public + * KeyObject used as a secret key. scr_pki.c, which makes those keys, + * installs it with them, so its messages stay out of other programs. */ +extern void (*scr_crypto_reject_asymmetric_secret)(const ScrDyn *key); const unsigned char *scr_key_object_bytes(const ScrDyn *value, size_t *len); /* crypto.createCipheriv (decrypt false) and crypto.createDecipheriv * (decrypt true) over checked-dynamic arguments (scr_cipher.c, linked @@ -97,6 +101,21 @@ ScrDyn *scr_crypto_cipher_new(bool decrypt, ScrDyn *cipher, ScrDyn *key, ScrDyn * or result, or NULL with Node's error pending. */ ScrDyn *scr_crypto_ecdh_new(ScrDyn *curve); ScrDyn *scr_crypto_ecdh_convert_key(ScrDyn *key, ScrDyn *curve, ScrDyn *in_enc, ScrDyn *out_enc, ScrDyn *format); +/* crypto.createDiffieHellman(sizeOrKey, keyEncoding, generator, + * generatorEncoding) and crypto.getDiffieHellman(groupName) over + * checked-dynamic arguments (scr_dh.c, linked with the vendored mbedTLS + * archive). +1 DiffieHellman or DiffieHellmanGroup handle, or NULL with + * Node's error pending. */ +ScrDyn *scr_crypto_dh_new(ScrDyn *size_or_key, ScrDyn *key_encoding, ScrDyn *generator, ScrDyn *generator_encoding); +ScrDyn *scr_crypto_dh_group_new(ScrDyn *name); +/* crypto.checkPrimeSync, checkPrime, generatePrimeSync and generatePrime + * (scr_dh.c): checked-dynamic arguments, borrowed; the sync forms answer + * +1 or NULL with the error pending. The callbacks receive undefined as + * the error, as Node passes it, or null when `null_error`. */ +ScrDyn *scr_crypto_check_prime_sync(ScrDyn *candidate, ScrDyn *options); +void scr_crypto_check_prime_async(ScrDyn *candidate, ScrDyn *options, ScrDyn *callback, bool null_error); +ScrDyn *scr_crypto_generate_prime_sync(ScrDyn *size, ScrDyn *options); +void scr_crypto_generate_prime_async(ScrDyn *size, ScrDyn *options, ScrDyn *callback, bool null_error); ScrBytes *scr_crypto_pbkdf2(ScrBytes *password, ScrBytes *salt, double iterations, double keylen, ScrStr *digest); ScrBytes *scr_crypto_pbkdf2_job(ScrBytes *password, ScrBytes *salt, double iterations, @@ -241,6 +260,13 @@ typedef ScrBytes *(*ScrWebCryptoAesFn)(const char *name, bool decrypt, const uns ScrPromise *scr_webcrypto_cipher(ScrDyn *args, bool decrypt, ScrWebCryptoAesFn aes); ScrPromise *scr_webcrypto_encrypt(ScrDyn *args); ScrPromise *scr_webcrypto_decrypt(ScrDyn *args); +/* wrapKey and unwrapKey run the same way (AES-KW wrapping, or the AES + * ciphers): wrapKey fulfills with an ArrayBuffer and unwrapKey with a + * CryptoKey on a later loop turn. */ +ScrPromise *scr_webcrypto_wrap(ScrDyn *args, ScrWebCryptoAesFn aes); +ScrPromise *scr_webcrypto_unwrap(ScrDyn *args, ScrWebCryptoAesFn aes); +ScrPromise *scr_webcrypto_wrap_key(ScrDyn *args); +ScrPromise *scr_webcrypto_unwrap_key(ScrDyn *args); /* deriveBits and deriveKey (PBKDF2 and HKDF) over node:crypto's KDF core: * deriveBits fulfills with an ArrayBuffer (at once for a zero length) and * deriveKey with a CryptoKey of the derived key type, on a later loop @@ -261,5 +287,17 @@ ScrStr *scr_crypto_x509_valid_from(ScrBytes *data); ScrStr *scr_crypto_x509_valid_from_str(ScrStr *pem); ScrStr *scr_crypto_x509_valid_to(ScrBytes *data); ScrStr *scr_crypto_x509_valid_to_str(ScrStr *pem); +/* Asymmetric keys and signatures (scr_pki.c, the pki unit). Arguments + * borrowed; results +1, or NULL with the error pending. */ +ScrDyn *scr_crypto_create_private_key(ScrDyn *key); +ScrDyn *scr_crypto_create_public_key(ScrDyn *key); +ScrDyn *scr_crypto_generate_key_pair_sync(ScrDyn *type, ScrDyn *options); +void scr_crypto_generate_key_pair_async(ScrDyn *type, ScrDyn *options, ScrDyn *callback); +ScrDyn *scr_crypto_sign_oneshot(ScrDyn *algorithm, ScrDyn *data, ScrDyn *key); +ScrDyn *scr_crypto_verify_oneshot(ScrDyn *algorithm, ScrDyn *data, ScrDyn *key, ScrDyn *signature); +ScrDyn *scr_crypto_create_sign(ScrDyn *algorithm); +ScrDyn *scr_crypto_create_verify(ScrDyn *algorithm); +bool scr_crypto_is_asymmetric_key(const ScrDyn *value); +const char *scr_crypto_asymmetric_key_type(const ScrDyn *value); #endif /* SCR_CRYPTO_H */ diff --git a/packages/runtime/src/scr_crypto_args.h b/packages/runtime/src/scr_crypto_args.h new file mode 100644 index 000000000..b8bdcc1c9 --- /dev/null +++ b/packages/runtime/src/scr_crypto_args.h @@ -0,0 +1,181 @@ +/* Argument and result handling shared by the checked-dynamic node:crypto + * key-exchange handles (scr_ecdh.c and scr_dh.c), as Node 24's + * lib/internal/crypto/diffiehellman.js and lib/internal/crypto/util.js + * read keys and encodings: getArrayBufferOrView for key inputs and + * encode() for Buffer results. `subject` names the value in the coded + * SC2020 error for encodings that have no lowering ("An ECDH encoding or + * format"). All arguments are borrowed. */ +#ifndef SCR_CRYPTO_ARGS_H +#define SCR_CRYPTO_ARGS_H + +#include "scr_runtime.h" + +#include + +/* An object's toString the program defined, as an own or inherited + * function property. */ +static bool ca_custom_to_string(ScrDyn *value) { + if (value->kind != SCR_DYN_OBJ && value->kind != SCR_DYN_TYPED_REF) return false; + ScrDyn *member = scr_dyn_obj_read(value, "toString", 8); + bool custom = member && member->kind == SCR_DYN_FUNC; + scr_dyn_release(member); + return custom; +} + +/* Whether value is a class instance whose class defines toString. */ +static bool ca_class_to_string(const ScrDyn *value) { + ScrStr *name = scr_str_new("toString", 8); + bool method = scr_dyn_typed_ref_class_has(value, name); + scr_str_release(name); + return method; +} + +/* `prefix` and the value as util.format's %s renders it: strings and + * objects with a program-defined toString as String(value), numbers as + * util.inspect does, other values inspected at depth 0. Functions and + * class instances that define toString have no lowering. */ +static void ca_throw_formatted(const char *subject, const char *code, const char *prefix, const ScrDyn *value) { + ScrDyn *v = (ScrDyn *)value; + if (v->kind == SCR_DYN_FUNC || ca_class_to_string(v)) { + ScrJsonBuf b; + scr_jb_init(&b); + scr_jb_puts(&b, subject); + scr_jb_puts(&b, " that is a function or an instance of a class defining toString has no lowering; " + "pass the name as a string"); + ScrStr *message = scr_jb_finish(&b); + scr_throw_error_msg_code(SCR_ERR_ERROR, message->data, message->len, "SC2020"); + scr_str_release(message); + return; + } + ScrStr *text; + if (v->kind == SCR_DYN_STR || ca_custom_to_string(v)) text = scr_dyn_string_coerce_js(v); + else if (v->kind == SCR_DYN_NUM) text = scr_insp_f64(v->v.num); + else text = scr_insp_dyn_s(v, 0); + if (!text) return; + ScrJsonBuf b; + scr_jb_init(&b); + scr_jb_puts(&b, prefix); + scr_jb_put_str(&b, text); + scr_str_release(text); + ScrStr *message = scr_jb_finish(&b); + scr_throw_error_msg_code(SCR_ERR_TYPE, message->data, message->len, code); + scr_str_release(message); +} + +static bool ca_ascii_eq_ci(const char *a, size_t a_len, const char *b) { + size_t b_len = strlen(b); + if (a_len != b_len) return false; + for (size_t i = 0; i < a_len; i++) { + char x = a[i]; + if (x >= 'A' && x <= 'Z') x = (char)(x - 'A' + 'a'); + if (x != b[i]) return false; + } + return true; +} + +static bool ca_str_is(const ScrStr *text, const char *literal) { + size_t len = strlen(literal); + return text->len == len && memcmp(text->data, literal, len) == 0; +} + +/* Node's normalizeEncoding: the canonical name, or NULL. */ +static const char *ca_normalize_encoding(const ScrStr *name) { + static const struct { const char *from; const char *to; } map[] = { + {"utf8", "utf8"}, {"utf-8", "utf8"}, {"hex", "hex"}, + {"base64", "base64"}, {"base64url", "base64url"}, {"latin1", "latin1"}, + {"binary", "latin1"}, {"ascii", "ascii"}, {"utf16le", "utf16le"}, + {"utf-16le", "utf16le"}, {"ucs2", "utf16le"}, {"ucs-2", "utf16le"}, + }; + for (size_t i = 0; i < sizeof map / sizeof map[0]; i++) + if (ca_ascii_eq_ci(name->data, name->len, map[i].from)) return map[i].to; + return NULL; +} + +/* The value a typed-ref box holds when it is a string, view, ArrayBuffer + * or native handle (+1). Other values are retained as passed, so argument + * errors describe the original value. NULL after throwing. */ +static ScrDyn *ca_unbox(ScrDyn *value) { + if (value->kind != SCR_DYN_TYPED_REF) return scr_dyn_retain(value); + ScrDyn *view = scr_dyn_typed_ref_materialize(value); + if (!view) return NULL; + if (view->kind == SCR_DYN_STR || view->kind == SCR_DYN_BYTES || view->kind == SCR_DYN_HANDLE || + scr_buffer_storage_is(view)) + return view; + scr_dyn_release(view); + return scr_dyn_retain(value); +} + +/* Buffer.from(text, encoding) as toBuf calls it: an encoding that is not + * a non-empty string, or is 'buffer', reads as utf8. +1 or NULL after + * throwing. */ +static ScrBytes *ca_string_bytes(const char *subject, const ScrStr *text, const ScrDyn *encoding) { + const char *name = "utf8"; + if (encoding->kind == SCR_DYN_STR && encoding->v.str->len > 0 && !ca_str_is(encoding->v.str, "buffer")) { + name = ca_normalize_encoding(encoding->v.str); + if (!name) { + ca_throw_formatted(subject, "ERR_UNKNOWN_ENCODING", "Unknown encoding: ", encoding); + return NULL; + } + } + ScrStr *canonical = scr_str_new(name, strlen(name)); + ScrBytes *bytes = scr_bytes_from_str(text, canonical); + scr_str_release(canonical); + return bytes; +} + +/* getArrayBufferOrView(key, 'key', encoding): a string is Buffer.from(key, + * encoding) with 'buffer' read as utf8; views and ArrayBuffers are their + * bytes. +1 or NULL after throwing. */ +static ScrBytes *ca_key_bytes(const char *subject, const ScrDyn *key, const ScrDyn *encoding) { + if (key->kind == SCR_DYN_STR) return ca_string_bytes(subject, key->v.str, encoding); + if (key->kind == SCR_DYN_BYTES || scr_buffer_storage_is(key)) return scr_bytes_buffer_source(key); + scr_dyn_arg_type_fail("key", "of type string or an instance of ArrayBuffer, Buffer, TypedArray, or DataView", key); + return NULL; +} + +/* encode(buffer, encoding): the Buffer, or buffer.toString(encoding) for + * a truthy encoding other than 'buffer'. Consumes bytes. */ +static ScrDyn *ca_encode(const char *subject, ScrBytes *bytes, const ScrDyn *encoding) { + if (!scr_dyn_truthy(encoding) || (encoding->kind == SCR_DYN_STR && ca_str_is(encoding->v.str, "buffer"))) { + ScrDyn *result = scr_dyn_new_buffer(bytes); + scr_bytes_release(bytes); + return result; + } + /* toString answers "" for no bytes before it reads the encoding. */ + ScrStr *text = NULL; + if (bytes->len == 0) { + text = scr_str_new("", 0); + } else { + /* getEncodingOps reads `encoding + ''`. */ + ScrStr *name = encoding->kind == SCR_DYN_STR ? scr_str_retain(encoding->v.str) : NULL; + if (!name) { + if (encoding->kind == SCR_DYN_FUNC || ca_class_to_string(encoding)) { + ca_throw_formatted(subject, "ERR_UNKNOWN_ENCODING", "Unknown encoding: ", encoding); + scr_bytes_release(bytes); + return NULL; + } + name = scr_dyn_string_coerce_js(encoding); + if (!name) { + scr_bytes_release(bytes); + return NULL; + } + } + const char *canonical = ca_normalize_encoding(name); + scr_str_release(name); + if (!canonical) { + ca_throw_formatted(subject, "ERR_UNKNOWN_ENCODING", "Unknown encoding: ", encoding); + scr_bytes_release(bytes); + return NULL; + } + ScrStr *canonical_text = scr_str_new(canonical, strlen(canonical)); + text = scr_bytes_to_str(bytes, canonical_text); + scr_str_release(canonical_text); + } + scr_bytes_release(bytes); + if (!text) return NULL; + ScrDyn *result = scr_dyn_new_str(text); + scr_str_release(text); + return result; +} + +#endif diff --git a/packages/runtime/src/scr_dh.c b/packages/runtime/src/scr_dh.c new file mode 100644 index 000000000..dfa6fd2f3 --- /dev/null +++ b/packages/runtime/src/scr_dh.c @@ -0,0 +1,1780 @@ +/* node:crypto DiffieHellman over the vendored mbedTLS bignum arithmetic: + * crypto.createDiffieHellman, crypto.getDiffieHellman and + * crypto.createDiffieHellmanGroup. A DiffieHellman or DiffieHellmanGroup + * is a native dyn handle. Construction follows Node 24's + * lib/internal/crypto/diffiehellman.js argument handling and + * src/crypto/crypto_dh.cc; key generation, verifyError, the public key + * range check and computeSecret follow the OpenSSL 3 paths of + * deps/ncrypto and OpenSSL 3.5's DH provider: + * - a group draws modp5 through modp18 private keys of a fixed width + * (200 to 400 bits) and modp1 and modp2 keys in [2, p - 2], and its + * verifyError is 0; + * - createDiffieHellman parameters that equal an OpenSSL named group + * draw keys below min(2^N, q) for the group's key length N; other + * parameters draw keys of bits(p) - 2 bits, and key generation and + * computeSecret need a 512- to 10000-bit prime; + * - createDiffieHellman(primeLength, generator) generates a safe prime + * p ≡ 23 (mod 24) for the generator 2, p ≡ 59 (mod 60) for 5 and + * p ≡ 11 (mod 12) otherwise, as OpenSSL's dh_builtin_genparams does. + * Primes longer than mbedTLS's 8192-bit limit throw a coded SC2020 + * error, as do keys and generators past mbedTLS's 80000-byte limit (leading + * zero bytes aside). All arguments are borrowed. */ +#include "scr_runtime.h" + +#include +#include +#include +#include +#include + +#include "mbedtls/bignum.h" +#include "mbedtls/platform_util.h" + +#include "scr_crypto_args.h" +#include "scr_dh_groups.h" + +/* The value the SC2020 error for an unlowered encoding names. */ +#define SD_SUBJECT "A DiffieHellman encoding" + +/* DH_MIN_MODULUS_BITS and OPENSSL_DH_MAX_MODULUS_BITS. */ +#define SD_MIN_MODULUS_BITS 512 +#define SD_MAX_MODULUS_BITS 10000 + +/* The longest value an mbedtls_mpi holds, in bytes. */ +#define SD_MAX_VALUE_BYTES (MBEDTLS_MPI_MAX_LIMBS * sizeof(mbedtls_mpi_uint)) + +/* DH_check result bits, as verifyError reports them. */ +enum { + SD_P_NOT_PRIME = 0x01, + SD_P_NOT_SAFE_PRIME = 0x02, + SD_NOT_SUITABLE_GENERATOR = 0x08, + SD_MODULUS_TOO_SMALL = 0x80, +}; + +/* A crypto.getDiffieHellman group. */ +typedef struct { + const char *name; + const unsigned char *p; + size_t p_len; + int private_bits; /* ncrypto's GetDhGroupPrivateBits; 0 draws keys in [2, p - 2] */ +} ScDhGroup; + +static const ScDhGroup sd_groups[] = { + {"modp1", scr_dh_modp1_p, sizeof scr_dh_modp1_p, 0}, + {"modp2", scr_dh_modp2_p, sizeof scr_dh_modp2_p, 0}, + {"modp5", scr_dh_modp5_p, sizeof scr_dh_modp5_p, 200}, + {"modp14", scr_dh_modp14_p, sizeof scr_dh_modp14_p, 225}, + {"modp15", scr_dh_modp15_p, sizeof scr_dh_modp15_p, 275}, + {"modp16", scr_dh_modp16_p, sizeof scr_dh_modp16_p, 325}, + {"modp17", scr_dh_modp17_p, sizeof scr_dh_modp17_p, 375}, + {"modp18", scr_dh_modp18_p, sizeof scr_dh_modp18_p, 400}, +}; + +/* An OpenSSL DH named group, which the DH provider recognizes in explicit + * p and g. A NULL q is (p - 1) / 2. */ +typedef struct { + const unsigned char *p; + size_t p_len; + const unsigned char *g; + size_t g_len; + const unsigned char *q; + size_t q_len; + int keylength; +} ScDhNamed; + +static const unsigned char sd_two[] = {2}; + +#define SD_SAFE(p, keylength) {p, sizeof p, sd_two, 1, NULL, 0, keylength} +#define SD_RFC5114(name, keylength) \ + {scr_dh_##name##_p, sizeof scr_dh_##name##_p, scr_dh_##name##_g, sizeof scr_dh_##name##_g, \ + scr_dh_##name##_q, sizeof scr_dh_##name##_q, keylength} + +static const ScDhNamed sd_named[] = { + SD_SAFE(scr_dh_ffdhe2048_p, 225), SD_SAFE(scr_dh_ffdhe3072_p, 275), SD_SAFE(scr_dh_ffdhe4096_p, 325), + SD_SAFE(scr_dh_ffdhe6144_p, 375), SD_SAFE(scr_dh_ffdhe8192_p, 400), SD_SAFE(scr_dh_modp5_p, 200), + SD_SAFE(scr_dh_modp14_p, 225), SD_SAFE(scr_dh_modp15_p, 275), SD_SAFE(scr_dh_modp16_p, 325), + SD_SAFE(scr_dh_modp17_p, 375), SD_SAFE(scr_dh_modp18_p, 400), SD_RFC5114(dh_1024_160, 160), + SD_RFC5114(dh_2048_224, 224), SD_RFC5114(dh_2048_256, 224), +}; + +typedef struct ScDh { + size_t rc; + const ScDhGroup *group; /* crypto.getDiffieHellman's group, or NULL */ + const ScDhNamed *named; /* the named group createDiffieHellman's parameters equal, or NULL */ + mbedtls_mpi p, g; + mbedtls_mpi pub; /* when has_pub */ + mbedtls_mpi priv; /* when has_priv */ + bool has_pub; + bool has_priv; + int verify_error; /* -1 until computed */ +} ScDh; + +static ScrDynHandleOps sd_ops; +static ScrDynHandleOps sd_group_ops; + +static void *sd_retain(void *h) { + ((ScDh *)h)->rc++; + return h; +} + +static void sd_release(void *h) { + ScDh *d = h; + if (--d->rc) return; + mbedtls_mpi_free(&d->p); + mbedtls_mpi_free(&d->g); + mbedtls_mpi_free(&d->pub); + mbedtls_mpi_free(&d->priv); + mbedtls_platform_zeroize(d, sizeof *d); + free(d); +} + +static int sd_random(void *state, unsigned char *out, size_t len) { + (void)state; + arc4random_buf(out, len); + return 0; +} + +/* ── errors ───────────────────────────────────────────────────────────── */ + +static void sd_throw(int kind, const char *code, const char *message) { + scr_throw_error_msg_code(kind, message, strlen(message), code); +} + +static void sd_operation_failed(const char *message) { + sd_throw(SCR_ERR_ERROR, "ERR_CRYPTO_OPERATION_FAILED", message); +} + +/* An OpenSSL DH error as ThrowCryptoError reports it: the formatted + * message plus own library, reason and code properties, in Node's key + * order. */ +static void sd_throw_openssl(const char *packed, const char *reason, const char *code) { + char message[96]; + snprintf(message, sizeof message, "error:%s:Diffie-Hellman routines::%s", packed, reason); + ScrStr *text = scr_str_new(message, strlen(message)); + ScrError *e = scr_error_new(SCR_ERR_ERROR, text); + scr_str_release(text); + ScrStr *library = scr_str_new("Diffie-Hellman routines", 23); + scr_error_set_prop(e, "library", 7, scr_dyn_new_str(library)); + scr_str_release(library); + ScrStr *reason_text = scr_str_new(reason, strlen(reason)); + scr_error_set_prop(e, "reason", 6, scr_dyn_new_str(reason_text)); + scr_str_release(reason_text); + scr_error_set_code(e, code); + ScrStr *code_text = scr_str_new(code, strlen(code)); + scr_error_set_prop(e, "code", 4, scr_dyn_new_str(code_text)); + scr_str_release(code_text); + scr_throw_obj(e, &scr_error_retain_v, &scr_error_release_v, scr_error_trace_arg()); +} + +static void sd_bad_generator(void) { + sd_throw_openssl("02800065", "bad generator", "ERR_OSSL_DH_BAD_GENERATOR"); +} + +static void sd_too_long(void) { + static const char message[] = "DiffieHellman primes longer than 8192 bits have no native implementation"; + scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); +} + +/* A key or generator past SD_MAX_VALUE_BYTES, which BN_bin2bn reads. */ +static void sd_value_too_long(void) { + char message[96]; + int n = snprintf(message, sizeof message, "DiffieHellman values longer than %zu bytes have no native implementation", + (size_t)SD_MAX_VALUE_BYTES); + scr_throw_error_msg_code(SCR_ERR_ERROR, message, n < 0 ? 0 : (size_t)n, "SC2020"); +} + +/* validateInt32(value, name, min). */ +static bool sd_validate_int32_min(const char *name, double value, int32_t min) { + bool integer = isfinite(value) && trunc(value) == value; + if (integer && value >= min && value <= INT32_MAX) return true; + char received[48]; + scr_num_received(value, received); + char text[192]; + int n = integer ? snprintf(text, sizeof text, + "The value of \"%s\" is out of range. It must be >= %ld && <= 2147483647. " + "Received %s", + name, (long)min, received) + : snprintf(text, sizeof text, + "The value of \"%s\" is out of range. It must be an integer. Received %s", name, + received); + scr_throw_error_msg_code(SCR_ERR_RANGE, text, n < 0 ? 0 : (size_t)n, "ERR_OUT_OF_RANGE"); + return false; +} + +/* validateInt32(value, name). */ +static bool sd_validate_int32(const char *name, double value) { + return sd_validate_int32_min(name, value, INT32_MIN); +} + +/* ── arithmetic ───────────────────────────────────────────────────────── */ + +/* The exponent digit width that keeps every mbedtls_mpi_exp_mod exponent + * within its 8192-bit limit. */ +#define SD_DIGIT_BITS (MBEDTLS_MPI_MAX_BITS - 1) + +/* base^e mod p, as BN_mod_exp_mont_consttime computes it: false for an + * even modulus or one below 3. Exponents past 8192 bits are split into + * SD_DIGIT_BITS-bit digits. */ +static bool sd_exp(mbedtls_mpi *out, const mbedtls_mpi *base, const mbedtls_mpi *e, const mbedtls_mpi *p) { + if (mbedtls_mpi_cmp_int(p, 1) <= 0 || mbedtls_mpi_get_bit(p, 0) == 0) return false; + size_t ebits = mbedtls_mpi_bitlen(e); + if (ebits <= MBEDTLS_MPI_MAX_BITS) return mbedtls_mpi_exp_mod(out, base, e, p, NULL) == 0; + /* base^e = Π (base^(2^(k·w)))^digit_k for the w-bit digits of e. */ + mbedtls_mpi acc, power, digit, term, step; + mbedtls_mpi_init(&acc); + mbedtls_mpi_init(&power); + mbedtls_mpi_init(&digit); + mbedtls_mpi_init(&term); + mbedtls_mpi_init(&step); + int ret = 0; + MBEDTLS_MPI_CHK(mbedtls_mpi_lset(&acc, 1)); + MBEDTLS_MPI_CHK(mbedtls_mpi_copy(&power, base)); + MBEDTLS_MPI_CHK(mbedtls_mpi_lset(&step, 1)); + MBEDTLS_MPI_CHK(mbedtls_mpi_shift_l(&step, SD_DIGIT_BITS)); + for (size_t offset = 0; offset < ebits; offset += SD_DIGIT_BITS) { + /* Bit by bit, so each digit costs its own width, not the exponent's. */ + MBEDTLS_MPI_CHK(mbedtls_mpi_lset(&digit, 0)); + for (size_t bit = 0; bit < SD_DIGIT_BITS && offset + bit < ebits; bit++) + if (mbedtls_mpi_get_bit(e, offset + bit)) MBEDTLS_MPI_CHK(mbedtls_mpi_set_bit(&digit, bit, 1)); + MBEDTLS_MPI_CHK(mbedtls_mpi_exp_mod(&term, &power, &digit, p, NULL)); + MBEDTLS_MPI_CHK(mbedtls_mpi_mul_mpi(&acc, &acc, &term)); + MBEDTLS_MPI_CHK(mbedtls_mpi_mod_mpi(&acc, &acc, p)); + if (offset + SD_DIGIT_BITS < ebits) MBEDTLS_MPI_CHK(mbedtls_mpi_exp_mod(&power, &power, &step, p, NULL)); + } + MBEDTLS_MPI_CHK(mbedtls_mpi_copy(out, &acc)); +cleanup: + mbedtls_mpi_free(&acc); + mbedtls_mpi_free(&power); + mbedtls_mpi_free(&digit); + mbedtls_mpi_free(&term); + mbedtls_mpi_free(&step); + return ret == 0; +} + +/* Drops a big-endian value's leading zero bytes, which BN_bin2bn ignores + * but mbedtls_mpi_read_binary counts against SD_MAX_VALUE_BYTES. */ +static void sd_strip(const unsigned char **data, size_t *len) { + while (*len > 0 && **data == 0) { + (*data)++; + (*len)--; + } +} + +/* BN_bin2bn: false when the value, without its leading zeros, is longer + * than SD_MAX_VALUE_BYTES. */ +static bool sd_read(mbedtls_mpi *x, const ScrBytes *bytes) { + const unsigned char *data = bytes->data; + size_t len = bytes->len; + sd_strip(&data, &len); + if (len > SD_MAX_VALUE_BYTES) return false; + if (mbedtls_mpi_read_binary(x, data, len) != 0) scr_trap("scriptc: out of memory\n"); + return true; +} + +/* BN_cmp over two big-endian values, for ones sd_read cannot hold. */ +static int sd_cmp_bytes(const ScrBytes *a, const ScrBytes *b) { + const unsigned char *a_data = a->data, *b_data = b->data; + size_t a_len = a->len, b_len = b->len; + sd_strip(&a_data, &a_len); + sd_strip(&b_data, &b_len); + if (a_len != b_len) return a_len < b_len ? -1 : 1; + return a_len ? memcmp(a_data, b_data, a_len) : 0; +} + +/* A uniformly random number of exactly `bits` bits (the top bit set), and + * odd when `odd`, as BN_priv_rand writes it. */ +static bool sd_random_bits(mbedtls_mpi *x, size_t bits, bool odd) { + size_t len = (bits + 7) / 8; + unsigned char *buf = malloc(len); + if (!buf) scr_trap("scriptc: out of memory\n"); + arc4random_buf(buf, len); + unsigned extra = (unsigned)(len * 8 - bits); + buf[0] &= (unsigned char)(0xff >> extra); + buf[0] |= (unsigned char)(0x80 >> extra); + if (odd) buf[len - 1] |= 1; + bool ok = mbedtls_mpi_read_binary(x, buf, len) == 0; + mbedtls_platform_zeroize(buf, len); + free(buf); + return ok; +} + +/* ── primes ───────────────────────────────────────────────────────────── */ + +/* The odd primes below 17864 (OpenSSL's first 2048 primes but 2), which + * trial division divides by. Consecutive primes are grouped into products + * below the half-limb bound mbedtls_mpi_mod_int divides correctly by, so + * one call reduces by several. */ +#define SD_SMALL_PRIMES 2047 +#define SD_SMALL_LIMIT 17864 +#define SD_PRODUCT_LIMIT ((uint64_t)1 << (sizeof(mbedtls_mpi_uint) * 4)) + +typedef struct { + uint16_t primes[SD_SMALL_PRIMES]; + uint64_t products[SD_SMALL_PRIMES]; + uint16_t starts[SD_SMALL_PRIMES + 1]; /* products[g] covers primes[starts[g]] to primes[starts[g + 1] - 1] */ + size_t groups; + uint32_t mods[SD_SMALL_PRIMES]; +} ScDhSmallPrimes; + +static ScDhSmallPrimes *sd_small_primes(void) { + ScDhSmallPrimes *t = calloc(1, sizeof *t); + unsigned char *composite = calloc(SD_SMALL_LIMIT, 1); + if (!t || !composite) scr_trap("scriptc: out of memory\n"); + size_t count = 0; + for (uint32_t n = 3; n < SD_SMALL_LIMIT && count < SD_SMALL_PRIMES; n += 2) { + if (composite[n]) continue; + t->primes[count++] = (uint16_t)n; + for (uint32_t m = n * n; m < SD_SMALL_LIMIT; m += 2 * n) composite[m] = 1; + } + free(composite); + uint64_t product = 1; + for (size_t i = 0; i < SD_SMALL_PRIMES; i++) { + if (product > SD_PRODUCT_LIMIT / t->primes[i]) { + t->products[t->groups++] = product; + t->starts[t->groups] = (uint16_t)i; + product = 1; + } + product *= t->primes[i]; + } + t->products[t->groups++] = product; + t->starts[t->groups] = SD_SMALL_PRIMES; + return t; +} + +/* x mod each small prime, into t->mods. */ +static bool sd_small_residues(ScDhSmallPrimes *t, const mbedtls_mpi *x) { + for (size_t g = 0; g < t->groups; g++) { + mbedtls_mpi_uint r; + if (mbedtls_mpi_mod_int(&r, x, (mbedtls_mpi_sint)t->products[g]) != 0) return false; + for (size_t i = t->starts[g]; i < t->starts[g + 1]; i++) t->mods[i] = (uint32_t)(r % t->primes[i]); + } + return true; +} + +#if defined(__SIZEOF_INT128__) +#define SD_MONT 1 +typedef unsigned __int128 sd_u128; + +/* Montgomery arithmetic modulo an odd n of k 64-bit limbs, little-endian, + * with R = 2^(64k), for the Miller-Rabin rounds. It runs in variable time + * and reads only public values. */ +typedef struct { + size_t k; + uint64_t mm; /* -n^-1 mod 2^64 */ + uint64_t *n; /* k limbs */ + uint64_t *rr; /* R^2 mod n, k limbs */ + uint64_t *acc; /* k limbs */ + uint64_t *table; /* 16·k limbs: base^i·R mod n */ + uint64_t *t; /* 2k + 1 limbs of products */ +} ScDhMont; + +static void sd_limbs_read(uint64_t *out, size_t k, const mbedtls_mpi *x, unsigned char *buf) { + if (mbedtls_mpi_write_binary_le(x, buf, 8 * k) != 0) scr_trap("scriptc: internal error: limb overflow\n"); + for (size_t i = 0; i < k; i++) { + uint64_t w = 0; + for (size_t b = 8; b-- > 0;) w = (w << 8) | buf[8 * i + b]; + out[i] = w; + } +} + +static bool sd_limbs_write(mbedtls_mpi *x, const uint64_t *in, size_t k, unsigned char *buf) { + for (size_t i = 0; i < k; i++) + for (size_t b = 0; b < 8; b++) buf[8 * i + b] = (unsigned char)(in[i] >> (8 * b)); + return mbedtls_mpi_read_binary_le(x, buf, 8 * k) == 0; +} + +static bool sd_mont_init(ScDhMont *m, const mbedtls_mpi *n) { + size_t k = (mbedtls_mpi_bitlen(n) + 63) / 64; + m->k = k; + m->n = calloc(21 * k + 1, sizeof(uint64_t)); + if (!m->n) scr_trap("scriptc: out of memory\n"); + m->rr = m->n + k; + m->acc = m->rr + k; + m->table = m->acc + k; + m->t = m->table + 16 * k; + unsigned char *buf = malloc(8 * k); + if (!buf) scr_trap("scriptc: out of memory\n"); + sd_limbs_read(m->n, k, n, buf); + /* Newton's iteration doubles the correct low bits of n^-1 from 3. */ + uint64_t inverse = m->n[0]; + for (int i = 0; i < 6; i++) inverse *= 2 - m->n[0] * inverse; + m->mm = (uint64_t)0 - inverse; + mbedtls_mpi rr; + mbedtls_mpi_init(&rr); + bool ok = mbedtls_mpi_lset(&rr, 1) == 0 && mbedtls_mpi_shift_l(&rr, 128 * k) == 0 && + mbedtls_mpi_mod_mpi(&rr, &rr, n) == 0; + if (ok) sd_limbs_read(m->rr, k, &rr, buf); + mbedtls_mpi_free(&rr); + free(buf); + return ok; +} + +static void sd_mont_free(ScDhMont *m) { + free(m->n); +} + +/* r = v mod n for v < 2n, given as k limbs and a top limb. */ +static void sd_mont_final(const ScDhMont *m, uint64_t *r, const uint64_t *v, uint64_t top) { + size_t k = m->k; + bool ge = top != 0; + if (!ge) { + ge = true; + for (size_t j = k; j-- > 0;) + if (v[j] != m->n[j]) { + ge = v[j] > m->n[j]; + break; + } + } + if (!ge) { + if (r != v) memmove(r, v, k * sizeof *r); + return; + } + uint64_t borrow = 0; + for (size_t j = 0; j < k; j++) { + sd_u128 d = (sd_u128)v[j] - m->n[j] - borrow; + r[j] = (uint64_t)d; + borrow = (uint64_t)(d >> 64) & 1; + } +} + +/* r = t·R^-1 mod n for the 2k-limb product in t. */ +static void sd_mont_redc(ScDhMont *m, uint64_t *r) { + size_t k = m->k; + uint64_t *t = m->t; + t[2 * k] = 0; + for (size_t i = 0; i < k; i++) { + uint64_t q = t[i] * m->mm; + uint64_t carry = 0; + for (size_t j = 0; j < k; j++) { + sd_u128 v = (sd_u128)q * m->n[j] + t[i + j] + carry; + t[i + j] = (uint64_t)v; + carry = (uint64_t)(v >> 64); + } + for (size_t j = i + k; carry != 0 && j <= 2 * k; j++) { + sd_u128 v = (sd_u128)t[j] + carry; + t[j] = (uint64_t)v; + carry = (uint64_t)(v >> 64); + } + } + sd_mont_final(m, r, t + k, t[2 * k]); +} + +static void sd_mont_mul(ScDhMont *m, uint64_t *r, const uint64_t *a, const uint64_t *b) { + size_t k = m->k; + uint64_t *t = m->t; + memset(t, 0, 2 * k * sizeof *t); + for (size_t i = 0; i < k; i++) { + uint64_t carry = 0; + for (size_t j = 0; j < k; j++) { + sd_u128 v = (sd_u128)a[i] * b[j] + t[i + j] + carry; + t[i + j] = (uint64_t)v; + carry = (uint64_t)(v >> 64); + } + t[i + k] = carry; + } + sd_mont_redc(m, r); +} + +/* r = a^2·R^-1 mod n: the cross products once, doubled, plus the squares. */ +static void sd_mont_sqr(ScDhMont *m, uint64_t *r, const uint64_t *a) { + size_t k = m->k; + uint64_t *t = m->t; + memset(t, 0, 2 * k * sizeof *t); + for (size_t i = 0; i < k; i++) { + uint64_t carry = 0; + for (size_t j = i + 1; j < k; j++) { + sd_u128 v = (sd_u128)a[i] * a[j] + t[i + j] + carry; + t[i + j] = (uint64_t)v; + carry = (uint64_t)(v >> 64); + } + t[i + k] = carry; + } + uint64_t shifted = 0; + for (size_t i = 0; i < 2 * k; i++) { + uint64_t v = t[i]; + t[i] = (v << 1) | shifted; + shifted = v >> 63; + } + uint64_t carry = 0; + for (size_t i = 0; i < k; i++) { + sd_u128 square = (sd_u128)a[i] * a[i]; + sd_u128 low = (sd_u128)t[2 * i] + (uint64_t)square + carry; + t[2 * i] = (uint64_t)low; + sd_u128 high = (sd_u128)t[2 * i + 1] + (uint64_t)(square >> 64) + (uint64_t)(low >> 64); + t[2 * i + 1] = (uint64_t)high; + carry = (uint64_t)(high >> 64); + } + sd_mont_redc(m, r); +} + +/* x = 2x mod n. */ +static void sd_mont_double(const ScDhMont *m, uint64_t *x) { + size_t k = m->k; + uint64_t top = x[k - 1] >> 63; + for (size_t j = k; j-- > 1;) x[j] = (x[j] << 1) | (x[j - 1] >> 63); + x[0] <<= 1; + sd_mont_final(m, x, x, top); +} + +/* out = base^e mod n, or 2^e mod n for a NULL base. A base of 2 doubles + * instead of multiplying; others use a 4-bit fixed window. */ +static bool sd_mont_pow(ScDhMont *m, mbedtls_mpi *out, const mbedtls_mpi *base, const mbedtls_mpi *e) { + size_t k = m->k; + unsigned char *buf = malloc(8 * k); + if (!buf) scr_trap("scriptc: out of memory\n"); + /* acc = R mod n, the Montgomery form of 1. */ + memset(m->t, 0, 2 * k * sizeof *m->t); + memcpy(m->t, m->rr, k * sizeof *m->t); + sd_mont_redc(m, m->acc); + size_t bits = mbedtls_mpi_bitlen(e); + if (!base) { + for (size_t i = bits; i-- > 0;) { + sd_mont_sqr(m, m->acc, m->acc); + if (mbedtls_mpi_get_bit(e, i)) sd_mont_double(m, m->acc); + } + } else { + memcpy(m->table, m->acc, k * sizeof *m->table); + sd_limbs_read(m->table + k, k, base, buf); + sd_mont_mul(m, m->table + k, m->table + k, m->rr); + for (size_t i = 2; i < 16; i++) sd_mont_mul(m, m->table + i * k, m->table + (i - 1) * k, m->table + k); + size_t windows = (bits + 3) / 4; + for (size_t w = windows; w-- > 0;) { + if (w + 1 < windows) + for (int s = 0; s < 4; s++) sd_mont_sqr(m, m->acc, m->acc); + unsigned digit = 0; + for (size_t b = 4; b-- > 0;) digit = (digit << 1) | (unsigned)mbedtls_mpi_get_bit(e, 4 * w + b); + if (digit) sd_mont_mul(m, m->acc, m->acc, m->table + digit * k); + } + } + /* Leave the Montgomery form. */ + memset(m->t, 0, 2 * k * sizeof *m->t); + memcpy(m->t, m->acc, k * sizeof *m->t); + sd_mont_redc(m, m->acc); + bool ok = sd_limbs_write(out, m->acc, k, buf); + free(buf); + return ok; +} +#else +#define SD_MONT 0 +#endif + +/* Miller-Rabin state for an odd n > 3: n - 1 = d·2^s. */ +typedef struct { + mbedtls_mpi n1, d, a, x; + size_t s; +#if SD_MONT + ScDhMont mont; +#else + mbedtls_mpi rr; /* the Montgomery constant mbedtls_mpi_exp_mod caches */ +#endif +} ScDhMillerRabin; + +static bool sd_mr_init(ScDhMillerRabin *m, const mbedtls_mpi *n) { + mbedtls_mpi_init(&m->n1); + mbedtls_mpi_init(&m->d); + mbedtls_mpi_init(&m->a); + mbedtls_mpi_init(&m->x); +#if SD_MONT + if (!sd_mont_init(&m->mont, n)) return false; +#else + mbedtls_mpi_init(&m->rr); +#endif + if (mbedtls_mpi_sub_int(&m->n1, n, 1) != 0 || mbedtls_mpi_copy(&m->d, &m->n1) != 0) return false; + m->s = mbedtls_mpi_lsb(&m->d); + return mbedtls_mpi_shift_r(&m->d, m->s) == 0; +} + +static void sd_mr_free(ScDhMillerRabin *m) { + mbedtls_mpi_free(&m->n1); + mbedtls_mpi_free(&m->d); + mbedtls_mpi_free(&m->a); + mbedtls_mpi_free(&m->x); +#if SD_MONT + sd_mont_free(&m->mont); +#else + mbedtls_mpi_free(&m->rr); +#endif +} + +/* x = a^e mod n, or 2^e mod n when base2. */ +static bool sd_mr_pow(ScDhMillerRabin *m, const mbedtls_mpi *n, const mbedtls_mpi *e, bool base2) { +#if SD_MONT + (void)n; + return sd_mont_pow(&m->mont, &m->x, base2 ? NULL : &m->a, e); +#else + if (base2 && mbedtls_mpi_lset(&m->a, 2) != 0) return false; + return mbedtls_mpi_exp_mod(&m->x, &m->a, e, n, &m->rr) == 0; +#endif +} + +/* One round with the base 2, or with a uniformly random base in + * [2, n - 2]: 1 when n is a strong probable prime to the base, 0 when it + * is composite, -1 when an operation fails. */ +static int sd_mr_round(ScDhMillerRabin *m, const mbedtls_mpi *n, bool base2) { + if (!base2 && mbedtls_mpi_random(&m->a, 2, &m->n1, sd_random, NULL) != 0) return -1; + if (!sd_mr_pow(m, n, &m->d, base2)) return -1; + if (mbedtls_mpi_cmp_int(&m->x, 1) == 0 || mbedtls_mpi_cmp_mpi(&m->x, &m->n1) == 0) return 1; + for (size_t i = 1; i < m->s; i++) { + if (mbedtls_mpi_mul_mpi(&m->x, &m->x, &m->x) != 0 || mbedtls_mpi_mod_mpi(&m->x, &m->x, n) != 0) return -1; + if (mbedtls_mpi_cmp_mpi(&m->x, &m->n1) == 0) return 1; + if (mbedtls_mpi_cmp_int(&m->x, 1) == 0) return 0; + } + return 0; +} + +/* 2^(n-1) ≡ 1 (mod n) for an odd n > 3. */ +static int sd_fermat2(const mbedtls_mpi *n) { + ScDhMillerRabin m; + int ret = sd_mr_init(&m, n) && sd_mr_pow(&m, n, &m.n1, true) ? mbedtls_mpi_cmp_int(&m.x, 1) == 0 : -1; + sd_mr_free(&m); + return ret; +} + +/* OpenSSL's Miller-Rabin round count: 64, or 128 past 2048 bits. */ +static int sd_mr_rounds(size_t bits) { + return bits > 2048 ? 128 : 64; +} + +/* BN_is_prime_ex(x, checks): trial division by the small primes, then + * Miller-Rabin rounds with random bases, at least OpenSSL's round count. */ +static bool sd_is_prime_checks(ScDhSmallPrimes *t, const mbedtls_mpi *x, int checks) { + if (mbedtls_mpi_cmp_int(x, 2) < 0) return false; + if (mbedtls_mpi_get_bit(x, 0) == 0) return mbedtls_mpi_cmp_int(x, 2) == 0; + if (!sd_small_residues(t, x)) return false; + for (size_t i = 0; i < SD_SMALL_PRIMES; i++) + if (t->mods[i] == 0) return mbedtls_mpi_cmp_int(x, t->primes[i]) == 0; + /* No prime factor up to sqrt(x). */ + if (mbedtls_mpi_cmp_int(x, (mbedtls_mpi_sint)SD_SMALL_LIMIT * SD_SMALL_LIMIT) < 0) return true; + ScDhMillerRabin m; + bool prime = sd_mr_init(&m, x); + int rounds = sd_mr_rounds(mbedtls_mpi_bitlen(x)); + if (checks > rounds) rounds = checks; + for (int i = 0; i < rounds && prime; i++) prime = sd_mr_round(&m, x, false) == 1; + sd_mr_free(&m); + return prime; +} + +/* BN_check_prime. */ +static bool sd_is_prime(ScDhSmallPrimes *t, const mbedtls_mpi *x) { + return sd_is_prime_checks(t, x, 0); +} + +/* DH_check for parameters without q, as ncrypto's CheckDhParams reports + * it. */ +static int sd_check_params(const mbedtls_mpi *p, const mbedtls_mpi *g) { + int codes = 0; + if (mbedtls_mpi_get_bit(p, 0) == 0) codes |= SD_P_NOT_PRIME; + if (mbedtls_mpi_cmp_int(g, 1) <= 0) codes |= SD_NOT_SUITABLE_GENERATOR; + if (mbedtls_mpi_bitlen(p) < SD_MIN_MODULUS_BITS) codes |= SD_MODULUS_TOO_SMALL; + mbedtls_mpi t; + mbedtls_mpi_init(&t); + if (mbedtls_mpi_sub_int(&t, p, 1) != 0) scr_trap("scriptc: out of memory\n"); + if (mbedtls_mpi_cmp_mpi(g, &t) >= 0) codes |= SD_NOT_SUITABLE_GENERATOR; + /* BN_check_prime(p), then BN_check_prime((p - 1) / 2) for a prime p. + * For an odd p > 3 whose (p - 1) / 2 is prime, p is prime exactly when + * 3 does not divide it and 2^(p-1) ≡ 1 (mod p) (Pocklington's + * criterion), so (p - 1) / 2 is tested first. */ + ScDhSmallPrimes *small = sd_small_primes(); + if (mbedtls_mpi_copy(&t, p) != 0 || mbedtls_mpi_shift_r(&t, 1) != 0) scr_trap("scriptc: out of memory\n"); + bool p_prime, q_prime; + if (mbedtls_mpi_get_bit(p, 0) == 0 || mbedtls_mpi_cmp_int(p, 3) <= 0) { + p_prime = sd_is_prime(small, p); + q_prime = p_prime && sd_is_prime(small, &t); + } else { + q_prime = sd_is_prime(small, &t); + mbedtls_mpi_uint r = 0; + if (!q_prime) p_prime = sd_is_prime(small, p); + else p_prime = mbedtls_mpi_mod_int(&r, p, 3) == 0 && r != 0 && sd_fermat2(p) == 1; + } + if (!p_prime) codes |= SD_P_NOT_PRIME; + else if (!q_prime) codes |= SD_P_NOT_SAFE_PRIME; + free(small); + mbedtls_mpi_free(&t); + return codes; +} + +/* The named group whose p and g equal these, as ossl_dh_cache_named_group + * finds it, or NULL. */ +static const ScDhNamed *sd_find_named(const mbedtls_mpi *p, const mbedtls_mpi *g) { + size_t p_len = mbedtls_mpi_size(p); + const ScDhNamed *found = NULL; + mbedtls_mpi x; + mbedtls_mpi_init(&x); + for (size_t i = 0; i < sizeof sd_named / sizeof sd_named[0] && !found; i++) { + const ScDhNamed *n = &sd_named[i]; + if (n->p_len != p_len) continue; + if (mbedtls_mpi_read_binary(&x, n->p, n->p_len) != 0 || mbedtls_mpi_cmp_mpi(&x, p) != 0) continue; + if (mbedtls_mpi_read_binary(&x, n->g, n->g_len) == 0 && mbedtls_mpi_cmp_mpi(&x, g) == 0) found = n; + } + mbedtls_mpi_free(&x); + return found; +} + +/* The safe-prime sieve: the odd primes below a bound that grows with the + * prime's length and do not divide the step `add`, each with add^-1 and a + * start's residue modulo it, and the marks of a window of candidates + * start + k·add. */ +#define SD_SIEVE_WINDOW 65536 + +static uint32_t sd_sieve_bound(size_t bits) { + /* mbedtls_mpi_mod_int divides correctly by values below 2^(limb bits / 2). */ + if (sizeof(mbedtls_mpi_uint) < 8) return 1u << 16; + return bits < 1024 ? 1u << 18 : bits < 2048 ? 1u << 22 : 1u << 24; +} + +typedef struct { + uint32_t *primes; + uint32_t *inverse; + uint32_t *residue; + size_t count; + unsigned char marks[SD_SIEVE_WINDOW]; +} ScDhSieve; + +/* a^-1 mod m for gcd(a, m) = 1. */ +static uint32_t sd_inverse(uint32_t a, uint32_t m) { + int64_t t = 0, next = 1; + int64_t r = m, rest = a; + while (rest != 0) { + int64_t quotient = r / rest; + int64_t t_next = t - quotient * next; + t = next; + next = t_next; + int64_t r_next = r - quotient * rest; + r = rest; + rest = r_next; + } + return (uint32_t)(t < 0 ? t + m : t); +} + +static ScDhSieve *sd_sieve_new(size_t bits, unsigned add) { + uint32_t bound = sd_sieve_bound(bits); + ScDhSieve *sv = calloc(1, sizeof *sv); + unsigned char *composite = calloc(bound / 2, 1); /* composite[i] for 2i + 1 */ + if (!sv || !composite) scr_trap("scriptc: out of memory\n"); + size_t count = 0; + for (uint32_t i = 1; i < bound / 2; i++) { + if (composite[i]) continue; + uint32_t n = 2 * i + 1; + for (uint64_t m = (uint64_t)n * n; m < bound; m += 2 * n) composite[m / 2] = 1; + if (add % n != 0) count++; + } + sv->primes = malloc(count * sizeof(uint32_t)); + sv->inverse = malloc(count * sizeof(uint32_t)); + sv->residue = malloc(count * sizeof(uint32_t)); + if (!sv->primes || !sv->inverse || !sv->residue) scr_trap("scriptc: out of memory\n"); + for (uint32_t i = 1; i < bound / 2; i++) { + uint32_t n = 2 * i + 1; + if (composite[i] || add % n == 0) continue; + sv->inverse[sv->count] = sd_inverse(add % n, n); + sv->primes[sv->count++] = n; + } + free(composite); + return sv; +} + +static void sd_sieve_free(ScDhSieve *sv) { + free(sv->primes); + free(sv->inverse); + free(sv->residue); + free(sv); +} + +/* Marks the candidates start + k·add of the window that a sieve prime + * divides, or whose (p - 1) / 2 it divides, then moves the residues to + * the next window. */ +static void sd_sieve_window(ScDhSieve *sv, unsigned add) { + memset(sv->marks, 0, sizeof sv->marks); + for (size_t i = 0; i < sv->count; i++) { + uint64_t r = sv->primes[i]; + uint64_t s = sv->residue[i]; + /* start + k·add ≡ t (mod r) for t = 0 and t = 1. */ + for (uint64_t t = 0; t < 2; t++) { + uint64_t k = (r + t - s) % r * sv->inverse[i] % r; + for (; k < SD_SIEVE_WINDOW; k += r) sv->marks[k] = 1; + } + sv->residue[i] = (uint32_t)((s + (uint64_t)SD_SIEVE_WINDOW * add) % r); + } +} + +/* A random safe prime of exactly `bits` bits with p ≡ rem (mod add), + * rem ≡ 2 (mod 3), as BN_generate_prime_ex2(p, bits, safe = 1, add, rem) + * draws one: a random start of `bits` bits moved to p ≡ rem (mod add), + * then candidates stepping by `add` past values that a sieve prime + * divides or that make (p - 1) / 2 divisible by one. A candidate is kept + * when p is a strong probable prime to the base 2 and q = (p - 1) / 2 + * passes OpenSSL's Miller-Rabin round count; a prime q then proves p prime + * (Pocklington's criterion with 2^(p-1) ≡ 1 and gcd(2^2 - 1, p) = 1). + * Candidates past `bits` bits take a new start. With `top_two`, starts + * also set the second-highest bit, as BN_RAND_TOP_TWO does. */ +static bool sd_generate_safe_prime(mbedtls_mpi *p, size_t bits, unsigned add, unsigned rem, bool top_two) { + ScDhSieve *sv = sd_sieve_new(bits, add); + int rounds = sd_mr_rounds(bits); + mbedtls_mpi start, q; + mbedtls_mpi_init(&start); + mbedtls_mpi_init(&q); + int ret = 0; + while (ret == 0) { + mbedtls_mpi_uint r; + if (!sd_random_bits(&start, bits, true) || (top_two && mbedtls_mpi_set_bit(&start, bits - 2, 1) != 0) || + mbedtls_mpi_mod_int(&r, &start, add) != 0 || + mbedtls_mpi_sub_int(&start, &start, (mbedtls_mpi_sint)r) != 0 || + mbedtls_mpi_add_int(&start, &start, rem) != 0 || + (mbedtls_mpi_bitlen(&start) < bits && mbedtls_mpi_add_int(&start, &start, add) != 0)) { + ret = -1; + break; + } + for (size_t i = 0; i < sv->count && ret == 0; i++) { + if (mbedtls_mpi_mod_int(&r, &start, sv->primes[i]) != 0) ret = -1; + sv->residue[i] = (uint32_t)r; + } + bool fits = true; + while (ret == 0 && fits) { + sd_sieve_window(sv, add); + for (uint32_t k = 0; k < SD_SIEVE_WINDOW && ret == 0 && fits; k++) { + if (sv->marks[k]) continue; + if (mbedtls_mpi_add_int(p, &start, (mbedtls_mpi_sint)k * add) != 0 || mbedtls_mpi_copy(&q, p) != 0 || + mbedtls_mpi_shift_r(&q, 1) != 0) { + ret = -1; + break; + } + fits = mbedtls_mpi_bitlen(p) == bits; + if (!fits) break; + ScDhMillerRabin mr; + ret = sd_mr_init(&mr, p) ? sd_mr_round(&mr, p, true) : -1; + sd_mr_free(&mr); + if (ret != 1) continue; + ret = sd_mr_init(&mr, &q) ? sd_mr_round(&mr, &q, true) : -1; + for (int i = 1; i < rounds && ret == 1; i++) ret = sd_mr_round(&mr, &q, false); + sd_mr_free(&mr); + } + if (ret == 0 && fits && mbedtls_mpi_add_int(&start, &start, (mbedtls_mpi_sint)SD_SIEVE_WINDOW * add) != 0) + ret = -1; + } + } + sd_sieve_free(sv); + mbedtls_mpi_free(&start); + mbedtls_mpi_free(&q); + return ret == 1; +} + +/* ── construction ─────────────────────────────────────────────────────── */ + +static ScDh *sd_alloc(void) { + ScDh *d = calloc(1, sizeof *d); + if (!d) scr_trap("scriptc: out of memory\n"); + d->rc = 1; + mbedtls_mpi_init(&d->p); + mbedtls_mpi_init(&d->g); + mbedtls_mpi_init(&d->pub); + mbedtls_mpi_init(&d->priv); + return d; +} + +static ScrDyn *sd_box(ScDh *d) { + ScrDynHandleTag tag = d->group ? SCR_DYNH_DIFFIE_HELLMAN_GROUP : SCR_DYNH_DIFFIE_HELLMAN; + scr_dyn_handle_install(tag, d->group ? &sd_group_ops : &sd_ops); + ScrDyn *result = scr_dyn_new_handle(d, tag); /* retains */ + sd_release(d); + return result; +} + +static bool sd_is_view(const ScrDyn *value) { + return value->kind == SCR_DYN_BYTES || scr_buffer_storage_is(value); +} + +/* Buffer.isEncoding(value). */ +static bool sd_is_encoding(const ScrDyn *value) { + return value->kind == SCR_DYN_STR && value->v.str->len > 0 && ca_normalize_encoding(value->v.str) != NULL; +} + +/* new DiffieHellman(primeLength, generator): generated parameters. */ +static ScrDyn *sd_generate(double bits, const ScrBytes *generator_bytes, double generator) { + if (bits < 2) { + sd_throw_openssl("0280007E", "modulus too small", "ERR_OSSL_DH_MODULUS_TOO_SMALL"); + return NULL; + } + if (generator_bytes) { + sd_throw(SCR_ERR_TYPE, "ERR_INVALID_ARG_TYPE", "Second argument must be an int32"); + return NULL; + } + if (generator < 2) { + sd_bad_generator(); + return NULL; + } + if (bits < SD_MIN_MODULUS_BITS || bits > SD_MAX_MODULUS_BITS) { + sd_throw(SCR_ERR_TYPE, "ERR_INVALID_ARG_VALUE", "Invalid DH parameters"); + return NULL; + } + if (bits > MBEDTLS_MPI_MAX_BITS) { + sd_too_long(); + return NULL; + } + unsigned add = 12; + if (generator == 2) add = 24; + else if (generator == 5) add = 60; + ScDh *d = sd_alloc(); + if (!sd_generate_safe_prime(&d->p, (size_t)bits, add, add - 1, false) || mbedtls_mpi_lset(&d->g, (mbedtls_mpi_sint)generator) != 0) { + sd_release(d); + sd_throw(SCR_ERR_TYPE, "ERR_INVALID_ARG_VALUE", "Invalid DH parameters"); + return NULL; + } + /* A safe prime of at least 512 bits with a generator below p - 1 has no + * DH_check findings. */ + d->verify_error = 0; + return sd_box(d); +} + +/* new DiffieHellman(prime, generator) over explicit parameters. */ +static ScrDyn *sd_from_prime(const ScrBytes *prime, const ScrBytes *generator_bytes, double generator) { + ScDh *d = sd_alloc(); + bool p_held = sd_read(&d->p, prime); + bool g_held = true; + if (generator_bytes) g_held = sd_read(&d->g, generator_bytes); + else if (mbedtls_mpi_lset(&d->g, (mbedtls_mpi_sint)generator) != 0) scr_trap("scriptc: out of memory\n"); + if (!p_held || !g_held) { + /* A prime this long is past MBEDTLS_MPI_MAX_BITS, and a generator + * this long is at least a prime of SD_MIN_MODULUS_BITS. */ + bool bad = g_held ? mbedtls_mpi_cmp_int(&d->g, 2) < 0 + : !p_held ? sd_cmp_bytes(generator_bytes, prime) >= 0 + : mbedtls_mpi_bitlen(&d->p) >= SD_MIN_MODULUS_BITS; + sd_release(d); + if (bad) sd_bad_generator(); + else if (!p_held) sd_too_long(); + else sd_value_too_long(); + return NULL; + } + if (mbedtls_mpi_cmp_int(&d->g, 2) < 0 || + (mbedtls_mpi_bitlen(&d->p) >= SD_MIN_MODULUS_BITS && mbedtls_mpi_cmp_mpi(&d->g, &d->p) >= 0)) { + sd_release(d); + sd_bad_generator(); + return NULL; + } + if (mbedtls_mpi_bitlen(&d->p) > MBEDTLS_MPI_MAX_BITS) { + sd_release(d); + sd_too_long(); + return NULL; + } + d->named = sd_find_named(&d->p, &d->g); + /* A named group's parameters have no DH_check findings. Other + * parameters are checked when verifyError is first read. */ + d->verify_error = d->named ? 0 : -1; + return sd_box(d); +} + +/* new DiffieHellman(sizeOrKey, keyEncoding, generator, genEncoding). */ +static ScrDyn *sd_new(const ScrDyn *size_or_key, const ScrDyn *key_encoding, const ScrDyn *generator, + const ScrDyn *generator_encoding) { + static const char *const types = + "one of type number or string or an instance of ArrayBuffer, Buffer, TypedArray, or DataView"; + bool size = size_or_key->kind == SCR_DYN_NUM; + if (!size && size_or_key->kind != SCR_DYN_STR && !sd_is_view(size_or_key)) { + scr_dyn_arg_type_fail("sizeOrKey", types, size_or_key); + return NULL; + } + if (size && !sd_validate_int32("sizeOrKey", size_or_key->v.num)) return NULL; + /* A truthy keyEncoding that names no encoding is the generator. */ + const ScrDyn *gen = generator; + const ScrDyn *gen_encoding = generator_encoding; + const ScrDyn *encoding = key_encoding; + if (scr_dyn_truthy(key_encoding) && !sd_is_encoding(key_encoding) && + !(key_encoding->kind == SCR_DYN_STR && ca_str_is(key_encoding->v.str, "buffer"))) { + gen_encoding = generator; + gen = key_encoding; + encoding = scr_dyn_undefined(); + } + ScrBytes *prime = NULL; + if (size_or_key->kind == SCR_DYN_STR) prime = ca_string_bytes(SD_SUBJECT, size_or_key->v.str, encoding); + else if (!size) prime = scr_bytes_buffer_source(size_or_key); + if (!size && !prime) return NULL; + ScrBytes *gen_bytes = NULL; + double gen_number = 2; + ScrDyn *result = NULL; + if (!scr_dyn_truthy(gen)) { + gen_number = 2; + } else if (gen->kind == SCR_DYN_NUM) { + if (!sd_validate_int32("generator", gen->v.num)) goto done; + gen_number = gen->v.num; + } else if (gen->kind == SCR_DYN_STR) { + if (!(gen_bytes = ca_string_bytes(SD_SUBJECT, gen->v.str, gen_encoding))) goto done; + } else if (sd_is_view(gen)) { + gen_bytes = scr_bytes_buffer_source(gen); + } else { + scr_dyn_arg_type_fail("generator", types, gen); + goto done; + } + result = size ? sd_generate(size_or_key->v.num + 0, gen_bytes, gen_number) + : sd_from_prime(prime, gen_bytes, gen_number); +done: + if (prime) scr_bytes_release(prime); + if (gen_bytes) scr_bytes_release(gen_bytes); + return result; +} + +ScrDyn *scr_crypto_dh_new(ScrDyn *size_or_key, ScrDyn *key_encoding, ScrDyn *generator, ScrDyn *generator_encoding) { + ScrDyn *args[4] = {size_or_key, key_encoding, generator, generator_encoding}; + ScrDyn *unboxed[4] = {NULL, NULL, NULL, NULL}; + ScrDyn *result = NULL; + for (size_t i = 0; i < 4; i++) + if (!(unboxed[i] = ca_unbox(args[i]))) goto done; + result = sd_new(unboxed[0], unboxed[1], unboxed[2], unboxed[3]); +done: + for (size_t i = 0; i < 4; i++) scr_dyn_release(unboxed[i]); + return result; +} + +/* new DiffieHellmanGroup(name): names match case-insensitively. */ +ScrDyn *scr_crypto_dh_group_new(ScrDyn *name) { + ScrDyn *unboxed = ca_unbox(name); + if (!unboxed) return NULL; + if (unboxed->kind != SCR_DYN_STR) { + scr_dyn_release(unboxed); + sd_throw(SCR_ERR_TYPE, "ERR_INVALID_ARG_TYPE", "Group name must be a string"); + return NULL; + } + const ScDhGroup *group = NULL; + for (size_t i = 0; i < sizeof sd_groups / sizeof sd_groups[0] && !group; i++) + if (ca_ascii_eq_ci(unboxed->v.str->data, unboxed->v.str->len, sd_groups[i].name)) group = &sd_groups[i]; + scr_dyn_release(unboxed); + if (!group) { + sd_throw(SCR_ERR_ERROR, "ERR_CRYPTO_UNKNOWN_DH_GROUP", "Unknown DH group"); + return NULL; + } + ScDh *d = sd_alloc(); + d->group = group; + if (mbedtls_mpi_read_binary(&d->p, group->p, group->p_len) != 0 || mbedtls_mpi_lset(&d->g, 2) != 0) + scr_trap("scriptc: out of memory\n"); + d->verify_error = 0; + return sd_box(d); +} + +/* ── checkPrime and generatePrime ───────────────────────────────────────── + * crypto.checkPrime(Sync) and generatePrime(Sync) over the primality test + * and prime search above, with random.js's argument checks and ncrypto's + * RandomPrimeConfig and BN_generate_prime_ex2 errors. The callback forms + * compute at once and call back in a later macrotask. */ + +/* The longest candidate the Miller-Rabin rounds take: sd_mont_init's R^2 + * has twice the candidate's limbs, within MBEDTLS_MPI_MAX_LIMBS, and + * mbedtls_mpi_exp_mod stops at MBEDTLS_MPI_MAX_BITS. */ +#if SD_MONT +#define SD_MAX_PRIME_BITS 262144 +#else +#define SD_MAX_PRIME_BITS MBEDTLS_MPI_MAX_BITS +#endif + +static void sd_prime_too_long(const char *what) { + char message[128]; + int n = snprintf(message, sizeof message, "%s longer than %d bits have no native implementation", what, + SD_MAX_PRIME_BITS); + scr_throw_error_msg_code(SCR_ERR_ERROR, message, n < 0 ? 0 : (size_t)n, "SC2020"); +} + +/* An OpenSSL bignum error as a crypto job reports it: an Error with the + * formatted message and no other properties. */ +static void sd_throw_bignum(const char *packed, const char *reason) { + char message[96]; + int n = snprintf(message, sizeof message, "error:%s:bignum routines::%s", packed, reason); + ScrStr *text = scr_str_new(message, n < 0 ? 0 : (size_t)n); + ScrError *e = scr_error_new(SCR_ERR_ERROR, text); + scr_str_release(text); + scr_throw_obj(e, &scr_error_retain_v, &scr_error_release_v, scr_error_trace_arg()); +} + +/* validateObject(value, name): an object that is neither null nor an + * array. */ +static bool sd_validate_object(const char *name, const ScrDyn *value) { + ScrStr *type = scr_dyn_typeof(value); + bool object = type->len == 6 && memcmp(type->data, "object", 6) == 0; + scr_str_release(type); + if (object && value->kind != SCR_DYN_NULL && value->kind != SCR_DYN_ARR) return true; + scr_dyn_arg_type_fail(name, "of type object", value); + return false; +} + +static bool sd_validate_function(const char *name, const ScrDyn *value) { + if (scr_dyn_is_callable(value)) return true; + scr_dyn_arg_type_fail(name, "of type function", value); + return false; +} + +/* options[key] (borrowed), or undefined. */ +static const ScrDyn *sd_option(const ScrDyn *options, const char *key) { + const ScrDyn *value = options->kind == SCR_DYN_OBJ ? scr_dyn_obj_get(options, key, strlen(key)) : NULL; + return value ? value : scr_dyn_undefined(); +} + +static const char sd_large_number_types[] = + "of type bigint or an instance of ArrayBuffer, TypedArray, Buffer, or DataView"; + +/* A LargeNumberLike argument as a bignum: a bigint (unsignedBigIntToBuffer, + * which rejects negative values), or the bytes of an ArrayBuffer or view. + * False with the error pending. */ +static bool sd_large_number(mbedtls_mpi *x, const char *name, bool property, const ScrDyn *value) { + if (value->kind == SCR_DYN_BIGINT) { + ScrStr *text = scr_bigint_to_string(value->v.bigint, 16); + if (!text) return false; + bool negative = text->len > 0 && text->data[0] == '-'; + if (negative) { + scr_str_release(text); + ScrStr *decimal = scr_bigint_to_string(value->v.bigint, 10); + if (!decimal) return false; + /* ERR_OUT_OF_RANGE's received value groups the digits of a bigint + * past 2^32 in threes. */ + ScrBigInt *limit = scr_bigint_from_i64(-((int64_t)1 << 32)); + bool group = scr_bigint_cmp_f64(value->v.bigint, limit) < 0; + scr_bigint_release(limit); + char digits[160]; + size_t len = decimal->len < 120 ? decimal->len : 120, out = 0; + for (size_t i = 0; i < len; i++) { + size_t rest = len - i; + if (group && i > 1 && rest % 3 == 0) digits[out++] = '_'; + digits[out++] = decimal->data[i]; + } + digits[out] = '\0'; + scr_str_release(decimal); + char message[256]; + int n = snprintf(message, sizeof message, "The value of \"%s\" is out of range. It must be >= 0. Received %sn", + name, digits); + scr_throw_error_msg_code(SCR_ERR_RANGE, message, n < 0 ? 0 : (size_t)n, "ERR_OUT_OF_RANGE"); + return false; + } + char *hex = malloc(text->len + 1); + if (!hex) scr_trap("scriptc: out of memory\n"); + memcpy(hex, text->data, text->len); + hex[text->len] = '\0'; + scr_str_release(text); + bool ok = strlen(hex) <= SD_MAX_VALUE_BYTES * 2; + if (ok && mbedtls_mpi_read_string(x, 16, hex) != 0) scr_trap("scriptc: out of memory\n"); + free(hex); + if (!ok) sd_value_too_long(); + return ok; + } + if (!sd_is_view(value)) { + if (property) scr_dyn_prop_type_fail(name, sd_large_number_types, value); + else scr_dyn_arg_type_fail(name, sd_large_number_types, value); + return false; + } + ScrBytes *bytes = scr_bytes_buffer_source(value); + if (!bytes) return false; + bool held = sd_read(x, bytes); + scr_bytes_release(bytes); + if (!held) sd_value_too_long(); + return held; +} + +/* calc_trial_divisions: how many of the first primes (2 included) the + * search divides a candidate of `bits` bits by. */ +static size_t sd_trial_divisions(size_t bits) { + return bits <= 512 ? 64 : bits <= 1024 ? 128 : bits <= 2048 ? 384 : bits <= 4096 ? 1024 : 2048; +} + +static uint64_t sd_word(const mbedtls_mpi *x) { + if (mbedtls_mpi_bitlen(x) > 64) return UINT64_MAX; + unsigned char buf[8]; + if (mbedtls_mpi_write_binary(x, buf, 8) != 0) return UINT64_MAX; + uint64_t w = 0; + for (int i = 0; i < 8; i++) w = w << 8 | buf[i]; + return w; +} + +static bool sd_add_word(mbedtls_mpi *x, uint64_t w) { + unsigned char buf[8]; + for (int i = 7; i >= 0; i--, w >>= 8) buf[i] = (unsigned char)w; + mbedtls_mpi t; + mbedtls_mpi_init(&t); + bool ok = mbedtls_mpi_read_binary(&t, buf, 8) == 0 && mbedtls_mpi_add_mpi(x, x, &t) == 0; + mbedtls_mpi_free(&t); + return ok; +} + +/* probable_prime and bn_probable_prime_dh: a random odd candidate of + * `bits` bits (the top two bits set, or with `add` the top bit, moved to + * rem modulo add), then the first one stepping by 2, by 4 for a safe + * prime, or by add, that no trial-division prime divides, nor for a safe + * prime divides (p - 1) / 2. */ +static bool sd_probable_prime(ScDhSmallPrimes *t, mbedtls_mpi *rnd, size_t bits, bool safe, const mbedtls_mpi *add, + const mbedtls_mpi *rem) { + size_t trials = sd_trial_divisions(bits) - 1; /* odd primes */ + uint64_t step = safe ? 4 : 2; + uint64_t maxdelta = UINT64_MAX - t->primes[trials - 1]; + if (add) { + step = sd_word(add); + if (maxdelta > UINT64_MAX - step) maxdelta = UINT64_MAX - step; + } + mbedtls_mpi r; + mbedtls_mpi_init(&r); + bool ok = true; + for (;;) { + if (!sd_random_bits(rnd, bits, true)) ok = false; + if (ok && !add) { + if ((bits >= 2 && mbedtls_mpi_set_bit(rnd, bits - 2, 1) != 0) || (safe && mbedtls_mpi_set_bit(rnd, 1, 1) != 0)) + ok = false; + } else if (ok) { + ok = mbedtls_mpi_mod_mpi(&r, rnd, add) == 0 && mbedtls_mpi_sub_mpi(rnd, rnd, &r) == 0 && + (rem ? mbedtls_mpi_add_mpi(rnd, rnd, rem) == 0 : mbedtls_mpi_add_int(rnd, rnd, safe ? 3 : 1) == 0); + if (ok && (mbedtls_mpi_bitlen(rnd) < bits || sd_word(rnd) < (safe ? 5u : 3u))) + ok = mbedtls_mpi_add_mpi(rnd, rnd, add) == 0; + } + if (!ok || !sd_small_residues(t, rnd)) { + ok = false; + break; + } + uint64_t delta = 0, word = sd_word(rnd); + bool again = false; + for (size_t i = 0; i < trials && !again;) { + uint64_t prime = t->primes[i]; + /* A one-word candidate needs only the primes up to its square root. */ + if (bits <= 31 && delta <= 0x7fffffff && prime * prime > word + delta) break; + uint64_t m = (t->mods[i] + delta) % prime; + if (safe ? m <= 1 : m == 0) { + delta += step; + if (delta > maxdelta) again = true; + i = 0; + continue; + } + i++; + } + if (again) continue; + if (!sd_add_word(rnd, delta)) { + ok = false; + break; + } + if (add || mbedtls_mpi_bitlen(rnd) == bits) break; + } + mbedtls_mpi_free(&r); + return ok; +} + +/* BN_generate_prime_ex2(p, bits, safe, add, rem): false with OpenSSL's + * error pending. Long safe primes without add take the sieve over + * p ≡ 11 (mod 12), which every safe prime above 7 satisfies. */ +static bool sd_generate_prime(mbedtls_mpi *p, size_t bits, bool safe, const mbedtls_mpi *add, const mbedtls_mpi *rem) { + if (bits < 2 || (!add && safe && bits < 6 && bits != 3)) { + sd_throw_bignum("01800076", "bits too small"); + return false; + } + if (add && mbedtls_mpi_cmp_int(add, 0) == 0) { + sd_throw_bignum("01800067", "div by zero"); + return false; + } + if (!add && safe && bits >= 64) { + if (!sd_generate_safe_prime(p, bits, 12, 11, true)) scr_trap("scriptc: out of memory\n"); + return true; + } + ScDhSmallPrimes *t = sd_small_primes(); + mbedtls_mpi q; + mbedtls_mpi_init(&q); + bool found = false; + while (!found) { + if (!sd_probable_prime(t, p, bits, safe, add, rem)) scr_trap("scriptc: out of memory\n"); + if (!safe) { + found = sd_is_prime(t, p); + } else { + if (mbedtls_mpi_copy(&q, p) != 0 || mbedtls_mpi_shift_r(&q, 1) != 0) scr_trap("scriptc: out of memory\n"); + found = sd_is_prime(t, p) && sd_is_prime(t, &q); + } + } + mbedtls_mpi_free(&q); + free(t); + return true; +} + +/* checkPrime's arguments after the candidate: Node's options and checks + * rules. -1 with the error pending. */ +static int sd_prime_checks(const ScrDyn *options) { + if (!sd_validate_object("options", options)) return -1; + const ScrDyn *checks = sd_option(options, "checks"); + if (checks->kind == SCR_DYN_UNDEF) return 0; + if (checks->kind != SCR_DYN_NUM) { + scr_dyn_prop_type_fail("options.checks", "of type number", checks); + return -1; + } + if (!sd_validate_int32_min("options.checks", checks->v.num, 0)) return -1; + return (int)checks->v.num; +} + +/* The candidate's verdict as a boolean dyn (+1), or NULL with the error + * pending. */ +static ScrDyn *sd_check_prime(const mbedtls_mpi *candidate, int checks) { + if (mbedtls_mpi_bitlen(candidate) > SD_MAX_PRIME_BITS) { + sd_prime_too_long("checkPrime candidates"); + return NULL; + } + ScDhSmallPrimes *t = sd_small_primes(); + bool prime = sd_is_prime_checks(t, candidate, checks); + free(t); + return scr_dyn_new_bool(prime); +} + +/* A borrowed, unboxed view of each argument (+1 each); false after a + * throw. */ +static bool sd_unbox_all(ScrDyn **out, ScrDyn *const *args, size_t count) { + for (size_t i = 0; i < count; i++) out[i] = NULL; + for (size_t i = 0; i < count; i++) { + ScrDyn *value = args[i]; + out[i] = value->kind == SCR_DYN_TYPED_REF ? scr_dyn_typed_ref_materialize(value) : scr_dyn_retain(value); + if (!out[i]) return false; + } + return true; +} + +static void sd_release_all(ScrDyn **values, size_t count) { + for (size_t i = 0; i < count; i++) + if (values[i]) scr_dyn_release(values[i]); +} + +/* checkPrimeSync(candidate[, options]). */ +ScrDyn *scr_crypto_check_prime_sync(ScrDyn *candidate, ScrDyn *options) { + ScrDyn *args[2] = {candidate, options}; + ScrDyn *v[2]; + ScrDyn *result = NULL; + mbedtls_mpi x; + mbedtls_mpi_init(&x); + if (sd_unbox_all(v, args, 2) && sd_large_number(&x, "candidate", false, v[0])) { + ScrDyn *empty = v[1]->kind == SCR_DYN_UNDEF ? scr_dyn_new_obj() : NULL; + int checks = sd_prime_checks(empty ? empty : v[1]); + if (empty) scr_dyn_release(empty); + if (checks >= 0) result = sd_check_prime(&x, checks); + } + mbedtls_mpi_free(&x); + sd_release_all(v, 2); + return result; +} + +/* A deferred callback(...args): the callback and its one or two + * arguments, each in a box. */ +static void sd_deliver(ScrClosure *self) { + ScrDyn *callback = (ScrDyn *)scr_box_get_ref(self->caps[0]); + size_t argc = self->ncaps - 1; + ScrDyn *args[2] = {NULL, NULL}; + for (size_t i = 0; i < argc; i++) args[i] = (ScrDyn *)scr_box_get_ref(self->caps[i + 1]); + ScrDyn *result = scr_dyn_call(callback, args, argc, "callback"); + if (result) scr_dyn_release(result); + for (size_t i = 0; i < argc; i++) scr_dyn_release(args[i]); + scr_dyn_release(callback); +} + +/* Calls callback(first[, second]) in a later macrotask (moves first and + * second). */ +static void sd_defer(const ScrDyn *callback, ScrDyn *first, ScrDyn *second) { + ScrDyn *values[3] = {scr_dyn_retain((ScrDyn *)callback), first, second}; + size_t count = second ? 3 : 2; + ScrClosure *c = scr_closure_new((void *)sd_deliver, count); + for (size_t i = 0; i < count; i++) { + c->caps[i] = scr_box_new_obj(scr_dyn_retain_v, scr_dyn_release_v, NULL); + scr_box_set_ref(c->caps[i], values[i]); + } + scr_loop_defer(c); +} + +/* The callback's error argument on success (+1): Node's undefined, or + * null for a callback typed to take Error | null. */ +static ScrDyn *sd_no_error(bool null_error) { + return null_error ? scr_dyn_new_null() : scr_dyn_retain((ScrDyn *)scr_dyn_undefined()); +} + +/* The pending error, taken as a dyn value (+1). */ +static ScrDyn *sd_take_error(void) { + ScrCaught *caught = scr_exc_take(); + ScrDyn *error = scr_caught_to_dyn(caught); + scr_caught_release(caught); + return error; +} + +/* checkPrime(candidate[, options], callback): argument errors throw; the + * callback receives (undefined, verdict) in a later macrotask (null for + * undefined when `null_error`). */ +void scr_crypto_check_prime_async(ScrDyn *candidate, ScrDyn *options, ScrDyn *callback, bool null_error) { + ScrDyn *args[3] = {candidate, options, callback}; + ScrDyn *v[3]; + mbedtls_mpi x; + mbedtls_mpi_init(&x); + if (sd_unbox_all(v, args, 3) && sd_large_number(&x, "candidate", false, v[0])) { + ScrDyn *bag = v[1], *fn = v[2]; + ScrDyn *empty = NULL; + if (scr_dyn_is_callable(bag)) { + fn = bag; + bag = empty = scr_dyn_new_obj(); + } else if (bag->kind == SCR_DYN_UNDEF) { + bag = empty = scr_dyn_new_obj(); + } + int checks = sd_validate_function("callback", fn) ? sd_prime_checks(bag) : -1; + ScrDyn *verdict = checks >= 0 ? sd_check_prime(&x, checks) : NULL; + if (verdict) sd_defer(fn, sd_no_error(null_error), verdict); + if (empty) scr_dyn_release(empty); + } + mbedtls_mpi_free(&x); + sd_release_all(v, 3); +} + +/* RandomPrimeConfig: the size and options checks, then the add and rem + * bounds. False with the error pending. */ +typedef struct { + size_t bits; + bool safe, bigint, has_add, has_rem; + mbedtls_mpi add, rem; +} ScDhPrimeConfig; + +static void sd_prime_config_free(ScDhPrimeConfig *c) { + mbedtls_mpi_free(&c->add); + mbedtls_mpi_free(&c->rem); +} + +static bool sd_prime_size(const ScrDyn *size) { + if (size->kind != SCR_DYN_NUM) { + scr_dyn_arg_type_fail("size", "of type number", size); + return false; + } + return sd_validate_int32_min("size", size->v.num, 1); +} + +static bool sd_prime_config(ScDhPrimeConfig *c, double size, const ScrDyn *options) { + if (!sd_validate_object("options", options)) return false; + const ScrDyn *safe = sd_option(options, "safe"); + const ScrDyn *bigint = sd_option(options, "bigint"); + const ScrDyn *add = sd_option(options, "add"); + const ScrDyn *rem = sd_option(options, "rem"); + if (safe->kind != SCR_DYN_UNDEF && safe->kind != SCR_DYN_BOOL) { + scr_dyn_prop_type_fail("options.safe", "of type boolean", safe); + return false; + } + if (bigint->kind != SCR_DYN_UNDEF && bigint->kind != SCR_DYN_BOOL) { + scr_dyn_prop_type_fail("options.bigint", "of type boolean", bigint); + return false; + } + c->safe = safe->kind == SCR_DYN_BOOL && safe->v.b; + c->bigint = bigint->kind == SCR_DYN_BOOL && bigint->v.b; + c->has_add = add->kind != SCR_DYN_UNDEF; + c->has_rem = rem->kind != SCR_DYN_UNDEF; + if (c->has_add && !sd_large_number(&c->add, "options.add", true, add)) return false; + if (c->has_rem && !sd_large_number(&c->rem, "options.rem", true, rem)) return false; + c->bits = (size_t)size; + if (c->has_add) { + if (mbedtls_mpi_bitlen(&c->add) > c->bits) { + sd_throw(SCR_ERR_RANGE, "ERR_OUT_OF_RANGE", "invalid options.add"); + return false; + } + if (c->has_rem && mbedtls_mpi_cmp_mpi(&c->add, &c->rem) <= 0) { + sd_throw(SCR_ERR_RANGE, "ERR_OUT_OF_RANGE", "invalid options.rem"); + return false; + } + } + if (c->bits > SD_MAX_PRIME_BITS) { + sd_prime_too_long("generatePrime sizes"); + return false; + } + return true; +} + +/* The generated prime as an ArrayBuffer of its bytes, or a bigint (+1), + * or NULL with OpenSSL's error pending. */ +static ScrDyn *sd_prime_result(ScDhPrimeConfig *c) { + mbedtls_mpi p; + mbedtls_mpi_init(&p); + ScrDyn *result = NULL; + if (sd_generate_prime(&p, c->bits, c->safe, c->has_add ? &c->add : NULL, c->has_add && c->has_rem ? &c->rem : NULL)) { + if (c->bigint) { + size_t len = 0; + mbedtls_mpi_write_string(&p, 16, NULL, 0, &len); + char *hex = malloc(len + 2); + if (!hex) scr_trap("scriptc: out of memory\n"); + hex[0] = '0'; + hex[1] = 'x'; + if (mbedtls_mpi_write_string(&p, 16, hex + 2, len, &len) != 0) scr_trap("scriptc: out of memory\n"); + ScrStr *text = scr_str_new(hex, strlen(hex)); + free(hex); + ScrBigInt *value = scr_bigint_parse(text); + scr_str_release(text); + if (value) { + result = scr_dyn_new_bigint(value); + scr_bigint_release(value); + } + } else { + size_t len = mbedtls_mpi_size(&p); + ScrBytes *bytes = scr_bytes_new(SCR_BYTES_U8, (double)len); + if (mbedtls_mpi_write_binary(&p, bytes->data, len) != 0) scr_trap("scriptc: out of memory\n"); + result = scr_array_buffer_from_bytes(bytes); + scr_bytes_release(bytes); + } + } + mbedtls_mpi_free(&p); + return result; +} + +/* generatePrimeSync(size[, options]). */ +ScrDyn *scr_crypto_generate_prime_sync(ScrDyn *size, ScrDyn *options) { + ScrDyn *args[2] = {size, options}; + ScrDyn *v[2]; + ScrDyn *result = NULL; + ScDhPrimeConfig c = {0}; + mbedtls_mpi_init(&c.add); + mbedtls_mpi_init(&c.rem); + if (sd_unbox_all(v, args, 2) && sd_prime_size(v[0])) { + ScrDyn *empty = v[1]->kind == SCR_DYN_UNDEF ? scr_dyn_new_obj() : NULL; + if (sd_prime_config(&c, v[0]->v.num, empty ? empty : v[1])) result = sd_prime_result(&c); + if (empty) scr_dyn_release(empty); + } + sd_prime_config_free(&c); + sd_release_all(v, 2); + return result; +} + +/* generatePrime(size[, options], callback): argument errors throw; the + * callback receives (undefined, prime), or OpenSSL's error alone, in a + * later macrotask (null for undefined when `null_error`). */ +void scr_crypto_generate_prime_async(ScrDyn *size, ScrDyn *options, ScrDyn *callback, bool null_error) { + ScrDyn *args[3] = {size, options, callback}; + ScrDyn *v[3]; + ScDhPrimeConfig c = {0}; + mbedtls_mpi_init(&c.add); + mbedtls_mpi_init(&c.rem); + if (sd_unbox_all(v, args, 3) && sd_prime_size(v[0])) { + ScrDyn *bag = v[1], *fn = v[2]; + ScrDyn *empty = NULL; + if (scr_dyn_is_callable(bag)) { + fn = bag; + bag = empty = scr_dyn_new_obj(); + } + if (sd_validate_function("callback", fn) && sd_prime_config(&c, v[0]->v.num, bag)) { + ScrDyn *prime = sd_prime_result(&c); + if (prime) sd_defer(fn, sd_no_error(null_error), prime); + else sd_defer(fn, sd_take_error(), NULL); + } + if (empty) scr_dyn_release(empty); + } + sd_prime_config_free(&c); + sd_release_all(v, 3); +} + +/* ── methods ──────────────────────────────────────────────────────────── */ + +static ScrDyn *sd_arg(ScrDyn *const *args, size_t argc, size_t index) { + return index < argc ? args[index] : scr_dyn_undefined(); +} + +/* The minimal big-endian bytes of a value, as BignumPointer::Encode + * writes them (none for 0), passed through encode(). */ +static ScrDyn *sd_encode_value(const mbedtls_mpi *value, const ScrDyn *encoding) { + size_t len = mbedtls_mpi_size(value); + ScrBytes *out = scr_bytes_new(SCR_BYTES_U8, (double)len); + if (mbedtls_mpi_write_binary(value, out->data, len) != 0) scr_trap("scriptc: out of memory\n"); + return ca_encode(SD_SUBJECT, out, encoding); +} + +/* A new private key for generateKeys(). */ +static bool sd_new_private_key(ScDh *d, mbedtls_mpi *priv) { + if (d->group) { + if (d->group->private_bits) return sd_random_bits(priv, (size_t)d->group->private_bits, false); + /* BN_priv_rand_range(p - 3) + 2. */ + mbedtls_mpi bound; + mbedtls_mpi_init(&bound); + bool ok = mbedtls_mpi_sub_int(&bound, &d->p, 1) == 0 && mbedtls_mpi_random(priv, 2, &bound, sd_random, NULL) == 0; + mbedtls_mpi_free(&bound); + return ok; + } + size_t bits = mbedtls_mpi_bitlen(&d->p); + if (bits < SD_MIN_MODULUS_BITS || bits > SD_MAX_MODULUS_BITS) return false; + if (!d->named) return sd_random_bits(priv, bits - 2, false); + /* ossl_ffc_generate_private_key: 1 <= priv < min(2^N, q). */ + mbedtls_mpi bound, q; + mbedtls_mpi_init(&bound); + mbedtls_mpi_init(&q); + bool ok = mbedtls_mpi_lset(&bound, 1) == 0 && mbedtls_mpi_shift_l(&bound, (size_t)d->named->keylength) == 0; + if (ok && d->named->q) ok = mbedtls_mpi_read_binary(&q, d->named->q, d->named->q_len) == 0; + else if (ok) ok = mbedtls_mpi_copy(&q, &d->p) == 0 && mbedtls_mpi_shift_r(&q, 1) == 0; + if (ok && mbedtls_mpi_cmp_mpi(&q, &bound) < 0) ok = mbedtls_mpi_copy(&bound, &q) == 0; + ok = ok && mbedtls_mpi_random(priv, 1, &bound, sd_random, NULL) == 0; + mbedtls_mpi_free(&bound); + mbedtls_mpi_free(&q); + return ok; +} + +/* generateKeys(encoding): a new key pair, or the public key of the + * private key the object has. */ +static ScrDyn *sd_generate_keys(ScDh *d, const ScrDyn *encoding) { + mbedtls_mpi priv, pub; + mbedtls_mpi_init(&priv); + mbedtls_mpi_init(&pub); + bool fresh = !d->has_priv; + bool ok = fresh ? sd_new_private_key(d, &priv) : mbedtls_mpi_copy(&priv, &d->priv) == 0; + ok = ok && sd_exp(&pub, &d->g, &priv, &d->p); + if (ok) { + mbedtls_mpi_swap(&d->priv, &priv); + mbedtls_mpi_swap(&d->pub, &pub); + d->has_priv = true; + d->has_pub = true; + } + mbedtls_mpi_free(&priv); + mbedtls_mpi_free(&pub); + if (!ok || mbedtls_mpi_cmp_int(&d->pub, 0) == 0) { + sd_operation_failed("Key generation failed"); + return NULL; + } + return sd_encode_value(&d->pub, encoding); +} + +/* Whether OpenSSL's DH derivation accepts explicit parameters' private + * key: a nonzero key and a 512- to 10000-bit prime. */ +static bool sd_derives(const ScDh *d) { + size_t bits = mbedtls_mpi_bitlen(&d->p); + return mbedtls_mpi_cmp_int(&d->priv, 0) != 0 && bits >= SD_MIN_MODULUS_BITS && bits <= SD_MAX_MODULUS_BITS; +} + +/* DH_check_pub_key's subgroup test: under a named group with its own q + * (the RFC 5114 groups), a peer key must have key^q mod p = 1. The safe + * prime groups' q is (p - 1) / 2, which OpenSSL does not test. */ +static bool sd_in_subgroup(const ScDh *d, const mbedtls_mpi *peer) { + if (!d->named || !d->named->q) return true; + mbedtls_mpi q, r; + mbedtls_mpi_init(&q); + mbedtls_mpi_init(&r); + if (mbedtls_mpi_read_binary(&q, d->named->q, d->named->q_len) != 0 || !sd_exp(&r, peer, &q, &d->p)) + scr_trap("scriptc: out of memory\n"); + bool in = mbedtls_mpi_cmp_int(&r, 1) == 0; + mbedtls_mpi_free(&q); + mbedtls_mpi_free(&r); + return in; +} + +/* computeSecret(key, inputEncoding, outputEncoding): key^priv mod p, + * padded to the prime's length. A key outside [2, p - 2], or outside a + * named group's order-q subgroup, is rejected as DH_check_pub_key does. + * A secret of 0, 1 or p - 1 is predictable (a peer key of small order, + * or a composite modulus), so it fails like OpenSSL's derivation does. */ +static ScrDyn *sd_compute_secret(ScDh *d, const ScrDyn *key, const ScrDyn *in_enc, const ScrDyn *out_enc) { + ScrBytes *bytes = ca_key_bytes(SD_SUBJECT, key, in_enc); + if (!bytes) return NULL; + ScrDyn *result = NULL; + mbedtls_mpi peer, bound, secret; + mbedtls_mpi_init(&peer); + mbedtls_mpi_init(&bound); + mbedtls_mpi_init(&secret); + /* A key longer than p, past its leading zeros, is above p - 1. */ + const unsigned char *data = bytes->data; + size_t len = bytes->len; + sd_strip(&data, &len); + bool longer = len > mbedtls_mpi_size(&d->p); + if ((!longer && mbedtls_mpi_read_binary(&peer, data, len) != 0) || mbedtls_mpi_sub_int(&bound, &d->p, 1) != 0) + scr_trap("scriptc: out of memory\n"); + if (!longer && mbedtls_mpi_cmp_int(&peer, 1) <= 0) { + sd_throw(SCR_ERR_RANGE, "ERR_CRYPTO_INVALID_KEYLEN", "Supplied key is too small"); + } else if (longer || mbedtls_mpi_cmp_mpi(&peer, &bound) >= 0) { + sd_throw(SCR_ERR_RANGE, "ERR_CRYPTO_INVALID_KEYLEN", "Supplied key is too large"); + } else if (!sd_in_subgroup(d, &peer)) { + sd_throw(SCR_ERR_RANGE, "ERR_CRYPTO_INVALID_KEYTYPE", "Supplied key is invalid"); + } else if (!d->has_priv || (!d->group && !sd_derives(d)) || !sd_exp(&secret, &peer, &d->priv, &d->p) || + mbedtls_mpi_cmp_int(&secret, 1) <= 0 || mbedtls_mpi_cmp_mpi(&secret, &bound) == 0) { + sd_operation_failed("Failed to compute shared secret"); + } else { + size_t len = mbedtls_mpi_size(&d->p); + ScrBytes *out = scr_bytes_new(SCR_BYTES_U8, (double)len); + if (mbedtls_mpi_write_binary(&secret, out->data, len) != 0) scr_trap("scriptc: out of memory\n"); + result = ca_encode(SD_SUBJECT, out, out_enc); + } + mbedtls_mpi_free(&peer); + mbedtls_mpi_free(&bound); + mbedtls_mpi_free(&secret); + scr_bytes_release(bytes); + return result; +} + +static ScrDyn *sd_get_value(const mbedtls_mpi *value, bool present, const char *missing, const ScrDyn *encoding) { + if (!present || mbedtls_mpi_cmp_int(value, 0) == 0) { + sd_throw(SCR_ERR_ERROR, "ERR_CRYPTO_INVALID_STATE", missing); + return NULL; + } + return sd_encode_value(value, encoding); +} + +/* setPublicKey(key, encoding) and setPrivateKey(key, encoding). */ +static ScrDyn *sd_set_value(mbedtls_mpi *value, bool *present, ScrDyn *self, const ScrDyn *key, const ScrDyn *encoding) { + ScrBytes *bytes = ca_key_bytes(SD_SUBJECT, key, encoding); + if (!bytes) return NULL; + bool held = sd_read(value, bytes); + scr_bytes_release(bytes); + if (!held) { + sd_value_too_long(); + return NULL; + } + *present = true; + return scr_dyn_retain(self); +} + +static bool sd_name_is(const char *method, const char *name) { + return strcmp(method, name) == 0; +} + +static ScrDyn *sd_dispatch(ScDh *d, ScrDyn *self, const char *method, ScrDyn *const *args, size_t argc, + const char *what) { + const ScrDyn *a0 = sd_arg(args, argc, 0); + if (sd_name_is(method, "generateKeys")) return sd_generate_keys(d, a0); + if (sd_name_is(method, "computeSecret")) + return sd_compute_secret(d, a0, sd_arg(args, argc, 1), sd_arg(args, argc, 2)); + if (sd_name_is(method, "getPrime")) return sd_get_value(&d->p, true, "p is null", a0); + if (sd_name_is(method, "getGenerator")) return sd_get_value(&d->g, true, "g is null", a0); + if (sd_name_is(method, "getPublicKey")) + return sd_get_value(&d->pub, d->has_pub, "No public key - did you forget to generate one?", a0); + if (sd_name_is(method, "getPrivateKey")) + return sd_get_value(&d->priv, d->has_priv, "No private key - did you forget to generate one?", a0); + /* DiffieHellmanGroup has no key setters. */ + if (!d->group && sd_name_is(method, "setPublicKey")) + return sd_set_value(&d->pub, &d->has_pub, self, a0, sd_arg(args, argc, 1)); + if (!d->group && sd_name_is(method, "setPrivateKey")) + return sd_set_value(&d->priv, &d->has_priv, self, a0, sd_arg(args, argc, 1)); + /* The Object.prototype methods a DiffieHellman inherits. */ + static const char *const inherited[] = {"constructor", "hasOwnProperty", "isPrototypeOf", + "propertyIsEnumerable", "toLocaleString", "toString", + "valueOf"}; + for (size_t i = 0; i < sizeof inherited / sizeof inherited[0]; i++) + if (sd_name_is(method, inherited[i])) { + static const char message[] = "DiffieHellman's Object.prototype methods have no lowering"; + scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); + return NULL; + } + ScrJsonBuf b; + scr_jb_init(&b); + scr_jb_puts(&b, what); + scr_jb_puts(&b, " is not a function"); + scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&b)); + return NULL; +} + +/* Every method reads at most three arguments. */ +static ScrDyn *sd_invoke(void *h, ScrDyn *self, const char *method, ScrDyn *const *args, size_t argc, + const char *what) { + ScrDyn *unboxed[3] = {NULL, NULL, NULL}; + size_t count = argc < 3 ? argc : 3; + ScrDyn *result = NULL; + for (size_t i = 0; i < count; i++) + if (!(unboxed[i] = ca_unbox(args[i]))) goto done; + result = sd_dispatch(h, self, method, unboxed, count, what); +done: + for (size_t i = 0; i < count; i++) scr_dyn_release(unboxed[i]); + return result; +} + +/* verifyError, the one own data property. */ +static ScrDyn *sd_get(void *h, const char *key, size_t key_len) { + ScDh *d = h; + if (key_len == 11 && memcmp(key, "verifyError", 11) == 0) { + if (d->verify_error < 0) d->verify_error = sd_check_params(&d->p, &d->g); + return scr_dyn_new_num(d->verify_error); + } + static const char message[] = + "DiffieHellman property reads other than verifyError have no lowering; call generateKeys(), " + "computeSecret(), getPrime(), getGenerator(), getPublicKey(), getPrivateKey(), setPublicKey() or " + "setPrivateKey()"; + scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); + return NULL; +} + +static bool sd_set(void *h, const char *key, size_t key_len, const ScrDyn *value) { + (void)h; (void)key; (void)key_len; (void)value; + static const char message[] = "Writing DiffieHellman properties has no lowering"; + scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); + return true; +} + +static ScrDynHandleOps sd_ops = {"DiffieHellman", sd_retain, sd_release, sd_invoke, sd_get, sd_set, NULL, NULL, NULL}; +static ScrDynHandleOps sd_group_ops = {"DiffieHellmanGroup", sd_retain, sd_release, sd_invoke, sd_get, sd_set, + NULL, NULL, NULL}; diff --git a/packages/runtime/src/scr_dh_groups.h b/packages/runtime/src/scr_dh_groups.h new file mode 100644 index 000000000..c82ec7270 --- /dev/null +++ b/packages/runtime/src/scr_dh_groups.h @@ -0,0 +1,557 @@ +/* Diffie-Hellman group parameters, big-endian: the primes behind Node 24's + * crypto.getDiffieHellman groups (modp1 and modp2 from RFC 2409, modp5 and + * modp14 through modp18 from RFC 3526), and the RFC 7919 (ffdhe2048 through + * ffdhe8192) and RFC 5114 (dh_1024_160, dh_2048_224 and dh_2048_256) + * parameters OpenSSL 3.5 lists among its DH named groups + * (crypto/ffc/ffc_dh.c). Every group but RFC 5114's uses the generator 2 + * and a safe prime. */ +#ifndef SCR_DH_GROUPS_H +#define SCR_DH_GROUPS_H + +static const unsigned char scr_dh_modp1_p[96] = { + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc9, 0x0f, 0xda, 0xa2, 0x21, 0x68, 0xc2, 0x34, + 0xc4, 0xc6, 0x62, 0x8b, 0x80, 0xdc, 0x1c, 0xd1, 0x29, 0x02, 0x4e, 0x08, 0x8a, 0x67, 0xcc, 0x74, + 0x02, 0x0b, 0xbe, 0xa6, 0x3b, 0x13, 0x9b, 0x22, 0x51, 0x4a, 0x08, 0x79, 0x8e, 0x34, 0x04, 0xdd, + 0xef, 0x95, 0x19, 0xb3, 0xcd, 0x3a, 0x43, 0x1b, 0x30, 0x2b, 0x0a, 0x6d, 0xf2, 0x5f, 0x14, 0x37, + 0x4f, 0xe1, 0x35, 0x6d, 0x6d, 0x51, 0xc2, 0x45, 0xe4, 0x85, 0xb5, 0x76, 0x62, 0x5e, 0x7e, 0xc6, + 0xf4, 0x4c, 0x42, 0xe9, 0xa6, 0x3a, 0x36, 0x20, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, +}; + +static const unsigned char scr_dh_modp2_p[128] = { + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc9, 0x0f, 0xda, 0xa2, 0x21, 0x68, 0xc2, 0x34, + 0xc4, 0xc6, 0x62, 0x8b, 0x80, 0xdc, 0x1c, 0xd1, 0x29, 0x02, 0x4e, 0x08, 0x8a, 0x67, 0xcc, 0x74, + 0x02, 0x0b, 0xbe, 0xa6, 0x3b, 0x13, 0x9b, 0x22, 0x51, 0x4a, 0x08, 0x79, 0x8e, 0x34, 0x04, 0xdd, + 0xef, 0x95, 0x19, 0xb3, 0xcd, 0x3a, 0x43, 0x1b, 0x30, 0x2b, 0x0a, 0x6d, 0xf2, 0x5f, 0x14, 0x37, + 0x4f, 0xe1, 0x35, 0x6d, 0x6d, 0x51, 0xc2, 0x45, 0xe4, 0x85, 0xb5, 0x76, 0x62, 0x5e, 0x7e, 0xc6, + 0xf4, 0x4c, 0x42, 0xe9, 0xa6, 0x37, 0xed, 0x6b, 0x0b, 0xff, 0x5c, 0xb6, 0xf4, 0x06, 0xb7, 0xed, + 0xee, 0x38, 0x6b, 0xfb, 0x5a, 0x89, 0x9f, 0xa5, 0xae, 0x9f, 0x24, 0x11, 0x7c, 0x4b, 0x1f, 0xe6, + 0x49, 0x28, 0x66, 0x51, 0xec, 0xe6, 0x53, 0x81, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, +}; + +static const unsigned char scr_dh_modp5_p[192] = { + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc9, 0x0f, 0xda, 0xa2, 0x21, 0x68, 0xc2, 0x34, + 0xc4, 0xc6, 0x62, 0x8b, 0x80, 0xdc, 0x1c, 0xd1, 0x29, 0x02, 0x4e, 0x08, 0x8a, 0x67, 0xcc, 0x74, + 0x02, 0x0b, 0xbe, 0xa6, 0x3b, 0x13, 0x9b, 0x22, 0x51, 0x4a, 0x08, 0x79, 0x8e, 0x34, 0x04, 0xdd, + 0xef, 0x95, 0x19, 0xb3, 0xcd, 0x3a, 0x43, 0x1b, 0x30, 0x2b, 0x0a, 0x6d, 0xf2, 0x5f, 0x14, 0x37, + 0x4f, 0xe1, 0x35, 0x6d, 0x6d, 0x51, 0xc2, 0x45, 0xe4, 0x85, 0xb5, 0x76, 0x62, 0x5e, 0x7e, 0xc6, + 0xf4, 0x4c, 0x42, 0xe9, 0xa6, 0x37, 0xed, 0x6b, 0x0b, 0xff, 0x5c, 0xb6, 0xf4, 0x06, 0xb7, 0xed, + 0xee, 0x38, 0x6b, 0xfb, 0x5a, 0x89, 0x9f, 0xa5, 0xae, 0x9f, 0x24, 0x11, 0x7c, 0x4b, 0x1f, 0xe6, + 0x49, 0x28, 0x66, 0x51, 0xec, 0xe4, 0x5b, 0x3d, 0xc2, 0x00, 0x7c, 0xb8, 0xa1, 0x63, 0xbf, 0x05, + 0x98, 0xda, 0x48, 0x36, 0x1c, 0x55, 0xd3, 0x9a, 0x69, 0x16, 0x3f, 0xa8, 0xfd, 0x24, 0xcf, 0x5f, + 0x83, 0x65, 0x5d, 0x23, 0xdc, 0xa3, 0xad, 0x96, 0x1c, 0x62, 0xf3, 0x56, 0x20, 0x85, 0x52, 0xbb, + 0x9e, 0xd5, 0x29, 0x07, 0x70, 0x96, 0x96, 0x6d, 0x67, 0x0c, 0x35, 0x4e, 0x4a, 0xbc, 0x98, 0x04, + 0xf1, 0x74, 0x6c, 0x08, 0xca, 0x23, 0x73, 0x27, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, +}; + +static const unsigned char scr_dh_modp14_p[256] = { + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc9, 0x0f, 0xda, 0xa2, 0x21, 0x68, 0xc2, 0x34, + 0xc4, 0xc6, 0x62, 0x8b, 0x80, 0xdc, 0x1c, 0xd1, 0x29, 0x02, 0x4e, 0x08, 0x8a, 0x67, 0xcc, 0x74, + 0x02, 0x0b, 0xbe, 0xa6, 0x3b, 0x13, 0x9b, 0x22, 0x51, 0x4a, 0x08, 0x79, 0x8e, 0x34, 0x04, 0xdd, + 0xef, 0x95, 0x19, 0xb3, 0xcd, 0x3a, 0x43, 0x1b, 0x30, 0x2b, 0x0a, 0x6d, 0xf2, 0x5f, 0x14, 0x37, + 0x4f, 0xe1, 0x35, 0x6d, 0x6d, 0x51, 0xc2, 0x45, 0xe4, 0x85, 0xb5, 0x76, 0x62, 0x5e, 0x7e, 0xc6, + 0xf4, 0x4c, 0x42, 0xe9, 0xa6, 0x37, 0xed, 0x6b, 0x0b, 0xff, 0x5c, 0xb6, 0xf4, 0x06, 0xb7, 0xed, + 0xee, 0x38, 0x6b, 0xfb, 0x5a, 0x89, 0x9f, 0xa5, 0xae, 0x9f, 0x24, 0x11, 0x7c, 0x4b, 0x1f, 0xe6, + 0x49, 0x28, 0x66, 0x51, 0xec, 0xe4, 0x5b, 0x3d, 0xc2, 0x00, 0x7c, 0xb8, 0xa1, 0x63, 0xbf, 0x05, + 0x98, 0xda, 0x48, 0x36, 0x1c, 0x55, 0xd3, 0x9a, 0x69, 0x16, 0x3f, 0xa8, 0xfd, 0x24, 0xcf, 0x5f, + 0x83, 0x65, 0x5d, 0x23, 0xdc, 0xa3, 0xad, 0x96, 0x1c, 0x62, 0xf3, 0x56, 0x20, 0x85, 0x52, 0xbb, + 0x9e, 0xd5, 0x29, 0x07, 0x70, 0x96, 0x96, 0x6d, 0x67, 0x0c, 0x35, 0x4e, 0x4a, 0xbc, 0x98, 0x04, + 0xf1, 0x74, 0x6c, 0x08, 0xca, 0x18, 0x21, 0x7c, 0x32, 0x90, 0x5e, 0x46, 0x2e, 0x36, 0xce, 0x3b, + 0xe3, 0x9e, 0x77, 0x2c, 0x18, 0x0e, 0x86, 0x03, 0x9b, 0x27, 0x83, 0xa2, 0xec, 0x07, 0xa2, 0x8f, + 0xb5, 0xc5, 0x5d, 0xf0, 0x6f, 0x4c, 0x52, 0xc9, 0xde, 0x2b, 0xcb, 0xf6, 0x95, 0x58, 0x17, 0x18, + 0x39, 0x95, 0x49, 0x7c, 0xea, 0x95, 0x6a, 0xe5, 0x15, 0xd2, 0x26, 0x18, 0x98, 0xfa, 0x05, 0x10, + 0x15, 0x72, 0x8e, 0x5a, 0x8a, 0xac, 0xaa, 0x68, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, +}; + +static const unsigned char scr_dh_modp15_p[384] = { + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc9, 0x0f, 0xda, 0xa2, 0x21, 0x68, 0xc2, 0x34, + 0xc4, 0xc6, 0x62, 0x8b, 0x80, 0xdc, 0x1c, 0xd1, 0x29, 0x02, 0x4e, 0x08, 0x8a, 0x67, 0xcc, 0x74, + 0x02, 0x0b, 0xbe, 0xa6, 0x3b, 0x13, 0x9b, 0x22, 0x51, 0x4a, 0x08, 0x79, 0x8e, 0x34, 0x04, 0xdd, + 0xef, 0x95, 0x19, 0xb3, 0xcd, 0x3a, 0x43, 0x1b, 0x30, 0x2b, 0x0a, 0x6d, 0xf2, 0x5f, 0x14, 0x37, + 0x4f, 0xe1, 0x35, 0x6d, 0x6d, 0x51, 0xc2, 0x45, 0xe4, 0x85, 0xb5, 0x76, 0x62, 0x5e, 0x7e, 0xc6, + 0xf4, 0x4c, 0x42, 0xe9, 0xa6, 0x37, 0xed, 0x6b, 0x0b, 0xff, 0x5c, 0xb6, 0xf4, 0x06, 0xb7, 0xed, + 0xee, 0x38, 0x6b, 0xfb, 0x5a, 0x89, 0x9f, 0xa5, 0xae, 0x9f, 0x24, 0x11, 0x7c, 0x4b, 0x1f, 0xe6, + 0x49, 0x28, 0x66, 0x51, 0xec, 0xe4, 0x5b, 0x3d, 0xc2, 0x00, 0x7c, 0xb8, 0xa1, 0x63, 0xbf, 0x05, + 0x98, 0xda, 0x48, 0x36, 0x1c, 0x55, 0xd3, 0x9a, 0x69, 0x16, 0x3f, 0xa8, 0xfd, 0x24, 0xcf, 0x5f, + 0x83, 0x65, 0x5d, 0x23, 0xdc, 0xa3, 0xad, 0x96, 0x1c, 0x62, 0xf3, 0x56, 0x20, 0x85, 0x52, 0xbb, + 0x9e, 0xd5, 0x29, 0x07, 0x70, 0x96, 0x96, 0x6d, 0x67, 0x0c, 0x35, 0x4e, 0x4a, 0xbc, 0x98, 0x04, + 0xf1, 0x74, 0x6c, 0x08, 0xca, 0x18, 0x21, 0x7c, 0x32, 0x90, 0x5e, 0x46, 0x2e, 0x36, 0xce, 0x3b, + 0xe3, 0x9e, 0x77, 0x2c, 0x18, 0x0e, 0x86, 0x03, 0x9b, 0x27, 0x83, 0xa2, 0xec, 0x07, 0xa2, 0x8f, + 0xb5, 0xc5, 0x5d, 0xf0, 0x6f, 0x4c, 0x52, 0xc9, 0xde, 0x2b, 0xcb, 0xf6, 0x95, 0x58, 0x17, 0x18, + 0x39, 0x95, 0x49, 0x7c, 0xea, 0x95, 0x6a, 0xe5, 0x15, 0xd2, 0x26, 0x18, 0x98, 0xfa, 0x05, 0x10, + 0x15, 0x72, 0x8e, 0x5a, 0x8a, 0xaa, 0xc4, 0x2d, 0xad, 0x33, 0x17, 0x0d, 0x04, 0x50, 0x7a, 0x33, + 0xa8, 0x55, 0x21, 0xab, 0xdf, 0x1c, 0xba, 0x64, 0xec, 0xfb, 0x85, 0x04, 0x58, 0xdb, 0xef, 0x0a, + 0x8a, 0xea, 0x71, 0x57, 0x5d, 0x06, 0x0c, 0x7d, 0xb3, 0x97, 0x0f, 0x85, 0xa6, 0xe1, 0xe4, 0xc7, + 0xab, 0xf5, 0xae, 0x8c, 0xdb, 0x09, 0x33, 0xd7, 0x1e, 0x8c, 0x94, 0xe0, 0x4a, 0x25, 0x61, 0x9d, + 0xce, 0xe3, 0xd2, 0x26, 0x1a, 0xd2, 0xee, 0x6b, 0xf1, 0x2f, 0xfa, 0x06, 0xd9, 0x8a, 0x08, 0x64, + 0xd8, 0x76, 0x02, 0x73, 0x3e, 0xc8, 0x6a, 0x64, 0x52, 0x1f, 0x2b, 0x18, 0x17, 0x7b, 0x20, 0x0c, + 0xbb, 0xe1, 0x17, 0x57, 0x7a, 0x61, 0x5d, 0x6c, 0x77, 0x09, 0x88, 0xc0, 0xba, 0xd9, 0x46, 0xe2, + 0x08, 0xe2, 0x4f, 0xa0, 0x74, 0xe5, 0xab, 0x31, 0x43, 0xdb, 0x5b, 0xfc, 0xe0, 0xfd, 0x10, 0x8e, + 0x4b, 0x82, 0xd1, 0x20, 0xa9, 0x3a, 0xd2, 0xca, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, +}; + +static const unsigned char scr_dh_modp16_p[512] = { + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc9, 0x0f, 0xda, 0xa2, 0x21, 0x68, 0xc2, 0x34, + 0xc4, 0xc6, 0x62, 0x8b, 0x80, 0xdc, 0x1c, 0xd1, 0x29, 0x02, 0x4e, 0x08, 0x8a, 0x67, 0xcc, 0x74, + 0x02, 0x0b, 0xbe, 0xa6, 0x3b, 0x13, 0x9b, 0x22, 0x51, 0x4a, 0x08, 0x79, 0x8e, 0x34, 0x04, 0xdd, + 0xef, 0x95, 0x19, 0xb3, 0xcd, 0x3a, 0x43, 0x1b, 0x30, 0x2b, 0x0a, 0x6d, 0xf2, 0x5f, 0x14, 0x37, + 0x4f, 0xe1, 0x35, 0x6d, 0x6d, 0x51, 0xc2, 0x45, 0xe4, 0x85, 0xb5, 0x76, 0x62, 0x5e, 0x7e, 0xc6, + 0xf4, 0x4c, 0x42, 0xe9, 0xa6, 0x37, 0xed, 0x6b, 0x0b, 0xff, 0x5c, 0xb6, 0xf4, 0x06, 0xb7, 0xed, + 0xee, 0x38, 0x6b, 0xfb, 0x5a, 0x89, 0x9f, 0xa5, 0xae, 0x9f, 0x24, 0x11, 0x7c, 0x4b, 0x1f, 0xe6, + 0x49, 0x28, 0x66, 0x51, 0xec, 0xe4, 0x5b, 0x3d, 0xc2, 0x00, 0x7c, 0xb8, 0xa1, 0x63, 0xbf, 0x05, + 0x98, 0xda, 0x48, 0x36, 0x1c, 0x55, 0xd3, 0x9a, 0x69, 0x16, 0x3f, 0xa8, 0xfd, 0x24, 0xcf, 0x5f, + 0x83, 0x65, 0x5d, 0x23, 0xdc, 0xa3, 0xad, 0x96, 0x1c, 0x62, 0xf3, 0x56, 0x20, 0x85, 0x52, 0xbb, + 0x9e, 0xd5, 0x29, 0x07, 0x70, 0x96, 0x96, 0x6d, 0x67, 0x0c, 0x35, 0x4e, 0x4a, 0xbc, 0x98, 0x04, + 0xf1, 0x74, 0x6c, 0x08, 0xca, 0x18, 0x21, 0x7c, 0x32, 0x90, 0x5e, 0x46, 0x2e, 0x36, 0xce, 0x3b, + 0xe3, 0x9e, 0x77, 0x2c, 0x18, 0x0e, 0x86, 0x03, 0x9b, 0x27, 0x83, 0xa2, 0xec, 0x07, 0xa2, 0x8f, + 0xb5, 0xc5, 0x5d, 0xf0, 0x6f, 0x4c, 0x52, 0xc9, 0xde, 0x2b, 0xcb, 0xf6, 0x95, 0x58, 0x17, 0x18, + 0x39, 0x95, 0x49, 0x7c, 0xea, 0x95, 0x6a, 0xe5, 0x15, 0xd2, 0x26, 0x18, 0x98, 0xfa, 0x05, 0x10, + 0x15, 0x72, 0x8e, 0x5a, 0x8a, 0xaa, 0xc4, 0x2d, 0xad, 0x33, 0x17, 0x0d, 0x04, 0x50, 0x7a, 0x33, + 0xa8, 0x55, 0x21, 0xab, 0xdf, 0x1c, 0xba, 0x64, 0xec, 0xfb, 0x85, 0x04, 0x58, 0xdb, 0xef, 0x0a, + 0x8a, 0xea, 0x71, 0x57, 0x5d, 0x06, 0x0c, 0x7d, 0xb3, 0x97, 0x0f, 0x85, 0xa6, 0xe1, 0xe4, 0xc7, + 0xab, 0xf5, 0xae, 0x8c, 0xdb, 0x09, 0x33, 0xd7, 0x1e, 0x8c, 0x94, 0xe0, 0x4a, 0x25, 0x61, 0x9d, + 0xce, 0xe3, 0xd2, 0x26, 0x1a, 0xd2, 0xee, 0x6b, 0xf1, 0x2f, 0xfa, 0x06, 0xd9, 0x8a, 0x08, 0x64, + 0xd8, 0x76, 0x02, 0x73, 0x3e, 0xc8, 0x6a, 0x64, 0x52, 0x1f, 0x2b, 0x18, 0x17, 0x7b, 0x20, 0x0c, + 0xbb, 0xe1, 0x17, 0x57, 0x7a, 0x61, 0x5d, 0x6c, 0x77, 0x09, 0x88, 0xc0, 0xba, 0xd9, 0x46, 0xe2, + 0x08, 0xe2, 0x4f, 0xa0, 0x74, 0xe5, 0xab, 0x31, 0x43, 0xdb, 0x5b, 0xfc, 0xe0, 0xfd, 0x10, 0x8e, + 0x4b, 0x82, 0xd1, 0x20, 0xa9, 0x21, 0x08, 0x01, 0x1a, 0x72, 0x3c, 0x12, 0xa7, 0x87, 0xe6, 0xd7, + 0x88, 0x71, 0x9a, 0x10, 0xbd, 0xba, 0x5b, 0x26, 0x99, 0xc3, 0x27, 0x18, 0x6a, 0xf4, 0xe2, 0x3c, + 0x1a, 0x94, 0x68, 0x34, 0xb6, 0x15, 0x0b, 0xda, 0x25, 0x83, 0xe9, 0xca, 0x2a, 0xd4, 0x4c, 0xe8, + 0xdb, 0xbb, 0xc2, 0xdb, 0x04, 0xde, 0x8e, 0xf9, 0x2e, 0x8e, 0xfc, 0x14, 0x1f, 0xbe, 0xca, 0xa6, + 0x28, 0x7c, 0x59, 0x47, 0x4e, 0x6b, 0xc0, 0x5d, 0x99, 0xb2, 0x96, 0x4f, 0xa0, 0x90, 0xc3, 0xa2, + 0x23, 0x3b, 0xa1, 0x86, 0x51, 0x5b, 0xe7, 0xed, 0x1f, 0x61, 0x29, 0x70, 0xce, 0xe2, 0xd7, 0xaf, + 0xb8, 0x1b, 0xdd, 0x76, 0x21, 0x70, 0x48, 0x1c, 0xd0, 0x06, 0x91, 0x27, 0xd5, 0xb0, 0x5a, 0xa9, + 0x93, 0xb4, 0xea, 0x98, 0x8d, 0x8f, 0xdd, 0xc1, 0x86, 0xff, 0xb7, 0xdc, 0x90, 0xa6, 0xc0, 0x8f, + 0x4d, 0xf4, 0x35, 0xc9, 0x34, 0x06, 0x31, 0x99, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, +}; + +static const unsigned char scr_dh_modp17_p[768] = { + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc9, 0x0f, 0xda, 0xa2, 0x21, 0x68, 0xc2, 0x34, + 0xc4, 0xc6, 0x62, 0x8b, 0x80, 0xdc, 0x1c, 0xd1, 0x29, 0x02, 0x4e, 0x08, 0x8a, 0x67, 0xcc, 0x74, + 0x02, 0x0b, 0xbe, 0xa6, 0x3b, 0x13, 0x9b, 0x22, 0x51, 0x4a, 0x08, 0x79, 0x8e, 0x34, 0x04, 0xdd, + 0xef, 0x95, 0x19, 0xb3, 0xcd, 0x3a, 0x43, 0x1b, 0x30, 0x2b, 0x0a, 0x6d, 0xf2, 0x5f, 0x14, 0x37, + 0x4f, 0xe1, 0x35, 0x6d, 0x6d, 0x51, 0xc2, 0x45, 0xe4, 0x85, 0xb5, 0x76, 0x62, 0x5e, 0x7e, 0xc6, + 0xf4, 0x4c, 0x42, 0xe9, 0xa6, 0x37, 0xed, 0x6b, 0x0b, 0xff, 0x5c, 0xb6, 0xf4, 0x06, 0xb7, 0xed, + 0xee, 0x38, 0x6b, 0xfb, 0x5a, 0x89, 0x9f, 0xa5, 0xae, 0x9f, 0x24, 0x11, 0x7c, 0x4b, 0x1f, 0xe6, + 0x49, 0x28, 0x66, 0x51, 0xec, 0xe4, 0x5b, 0x3d, 0xc2, 0x00, 0x7c, 0xb8, 0xa1, 0x63, 0xbf, 0x05, + 0x98, 0xda, 0x48, 0x36, 0x1c, 0x55, 0xd3, 0x9a, 0x69, 0x16, 0x3f, 0xa8, 0xfd, 0x24, 0xcf, 0x5f, + 0x83, 0x65, 0x5d, 0x23, 0xdc, 0xa3, 0xad, 0x96, 0x1c, 0x62, 0xf3, 0x56, 0x20, 0x85, 0x52, 0xbb, + 0x9e, 0xd5, 0x29, 0x07, 0x70, 0x96, 0x96, 0x6d, 0x67, 0x0c, 0x35, 0x4e, 0x4a, 0xbc, 0x98, 0x04, + 0xf1, 0x74, 0x6c, 0x08, 0xca, 0x18, 0x21, 0x7c, 0x32, 0x90, 0x5e, 0x46, 0x2e, 0x36, 0xce, 0x3b, + 0xe3, 0x9e, 0x77, 0x2c, 0x18, 0x0e, 0x86, 0x03, 0x9b, 0x27, 0x83, 0xa2, 0xec, 0x07, 0xa2, 0x8f, + 0xb5, 0xc5, 0x5d, 0xf0, 0x6f, 0x4c, 0x52, 0xc9, 0xde, 0x2b, 0xcb, 0xf6, 0x95, 0x58, 0x17, 0x18, + 0x39, 0x95, 0x49, 0x7c, 0xea, 0x95, 0x6a, 0xe5, 0x15, 0xd2, 0x26, 0x18, 0x98, 0xfa, 0x05, 0x10, + 0x15, 0x72, 0x8e, 0x5a, 0x8a, 0xaa, 0xc4, 0x2d, 0xad, 0x33, 0x17, 0x0d, 0x04, 0x50, 0x7a, 0x33, + 0xa8, 0x55, 0x21, 0xab, 0xdf, 0x1c, 0xba, 0x64, 0xec, 0xfb, 0x85, 0x04, 0x58, 0xdb, 0xef, 0x0a, + 0x8a, 0xea, 0x71, 0x57, 0x5d, 0x06, 0x0c, 0x7d, 0xb3, 0x97, 0x0f, 0x85, 0xa6, 0xe1, 0xe4, 0xc7, + 0xab, 0xf5, 0xae, 0x8c, 0xdb, 0x09, 0x33, 0xd7, 0x1e, 0x8c, 0x94, 0xe0, 0x4a, 0x25, 0x61, 0x9d, + 0xce, 0xe3, 0xd2, 0x26, 0x1a, 0xd2, 0xee, 0x6b, 0xf1, 0x2f, 0xfa, 0x06, 0xd9, 0x8a, 0x08, 0x64, + 0xd8, 0x76, 0x02, 0x73, 0x3e, 0xc8, 0x6a, 0x64, 0x52, 0x1f, 0x2b, 0x18, 0x17, 0x7b, 0x20, 0x0c, + 0xbb, 0xe1, 0x17, 0x57, 0x7a, 0x61, 0x5d, 0x6c, 0x77, 0x09, 0x88, 0xc0, 0xba, 0xd9, 0x46, 0xe2, + 0x08, 0xe2, 0x4f, 0xa0, 0x74, 0xe5, 0xab, 0x31, 0x43, 0xdb, 0x5b, 0xfc, 0xe0, 0xfd, 0x10, 0x8e, + 0x4b, 0x82, 0xd1, 0x20, 0xa9, 0x21, 0x08, 0x01, 0x1a, 0x72, 0x3c, 0x12, 0xa7, 0x87, 0xe6, 0xd7, + 0x88, 0x71, 0x9a, 0x10, 0xbd, 0xba, 0x5b, 0x26, 0x99, 0xc3, 0x27, 0x18, 0x6a, 0xf4, 0xe2, 0x3c, + 0x1a, 0x94, 0x68, 0x34, 0xb6, 0x15, 0x0b, 0xda, 0x25, 0x83, 0xe9, 0xca, 0x2a, 0xd4, 0x4c, 0xe8, + 0xdb, 0xbb, 0xc2, 0xdb, 0x04, 0xde, 0x8e, 0xf9, 0x2e, 0x8e, 0xfc, 0x14, 0x1f, 0xbe, 0xca, 0xa6, + 0x28, 0x7c, 0x59, 0x47, 0x4e, 0x6b, 0xc0, 0x5d, 0x99, 0xb2, 0x96, 0x4f, 0xa0, 0x90, 0xc3, 0xa2, + 0x23, 0x3b, 0xa1, 0x86, 0x51, 0x5b, 0xe7, 0xed, 0x1f, 0x61, 0x29, 0x70, 0xce, 0xe2, 0xd7, 0xaf, + 0xb8, 0x1b, 0xdd, 0x76, 0x21, 0x70, 0x48, 0x1c, 0xd0, 0x06, 0x91, 0x27, 0xd5, 0xb0, 0x5a, 0xa9, + 0x93, 0xb4, 0xea, 0x98, 0x8d, 0x8f, 0xdd, 0xc1, 0x86, 0xff, 0xb7, 0xdc, 0x90, 0xa6, 0xc0, 0x8f, + 0x4d, 0xf4, 0x35, 0xc9, 0x34, 0x02, 0x84, 0x92, 0x36, 0xc3, 0xfa, 0xb4, 0xd2, 0x7c, 0x70, 0x26, + 0xc1, 0xd4, 0xdc, 0xb2, 0x60, 0x26, 0x46, 0xde, 0xc9, 0x75, 0x1e, 0x76, 0x3d, 0xba, 0x37, 0xbd, + 0xf8, 0xff, 0x94, 0x06, 0xad, 0x9e, 0x53, 0x0e, 0xe5, 0xdb, 0x38, 0x2f, 0x41, 0x30, 0x01, 0xae, + 0xb0, 0x6a, 0x53, 0xed, 0x90, 0x27, 0xd8, 0x31, 0x17, 0x97, 0x27, 0xb0, 0x86, 0x5a, 0x89, 0x18, + 0xda, 0x3e, 0xdb, 0xeb, 0xcf, 0x9b, 0x14, 0xed, 0x44, 0xce, 0x6c, 0xba, 0xce, 0xd4, 0xbb, 0x1b, + 0xdb, 0x7f, 0x14, 0x47, 0xe6, 0xcc, 0x25, 0x4b, 0x33, 0x20, 0x51, 0x51, 0x2b, 0xd7, 0xaf, 0x42, + 0x6f, 0xb8, 0xf4, 0x01, 0x37, 0x8c, 0xd2, 0xbf, 0x59, 0x83, 0xca, 0x01, 0xc6, 0x4b, 0x92, 0xec, + 0xf0, 0x32, 0xea, 0x15, 0xd1, 0x72, 0x1d, 0x03, 0xf4, 0x82, 0xd7, 0xce, 0x6e, 0x74, 0xfe, 0xf6, + 0xd5, 0x5e, 0x70, 0x2f, 0x46, 0x98, 0x0c, 0x82, 0xb5, 0xa8, 0x40, 0x31, 0x90, 0x0b, 0x1c, 0x9e, + 0x59, 0xe7, 0xc9, 0x7f, 0xbe, 0xc7, 0xe8, 0xf3, 0x23, 0xa9, 0x7a, 0x7e, 0x36, 0xcc, 0x88, 0xbe, + 0x0f, 0x1d, 0x45, 0xb7, 0xff, 0x58, 0x5a, 0xc5, 0x4b, 0xd4, 0x07, 0xb2, 0x2b, 0x41, 0x54, 0xaa, + 0xcc, 0x8f, 0x6d, 0x7e, 0xbf, 0x48, 0xe1, 0xd8, 0x14, 0xcc, 0x5e, 0xd2, 0x0f, 0x80, 0x37, 0xe0, + 0xa7, 0x97, 0x15, 0xee, 0xf2, 0x9b, 0xe3, 0x28, 0x06, 0xa1, 0xd5, 0x8b, 0xb7, 0xc5, 0xda, 0x76, + 0xf5, 0x50, 0xaa, 0x3d, 0x8a, 0x1f, 0xbf, 0xf0, 0xeb, 0x19, 0xcc, 0xb1, 0xa3, 0x13, 0xd5, 0x5c, + 0xda, 0x56, 0xc9, 0xec, 0x2e, 0xf2, 0x96, 0x32, 0x38, 0x7f, 0xe8, 0xd7, 0x6e, 0x3c, 0x04, 0x68, + 0x04, 0x3e, 0x8f, 0x66, 0x3f, 0x48, 0x60, 0xee, 0x12, 0xbf, 0x2d, 0x5b, 0x0b, 0x74, 0x74, 0xd6, + 0xe6, 0x94, 0xf9, 0x1e, 0x6d, 0xcc, 0x40, 0x24, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, +}; + +static const unsigned char scr_dh_modp18_p[1024] = { + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc9, 0x0f, 0xda, 0xa2, 0x21, 0x68, 0xc2, 0x34, + 0xc4, 0xc6, 0x62, 0x8b, 0x80, 0xdc, 0x1c, 0xd1, 0x29, 0x02, 0x4e, 0x08, 0x8a, 0x67, 0xcc, 0x74, + 0x02, 0x0b, 0xbe, 0xa6, 0x3b, 0x13, 0x9b, 0x22, 0x51, 0x4a, 0x08, 0x79, 0x8e, 0x34, 0x04, 0xdd, + 0xef, 0x95, 0x19, 0xb3, 0xcd, 0x3a, 0x43, 0x1b, 0x30, 0x2b, 0x0a, 0x6d, 0xf2, 0x5f, 0x14, 0x37, + 0x4f, 0xe1, 0x35, 0x6d, 0x6d, 0x51, 0xc2, 0x45, 0xe4, 0x85, 0xb5, 0x76, 0x62, 0x5e, 0x7e, 0xc6, + 0xf4, 0x4c, 0x42, 0xe9, 0xa6, 0x37, 0xed, 0x6b, 0x0b, 0xff, 0x5c, 0xb6, 0xf4, 0x06, 0xb7, 0xed, + 0xee, 0x38, 0x6b, 0xfb, 0x5a, 0x89, 0x9f, 0xa5, 0xae, 0x9f, 0x24, 0x11, 0x7c, 0x4b, 0x1f, 0xe6, + 0x49, 0x28, 0x66, 0x51, 0xec, 0xe4, 0x5b, 0x3d, 0xc2, 0x00, 0x7c, 0xb8, 0xa1, 0x63, 0xbf, 0x05, + 0x98, 0xda, 0x48, 0x36, 0x1c, 0x55, 0xd3, 0x9a, 0x69, 0x16, 0x3f, 0xa8, 0xfd, 0x24, 0xcf, 0x5f, + 0x83, 0x65, 0x5d, 0x23, 0xdc, 0xa3, 0xad, 0x96, 0x1c, 0x62, 0xf3, 0x56, 0x20, 0x85, 0x52, 0xbb, + 0x9e, 0xd5, 0x29, 0x07, 0x70, 0x96, 0x96, 0x6d, 0x67, 0x0c, 0x35, 0x4e, 0x4a, 0xbc, 0x98, 0x04, + 0xf1, 0x74, 0x6c, 0x08, 0xca, 0x18, 0x21, 0x7c, 0x32, 0x90, 0x5e, 0x46, 0x2e, 0x36, 0xce, 0x3b, + 0xe3, 0x9e, 0x77, 0x2c, 0x18, 0x0e, 0x86, 0x03, 0x9b, 0x27, 0x83, 0xa2, 0xec, 0x07, 0xa2, 0x8f, + 0xb5, 0xc5, 0x5d, 0xf0, 0x6f, 0x4c, 0x52, 0xc9, 0xde, 0x2b, 0xcb, 0xf6, 0x95, 0x58, 0x17, 0x18, + 0x39, 0x95, 0x49, 0x7c, 0xea, 0x95, 0x6a, 0xe5, 0x15, 0xd2, 0x26, 0x18, 0x98, 0xfa, 0x05, 0x10, + 0x15, 0x72, 0x8e, 0x5a, 0x8a, 0xaa, 0xc4, 0x2d, 0xad, 0x33, 0x17, 0x0d, 0x04, 0x50, 0x7a, 0x33, + 0xa8, 0x55, 0x21, 0xab, 0xdf, 0x1c, 0xba, 0x64, 0xec, 0xfb, 0x85, 0x04, 0x58, 0xdb, 0xef, 0x0a, + 0x8a, 0xea, 0x71, 0x57, 0x5d, 0x06, 0x0c, 0x7d, 0xb3, 0x97, 0x0f, 0x85, 0xa6, 0xe1, 0xe4, 0xc7, + 0xab, 0xf5, 0xae, 0x8c, 0xdb, 0x09, 0x33, 0xd7, 0x1e, 0x8c, 0x94, 0xe0, 0x4a, 0x25, 0x61, 0x9d, + 0xce, 0xe3, 0xd2, 0x26, 0x1a, 0xd2, 0xee, 0x6b, 0xf1, 0x2f, 0xfa, 0x06, 0xd9, 0x8a, 0x08, 0x64, + 0xd8, 0x76, 0x02, 0x73, 0x3e, 0xc8, 0x6a, 0x64, 0x52, 0x1f, 0x2b, 0x18, 0x17, 0x7b, 0x20, 0x0c, + 0xbb, 0xe1, 0x17, 0x57, 0x7a, 0x61, 0x5d, 0x6c, 0x77, 0x09, 0x88, 0xc0, 0xba, 0xd9, 0x46, 0xe2, + 0x08, 0xe2, 0x4f, 0xa0, 0x74, 0xe5, 0xab, 0x31, 0x43, 0xdb, 0x5b, 0xfc, 0xe0, 0xfd, 0x10, 0x8e, + 0x4b, 0x82, 0xd1, 0x20, 0xa9, 0x21, 0x08, 0x01, 0x1a, 0x72, 0x3c, 0x12, 0xa7, 0x87, 0xe6, 0xd7, + 0x88, 0x71, 0x9a, 0x10, 0xbd, 0xba, 0x5b, 0x26, 0x99, 0xc3, 0x27, 0x18, 0x6a, 0xf4, 0xe2, 0x3c, + 0x1a, 0x94, 0x68, 0x34, 0xb6, 0x15, 0x0b, 0xda, 0x25, 0x83, 0xe9, 0xca, 0x2a, 0xd4, 0x4c, 0xe8, + 0xdb, 0xbb, 0xc2, 0xdb, 0x04, 0xde, 0x8e, 0xf9, 0x2e, 0x8e, 0xfc, 0x14, 0x1f, 0xbe, 0xca, 0xa6, + 0x28, 0x7c, 0x59, 0x47, 0x4e, 0x6b, 0xc0, 0x5d, 0x99, 0xb2, 0x96, 0x4f, 0xa0, 0x90, 0xc3, 0xa2, + 0x23, 0x3b, 0xa1, 0x86, 0x51, 0x5b, 0xe7, 0xed, 0x1f, 0x61, 0x29, 0x70, 0xce, 0xe2, 0xd7, 0xaf, + 0xb8, 0x1b, 0xdd, 0x76, 0x21, 0x70, 0x48, 0x1c, 0xd0, 0x06, 0x91, 0x27, 0xd5, 0xb0, 0x5a, 0xa9, + 0x93, 0xb4, 0xea, 0x98, 0x8d, 0x8f, 0xdd, 0xc1, 0x86, 0xff, 0xb7, 0xdc, 0x90, 0xa6, 0xc0, 0x8f, + 0x4d, 0xf4, 0x35, 0xc9, 0x34, 0x02, 0x84, 0x92, 0x36, 0xc3, 0xfa, 0xb4, 0xd2, 0x7c, 0x70, 0x26, + 0xc1, 0xd4, 0xdc, 0xb2, 0x60, 0x26, 0x46, 0xde, 0xc9, 0x75, 0x1e, 0x76, 0x3d, 0xba, 0x37, 0xbd, + 0xf8, 0xff, 0x94, 0x06, 0xad, 0x9e, 0x53, 0x0e, 0xe5, 0xdb, 0x38, 0x2f, 0x41, 0x30, 0x01, 0xae, + 0xb0, 0x6a, 0x53, 0xed, 0x90, 0x27, 0xd8, 0x31, 0x17, 0x97, 0x27, 0xb0, 0x86, 0x5a, 0x89, 0x18, + 0xda, 0x3e, 0xdb, 0xeb, 0xcf, 0x9b, 0x14, 0xed, 0x44, 0xce, 0x6c, 0xba, 0xce, 0xd4, 0xbb, 0x1b, + 0xdb, 0x7f, 0x14, 0x47, 0xe6, 0xcc, 0x25, 0x4b, 0x33, 0x20, 0x51, 0x51, 0x2b, 0xd7, 0xaf, 0x42, + 0x6f, 0xb8, 0xf4, 0x01, 0x37, 0x8c, 0xd2, 0xbf, 0x59, 0x83, 0xca, 0x01, 0xc6, 0x4b, 0x92, 0xec, + 0xf0, 0x32, 0xea, 0x15, 0xd1, 0x72, 0x1d, 0x03, 0xf4, 0x82, 0xd7, 0xce, 0x6e, 0x74, 0xfe, 0xf6, + 0xd5, 0x5e, 0x70, 0x2f, 0x46, 0x98, 0x0c, 0x82, 0xb5, 0xa8, 0x40, 0x31, 0x90, 0x0b, 0x1c, 0x9e, + 0x59, 0xe7, 0xc9, 0x7f, 0xbe, 0xc7, 0xe8, 0xf3, 0x23, 0xa9, 0x7a, 0x7e, 0x36, 0xcc, 0x88, 0xbe, + 0x0f, 0x1d, 0x45, 0xb7, 0xff, 0x58, 0x5a, 0xc5, 0x4b, 0xd4, 0x07, 0xb2, 0x2b, 0x41, 0x54, 0xaa, + 0xcc, 0x8f, 0x6d, 0x7e, 0xbf, 0x48, 0xe1, 0xd8, 0x14, 0xcc, 0x5e, 0xd2, 0x0f, 0x80, 0x37, 0xe0, + 0xa7, 0x97, 0x15, 0xee, 0xf2, 0x9b, 0xe3, 0x28, 0x06, 0xa1, 0xd5, 0x8b, 0xb7, 0xc5, 0xda, 0x76, + 0xf5, 0x50, 0xaa, 0x3d, 0x8a, 0x1f, 0xbf, 0xf0, 0xeb, 0x19, 0xcc, 0xb1, 0xa3, 0x13, 0xd5, 0x5c, + 0xda, 0x56, 0xc9, 0xec, 0x2e, 0xf2, 0x96, 0x32, 0x38, 0x7f, 0xe8, 0xd7, 0x6e, 0x3c, 0x04, 0x68, + 0x04, 0x3e, 0x8f, 0x66, 0x3f, 0x48, 0x60, 0xee, 0x12, 0xbf, 0x2d, 0x5b, 0x0b, 0x74, 0x74, 0xd6, + 0xe6, 0x94, 0xf9, 0x1e, 0x6d, 0xbe, 0x11, 0x59, 0x74, 0xa3, 0x92, 0x6f, 0x12, 0xfe, 0xe5, 0xe4, + 0x38, 0x77, 0x7c, 0xb6, 0xa9, 0x32, 0xdf, 0x8c, 0xd8, 0xbe, 0xc4, 0xd0, 0x73, 0xb9, 0x31, 0xba, + 0x3b, 0xc8, 0x32, 0xb6, 0x8d, 0x9d, 0xd3, 0x00, 0x74, 0x1f, 0xa7, 0xbf, 0x8a, 0xfc, 0x47, 0xed, + 0x25, 0x76, 0xf6, 0x93, 0x6b, 0xa4, 0x24, 0x66, 0x3a, 0xab, 0x63, 0x9c, 0x5a, 0xe4, 0xf5, 0x68, + 0x34, 0x23, 0xb4, 0x74, 0x2b, 0xf1, 0xc9, 0x78, 0x23, 0x8f, 0x16, 0xcb, 0xe3, 0x9d, 0x65, 0x2d, + 0xe3, 0xfd, 0xb8, 0xbe, 0xfc, 0x84, 0x8a, 0xd9, 0x22, 0x22, 0x2e, 0x04, 0xa4, 0x03, 0x7c, 0x07, + 0x13, 0xeb, 0x57, 0xa8, 0x1a, 0x23, 0xf0, 0xc7, 0x34, 0x73, 0xfc, 0x64, 0x6c, 0xea, 0x30, 0x6b, + 0x4b, 0xcb, 0xc8, 0x86, 0x2f, 0x83, 0x85, 0xdd, 0xfa, 0x9d, 0x4b, 0x7f, 0xa2, 0xc0, 0x87, 0xe8, + 0x79, 0x68, 0x33, 0x03, 0xed, 0x5b, 0xdd, 0x3a, 0x06, 0x2b, 0x3c, 0xf5, 0xb3, 0xa2, 0x78, 0xa6, + 0x6d, 0x2a, 0x13, 0xf8, 0x3f, 0x44, 0xf8, 0x2d, 0xdf, 0x31, 0x0e, 0xe0, 0x74, 0xab, 0x6a, 0x36, + 0x45, 0x97, 0xe8, 0x99, 0xa0, 0x25, 0x5d, 0xc1, 0x64, 0xf3, 0x1c, 0xc5, 0x08, 0x46, 0x85, 0x1d, + 0xf9, 0xab, 0x48, 0x19, 0x5d, 0xed, 0x7e, 0xa1, 0xb1, 0xd5, 0x10, 0xbd, 0x7e, 0xe7, 0x4d, 0x73, + 0xfa, 0xf3, 0x6b, 0xc3, 0x1e, 0xcf, 0xa2, 0x68, 0x35, 0x90, 0x46, 0xf4, 0xeb, 0x87, 0x9f, 0x92, + 0x40, 0x09, 0x43, 0x8b, 0x48, 0x1c, 0x6c, 0xd7, 0x88, 0x9a, 0x00, 0x2e, 0xd5, 0xee, 0x38, 0x2b, + 0xc9, 0x19, 0x0d, 0xa6, 0xfc, 0x02, 0x6e, 0x47, 0x95, 0x58, 0xe4, 0x47, 0x56, 0x77, 0xe9, 0xaa, + 0x9e, 0x30, 0x50, 0xe2, 0x76, 0x56, 0x94, 0xdf, 0xc8, 0x1f, 0x56, 0xe8, 0x80, 0xb9, 0x6e, 0x71, + 0x60, 0xc9, 0x80, 0xdd, 0x98, 0xed, 0xd3, 0xdf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, +}; + +static const unsigned char scr_dh_ffdhe2048_p[256] = { + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xad, 0xf8, 0x54, 0x58, 0xa2, 0xbb, 0x4a, 0x9a, + 0xaf, 0xdc, 0x56, 0x20, 0x27, 0x3d, 0x3c, 0xf1, 0xd8, 0xb9, 0xc5, 0x83, 0xce, 0x2d, 0x36, 0x95, + 0xa9, 0xe1, 0x36, 0x41, 0x14, 0x64, 0x33, 0xfb, 0xcc, 0x93, 0x9d, 0xce, 0x24, 0x9b, 0x3e, 0xf9, + 0x7d, 0x2f, 0xe3, 0x63, 0x63, 0x0c, 0x75, 0xd8, 0xf6, 0x81, 0xb2, 0x02, 0xae, 0xc4, 0x61, 0x7a, + 0xd3, 0xdf, 0x1e, 0xd5, 0xd5, 0xfd, 0x65, 0x61, 0x24, 0x33, 0xf5, 0x1f, 0x5f, 0x06, 0x6e, 0xd0, + 0x85, 0x63, 0x65, 0x55, 0x3d, 0xed, 0x1a, 0xf3, 0xb5, 0x57, 0x13, 0x5e, 0x7f, 0x57, 0xc9, 0x35, + 0x98, 0x4f, 0x0c, 0x70, 0xe0, 0xe6, 0x8b, 0x77, 0xe2, 0xa6, 0x89, 0xda, 0xf3, 0xef, 0xe8, 0x72, + 0x1d, 0xf1, 0x58, 0xa1, 0x36, 0xad, 0xe7, 0x35, 0x30, 0xac, 0xca, 0x4f, 0x48, 0x3a, 0x79, 0x7a, + 0xbc, 0x0a, 0xb1, 0x82, 0xb3, 0x24, 0xfb, 0x61, 0xd1, 0x08, 0xa9, 0x4b, 0xb2, 0xc8, 0xe3, 0xfb, + 0xb9, 0x6a, 0xda, 0xb7, 0x60, 0xd7, 0xf4, 0x68, 0x1d, 0x4f, 0x42, 0xa3, 0xde, 0x39, 0x4d, 0xf4, + 0xae, 0x56, 0xed, 0xe7, 0x63, 0x72, 0xbb, 0x19, 0x0b, 0x07, 0xa7, 0xc8, 0xee, 0x0a, 0x6d, 0x70, + 0x9e, 0x02, 0xfc, 0xe1, 0xcd, 0xf7, 0xe2, 0xec, 0xc0, 0x34, 0x04, 0xcd, 0x28, 0x34, 0x2f, 0x61, + 0x91, 0x72, 0xfe, 0x9c, 0xe9, 0x85, 0x83, 0xff, 0x8e, 0x4f, 0x12, 0x32, 0xee, 0xf2, 0x81, 0x83, + 0xc3, 0xfe, 0x3b, 0x1b, 0x4c, 0x6f, 0xad, 0x73, 0x3b, 0xb5, 0xfc, 0xbc, 0x2e, 0xc2, 0x20, 0x05, + 0xc5, 0x8e, 0xf1, 0x83, 0x7d, 0x16, 0x83, 0xb2, 0xc6, 0xf3, 0x4a, 0x26, 0xc1, 0xb2, 0xef, 0xfa, + 0x88, 0x6b, 0x42, 0x38, 0x61, 0x28, 0x5c, 0x97, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, +}; + +static const unsigned char scr_dh_ffdhe3072_p[384] = { + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xad, 0xf8, 0x54, 0x58, 0xa2, 0xbb, 0x4a, 0x9a, + 0xaf, 0xdc, 0x56, 0x20, 0x27, 0x3d, 0x3c, 0xf1, 0xd8, 0xb9, 0xc5, 0x83, 0xce, 0x2d, 0x36, 0x95, + 0xa9, 0xe1, 0x36, 0x41, 0x14, 0x64, 0x33, 0xfb, 0xcc, 0x93, 0x9d, 0xce, 0x24, 0x9b, 0x3e, 0xf9, + 0x7d, 0x2f, 0xe3, 0x63, 0x63, 0x0c, 0x75, 0xd8, 0xf6, 0x81, 0xb2, 0x02, 0xae, 0xc4, 0x61, 0x7a, + 0xd3, 0xdf, 0x1e, 0xd5, 0xd5, 0xfd, 0x65, 0x61, 0x24, 0x33, 0xf5, 0x1f, 0x5f, 0x06, 0x6e, 0xd0, + 0x85, 0x63, 0x65, 0x55, 0x3d, 0xed, 0x1a, 0xf3, 0xb5, 0x57, 0x13, 0x5e, 0x7f, 0x57, 0xc9, 0x35, + 0x98, 0x4f, 0x0c, 0x70, 0xe0, 0xe6, 0x8b, 0x77, 0xe2, 0xa6, 0x89, 0xda, 0xf3, 0xef, 0xe8, 0x72, + 0x1d, 0xf1, 0x58, 0xa1, 0x36, 0xad, 0xe7, 0x35, 0x30, 0xac, 0xca, 0x4f, 0x48, 0x3a, 0x79, 0x7a, + 0xbc, 0x0a, 0xb1, 0x82, 0xb3, 0x24, 0xfb, 0x61, 0xd1, 0x08, 0xa9, 0x4b, 0xb2, 0xc8, 0xe3, 0xfb, + 0xb9, 0x6a, 0xda, 0xb7, 0x60, 0xd7, 0xf4, 0x68, 0x1d, 0x4f, 0x42, 0xa3, 0xde, 0x39, 0x4d, 0xf4, + 0xae, 0x56, 0xed, 0xe7, 0x63, 0x72, 0xbb, 0x19, 0x0b, 0x07, 0xa7, 0xc8, 0xee, 0x0a, 0x6d, 0x70, + 0x9e, 0x02, 0xfc, 0xe1, 0xcd, 0xf7, 0xe2, 0xec, 0xc0, 0x34, 0x04, 0xcd, 0x28, 0x34, 0x2f, 0x61, + 0x91, 0x72, 0xfe, 0x9c, 0xe9, 0x85, 0x83, 0xff, 0x8e, 0x4f, 0x12, 0x32, 0xee, 0xf2, 0x81, 0x83, + 0xc3, 0xfe, 0x3b, 0x1b, 0x4c, 0x6f, 0xad, 0x73, 0x3b, 0xb5, 0xfc, 0xbc, 0x2e, 0xc2, 0x20, 0x05, + 0xc5, 0x8e, 0xf1, 0x83, 0x7d, 0x16, 0x83, 0xb2, 0xc6, 0xf3, 0x4a, 0x26, 0xc1, 0xb2, 0xef, 0xfa, + 0x88, 0x6b, 0x42, 0x38, 0x61, 0x1f, 0xcf, 0xdc, 0xde, 0x35, 0x5b, 0x3b, 0x65, 0x19, 0x03, 0x5b, + 0xbc, 0x34, 0xf4, 0xde, 0xf9, 0x9c, 0x02, 0x38, 0x61, 0xb4, 0x6f, 0xc9, 0xd6, 0xe6, 0xc9, 0x07, + 0x7a, 0xd9, 0x1d, 0x26, 0x91, 0xf7, 0xf7, 0xee, 0x59, 0x8c, 0xb0, 0xfa, 0xc1, 0x86, 0xd9, 0x1c, + 0xae, 0xfe, 0x13, 0x09, 0x85, 0x13, 0x92, 0x70, 0xb4, 0x13, 0x0c, 0x93, 0xbc, 0x43, 0x79, 0x44, + 0xf4, 0xfd, 0x44, 0x52, 0xe2, 0xd7, 0x4d, 0xd3, 0x64, 0xf2, 0xe2, 0x1e, 0x71, 0xf5, 0x4b, 0xff, + 0x5c, 0xae, 0x82, 0xab, 0x9c, 0x9d, 0xf6, 0x9e, 0xe8, 0x6d, 0x2b, 0xc5, 0x22, 0x36, 0x3a, 0x0d, + 0xab, 0xc5, 0x21, 0x97, 0x9b, 0x0d, 0xea, 0xda, 0x1d, 0xbf, 0x9a, 0x42, 0xd5, 0xc4, 0x48, 0x4e, + 0x0a, 0xbc, 0xd0, 0x6b, 0xfa, 0x53, 0xdd, 0xef, 0x3c, 0x1b, 0x20, 0xee, 0x3f, 0xd5, 0x9d, 0x7c, + 0x25, 0xe4, 0x1d, 0x2b, 0x66, 0xc6, 0x2e, 0x37, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, +}; + +static const unsigned char scr_dh_ffdhe4096_p[512] = { + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xad, 0xf8, 0x54, 0x58, 0xa2, 0xbb, 0x4a, 0x9a, + 0xaf, 0xdc, 0x56, 0x20, 0x27, 0x3d, 0x3c, 0xf1, 0xd8, 0xb9, 0xc5, 0x83, 0xce, 0x2d, 0x36, 0x95, + 0xa9, 0xe1, 0x36, 0x41, 0x14, 0x64, 0x33, 0xfb, 0xcc, 0x93, 0x9d, 0xce, 0x24, 0x9b, 0x3e, 0xf9, + 0x7d, 0x2f, 0xe3, 0x63, 0x63, 0x0c, 0x75, 0xd8, 0xf6, 0x81, 0xb2, 0x02, 0xae, 0xc4, 0x61, 0x7a, + 0xd3, 0xdf, 0x1e, 0xd5, 0xd5, 0xfd, 0x65, 0x61, 0x24, 0x33, 0xf5, 0x1f, 0x5f, 0x06, 0x6e, 0xd0, + 0x85, 0x63, 0x65, 0x55, 0x3d, 0xed, 0x1a, 0xf3, 0xb5, 0x57, 0x13, 0x5e, 0x7f, 0x57, 0xc9, 0x35, + 0x98, 0x4f, 0x0c, 0x70, 0xe0, 0xe6, 0x8b, 0x77, 0xe2, 0xa6, 0x89, 0xda, 0xf3, 0xef, 0xe8, 0x72, + 0x1d, 0xf1, 0x58, 0xa1, 0x36, 0xad, 0xe7, 0x35, 0x30, 0xac, 0xca, 0x4f, 0x48, 0x3a, 0x79, 0x7a, + 0xbc, 0x0a, 0xb1, 0x82, 0xb3, 0x24, 0xfb, 0x61, 0xd1, 0x08, 0xa9, 0x4b, 0xb2, 0xc8, 0xe3, 0xfb, + 0xb9, 0x6a, 0xda, 0xb7, 0x60, 0xd7, 0xf4, 0x68, 0x1d, 0x4f, 0x42, 0xa3, 0xde, 0x39, 0x4d, 0xf4, + 0xae, 0x56, 0xed, 0xe7, 0x63, 0x72, 0xbb, 0x19, 0x0b, 0x07, 0xa7, 0xc8, 0xee, 0x0a, 0x6d, 0x70, + 0x9e, 0x02, 0xfc, 0xe1, 0xcd, 0xf7, 0xe2, 0xec, 0xc0, 0x34, 0x04, 0xcd, 0x28, 0x34, 0x2f, 0x61, + 0x91, 0x72, 0xfe, 0x9c, 0xe9, 0x85, 0x83, 0xff, 0x8e, 0x4f, 0x12, 0x32, 0xee, 0xf2, 0x81, 0x83, + 0xc3, 0xfe, 0x3b, 0x1b, 0x4c, 0x6f, 0xad, 0x73, 0x3b, 0xb5, 0xfc, 0xbc, 0x2e, 0xc2, 0x20, 0x05, + 0xc5, 0x8e, 0xf1, 0x83, 0x7d, 0x16, 0x83, 0xb2, 0xc6, 0xf3, 0x4a, 0x26, 0xc1, 0xb2, 0xef, 0xfa, + 0x88, 0x6b, 0x42, 0x38, 0x61, 0x1f, 0xcf, 0xdc, 0xde, 0x35, 0x5b, 0x3b, 0x65, 0x19, 0x03, 0x5b, + 0xbc, 0x34, 0xf4, 0xde, 0xf9, 0x9c, 0x02, 0x38, 0x61, 0xb4, 0x6f, 0xc9, 0xd6, 0xe6, 0xc9, 0x07, + 0x7a, 0xd9, 0x1d, 0x26, 0x91, 0xf7, 0xf7, 0xee, 0x59, 0x8c, 0xb0, 0xfa, 0xc1, 0x86, 0xd9, 0x1c, + 0xae, 0xfe, 0x13, 0x09, 0x85, 0x13, 0x92, 0x70, 0xb4, 0x13, 0x0c, 0x93, 0xbc, 0x43, 0x79, 0x44, + 0xf4, 0xfd, 0x44, 0x52, 0xe2, 0xd7, 0x4d, 0xd3, 0x64, 0xf2, 0xe2, 0x1e, 0x71, 0xf5, 0x4b, 0xff, + 0x5c, 0xae, 0x82, 0xab, 0x9c, 0x9d, 0xf6, 0x9e, 0xe8, 0x6d, 0x2b, 0xc5, 0x22, 0x36, 0x3a, 0x0d, + 0xab, 0xc5, 0x21, 0x97, 0x9b, 0x0d, 0xea, 0xda, 0x1d, 0xbf, 0x9a, 0x42, 0xd5, 0xc4, 0x48, 0x4e, + 0x0a, 0xbc, 0xd0, 0x6b, 0xfa, 0x53, 0xdd, 0xef, 0x3c, 0x1b, 0x20, 0xee, 0x3f, 0xd5, 0x9d, 0x7c, + 0x25, 0xe4, 0x1d, 0x2b, 0x66, 0x9e, 0x1e, 0xf1, 0x6e, 0x6f, 0x52, 0xc3, 0x16, 0x4d, 0xf4, 0xfb, + 0x79, 0x30, 0xe9, 0xe4, 0xe5, 0x88, 0x57, 0xb6, 0xac, 0x7d, 0x5f, 0x42, 0xd6, 0x9f, 0x6d, 0x18, + 0x77, 0x63, 0xcf, 0x1d, 0x55, 0x03, 0x40, 0x04, 0x87, 0xf5, 0x5b, 0xa5, 0x7e, 0x31, 0xcc, 0x7a, + 0x71, 0x35, 0xc8, 0x86, 0xef, 0xb4, 0x31, 0x8a, 0xed, 0x6a, 0x1e, 0x01, 0x2d, 0x9e, 0x68, 0x32, + 0xa9, 0x07, 0x60, 0x0a, 0x91, 0x81, 0x30, 0xc4, 0x6d, 0xc7, 0x78, 0xf9, 0x71, 0xad, 0x00, 0x38, + 0x09, 0x29, 0x99, 0xa3, 0x33, 0xcb, 0x8b, 0x7a, 0x1a, 0x1d, 0xb9, 0x3d, 0x71, 0x40, 0x00, 0x3c, + 0x2a, 0x4e, 0xce, 0xa9, 0xf9, 0x8d, 0x0a, 0xcc, 0x0a, 0x82, 0x91, 0xcd, 0xce, 0xc9, 0x7d, 0xcf, + 0x8e, 0xc9, 0xb5, 0x5a, 0x7f, 0x88, 0xa4, 0x6b, 0x4d, 0xb5, 0xa8, 0x51, 0xf4, 0x41, 0x82, 0xe1, + 0xc6, 0x8a, 0x00, 0x7e, 0x5e, 0x65, 0x5f, 0x6a, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, +}; + +static const unsigned char scr_dh_ffdhe6144_p[768] = { + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xad, 0xf8, 0x54, 0x58, 0xa2, 0xbb, 0x4a, 0x9a, + 0xaf, 0xdc, 0x56, 0x20, 0x27, 0x3d, 0x3c, 0xf1, 0xd8, 0xb9, 0xc5, 0x83, 0xce, 0x2d, 0x36, 0x95, + 0xa9, 0xe1, 0x36, 0x41, 0x14, 0x64, 0x33, 0xfb, 0xcc, 0x93, 0x9d, 0xce, 0x24, 0x9b, 0x3e, 0xf9, + 0x7d, 0x2f, 0xe3, 0x63, 0x63, 0x0c, 0x75, 0xd8, 0xf6, 0x81, 0xb2, 0x02, 0xae, 0xc4, 0x61, 0x7a, + 0xd3, 0xdf, 0x1e, 0xd5, 0xd5, 0xfd, 0x65, 0x61, 0x24, 0x33, 0xf5, 0x1f, 0x5f, 0x06, 0x6e, 0xd0, + 0x85, 0x63, 0x65, 0x55, 0x3d, 0xed, 0x1a, 0xf3, 0xb5, 0x57, 0x13, 0x5e, 0x7f, 0x57, 0xc9, 0x35, + 0x98, 0x4f, 0x0c, 0x70, 0xe0, 0xe6, 0x8b, 0x77, 0xe2, 0xa6, 0x89, 0xda, 0xf3, 0xef, 0xe8, 0x72, + 0x1d, 0xf1, 0x58, 0xa1, 0x36, 0xad, 0xe7, 0x35, 0x30, 0xac, 0xca, 0x4f, 0x48, 0x3a, 0x79, 0x7a, + 0xbc, 0x0a, 0xb1, 0x82, 0xb3, 0x24, 0xfb, 0x61, 0xd1, 0x08, 0xa9, 0x4b, 0xb2, 0xc8, 0xe3, 0xfb, + 0xb9, 0x6a, 0xda, 0xb7, 0x60, 0xd7, 0xf4, 0x68, 0x1d, 0x4f, 0x42, 0xa3, 0xde, 0x39, 0x4d, 0xf4, + 0xae, 0x56, 0xed, 0xe7, 0x63, 0x72, 0xbb, 0x19, 0x0b, 0x07, 0xa7, 0xc8, 0xee, 0x0a, 0x6d, 0x70, + 0x9e, 0x02, 0xfc, 0xe1, 0xcd, 0xf7, 0xe2, 0xec, 0xc0, 0x34, 0x04, 0xcd, 0x28, 0x34, 0x2f, 0x61, + 0x91, 0x72, 0xfe, 0x9c, 0xe9, 0x85, 0x83, 0xff, 0x8e, 0x4f, 0x12, 0x32, 0xee, 0xf2, 0x81, 0x83, + 0xc3, 0xfe, 0x3b, 0x1b, 0x4c, 0x6f, 0xad, 0x73, 0x3b, 0xb5, 0xfc, 0xbc, 0x2e, 0xc2, 0x20, 0x05, + 0xc5, 0x8e, 0xf1, 0x83, 0x7d, 0x16, 0x83, 0xb2, 0xc6, 0xf3, 0x4a, 0x26, 0xc1, 0xb2, 0xef, 0xfa, + 0x88, 0x6b, 0x42, 0x38, 0x61, 0x1f, 0xcf, 0xdc, 0xde, 0x35, 0x5b, 0x3b, 0x65, 0x19, 0x03, 0x5b, + 0xbc, 0x34, 0xf4, 0xde, 0xf9, 0x9c, 0x02, 0x38, 0x61, 0xb4, 0x6f, 0xc9, 0xd6, 0xe6, 0xc9, 0x07, + 0x7a, 0xd9, 0x1d, 0x26, 0x91, 0xf7, 0xf7, 0xee, 0x59, 0x8c, 0xb0, 0xfa, 0xc1, 0x86, 0xd9, 0x1c, + 0xae, 0xfe, 0x13, 0x09, 0x85, 0x13, 0x92, 0x70, 0xb4, 0x13, 0x0c, 0x93, 0xbc, 0x43, 0x79, 0x44, + 0xf4, 0xfd, 0x44, 0x52, 0xe2, 0xd7, 0x4d, 0xd3, 0x64, 0xf2, 0xe2, 0x1e, 0x71, 0xf5, 0x4b, 0xff, + 0x5c, 0xae, 0x82, 0xab, 0x9c, 0x9d, 0xf6, 0x9e, 0xe8, 0x6d, 0x2b, 0xc5, 0x22, 0x36, 0x3a, 0x0d, + 0xab, 0xc5, 0x21, 0x97, 0x9b, 0x0d, 0xea, 0xda, 0x1d, 0xbf, 0x9a, 0x42, 0xd5, 0xc4, 0x48, 0x4e, + 0x0a, 0xbc, 0xd0, 0x6b, 0xfa, 0x53, 0xdd, 0xef, 0x3c, 0x1b, 0x20, 0xee, 0x3f, 0xd5, 0x9d, 0x7c, + 0x25, 0xe4, 0x1d, 0x2b, 0x66, 0x9e, 0x1e, 0xf1, 0x6e, 0x6f, 0x52, 0xc3, 0x16, 0x4d, 0xf4, 0xfb, + 0x79, 0x30, 0xe9, 0xe4, 0xe5, 0x88, 0x57, 0xb6, 0xac, 0x7d, 0x5f, 0x42, 0xd6, 0x9f, 0x6d, 0x18, + 0x77, 0x63, 0xcf, 0x1d, 0x55, 0x03, 0x40, 0x04, 0x87, 0xf5, 0x5b, 0xa5, 0x7e, 0x31, 0xcc, 0x7a, + 0x71, 0x35, 0xc8, 0x86, 0xef, 0xb4, 0x31, 0x8a, 0xed, 0x6a, 0x1e, 0x01, 0x2d, 0x9e, 0x68, 0x32, + 0xa9, 0x07, 0x60, 0x0a, 0x91, 0x81, 0x30, 0xc4, 0x6d, 0xc7, 0x78, 0xf9, 0x71, 0xad, 0x00, 0x38, + 0x09, 0x29, 0x99, 0xa3, 0x33, 0xcb, 0x8b, 0x7a, 0x1a, 0x1d, 0xb9, 0x3d, 0x71, 0x40, 0x00, 0x3c, + 0x2a, 0x4e, 0xce, 0xa9, 0xf9, 0x8d, 0x0a, 0xcc, 0x0a, 0x82, 0x91, 0xcd, 0xce, 0xc9, 0x7d, 0xcf, + 0x8e, 0xc9, 0xb5, 0x5a, 0x7f, 0x88, 0xa4, 0x6b, 0x4d, 0xb5, 0xa8, 0x51, 0xf4, 0x41, 0x82, 0xe1, + 0xc6, 0x8a, 0x00, 0x7e, 0x5e, 0x0d, 0xd9, 0x02, 0x0b, 0xfd, 0x64, 0xb6, 0x45, 0x03, 0x6c, 0x7a, + 0x4e, 0x67, 0x7d, 0x2c, 0x38, 0x53, 0x2a, 0x3a, 0x23, 0xba, 0x44, 0x42, 0xca, 0xf5, 0x3e, 0xa6, + 0x3b, 0xb4, 0x54, 0x32, 0x9b, 0x76, 0x24, 0xc8, 0x91, 0x7b, 0xdd, 0x64, 0xb1, 0xc0, 0xfd, 0x4c, + 0xb3, 0x8e, 0x8c, 0x33, 0x4c, 0x70, 0x1c, 0x3a, 0xcd, 0xad, 0x06, 0x57, 0xfc, 0xcf, 0xec, 0x71, + 0x9b, 0x1f, 0x5c, 0x3e, 0x4e, 0x46, 0x04, 0x1f, 0x38, 0x81, 0x47, 0xfb, 0x4c, 0xfd, 0xb4, 0x77, + 0xa5, 0x24, 0x71, 0xf7, 0xa9, 0xa9, 0x69, 0x10, 0xb8, 0x55, 0x32, 0x2e, 0xdb, 0x63, 0x40, 0xd8, + 0xa0, 0x0e, 0xf0, 0x92, 0x35, 0x05, 0x11, 0xe3, 0x0a, 0xbe, 0xc1, 0xff, 0xf9, 0xe3, 0xa2, 0x6e, + 0x7f, 0xb2, 0x9f, 0x8c, 0x18, 0x30, 0x23, 0xc3, 0x58, 0x7e, 0x38, 0xda, 0x00, 0x77, 0xd9, 0xb4, + 0x76, 0x3e, 0x4e, 0x4b, 0x94, 0xb2, 0xbb, 0xc1, 0x94, 0xc6, 0x65, 0x1e, 0x77, 0xca, 0xf9, 0x92, + 0xee, 0xaa, 0xc0, 0x23, 0x2a, 0x28, 0x1b, 0xf6, 0xb3, 0xa7, 0x39, 0xc1, 0x22, 0x61, 0x16, 0x82, + 0x0a, 0xe8, 0xdb, 0x58, 0x47, 0xa6, 0x7c, 0xbe, 0xf9, 0xc9, 0x09, 0x1b, 0x46, 0x2d, 0x53, 0x8c, + 0xd7, 0x2b, 0x03, 0x74, 0x6a, 0xe7, 0x7f, 0x5e, 0x62, 0x29, 0x2c, 0x31, 0x15, 0x62, 0xa8, 0x46, + 0x50, 0x5d, 0xc8, 0x2d, 0xb8, 0x54, 0x33, 0x8a, 0xe4, 0x9f, 0x52, 0x35, 0xc9, 0x5b, 0x91, 0x17, + 0x8c, 0xcf, 0x2d, 0xd5, 0xca, 0xce, 0xf4, 0x03, 0xec, 0x9d, 0x18, 0x10, 0xc6, 0x27, 0x2b, 0x04, + 0x5b, 0x3b, 0x71, 0xf9, 0xdc, 0x6b, 0x80, 0xd6, 0x3f, 0xdd, 0x4a, 0x8e, 0x9a, 0xdb, 0x1e, 0x69, + 0x62, 0xa6, 0x95, 0x26, 0xd4, 0x31, 0x61, 0xc1, 0xa4, 0x1d, 0x57, 0x0d, 0x79, 0x38, 0xda, 0xd4, + 0xa4, 0x0e, 0x32, 0x9c, 0xd0, 0xe4, 0x0e, 0x65, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, +}; + +static const unsigned char scr_dh_ffdhe8192_p[1024] = { + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xad, 0xf8, 0x54, 0x58, 0xa2, 0xbb, 0x4a, 0x9a, + 0xaf, 0xdc, 0x56, 0x20, 0x27, 0x3d, 0x3c, 0xf1, 0xd8, 0xb9, 0xc5, 0x83, 0xce, 0x2d, 0x36, 0x95, + 0xa9, 0xe1, 0x36, 0x41, 0x14, 0x64, 0x33, 0xfb, 0xcc, 0x93, 0x9d, 0xce, 0x24, 0x9b, 0x3e, 0xf9, + 0x7d, 0x2f, 0xe3, 0x63, 0x63, 0x0c, 0x75, 0xd8, 0xf6, 0x81, 0xb2, 0x02, 0xae, 0xc4, 0x61, 0x7a, + 0xd3, 0xdf, 0x1e, 0xd5, 0xd5, 0xfd, 0x65, 0x61, 0x24, 0x33, 0xf5, 0x1f, 0x5f, 0x06, 0x6e, 0xd0, + 0x85, 0x63, 0x65, 0x55, 0x3d, 0xed, 0x1a, 0xf3, 0xb5, 0x57, 0x13, 0x5e, 0x7f, 0x57, 0xc9, 0x35, + 0x98, 0x4f, 0x0c, 0x70, 0xe0, 0xe6, 0x8b, 0x77, 0xe2, 0xa6, 0x89, 0xda, 0xf3, 0xef, 0xe8, 0x72, + 0x1d, 0xf1, 0x58, 0xa1, 0x36, 0xad, 0xe7, 0x35, 0x30, 0xac, 0xca, 0x4f, 0x48, 0x3a, 0x79, 0x7a, + 0xbc, 0x0a, 0xb1, 0x82, 0xb3, 0x24, 0xfb, 0x61, 0xd1, 0x08, 0xa9, 0x4b, 0xb2, 0xc8, 0xe3, 0xfb, + 0xb9, 0x6a, 0xda, 0xb7, 0x60, 0xd7, 0xf4, 0x68, 0x1d, 0x4f, 0x42, 0xa3, 0xde, 0x39, 0x4d, 0xf4, + 0xae, 0x56, 0xed, 0xe7, 0x63, 0x72, 0xbb, 0x19, 0x0b, 0x07, 0xa7, 0xc8, 0xee, 0x0a, 0x6d, 0x70, + 0x9e, 0x02, 0xfc, 0xe1, 0xcd, 0xf7, 0xe2, 0xec, 0xc0, 0x34, 0x04, 0xcd, 0x28, 0x34, 0x2f, 0x61, + 0x91, 0x72, 0xfe, 0x9c, 0xe9, 0x85, 0x83, 0xff, 0x8e, 0x4f, 0x12, 0x32, 0xee, 0xf2, 0x81, 0x83, + 0xc3, 0xfe, 0x3b, 0x1b, 0x4c, 0x6f, 0xad, 0x73, 0x3b, 0xb5, 0xfc, 0xbc, 0x2e, 0xc2, 0x20, 0x05, + 0xc5, 0x8e, 0xf1, 0x83, 0x7d, 0x16, 0x83, 0xb2, 0xc6, 0xf3, 0x4a, 0x26, 0xc1, 0xb2, 0xef, 0xfa, + 0x88, 0x6b, 0x42, 0x38, 0x61, 0x1f, 0xcf, 0xdc, 0xde, 0x35, 0x5b, 0x3b, 0x65, 0x19, 0x03, 0x5b, + 0xbc, 0x34, 0xf4, 0xde, 0xf9, 0x9c, 0x02, 0x38, 0x61, 0xb4, 0x6f, 0xc9, 0xd6, 0xe6, 0xc9, 0x07, + 0x7a, 0xd9, 0x1d, 0x26, 0x91, 0xf7, 0xf7, 0xee, 0x59, 0x8c, 0xb0, 0xfa, 0xc1, 0x86, 0xd9, 0x1c, + 0xae, 0xfe, 0x13, 0x09, 0x85, 0x13, 0x92, 0x70, 0xb4, 0x13, 0x0c, 0x93, 0xbc, 0x43, 0x79, 0x44, + 0xf4, 0xfd, 0x44, 0x52, 0xe2, 0xd7, 0x4d, 0xd3, 0x64, 0xf2, 0xe2, 0x1e, 0x71, 0xf5, 0x4b, 0xff, + 0x5c, 0xae, 0x82, 0xab, 0x9c, 0x9d, 0xf6, 0x9e, 0xe8, 0x6d, 0x2b, 0xc5, 0x22, 0x36, 0x3a, 0x0d, + 0xab, 0xc5, 0x21, 0x97, 0x9b, 0x0d, 0xea, 0xda, 0x1d, 0xbf, 0x9a, 0x42, 0xd5, 0xc4, 0x48, 0x4e, + 0x0a, 0xbc, 0xd0, 0x6b, 0xfa, 0x53, 0xdd, 0xef, 0x3c, 0x1b, 0x20, 0xee, 0x3f, 0xd5, 0x9d, 0x7c, + 0x25, 0xe4, 0x1d, 0x2b, 0x66, 0x9e, 0x1e, 0xf1, 0x6e, 0x6f, 0x52, 0xc3, 0x16, 0x4d, 0xf4, 0xfb, + 0x79, 0x30, 0xe9, 0xe4, 0xe5, 0x88, 0x57, 0xb6, 0xac, 0x7d, 0x5f, 0x42, 0xd6, 0x9f, 0x6d, 0x18, + 0x77, 0x63, 0xcf, 0x1d, 0x55, 0x03, 0x40, 0x04, 0x87, 0xf5, 0x5b, 0xa5, 0x7e, 0x31, 0xcc, 0x7a, + 0x71, 0x35, 0xc8, 0x86, 0xef, 0xb4, 0x31, 0x8a, 0xed, 0x6a, 0x1e, 0x01, 0x2d, 0x9e, 0x68, 0x32, + 0xa9, 0x07, 0x60, 0x0a, 0x91, 0x81, 0x30, 0xc4, 0x6d, 0xc7, 0x78, 0xf9, 0x71, 0xad, 0x00, 0x38, + 0x09, 0x29, 0x99, 0xa3, 0x33, 0xcb, 0x8b, 0x7a, 0x1a, 0x1d, 0xb9, 0x3d, 0x71, 0x40, 0x00, 0x3c, + 0x2a, 0x4e, 0xce, 0xa9, 0xf9, 0x8d, 0x0a, 0xcc, 0x0a, 0x82, 0x91, 0xcd, 0xce, 0xc9, 0x7d, 0xcf, + 0x8e, 0xc9, 0xb5, 0x5a, 0x7f, 0x88, 0xa4, 0x6b, 0x4d, 0xb5, 0xa8, 0x51, 0xf4, 0x41, 0x82, 0xe1, + 0xc6, 0x8a, 0x00, 0x7e, 0x5e, 0x0d, 0xd9, 0x02, 0x0b, 0xfd, 0x64, 0xb6, 0x45, 0x03, 0x6c, 0x7a, + 0x4e, 0x67, 0x7d, 0x2c, 0x38, 0x53, 0x2a, 0x3a, 0x23, 0xba, 0x44, 0x42, 0xca, 0xf5, 0x3e, 0xa6, + 0x3b, 0xb4, 0x54, 0x32, 0x9b, 0x76, 0x24, 0xc8, 0x91, 0x7b, 0xdd, 0x64, 0xb1, 0xc0, 0xfd, 0x4c, + 0xb3, 0x8e, 0x8c, 0x33, 0x4c, 0x70, 0x1c, 0x3a, 0xcd, 0xad, 0x06, 0x57, 0xfc, 0xcf, 0xec, 0x71, + 0x9b, 0x1f, 0x5c, 0x3e, 0x4e, 0x46, 0x04, 0x1f, 0x38, 0x81, 0x47, 0xfb, 0x4c, 0xfd, 0xb4, 0x77, + 0xa5, 0x24, 0x71, 0xf7, 0xa9, 0xa9, 0x69, 0x10, 0xb8, 0x55, 0x32, 0x2e, 0xdb, 0x63, 0x40, 0xd8, + 0xa0, 0x0e, 0xf0, 0x92, 0x35, 0x05, 0x11, 0xe3, 0x0a, 0xbe, 0xc1, 0xff, 0xf9, 0xe3, 0xa2, 0x6e, + 0x7f, 0xb2, 0x9f, 0x8c, 0x18, 0x30, 0x23, 0xc3, 0x58, 0x7e, 0x38, 0xda, 0x00, 0x77, 0xd9, 0xb4, + 0x76, 0x3e, 0x4e, 0x4b, 0x94, 0xb2, 0xbb, 0xc1, 0x94, 0xc6, 0x65, 0x1e, 0x77, 0xca, 0xf9, 0x92, + 0xee, 0xaa, 0xc0, 0x23, 0x2a, 0x28, 0x1b, 0xf6, 0xb3, 0xa7, 0x39, 0xc1, 0x22, 0x61, 0x16, 0x82, + 0x0a, 0xe8, 0xdb, 0x58, 0x47, 0xa6, 0x7c, 0xbe, 0xf9, 0xc9, 0x09, 0x1b, 0x46, 0x2d, 0x53, 0x8c, + 0xd7, 0x2b, 0x03, 0x74, 0x6a, 0xe7, 0x7f, 0x5e, 0x62, 0x29, 0x2c, 0x31, 0x15, 0x62, 0xa8, 0x46, + 0x50, 0x5d, 0xc8, 0x2d, 0xb8, 0x54, 0x33, 0x8a, 0xe4, 0x9f, 0x52, 0x35, 0xc9, 0x5b, 0x91, 0x17, + 0x8c, 0xcf, 0x2d, 0xd5, 0xca, 0xce, 0xf4, 0x03, 0xec, 0x9d, 0x18, 0x10, 0xc6, 0x27, 0x2b, 0x04, + 0x5b, 0x3b, 0x71, 0xf9, 0xdc, 0x6b, 0x80, 0xd6, 0x3f, 0xdd, 0x4a, 0x8e, 0x9a, 0xdb, 0x1e, 0x69, + 0x62, 0xa6, 0x95, 0x26, 0xd4, 0x31, 0x61, 0xc1, 0xa4, 0x1d, 0x57, 0x0d, 0x79, 0x38, 0xda, 0xd4, + 0xa4, 0x0e, 0x32, 0x9c, 0xcf, 0xf4, 0x6a, 0xaa, 0x36, 0xad, 0x00, 0x4c, 0xf6, 0x00, 0xc8, 0x38, + 0x1e, 0x42, 0x5a, 0x31, 0xd9, 0x51, 0xae, 0x64, 0xfd, 0xb2, 0x3f, 0xce, 0xc9, 0x50, 0x9d, 0x43, + 0x68, 0x7f, 0xeb, 0x69, 0xed, 0xd1, 0xcc, 0x5e, 0x0b, 0x8c, 0xc3, 0xbd, 0xf6, 0x4b, 0x10, 0xef, + 0x86, 0xb6, 0x31, 0x42, 0xa3, 0xab, 0x88, 0x29, 0x55, 0x5b, 0x2f, 0x74, 0x7c, 0x93, 0x26, 0x65, + 0xcb, 0x2c, 0x0f, 0x1c, 0xc0, 0x1b, 0xd7, 0x02, 0x29, 0x38, 0x88, 0x39, 0xd2, 0xaf, 0x05, 0xe4, + 0x54, 0x50, 0x4a, 0xc7, 0x8b, 0x75, 0x82, 0x82, 0x28, 0x46, 0xc0, 0xba, 0x35, 0xc3, 0x5f, 0x5c, + 0x59, 0x16, 0x0c, 0xc0, 0x46, 0xfd, 0x82, 0x51, 0x54, 0x1f, 0xc6, 0x8c, 0x9c, 0x86, 0xb0, 0x22, + 0xbb, 0x70, 0x99, 0x87, 0x6a, 0x46, 0x0e, 0x74, 0x51, 0xa8, 0xa9, 0x31, 0x09, 0x70, 0x3f, 0xee, + 0x1c, 0x21, 0x7e, 0x6c, 0x38, 0x26, 0xe5, 0x2c, 0x51, 0xaa, 0x69, 0x1e, 0x0e, 0x42, 0x3c, 0xfc, + 0x99, 0xe9, 0xe3, 0x16, 0x50, 0xc1, 0x21, 0x7b, 0x62, 0x48, 0x16, 0xcd, 0xad, 0x9a, 0x95, 0xf9, + 0xd5, 0xb8, 0x01, 0x94, 0x88, 0xd9, 0xc0, 0xa0, 0xa1, 0xfe, 0x30, 0x75, 0xa5, 0x77, 0xe2, 0x31, + 0x83, 0xf8, 0x1d, 0x4a, 0x3f, 0x2f, 0xa4, 0x57, 0x1e, 0xfc, 0x8c, 0xe0, 0xba, 0x8a, 0x4f, 0xe8, + 0xb6, 0x85, 0x5d, 0xfe, 0x72, 0xb0, 0xa6, 0x6e, 0xde, 0xd2, 0xfb, 0xab, 0xfb, 0xe5, 0x8a, 0x30, + 0xfa, 0xfa, 0xbe, 0x1c, 0x5d, 0x71, 0xa8, 0x7e, 0x2f, 0x74, 0x1e, 0xf8, 0xc1, 0xfe, 0x86, 0xfe, + 0xa6, 0xbb, 0xfd, 0xe5, 0x30, 0x67, 0x7f, 0x0d, 0x97, 0xd1, 0x1d, 0x49, 0xf7, 0xa8, 0x44, 0x3d, + 0x08, 0x22, 0xe5, 0x06, 0xa9, 0xf4, 0x61, 0x4e, 0x01, 0x1e, 0x2a, 0x94, 0x83, 0x8f, 0xf8, 0x8c, + 0xd6, 0x8c, 0x8b, 0xb7, 0xc5, 0xc6, 0x42, 0x4c, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, +}; + +static const unsigned char scr_dh_dh_1024_160_p[128] = { + 0xb1, 0x0b, 0x8f, 0x96, 0xa0, 0x80, 0xe0, 0x1d, 0xde, 0x92, 0xde, 0x5e, 0xae, 0x5d, 0x54, 0xec, + 0x52, 0xc9, 0x9f, 0xbc, 0xfb, 0x06, 0xa3, 0xc6, 0x9a, 0x6a, 0x9d, 0xca, 0x52, 0xd2, 0x3b, 0x61, + 0x60, 0x73, 0xe2, 0x86, 0x75, 0xa2, 0x3d, 0x18, 0x98, 0x38, 0xef, 0x1e, 0x2e, 0xe6, 0x52, 0xc0, + 0x13, 0xec, 0xb4, 0xae, 0xa9, 0x06, 0x11, 0x23, 0x24, 0x97, 0x5c, 0x3c, 0xd4, 0x9b, 0x83, 0xbf, + 0xac, 0xcb, 0xdd, 0x7d, 0x90, 0xc4, 0xbd, 0x70, 0x98, 0x48, 0x8e, 0x9c, 0x21, 0x9a, 0x73, 0x72, + 0x4e, 0xff, 0xd6, 0xfa, 0xe5, 0x64, 0x47, 0x38, 0xfa, 0xa3, 0x1a, 0x4f, 0xf5, 0x5b, 0xcc, 0xc0, + 0xa1, 0x51, 0xaf, 0x5f, 0x0d, 0xc8, 0xb4, 0xbd, 0x45, 0xbf, 0x37, 0xdf, 0x36, 0x5c, 0x1a, 0x65, + 0xe6, 0x8c, 0xfd, 0xa7, 0x6d, 0x4d, 0xa7, 0x08, 0xdf, 0x1f, 0xb2, 0xbc, 0x2e, 0x4a, 0x43, 0x71, +}; + +static const unsigned char scr_dh_dh_1024_160_g[128] = { + 0xa4, 0xd1, 0xcb, 0xd5, 0xc3, 0xfd, 0x34, 0x12, 0x67, 0x65, 0xa4, 0x42, 0xef, 0xb9, 0x99, 0x05, + 0xf8, 0x10, 0x4d, 0xd2, 0x58, 0xac, 0x50, 0x7f, 0xd6, 0x40, 0x6c, 0xff, 0x14, 0x26, 0x6d, 0x31, + 0x26, 0x6f, 0xea, 0x1e, 0x5c, 0x41, 0x56, 0x4b, 0x77, 0x7e, 0x69, 0x0f, 0x55, 0x04, 0xf2, 0x13, + 0x16, 0x02, 0x17, 0xb4, 0xb0, 0x1b, 0x88, 0x6a, 0x5e, 0x91, 0x54, 0x7f, 0x9e, 0x27, 0x49, 0xf4, + 0xd7, 0xfb, 0xd7, 0xd3, 0xb9, 0xa9, 0x2e, 0xe1, 0x90, 0x9d, 0x0d, 0x22, 0x63, 0xf8, 0x0a, 0x76, + 0xa6, 0xa2, 0x4c, 0x08, 0x7a, 0x09, 0x1f, 0x53, 0x1d, 0xbf, 0x0a, 0x01, 0x69, 0xb6, 0xa2, 0x8a, + 0xd6, 0x62, 0xa4, 0xd1, 0x8e, 0x73, 0xaf, 0xa3, 0x2d, 0x77, 0x9d, 0x59, 0x18, 0xd0, 0x8b, 0xc8, + 0x85, 0x8f, 0x4d, 0xce, 0xf9, 0x7c, 0x2a, 0x24, 0x85, 0x5e, 0x6e, 0xeb, 0x22, 0xb3, 0xb2, 0xe5, +}; + +static const unsigned char scr_dh_dh_1024_160_q[20] = { + 0xf5, 0x18, 0xaa, 0x87, 0x81, 0xa8, 0xdf, 0x27, 0x8a, 0xba, 0x4e, 0x7d, 0x64, 0xb7, 0xcb, 0x9d, + 0x49, 0x46, 0x23, 0x53, +}; + +static const unsigned char scr_dh_dh_2048_224_p[256] = { + 0xad, 0x10, 0x7e, 0x1e, 0x91, 0x23, 0xa9, 0xd0, 0xd6, 0x60, 0xfa, 0xa7, 0x95, 0x59, 0xc5, 0x1f, + 0xa2, 0x0d, 0x64, 0xe5, 0x68, 0x3b, 0x9f, 0xd1, 0xb5, 0x4b, 0x15, 0x97, 0xb6, 0x1d, 0x0a, 0x75, + 0xe6, 0xfa, 0x14, 0x1d, 0xf9, 0x5a, 0x56, 0xdb, 0xaf, 0x9a, 0x3c, 0x40, 0x7b, 0xa1, 0xdf, 0x15, + 0xeb, 0x3d, 0x68, 0x8a, 0x30, 0x9c, 0x18, 0x0e, 0x1d, 0xe6, 0xb8, 0x5a, 0x12, 0x74, 0xa0, 0xa6, + 0x6d, 0x3f, 0x81, 0x52, 0xad, 0x6a, 0xc2, 0x12, 0x90, 0x37, 0xc9, 0xed, 0xef, 0xda, 0x4d, 0xf8, + 0xd9, 0x1e, 0x8f, 0xef, 0x55, 0xb7, 0x39, 0x4b, 0x7a, 0xd5, 0xb7, 0xd0, 0xb6, 0xc1, 0x22, 0x07, + 0xc9, 0xf9, 0x8d, 0x11, 0xed, 0x34, 0xdb, 0xf6, 0xc6, 0xba, 0x0b, 0x2c, 0x8b, 0xbc, 0x27, 0xbe, + 0x6a, 0x00, 0xe0, 0xa0, 0xb9, 0xc4, 0x97, 0x08, 0xb3, 0xbf, 0x8a, 0x31, 0x70, 0x91, 0x88, 0x36, + 0x81, 0x28, 0x61, 0x30, 0xbc, 0x89, 0x85, 0xdb, 0x16, 0x02, 0xe7, 0x14, 0x41, 0x5d, 0x93, 0x30, + 0x27, 0x82, 0x73, 0xc7, 0xde, 0x31, 0xef, 0xdc, 0x73, 0x10, 0xf7, 0x12, 0x1f, 0xd5, 0xa0, 0x74, + 0x15, 0x98, 0x7d, 0x9a, 0xdc, 0x0a, 0x48, 0x6d, 0xcd, 0xf9, 0x3a, 0xcc, 0x44, 0x32, 0x83, 0x87, + 0x31, 0x5d, 0x75, 0xe1, 0x98, 0xc6, 0x41, 0xa4, 0x80, 0xcd, 0x86, 0xa1, 0xb9, 0xe5, 0x87, 0xe8, + 0xbe, 0x60, 0xe6, 0x9c, 0xc9, 0x28, 0xb2, 0xb9, 0xc5, 0x21, 0x72, 0xe4, 0x13, 0x04, 0x2e, 0x9b, + 0x23, 0xf1, 0x0b, 0x0e, 0x16, 0xe7, 0x97, 0x63, 0xc9, 0xb5, 0x3d, 0xcf, 0x4b, 0xa8, 0x0a, 0x29, + 0xe3, 0xfb, 0x73, 0xc1, 0x6b, 0x8e, 0x75, 0xb9, 0x7e, 0xf3, 0x63, 0xe2, 0xff, 0xa3, 0x1f, 0x71, + 0xcf, 0x9d, 0xe5, 0x38, 0x4e, 0x71, 0xb8, 0x1c, 0x0a, 0xc4, 0xdf, 0xfe, 0x0c, 0x10, 0xe6, 0x4f, +}; + +static const unsigned char scr_dh_dh_2048_224_g[256] = { + 0xac, 0x40, 0x32, 0xef, 0x4f, 0x2d, 0x9a, 0xe3, 0x9d, 0xf3, 0x0b, 0x5c, 0x8f, 0xfd, 0xac, 0x50, + 0x6c, 0xde, 0xbe, 0x7b, 0x89, 0x99, 0x8c, 0xaf, 0x74, 0x86, 0x6a, 0x08, 0xcf, 0xe4, 0xff, 0xe3, + 0xa6, 0x82, 0x4a, 0x4e, 0x10, 0xb9, 0xa6, 0xf0, 0xdd, 0x92, 0x1f, 0x01, 0xa7, 0x0c, 0x4a, 0xfa, + 0xab, 0x73, 0x9d, 0x77, 0x00, 0xc2, 0x9f, 0x52, 0xc5, 0x7d, 0xb1, 0x7c, 0x62, 0x0a, 0x86, 0x52, + 0xbe, 0x5e, 0x90, 0x01, 0xa8, 0xd6, 0x6a, 0xd7, 0xc1, 0x76, 0x69, 0x10, 0x19, 0x99, 0x02, 0x4a, + 0xf4, 0xd0, 0x27, 0x27, 0x5a, 0xc1, 0x34, 0x8b, 0xb8, 0xa7, 0x62, 0xd0, 0x52, 0x1b, 0xc9, 0x8a, + 0xe2, 0x47, 0x15, 0x04, 0x22, 0xea, 0x1e, 0xd4, 0x09, 0x93, 0x9d, 0x54, 0xda, 0x74, 0x60, 0xcd, + 0xb5, 0xf6, 0xc6, 0xb2, 0x50, 0x71, 0x7c, 0xbe, 0xf1, 0x80, 0xeb, 0x34, 0x11, 0x8e, 0x98, 0xd1, + 0x19, 0x52, 0x9a, 0x45, 0xd6, 0xf8, 0x34, 0x56, 0x6e, 0x30, 0x25, 0xe3, 0x16, 0xa3, 0x30, 0xef, + 0xbb, 0x77, 0xa8, 0x6f, 0x0c, 0x1a, 0xb1, 0x5b, 0x05, 0x1a, 0xe3, 0xd4, 0x28, 0xc8, 0xf8, 0xac, + 0xb7, 0x0a, 0x81, 0x37, 0x15, 0x0b, 0x8e, 0xeb, 0x10, 0xe1, 0x83, 0xed, 0xd1, 0x99, 0x63, 0xdd, + 0xd9, 0xe2, 0x63, 0xe4, 0x77, 0x05, 0x89, 0xef, 0x6a, 0xa2, 0x1e, 0x7f, 0x5f, 0x2f, 0xf3, 0x81, + 0xb5, 0x39, 0xcc, 0xe3, 0x40, 0x9d, 0x13, 0xcd, 0x56, 0x6a, 0xfb, 0xb4, 0x8d, 0x6c, 0x01, 0x91, + 0x81, 0xe1, 0xbc, 0xfe, 0x94, 0xb3, 0x02, 0x69, 0xed, 0xfe, 0x72, 0xfe, 0x9b, 0x6a, 0xa4, 0xbd, + 0x7b, 0x5a, 0x0f, 0x1c, 0x71, 0xcf, 0xff, 0x4c, 0x19, 0xc4, 0x18, 0xe1, 0xf6, 0xec, 0x01, 0x79, + 0x81, 0xbc, 0x08, 0x7f, 0x2a, 0x70, 0x65, 0xb3, 0x84, 0xb8, 0x90, 0xd3, 0x19, 0x1f, 0x2b, 0xfa, +}; + +static const unsigned char scr_dh_dh_2048_224_q[28] = { + 0x80, 0x1c, 0x0d, 0x34, 0xc5, 0x8d, 0x93, 0xfe, 0x99, 0x71, 0x77, 0x10, 0x1f, 0x80, 0x53, 0x5a, + 0x47, 0x38, 0xce, 0xbc, 0xbf, 0x38, 0x9a, 0x99, 0xb3, 0x63, 0x71, 0xeb, +}; + +static const unsigned char scr_dh_dh_2048_256_p[256] = { + 0x87, 0xa8, 0xe6, 0x1d, 0xb4, 0xb6, 0x66, 0x3c, 0xff, 0xbb, 0xd1, 0x9c, 0x65, 0x19, 0x59, 0x99, + 0x8c, 0xee, 0xf6, 0x08, 0x66, 0x0d, 0xd0, 0xf2, 0x5d, 0x2c, 0xee, 0xd4, 0x43, 0x5e, 0x3b, 0x00, + 0xe0, 0x0d, 0xf8, 0xf1, 0xd6, 0x19, 0x57, 0xd4, 0xfa, 0xf7, 0xdf, 0x45, 0x61, 0xb2, 0xaa, 0x30, + 0x16, 0xc3, 0xd9, 0x11, 0x34, 0x09, 0x6f, 0xaa, 0x3b, 0xf4, 0x29, 0x6d, 0x83, 0x0e, 0x9a, 0x7c, + 0x20, 0x9e, 0x0c, 0x64, 0x97, 0x51, 0x7a, 0xbd, 0x5a, 0x8a, 0x9d, 0x30, 0x6b, 0xcf, 0x67, 0xed, + 0x91, 0xf9, 0xe6, 0x72, 0x5b, 0x47, 0x58, 0xc0, 0x22, 0xe0, 0xb1, 0xef, 0x42, 0x75, 0xbf, 0x7b, + 0x6c, 0x5b, 0xfc, 0x11, 0xd4, 0x5f, 0x90, 0x88, 0xb9, 0x41, 0xf5, 0x4e, 0xb1, 0xe5, 0x9b, 0xb8, + 0xbc, 0x39, 0xa0, 0xbf, 0x12, 0x30, 0x7f, 0x5c, 0x4f, 0xdb, 0x70, 0xc5, 0x81, 0xb2, 0x3f, 0x76, + 0xb6, 0x3a, 0xca, 0xe1, 0xca, 0xa6, 0xb7, 0x90, 0x2d, 0x52, 0x52, 0x67, 0x35, 0x48, 0x8a, 0x0e, + 0xf1, 0x3c, 0x6d, 0x9a, 0x51, 0xbf, 0xa4, 0xab, 0x3a, 0xd8, 0x34, 0x77, 0x96, 0x52, 0x4d, 0x8e, + 0xf6, 0xa1, 0x67, 0xb5, 0xa4, 0x18, 0x25, 0xd9, 0x67, 0xe1, 0x44, 0xe5, 0x14, 0x05, 0x64, 0x25, + 0x1c, 0xca, 0xcb, 0x83, 0xe6, 0xb4, 0x86, 0xf6, 0xb3, 0xca, 0x3f, 0x79, 0x71, 0x50, 0x60, 0x26, + 0xc0, 0xb8, 0x57, 0xf6, 0x89, 0x96, 0x28, 0x56, 0xde, 0xd4, 0x01, 0x0a, 0xbd, 0x0b, 0xe6, 0x21, + 0xc3, 0xa3, 0x96, 0x0a, 0x54, 0xe7, 0x10, 0xc3, 0x75, 0xf2, 0x63, 0x75, 0xd7, 0x01, 0x41, 0x03, + 0xa4, 0xb5, 0x43, 0x30, 0xc1, 0x98, 0xaf, 0x12, 0x61, 0x16, 0xd2, 0x27, 0x6e, 0x11, 0x71, 0x5f, + 0x69, 0x38, 0x77, 0xfa, 0xd7, 0xef, 0x09, 0xca, 0xdb, 0x09, 0x4a, 0xe9, 0x1e, 0x1a, 0x15, 0x97, +}; + +static const unsigned char scr_dh_dh_2048_256_g[256] = { + 0x3f, 0xb3, 0x2c, 0x9b, 0x73, 0x13, 0x4d, 0x0b, 0x2e, 0x77, 0x50, 0x66, 0x60, 0xed, 0xbd, 0x48, + 0x4c, 0xa7, 0xb1, 0x8f, 0x21, 0xef, 0x20, 0x54, 0x07, 0xf4, 0x79, 0x3a, 0x1a, 0x0b, 0xa1, 0x25, + 0x10, 0xdb, 0xc1, 0x50, 0x77, 0xbe, 0x46, 0x3f, 0xff, 0x4f, 0xed, 0x4a, 0xac, 0x0b, 0xb5, 0x55, + 0xbe, 0x3a, 0x6c, 0x1b, 0x0c, 0x6b, 0x47, 0xb1, 0xbc, 0x37, 0x73, 0xbf, 0x7e, 0x8c, 0x6f, 0x62, + 0x90, 0x12, 0x28, 0xf8, 0xc2, 0x8c, 0xbb, 0x18, 0xa5, 0x5a, 0xe3, 0x13, 0x41, 0x00, 0x0a, 0x65, + 0x01, 0x96, 0xf9, 0x31, 0xc7, 0x7a, 0x57, 0xf2, 0xdd, 0xf4, 0x63, 0xe5, 0xe9, 0xec, 0x14, 0x4b, + 0x77, 0x7d, 0xe6, 0x2a, 0xaa, 0xb8, 0xa8, 0x62, 0x8a, 0xc3, 0x76, 0xd2, 0x82, 0xd6, 0xed, 0x38, + 0x64, 0xe6, 0x79, 0x82, 0x42, 0x8e, 0xbc, 0x83, 0x1d, 0x14, 0x34, 0x8f, 0x6f, 0x2f, 0x91, 0x93, + 0xb5, 0x04, 0x5a, 0xf2, 0x76, 0x71, 0x64, 0xe1, 0xdf, 0xc9, 0x67, 0xc1, 0xfb, 0x3f, 0x2e, 0x55, + 0xa4, 0xbd, 0x1b, 0xff, 0xe8, 0x3b, 0x9c, 0x80, 0xd0, 0x52, 0xb9, 0x85, 0xd1, 0x82, 0xea, 0x0a, + 0xdb, 0x2a, 0x3b, 0x73, 0x13, 0xd3, 0xfe, 0x14, 0xc8, 0x48, 0x4b, 0x1e, 0x05, 0x25, 0x88, 0xb9, + 0xb7, 0xd2, 0xbb, 0xd2, 0xdf, 0x01, 0x61, 0x99, 0xec, 0xd0, 0x6e, 0x15, 0x57, 0xcd, 0x09, 0x15, + 0xb3, 0x35, 0x3b, 0xbb, 0x64, 0xe0, 0xec, 0x37, 0x7f, 0xd0, 0x28, 0x37, 0x0d, 0xf9, 0x2b, 0x52, + 0xc7, 0x89, 0x14, 0x28, 0xcd, 0xc6, 0x7e, 0xb6, 0x18, 0x4b, 0x52, 0x3d, 0x1d, 0xb2, 0x46, 0xc3, + 0x2f, 0x63, 0x07, 0x84, 0x90, 0xf0, 0x0e, 0xf8, 0xd6, 0x47, 0xd1, 0x48, 0xd4, 0x79, 0x54, 0x51, + 0x5e, 0x23, 0x27, 0xcf, 0xef, 0x98, 0xc5, 0x82, 0x66, 0x4b, 0x4c, 0x0f, 0x6c, 0xc4, 0x16, 0x59, +}; + +static const unsigned char scr_dh_dh_2048_256_q[32] = { + 0x8c, 0xf8, 0x36, 0x42, 0xa7, 0x09, 0xa0, 0x97, 0xb4, 0x47, 0x99, 0x76, 0x40, 0x12, 0x9d, 0xa2, + 0x99, 0xb1, 0xa4, 0x7d, 0x1e, 0xb3, 0x75, 0x0b, 0xa3, 0x08, 0xb0, 0xfe, 0x64, 0xf5, 0xfb, 0xd3, +}; + +#endif diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index a427fc9a1..edc939ece 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -1,6 +1,7 @@ /* scriptc runtime — dynamic values: the checked-dynamic tree, native handles, * dyn promises, the ambient receiver and listener adapters (scr_json.c, - * scr_dyn_handle.c, scr_async_dyn.c). Included by scr_runtime.h, which says + * scr_dyn_handle.c, scr_async_dyn.c), and the message readers of typed + * 'message' listeners (scr_message.c). Included by scr_runtime.h, which says * where new declarations go. */ #ifndef SCR_DYN_H #define SCR_DYN_H @@ -143,6 +144,7 @@ typedef enum { X(ABORT_CONTROLLER, 15) /* native static-fetch AbortController */ \ X(ABORT_SIGNAL, 8) /* native static-fetch AbortSignal */ \ X(ARRAY_BUFFER, 18) /* fixed-length ArrayBuffer storage */ \ + X(ASYMMETRIC_CRYPTO_KEY, 69) /* WebCrypto ECDSA and RSA CryptoKey (scr_webcrypto_pki.c) */ \ X(BLOB, 41) /* ScrBlob — new Blob() and Blob#slice() */ \ X(BROADCAST_CHANNEL, 51) /* worker_threads BroadcastChannel (scr_worker_events.c) */ \ X(BUILTIN_MODULE, 21) /* native builtin export object */ \ @@ -155,6 +157,8 @@ typedef enum { X(DATE, 30) /* JavaScript Date with stable native identity */ \ X(DECIPHER, 46) /* node:crypto Decipheriv (scr_cipher.c) */ \ X(DGRAM_SOCKET, 62) /* dgram.Socket (scr_dgram.c) */ \ + X(DIFFIE_HELLMAN, 70) /* node:crypto DiffieHellman (scr_dh.c) */ \ + X(DIFFIE_HELLMAN_GROUP, 71) /* node:crypto DiffieHellmanGroup (scr_dh.c) */ \ X(DIR, 44) /* fs.Dir (scr_lib.c) */ \ X(DISPOSABLE_STACK, 49) /* DisposableStack (scr_symbol.c) */ \ X(ECDH, 64) /* node:crypto ECDH (scr_ecdh.c) */ \ @@ -185,14 +189,18 @@ typedef enum { X(NET_SERVER, 3) /* ScrNetServer — net.Server (http.Server rides the same handle) */ \ X(NET_SOCKET, 2) /* ScrNetSocket — net.Socket */ \ X(NET_SOCKET_ADDRESS, 47) /* net.SocketAddress (scr_net.c) */ \ + X(PRIVATE_KEY, 65) /* node:crypto PrivateKeyObject (scr_pki.c) */ \ + X(PUBLIC_KEY, 66) /* node:crypto PublicKeyObject (scr_pki.c) */ \ X(REGEXP, 26) /* native RegExp, shared compiled pattern */ \ X(SEARCH_PARAMS, 33) /* live WHATWG URLSearchParams */ \ X(SEGMENTER, 19) /* default Unicode grapheme segmentation */ \ X(SEGMENTS, 20) /* iterable immutable segmentation result */ \ X(SET, 25) /* native Set, shared backing map */ \ X(SHARED_ARRAY_BUFFER, 37) \ + X(SIGN, 67) /* node:crypto Sign (scr_pki.c) */ \ X(STDIO, 27) /* stable process stdin/stdout/stderr values */ \ X(URL, 28) /* native WHATWG URL */ \ + X(VERIFY, 68) /* node:crypto Verify (scr_pki.c) */ \ X(WASM_GLOBAL, 61) /* WebAssembly.Global */ \ X(WASM_INSTANCE, 58) /* WebAssembly.Instance */ \ X(WASM_MEMORY, 59) /* WebAssembly.Memory */ \ @@ -247,6 +255,55 @@ typedef struct ScrDynTypedCast { * result as an owned (+1) dyn value. `args` entries are BORROWED. */ typedef ScrDyn *(*ScrDynThunk)(ScrClosure *clo, ScrDyn *const *args, size_t argc); +/* A compiled 'message' listener's typed entry: builds the closure's + * parameter from the message's nodes (the readers below) and calls + * the closure. Answers false, without calling it, when the message does + * not have the parameter's layout; the caller then decodes the message + * and calls the thunk. */ +struct ScrMessage; +typedef bool (*ScrDynMessageListener)(ScrClosure *clo, const struct ScrMessage *message); + +/* The kinds of a message's nodes (scr_message.c). */ +typedef enum { + SCR_MESSAGE_UNDEFINED, + SCR_MESSAGE_NULL, + SCR_MESSAGE_BOOL, + SCR_MESSAGE_NUMBER, + SCR_MESSAGE_STRING, + SCR_MESSAGE_BIGINT, + SCR_MESSAGE_ARRAY, + SCR_MESSAGE_OBJECT, + SCR_MESSAGE_STORAGE, + SCR_MESSAGE_BYTES, + SCR_MESSAGE_ARRAY_BUFFER, + SCR_MESSAGE_ERROR, + /* A deeply frozen plain-data graph shared by pointer (see publication). */ + SCR_MESSAGE_PUBLISHED, + /* A typed value published with @scriptc/threads (immortal), delivered as + * a fresh typed-ref capsule over the same pointer. */ + SCR_MESSAGE_TYPED_REF, + /* A transferred MessagePort: its channel end (+1) and side. */ + SCR_MESSAGE_PORT, + /* A native handle cloned through its unit's ScrDynHandleTransfer. */ + SCR_MESSAGE_HANDLE, +} ScrMessageKind; + +/* Readers for compiled 'message' listeners (ScrDynMessageListener), which + * build their typed parameter from the nodes. Node 0 is the root, and a + * node's values have higher numbers. scr_message_readable answers whether + * the message can reach these listeners: no node is reached twice, and it + * holds no ports or native handles, which the receiver adopts when it + * decodes the message. kind answers a ScrMessageKind; number reads a + * NUMBER or BOOL node, string a STRING node (+1). fields answers whether an + * OBJECT node has exactly the given keys, in order, and writes their value + * nodes. */ +bool scr_message_readable(const struct ScrMessage *message); +int32_t scr_message_kind(const struct ScrMessage *message, size_t node); +double scr_message_number(const struct ScrMessage *message, size_t node); +ScrStr *scr_message_string(const struct ScrMessage *message, size_t node); +bool scr_message_fields(const struct ScrMessage *message, size_t node, const char *const *names, + const size_t *lengths, size_t count, size_t *values); + /* Object member. Keys own shared immutable UTF-8 bytes (NUL-terminated for * convenience; key_len excludes the NUL) — duplicate keys were already * collapsed at parse time (later wins, like JS JSON.parse). */ @@ -307,6 +364,9 @@ typedef struct ScrDynTypedRecordOps { * for-in over a record with every field present (atomic slot). */ ScrArr *keys; } ScrDynTypedRecordOps; +/* A record capsule's ops while it answers from the native record (no view + * created yet), or NULL. */ +const ScrDynTypedRecordOps *scr_dyn_typed_ref_live_record(const ScrDyn *d); /* Prototype members of a compiled class seen as `unknown`, emitted per * class by the compiler: the string names of methods, getters and setters @@ -384,7 +444,7 @@ struct ScrDyn { * and `name` are static compiler-emitted literals (never freed); name * may be NULL (anonymous — inspect prints [Function (anonymous)]). * The dyn→closure ownership edge is traced by the cycle collector. */ - struct { ScrClosure *clo; ScrDynThunk thunk; const char *sig; const char *name; uint32_t arity; ScrClassObj *class_obj; } fn; + struct { ScrClosure *clo; ScrDynThunk thunk; const char *sig; const char *name; uint32_t arity; ScrClassObj *class_obj; ScrDynMessageListener message; } fn; /* Native handles normally keep conservative ownership edges. Sets * backed by cycle-capable maps expose their edge to the collector. * Cache that allocation property: during collection the map may be @@ -759,6 +819,9 @@ ScrDyn *scr_dyn_array_constructor(void); /* The interned builtin constructor `name` (Object, Date, RegExp, Map, Set, * Promise, WeakMap, WeakSet) as a dynamic function value: +1. */ ScrDyn *scr_dyn_global_constructor(const ScrStr *name); +/* The interned function value of an error constructor ("TypeError"), or + * NULL for other names. */ +ScrDyn *scr_dyn_error_constructor(const char *name); /* A builtin global function (setTimeout, setImmediate, process.nextTick, ...) * as an interned dynamic function value: +1. */ ScrDyn *scr_dyn_builtin_function(const ScrStr *name); @@ -868,6 +931,7 @@ void scr_dyn_proxy_unsupported(const char *operation); ScrDyn *scr_dyn_new_bytes(const ScrBytes *b); ScrDyn *scr_bytes_constructor(const ScrStr *name); bool scr_bytes_instanceof(const ScrDyn *value, const ScrDyn *callee); +bool scr_bytes_instanceof_at(const ScrDyn *value, const ScrDyn *callee, const ScrStr *refusal); bool scr_dyn_data_view_is(const ScrDyn *value); ScrDyn *scr_dyn_construct(const ScrDyn *callee, const ScrDyn *args, const ScrStr *what); ScrDyn *scr_bytes_construct(const ScrDyn *callee, const ScrDyn *args, const ScrStr *what); @@ -1236,6 +1300,9 @@ ScrDyn *scr_dyn_define_prototype_props(ScrDyn *target, ScrDyn *descs, ScrDyn *pr * (callers retain first when they keep their own reference); `sig`/`name` * must be static literals (the box never frees them; name may be NULL). */ ScrDyn *scr_dyn_new_func(ScrClosure *clo, ScrDynThunk thunk, uint32_t arity, const char *sig, const char *name); +/* Gives a function box made with thunk its typed message entry (an + * EventEmitter listener registered from it keeps the entry). */ +void scr_dyn_func_set_message(ScrDyn *fn, ScrDynThunk thunk, ScrDynMessageListener listener); ScrDyn *scr_dyn_new_class(ScrClassObj *cls, const char *type_key); /* EventEmitter and the stream constructors as untyped constructor values, * and instanceof over untyped constructor functions diff --git a/packages/runtime/src/scr_dyn_builtin_stream.c b/packages/runtime/src/scr_dyn_builtin_stream.c index 209c8c39a..cdd815642 100644 --- a/packages/runtime/src/scr_dyn_builtin_stream.c +++ b/packages/runtime/src/scr_dyn_builtin_stream.c @@ -34,8 +34,7 @@ * receiver without a reference; the hook closures outlive the record. * * Not supported here (each throws the "not supported yet" Error where it - * runs): objectMode and the other options scr_stream.c's dyn option walk - * refuses, `_writev` and `_construct`, symbol event names, listeners() / + * runs): the options scr_stream.c's dyn option walk refuses, `_writev` and `_construct`, symbol event names, listeners() / * rawListeners(), `wrap()`, and pipes into values that are neither runtime * streams nor constructor-function streams. */ #include "scr_runtime.h" @@ -275,11 +274,10 @@ static ScrDyn *bcs_done_call(ScrClosure *closure, ScrDyn *const *args, size_t ar break; } default: { + /* the data is pushed as push(data) pushes it */ ScrDyn *data = bcs_arg(args, argc, 1); - ScrBytes *bytes = data->kind == SCR_DYN_BYTES ? scr_bytes_raw_view(data->v.bytes) : NULL; - ScrStr *text = data->kind == SCR_DYN_STR ? scr_str_retain(data->v.str) : NULL; - if (which == BCS_DONE_TRANSFORM) scr_stream_transform_done(s, err, bytes, text); - else scr_stream_flush_done(s, err, bytes, text); + if (which == BCS_DONE_TRANSFORM) scr_stream_transform_done_value(s, err, data); + else scr_stream_flush_done_value(s, err, data); break; } } @@ -362,13 +360,14 @@ static void bcs_read_inv(ScrClosure *cb, ScrStream *s, double size) { scr_dyn_release(object); } -/* _write(chunk, 'buffer', cb) and _transform(chunk, 'buffer', cb): a - * throw propagates to the write() caller, as in Node. */ +/* _write(chunk, 'buffer', cb) and _transform(chunk, 'buffer', cb), or the + * value and the write's encoding argument on an object-mode writable + * side: a throw propagates to the write() caller, as in Node. */ static void bcs_chunk_call(ScrClosure *cb, ScrStream *s, ScrBytes *chunk, const char *method, int done) { ScrDyn *object = scr_box_get_ref(cb->caps[0]); ScrDyn *args[3]; - args[0] = scr_dyn_new_buffer(chunk); - args[1] = bcs_str("buffer"); + args[0] = scr_stream_chunk_value(s, chunk); + args[1] = scr_stream_chunk_encoding_value(s); args[2] = bcs_done(s, done); ScrDyn *result = bcs_invoke(object, method, args, 3); for (int i = 0; i < 3; i++) scr_dyn_release(args[i]); @@ -488,8 +487,8 @@ static void bcs_listener0(ScrClosure *adapter) { bcs_listener_call(adapter, NULL, 0); } -static void bcs_listener_data(ScrClosure *adapter, ScrBytes *bytes, ScrStr *text) { - ScrDyn *arg = text ? scr_dyn_new_str(text) : scr_dyn_new_buffer(bytes); +static void bcs_listener_data(ScrClosure *adapter, ScrBytes *bytes, ScrStr *text, ScrDyn *value) { + ScrDyn *arg = value ? scr_dyn_retain(value) : text ? scr_dyn_new_str(text) : scr_dyn_new_buffer(bytes); bcs_listener_call(adapter, &arg, 1); scr_dyn_release(arg); } @@ -583,7 +582,7 @@ static bool bcs_add_listener(ScrStream *s, ScrDyn *self, ScrDyn *type, ScrDyn *l break; case 1: chained = scr_emitter_on_dyn(em, name, listener, bcs_adapter(listener, self, (void *)bcs_listener_data), - scr_ee_inv_fixed2, once, prepend); + scr_ee_inv_fixed3, once, prepend); break; case 2: chained = scr_emitter_on_dyn(em, name, listener, bcs_adapter(listener, self, (void *)bcs_listener_error), @@ -625,14 +624,15 @@ static ScrDyn *bcs_emit(ScrStream *s, ScrDyn *const *args, size_t argc) { had = scr_emitter_emit(em, name); break; case 1: + /* 'data' carries the bytes, string and value slots (scr_stream_emit_data) */ if (first->kind == SCR_DYN_BYTES) { ScrBytes *bytes = scr_bytes_raw_view(first->v.bytes); - had = scr_emitter_emit(em, name, bytes, (ScrStr *)NULL); + had = scr_emitter_emit(em, name, bytes, (ScrStr *)NULL, first); scr_bytes_release(bytes); } else if (first->kind == SCR_DYN_STR) { - had = scr_emitter_emit(em, name, (ScrBytes *)NULL, first->v.str); + had = scr_emitter_emit(em, name, (ScrBytes *)NULL, first->v.str, first); } else { - bcs_unsupported("emit('data') with a chunk other than a Buffer or string on a constructor-function stream is not supported yet"); + had = scr_emitter_emit(em, name, (ScrBytes *)NULL, (ScrStr *)NULL, first); } break; case 2: @@ -1037,10 +1037,16 @@ static ScrDyn *bcs_write(ScrStream *s, ScrDyn *self, ScrDyn *const *args, size_t ScrDyn *cb = bcs_arg(args, argc, 2); if (scr_dyn_is_callable(encoding)) { cb = encoding; - encoding = scr_dyn_undefined(); + encoding = scr_stream_writes_values(s) ? scr_dyn_new_null() : scr_dyn_undefined(); + } + bool below; + if (scr_stream_writes_values(s)) { + /* an object-mode side hands the encoding argument to _write */ + below = scr_stream_write_enc(s, chunk, encoding, bcs_callback(cb, self, s)); + } else { + if (chunk->kind == SCR_DYN_STR && !bcs_utf8_only(encoding)) return NULL; + below = scr_stream_write_dyn(s, chunk, bcs_callback(cb, self, s)); } - if (chunk->kind == SCR_DYN_STR && !bcs_utf8_only(encoding)) return NULL; - bool below = scr_stream_write_dyn(s, chunk, bcs_callback(cb, self, s)); if (scr_exc_pending()) return NULL; return scr_dyn_new_bool(below); } @@ -1055,11 +1061,17 @@ static ScrDyn *bcs_end(ScrStream *s, ScrDyn *self, ScrDyn *const *args, size_t a encoding = scr_dyn_undefined(); } else if (scr_dyn_is_callable(encoding)) { cb = encoding; - encoding = scr_dyn_undefined(); + encoding = scr_dyn_new_null(); } - if (chunk->kind == SCR_DYN_STR && !bcs_utf8_only(encoding)) return NULL; ScrStream *chained = NULL; - if (bcs_nullish(chunk)) { + if (!bcs_nullish(chunk) && scr_stream_writes_values(s)) { + /* end(chunk) writes the value with the encoding argument */ + scr_stream_write_enc(s, chunk, encoding, NULL); + if (scr_exc_pending()) return NULL; + chained = scr_stream_end(s, NULL, NULL, bcs_callback(cb, self, s)); + } else if (chunk->kind == SCR_DYN_STR && !bcs_utf8_only(encoding)) { + return NULL; + } else if (bcs_nullish(chunk)) { chained = scr_stream_end(s, NULL, NULL, bcs_callback(cb, self, s)); } else if (chunk->kind == SCR_DYN_STR) { chained = scr_stream_end(s, NULL, chunk->v.str, bcs_callback(cb, self, s)); @@ -1376,8 +1388,6 @@ static ScrDyn *bcs_getter_call(ScrClosure *closure, ScrDyn *const *args, size_t return bcs_flowing(s); case BCS_P_ERRORED: return bcs_errored(s); - case BCS_P_WRITABLE_OBJECT_MODE: - return scr_dyn_new_bool(false); case BCS_P_READABLE_LENGTH: case BCS_P_READABLE_HWM: case BCS_P_WRITABLE_LENGTH: diff --git a/packages/runtime/src/scr_dyn_invoke.c b/packages/runtime/src/scr_dyn_invoke.c index ec49dd383..80e01b08c 100644 --- a/packages/runtime/src/scr_dyn_invoke.c +++ b/packages/runtime/src/scr_dyn_invoke.c @@ -1923,6 +1923,8 @@ static ScrDyn *dyn_function_constructor(void) { ScrDyn *scr_dyn_global_constructor(const ScrStr *name) { if (strcmp(name->data, dyn_s20) == 0) return dyn_function_constructor(); + ScrDyn *error = scr_dyn_error_constructor(name->data); + if (error) return error; ScrDyn *ctor = dyn_global_ctor(name->data); if (!ctor) scr_trap(dyn_s3); return ctor; diff --git a/packages/runtime/src/scr_ecdh.c b/packages/runtime/src/scr_ecdh.c index fc8797571..5edbadba6 100644 --- a/packages/runtime/src/scr_ecdh.c +++ b/packages/runtime/src/scr_ecdh.c @@ -21,6 +21,7 @@ #include "mbedtls/ecp.h" #include "mbedtls/platform_util.h" +#include "scr_crypto_args.h" #include "scr_ecdh_names.h" typedef struct { @@ -73,6 +74,9 @@ static int se_random(void *state, unsigned char *out, size_t len) { return 0; } +/* The value the SC2020 error for an unlowered encoding or format names. */ +#define SE_SUBJECT "An ECDH encoding or format" + /* ── errors ───────────────────────────────────────────────────────────── */ static void se_throw(int kind, const char *code, const char *message) { @@ -83,51 +87,6 @@ static void se_operation_failed(const char *message) { se_throw(SCR_ERR_ERROR, "ERR_CRYPTO_OPERATION_FAILED", message); } -/* An object's toString the program defined, as an own or inherited - * function property. */ -static bool se_custom_to_string(ScrDyn *value) { - if (value->kind != SCR_DYN_OBJ && value->kind != SCR_DYN_TYPED_REF) return false; - ScrDyn *member = scr_dyn_obj_read(value, "toString", 8); - bool custom = member && member->kind == SCR_DYN_FUNC; - scr_dyn_release(member); - return custom; -} - -/* Whether value is a class instance whose class defines toString. */ -static bool se_class_to_string(const ScrDyn *value) { - ScrStr *name = scr_str_new("toString", 8); - bool method = scr_dyn_typed_ref_class_has(value, name); - scr_str_release(name); - return method; -} - -/* `prefix` and the value as util.format's %s renders it: strings and - * objects with a program-defined toString as String(value), numbers as - * util.inspect does, other values inspected at depth 0. Functions and - * class instances that define toString have no lowering. */ -static void se_throw_formatted(const char *code, const char *prefix, const ScrDyn *value) { - ScrDyn *v = (ScrDyn *)value; - if (v->kind == SCR_DYN_FUNC || se_class_to_string(v)) { - static const char message[] = "An ECDH encoding or format that is a function or an instance of a class " - "defining toString has no lowering; pass the name as a string"; - scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); - return; - } - ScrStr *text; - if (v->kind == SCR_DYN_STR || se_custom_to_string(v)) text = scr_dyn_string_coerce_js(v); - else if (v->kind == SCR_DYN_NUM) text = scr_insp_f64(v->v.num); - else text = scr_insp_dyn_s(v, 0); - if (!text) return; - ScrJsonBuf b; - scr_jb_init(&b); - scr_jb_puts(&b, prefix); - scr_jb_put_str(&b, text); - scr_str_release(text); - ScrStr *message = scr_jb_finish(&b); - scr_throw_error_msg_code(SCR_ERR_TYPE, message->data, message->len, code); - scr_str_release(message); -} - /* ── names and encodings ──────────────────────────────────────────────── */ static int se_name_cmp(const void *key, const void *entry) { @@ -198,129 +157,16 @@ static bool se_load_group(const ScrStr *name, mbedtls_ecp_group *grp, const char return false; } -static bool se_ascii_eq_ci(const char *a, size_t a_len, const char *b) { - size_t b_len = strlen(b); - if (a_len != b_len) return false; - for (size_t i = 0; i < a_len; i++) { - char x = a[i]; - if (x >= 'A' && x <= 'Z') x = (char)(x - 'A' + 'a'); - if (x != b[i]) return false; - } - return true; -} - -static bool se_str_is(const ScrStr *text, const char *literal) { - size_t len = strlen(literal); - return text->len == len && memcmp(text->data, literal, len) == 0; -} - -/* Node's normalizeEncoding: the canonical name, or NULL. */ -static const char *se_normalize_encoding(const ScrStr *name) { - static const struct { const char *from; const char *to; } map[] = { - {"utf8", "utf8"}, {"utf-8", "utf8"}, {"hex", "hex"}, - {"base64", "base64"}, {"base64url", "base64url"}, {"latin1", "latin1"}, - {"binary", "latin1"}, {"ascii", "ascii"}, {"utf16le", "utf16le"}, - {"utf-16le", "utf16le"}, {"ucs2", "utf16le"}, {"ucs-2", "utf16le"}, - }; - for (size_t i = 0; i < sizeof map / sizeof map[0]; i++) - if (se_ascii_eq_ci(name->data, name->len, map[i].from)) return map[i].to; - return NULL; -} - -/* The value a typed-ref box holds when it is a string, view, ArrayBuffer - * or native handle (+1). Other values are retained as passed, so argument - * errors describe the original value. NULL after throwing. */ -static ScrDyn *se_unbox(ScrDyn *value) { - if (value->kind != SCR_DYN_TYPED_REF) return scr_dyn_retain(value); - ScrDyn *view = scr_dyn_typed_ref_materialize(value); - if (!view) return NULL; - if (view->kind == SCR_DYN_STR || view->kind == SCR_DYN_BYTES || view->kind == SCR_DYN_HANDLE || - scr_buffer_storage_is(view)) - return view; - scr_dyn_release(view); - return scr_dyn_retain(value); -} - -/* getArrayBufferOrView(key, 'key', encoding): a string is Buffer.from(key, - * encoding) with 'buffer' read as utf8; views and ArrayBuffers are their - * bytes. +1 or NULL after throwing. */ -static ScrBytes *se_key_bytes(const ScrDyn *key, const ScrDyn *encoding) { - if (key->kind == SCR_DYN_STR) { - /* Buffer.from(string, encoding): an encoding that is not a non-empty - * string reads as utf8. */ - const char *name = "utf8"; - if (encoding->kind == SCR_DYN_STR && encoding->v.str->len > 0 && !se_str_is(encoding->v.str, "buffer")) { - name = se_normalize_encoding(encoding->v.str); - if (!name) { - se_throw_formatted("ERR_UNKNOWN_ENCODING", "Unknown encoding: ", encoding); - return NULL; - } - } - ScrStr *canonical = scr_str_new(name, strlen(name)); - ScrBytes *bytes = scr_bytes_from_str(key->v.str, canonical); - scr_str_release(canonical); - return bytes; - } - if (key->kind == SCR_DYN_BYTES || scr_buffer_storage_is(key)) return scr_bytes_buffer_source(key); - scr_dyn_arg_type_fail("key", "of type string or an instance of ArrayBuffer, Buffer, TypedArray, or DataView", key); - return NULL; -} - -/* encode(buffer, encoding): the Buffer, or buffer.toString(encoding) for - * a truthy encoding other than 'buffer'. Consumes bytes. */ -static ScrDyn *se_encode(ScrBytes *bytes, const ScrDyn *encoding) { - if (!scr_dyn_truthy(encoding) || (encoding->kind == SCR_DYN_STR && se_str_is(encoding->v.str, "buffer"))) { - ScrDyn *result = scr_dyn_new_buffer(bytes); - scr_bytes_release(bytes); - return result; - } - /* toString answers "" for no bytes before it reads the encoding. */ - ScrStr *text = NULL; - if (bytes->len == 0) { - text = scr_str_new("", 0); - } else { - /* getEncodingOps reads `encoding + ''`. */ - ScrStr *name = encoding->kind == SCR_DYN_STR ? scr_str_retain(encoding->v.str) : NULL; - if (!name) { - if (encoding->kind == SCR_DYN_FUNC || se_class_to_string(encoding)) { - se_throw_formatted("ERR_UNKNOWN_ENCODING", "Unknown encoding: ", encoding); - scr_bytes_release(bytes); - return NULL; - } - name = scr_dyn_string_coerce_js(encoding); - if (!name) { - scr_bytes_release(bytes); - return NULL; - } - } - const char *canonical = se_normalize_encoding(name); - scr_str_release(name); - if (!canonical) { - se_throw_formatted("ERR_UNKNOWN_ENCODING", "Unknown encoding: ", encoding); - scr_bytes_release(bytes); - return NULL; - } - ScrStr *canonical_text = scr_str_new(canonical, strlen(canonical)); - text = scr_bytes_to_str(bytes, canonical_text); - scr_str_release(canonical_text); - } - scr_bytes_release(bytes); - if (!text) return NULL; - ScrDyn *result = scr_dyn_new_str(text); - scr_str_release(text); - return result; -} - /* getFormat: a falsy format is uncompressed; other values must be * 'compressed', 'hybrid' or 'uncompressed'. 0 after throwing. */ static int se_format(const ScrDyn *format) { if (!scr_dyn_truthy(format)) return SE_UNCOMPRESSED; if (format->kind == SCR_DYN_STR) { - if (se_str_is(format->v.str, "compressed")) return SE_COMPRESSED; - if (se_str_is(format->v.str, "hybrid")) return SE_HYBRID; - if (se_str_is(format->v.str, "uncompressed")) return SE_UNCOMPRESSED; + if (ca_str_is(format->v.str, "compressed")) return SE_COMPRESSED; + if (ca_str_is(format->v.str, "hybrid")) return SE_HYBRID; + if (ca_str_is(format->v.str, "uncompressed")) return SE_UNCOMPRESSED; } - se_throw_formatted("ERR_CRYPTO_ECDH_INVALID_FORMAT", "Invalid ECDH format: ", format); + ca_throw_formatted(SE_SUBJECT, "ERR_CRYPTO_ECDH_INVALID_FORMAT", "Invalid ECDH format: ", format); return 0; } @@ -516,7 +362,7 @@ static ScrDyn *se_new(const ScrDyn *curve) { } ScrDyn *scr_crypto_ecdh_new(ScrDyn *curve) { - ScrDyn *unboxed = se_unbox(curve); + ScrDyn *unboxed = ca_unbox(curve); if (!unboxed) return NULL; ScrDyn *result = se_new(unboxed); scr_dyn_release(unboxed); @@ -530,7 +376,7 @@ static ScrDyn *se_convert_key(const ScrDyn *key, const ScrDyn *curve, const ScrD scr_dyn_arg_type_fail("curve", "of type string", curve); return NULL; } - ScrBytes *bytes = se_key_bytes(key, in_enc); + ScrBytes *bytes = ca_key_bytes(SE_SUBJECT, key, in_enc); if (!bytes) return NULL; int form = se_format(format); if (!form) { @@ -557,7 +403,7 @@ static ScrDyn *se_convert_key(const ScrDyn *key, const ScrDyn *curve, const ScrD } else { ScrBytes *out = se_write_point(&grp, &pt, form); if (!out) se_operation_failed("Failed to convert EC_POINT to Buffer"); - else result = se_encode(out, out_enc); + else result = ca_encode(SE_SUBJECT, out, out_enc); } } mbedtls_ecp_point_free(&pt); @@ -571,7 +417,7 @@ ScrDyn *scr_crypto_ecdh_convert_key(ScrDyn *key, ScrDyn *curve, ScrDyn *in_enc, ScrDyn *unboxed[5] = {NULL, NULL, NULL, NULL, NULL}; ScrDyn *result = NULL; for (size_t i = 0; i < 5; i++) - if (!(unboxed[i] = se_unbox(args[i]))) goto done; + if (!(unboxed[i] = ca_unbox(args[i]))) goto done; result = se_convert_key(unboxed[0], unboxed[1], unboxed[2], unboxed[3], unboxed[4]); done: for (size_t i = 0; i < 5; i++) scr_dyn_release(unboxed[i]); @@ -597,7 +443,7 @@ static ScrDyn *se_get_public_key(ScEcdh *e, const ScrDyn *encoding, const ScrDyn se_operation_failed("Failed to get ECDH public key"); return NULL; } - return se_encode(out, encoding); + return ca_encode(SE_SUBJECT, out, encoding); } static ScrDyn *se_generate_keys(ScEcdh *e, const ScrDyn *encoding, const ScrDyn *format) { @@ -628,12 +474,12 @@ static ScrDyn *se_get_private_key(ScEcdh *e, const ScrDyn *encoding) { se_operation_failed("Failed to get ECDH private key"); return NULL; } - return se_encode(out, encoding); + return ca_encode(SE_SUBJECT, out, encoding); } /* setPrivateKey(key, encoding): 1 <= d < n, and the public key d·G. */ static ScrDyn *se_set_private_key(ScEcdh *e, ScrDyn *self, const ScrDyn *key, const ScrDyn *encoding) { - ScrBytes *bytes = se_key_bytes(key, encoding); + ScrBytes *bytes = ca_key_bytes(SE_SUBJECT, key, encoding); if (!bytes) return NULL; mbedtls_mpi d; mbedtls_ecp_point q; @@ -680,7 +526,7 @@ static void se_dep0031(void) { static ScrDyn *se_set_public_key(ScEcdh *e, ScrDyn *self, const ScrDyn *key, const ScrDyn *encoding) { se_dep0031(); - ScrBytes *bytes = se_key_bytes(key, encoding); + ScrBytes *bytes = ca_key_bytes(SE_SUBJECT, key, encoding); if (!bytes) return NULL; mbedtls_ecp_point q; mbedtls_ecp_point_init(&q); @@ -716,7 +562,7 @@ static bool se_pair_valid(ScEcdh *e) { /* computeSecret(key, inputEncoding, outputEncoding): the x coordinate of * d·P, padded to the field size as ECDH_compute_key writes it. */ static ScrDyn *se_compute_secret(ScEcdh *e, const ScrDyn *key, const ScrDyn *in_enc, const ScrDyn *out_enc) { - ScrBytes *bytes = se_key_bytes(key, in_enc); + ScrBytes *bytes = ca_key_bytes(SE_SUBJECT, key, in_enc); if (!bytes) return NULL; ScrDyn *result = NULL; mbedtls_ecp_point pub; @@ -737,7 +583,7 @@ static ScrDyn *se_compute_secret(ScEcdh *e, const ScrDyn *key, const ScrDyn *in_ scr_bytes_release(out); se_operation_failed("Failed to compute ECDH key"); } else { - result = se_encode(out, out_enc); + result = ca_encode(SE_SUBJECT, out, out_enc); } } mbedtls_mpi_free(&z); @@ -776,7 +622,7 @@ static ScrDyn *se_invoke(void *h, ScrDyn *self, const char *method, ScrDyn *cons size_t count = argc < 3 ? argc : 3; ScrDyn *result = NULL; for (size_t i = 0; i < count; i++) - if (!(unboxed[i] = se_unbox(args[i]))) goto done; + if (!(unboxed[i] = ca_unbox(args[i]))) goto done; result = se_dispatch(h, self, method, unboxed, count, what); done: for (size_t i = 0; i < count; i++) scr_dyn_release(unboxed[i]); diff --git a/packages/runtime/src/scr_ed25519.c b/packages/runtime/src/scr_ed25519.c new file mode 100644 index 000000000..c6d6ce370 --- /dev/null +++ b/packages/runtime/src/scr_ed25519.c @@ -0,0 +1,76 @@ +/* Ed25519 for scr_pki.c: libsodium's ref10 implementation (vendored, see + * ../vendor/README.md), textually included so it builds as this one unit. + * It links only into programs that name Ed25519 (the ed25519 runtime + * feature) and installs itself into scr_pki.c at startup; without it, + * Ed25519 signing, verification and public-key derivation throw a coded + * SC2020 error. + * + * Signing runs in constant time. Verification rejects a non-canonical S + * and the small-order R and public-key encodings, then checks the + * cofactorless equation, which is what Node 24 answers. */ +#include "scr_runtime.h" + +#include +#include +#include +#include + +#include "scr_pki.h" + +#define CONFIGURED 1 +#include "../vendor/libsodium/ed25519_ref10.c" +#include "../vendor/libsodium/hash_sha512_cp.c" +#include "../vendor/libsodium/verify.c" +#include "../vendor/libsodium/sign.c" +#include "../vendor/libsodium/open.c" +#include "../vendor/libsodium/keypair.c" +#undef CONFIGURED + +/* The libsodium utilities those units call. */ +void sodium_memzero(void *const pnt, const size_t len) { + volatile unsigned char *p = (volatile unsigned char *)pnt; + for (size_t i = 0; i < len; i++) p[i] = 0U; +} + +int sodium_memcmp(const void *const b1_, const void *const b2_, size_t len) { + const volatile unsigned char *b1 = (const volatile unsigned char *)b1_; + const volatile unsigned char *b2 = (const volatile unsigned char *)b2_; + volatile unsigned char d = 0U; + for (size_t i = 0; i < len; i++) d |= b1[i] ^ b2[i]; + return (1 & ((d - 1) >> 8)) - 1; +} + +int sodium_is_zero(const unsigned char *n, const size_t nlen) { + volatile unsigned char d = 0U; + for (size_t i = 0; i < nlen; i++) d |= n[i]; + return 1 & ((d - 1) >> 8); +} + +void randombytes_buf(void *const buf, const size_t size) { arc4random_buf(buf, size); } + +static void se_public_key(uint8_t pub[32], const uint8_t seed[32]) { + uint8_t sk[64]; + crypto_sign_ed25519_seed_keypair(pub, sk, seed); + sodium_memzero(sk, sizeof sk); +} + +static void se_sign(uint8_t sig[64], const uint8_t seed[32], const uint8_t pub[32], const uint8_t *m, + size_t n) { + uint8_t sk[64]; + memcpy(sk, seed, 32); + memcpy(sk + 32, pub, 32); + crypto_sign_ed25519_detached(sig, NULL, m, n, sk); + sodium_memzero(sk, sizeof sk); +} + +static bool se_verify(const uint8_t pub[32], const uint8_t *m, size_t n, const uint8_t sig[64]) { + return crypto_sign_ed25519_verify_detached(sig, m, n, pub) == 0; +} + +static const ScrPkiEd25519 se_table = { + .public_key = se_public_key, + .sign = se_sign, + .verify = se_verify, +}; + +__attribute__((constructor)) static void se_install(void) { scr_pki_ed25519 = &se_table; } diff --git a/packages/runtime/src/scr_events_emitter.c b/packages/runtime/src/scr_events_emitter.c index 901e2d114..93a724ecb 100644 --- a/packages/runtime/src/scr_events_emitter.c +++ b/packages/runtime/src/scr_events_emitter.c @@ -82,6 +82,8 @@ typedef struct ScrEeEntry { * the meta events. */ ScrClosure *flex_cb; ScrDynThunk flex_invoke; + /* The function box's typed message entry (scr_emitter_emit_message). */ + ScrDynMessageListener flex_message; const char *flex_sig; const char *flex_name; uint32_t flex_arity; @@ -512,6 +514,7 @@ static ScrEmitter *scr_ee_add(ScrEmitter *em, ScrStr *name, ScrClosure *cb /*mov if (dyn) { e->flex_cb = scr_closure_retain(dyn->v.fn.clo); e->flex_invoke = dyn->v.fn.thunk; + e->flex_message = dyn->v.fn.message; e->flex_sig = dyn->v.fn.sig; e->flex_name = dyn->v.fn.name; e->flex_arity = dyn->v.fn.arity; @@ -1013,15 +1016,16 @@ static ScrDyn *const scr_ee_no_args[1] = { NULL }; * a nested emit's snapshot still holds it (the shared fired flag — Node's * wrapper.fired). A listener throw stops the pass (the exception stays * pending for the caller). Returns whether the bucket had listeners. */ +typedef struct ScrEeMessage ScrEeMessage; static bool scr_ee_emit_core(ScrEmitter *em, ScrStr *name, va_list *ap_template, - ScrDyn *const *flex_args, size_t flex_argc); + ScrDyn *const *flex_args, size_t flex_argc, ScrEeMessage *message); /* emit('error', ...) first emits the same arguments to errorMonitor. */ static void scr_ee_emit_error_monitor(ScrEmitter *em, va_list *ap_template, ScrDyn *const *flex_args, size_t flex_argc) { ScrEeBucket *b = scr_ee_bucket_of(em->reg, SCR_EE_ERROR_MONITOR); if (!b || b->n == 0) return; - scr_ee_emit_core(em, SCR_EE_ERROR_MONITOR, ap_template, flex_args, flex_argc); + scr_ee_emit_core(em, SCR_EE_ERROR_MONITOR, ap_template, flex_args, flex_argc, NULL); } /* A listener this emit has no argument form for: an untyped emit whose @@ -1044,8 +1048,17 @@ SCR_COLD static void scr_ee_listener_mismatch(const ScrStr *name, bool typed_emi scr_throw_error(typed_emit ? SCR_ERR_ERROR : SCR_ERR_TYPE, message); } +/* A message emit (scr_emitter_emit_message): listeners with a typed message + * entry read the message until a listener without one runs; that listener + * and every later one share one decoded argument. */ +struct ScrEeMessage { + const struct ScrMessage *message; + ScrDyn *(*decode)(const struct ScrMessage *); + ScrDyn *decoded; +}; + static bool scr_ee_emit_core(ScrEmitter *em, ScrStr *name, va_list *ap_template, - ScrDyn *const *flex_args, size_t flex_argc) { + ScrDyn *const *flex_args, size_t flex_argc, ScrEeMessage *message) { if (name->len == 5 && memcmp(name->data, "error", 5) == 0) { scr_ee_emit_error_monitor(em, ap_template, flex_args, flex_argc); if (scr_exc_pending()) return true; @@ -1088,6 +1101,22 @@ static bool scr_ee_emit_core(ScrEmitter *em, ScrStr *name, va_list *ap_template, continue; } } + /* Once a listener has the decoded value, later listeners read that + * value: an earlier listener may have changed it, as in Node. */ + if (message && !message->decoded && e->flex_message && + e->flex_message(e->flex_cb, message->message)) { + scr_ee_entry_unref(e); + continue; + } + if (message && !message->decoded) { + message->decoded = message->decode(message->message); + if (!message->decoded) { + scr_ee_entry_unref(e); + continue; + } + flex_args = &message->decoded; + flex_argc = 1; + } if (flex_args && e->flex_invoke) { ScrDyn *answer = e->flex_invoke(e->flex_cb, flex_args, flex_argc); scr_dyn_release(answer); /* EventEmitter ignores callback results. */ @@ -1108,7 +1137,7 @@ static bool scr_ee_emit_core(ScrEmitter *em, ScrStr *name, va_list *ap_template, static bool scr_ee_emit_va(ScrEmitter *em, ScrStr *name, ...) { va_list ap; va_start(ap, name); - bool had = scr_ee_emit_core(em, name, &ap, NULL, 0); + bool had = scr_ee_emit_core(em, name, &ap, NULL, 0, NULL); va_end(ap); return had; } @@ -1119,7 +1148,7 @@ static bool scr_ee_emit_mixed_va(ScrEmitter *em, ScrStr *name, ScrDyn *const *ar int typed, ...) { va_list ap; va_start(ap, typed); - bool had = scr_ee_emit_core(em, name, typed ? &ap : NULL, args ? args : scr_ee_no_args, argc); + bool had = scr_ee_emit_core(em, name, typed ? &ap : NULL, args ? args : scr_ee_no_args, argc, NULL); va_end(ap); return had; } @@ -1131,7 +1160,7 @@ static bool scr_ee_emit_mixed_va(ScrEmitter *em, ScrStr *name, ScrDyn *const *ar bool scr_emitter_emit(ScrEmitter *em, ScrStr *name, ...) { va_list ap; va_start(ap, name); - bool had = scr_ee_emit_core(em, name, &ap, NULL, 0); + bool had = scr_ee_emit_core(em, name, &ap, NULL, 0, NULL); va_end(ap); return had; } @@ -1143,7 +1172,7 @@ bool scr_emitter_emit(ScrEmitter *em, ScrStr *name, ...) { bool scr_emitter_emit_mixed(ScrEmitter *em, ScrStr *name, ScrDyn *const *args, size_t argc, ...) { va_list ap; va_start(ap, argc); - bool had = scr_ee_emit_core(em, name, &ap, args ? args : scr_ee_no_args, argc); + bool had = scr_ee_emit_core(em, name, &ap, args ? args : scr_ee_no_args, argc, NULL); va_end(ap); return had; } @@ -1241,7 +1270,15 @@ static bool scr_ee_emit_error_dyn(ScrEmitter *em, ScrStr *name, ScrDyn *const *a /* An untyped emit of a name typed code never fixes a tuple for (computed * names, runtime-internal registries): the arguments as given. */ bool scr_emitter_emit_flex(ScrEmitter *em, ScrStr *name, ScrDyn *const *args, size_t argc) { - return scr_ee_emit_core(em, name, NULL, args ? args : scr_ee_no_args, argc); + return scr_ee_emit_core(em, name, NULL, args ? args : scr_ee_no_args, argc, NULL); +} + +bool scr_emitter_emit_message(ScrEmitter *em, ScrStr *name, const struct ScrMessage *message, + ScrDyn *(*decode)(const struct ScrMessage *)) { + ScrEeMessage state = {message, decode, NULL}; + bool had = scr_ee_emit_core(em, name, NULL, NULL, 0, &state); + scr_dyn_release(state.decoded); + return had; } /* emit(name, ...args) through an untyped value: the arguments as given. @@ -1255,7 +1292,7 @@ bool scr_emitter_emit_any(ScrEmitter *em, ScrStr *name, ScrDyn *const *args, siz return scr_ee_emit_mixed_va(em, name, args, argc, s != NULL, s); } } - return scr_ee_emit_core(em, name, NULL, args ? args : scr_ee_no_args, argc); + return scr_ee_emit_core(em, name, NULL, args ? args : scr_ee_no_args, argc, NULL); } /* Handle event registries use checked callbacks for every tuple, including @@ -1268,7 +1305,7 @@ bool scr_emitter_emit_error_flex(ScrEmitter *em, ScrDyn *error) { return false; } ScrStr *name = scr_str_new("error", 5); - bool had = scr_ee_emit_core(em, name, NULL, &error, 1); + bool had = scr_ee_emit_core(em, name, NULL, &error, 1, NULL); scr_str_release(name); return had; } diff --git a/packages/runtime/src/scr_events_emitter.h b/packages/runtime/src/scr_events_emitter.h index b66de9e5a..29d4c397f 100644 --- a/packages/runtime/src/scr_events_emitter.h +++ b/packages/runtime/src/scr_events_emitter.h @@ -154,6 +154,14 @@ ScrEmitter *scr_emitter_remove_all(ScrEmitter *em, ScrStr *name, bool all); * emit('error', err): no listener ⇒ THROWS err (borrowed; +1 taken). */ bool scr_emitter_emit(ScrEmitter *em, ScrStr *name, ...); bool scr_emitter_emit_flex(ScrEmitter *em, ScrStr *name, ScrDyn *const *args, size_t argc); +/* An untyped emit of one message: a listener registered from a function + * box with a typed message entry (scr_dyn_func_set_message) reads the + * message itself while no listener has needed the decoded value; from the + * first listener without an entry on, every listener gets decode(message), + * made once. */ +struct ScrMessage; +bool scr_emitter_emit_message(ScrEmitter *em, ScrStr *name, const struct ScrMessage *message, + ScrDyn *(*decode)(const struct ScrMessage *)); /* An untyped emit of an event with a fixed tuple: the arguments as given * for dynamic listeners, plus the tuple converted by the caller as * scr_emitter_emit's variadics for typed listeners. has_typed answers diff --git a/packages/runtime/src/scr_http.c b/packages/runtime/src/scr_http.c index d9876c30b..a5847e59b 100644 --- a/packages/runtime/src/scr_http.c +++ b/packages/runtime/src/scr_http.c @@ -2918,22 +2918,6 @@ static bool scr_http_conn_parse_head(ScrHttpConn *conn, size_t head_len, bool *o conn->trailer_bytes = 0; conn->trailer_fields = 0; - if (!http10 && conn->srv->require_host_header) { - bool has_host = false; - for (size_t i = 0; i < req->nheaders; i++) { - const ScrStr *name = req->hnames[i]; - if (name->len == 4 && memcmp(name->data, "host", 4) == 0) { - has_host = true; - break; - } - } - if (!has_host) { - scr_http_req_release(req); - scr_http_conn_missing_host(conn); - return true; - } - } - /* framing: chunked wins over Content-Length (RFC 9112) */ bool chunked = false; long long content_length = 0; @@ -2958,12 +2942,33 @@ static bool scr_http_conn_parse_head(ScrHttpConn *conn, size_t head_len, bool *o } } + /* HTTP/1.1 requires Host (requireHostHeader). CONNECT requests, and + * upgrade requests an 'upgrade' listener takes, leave the parser before + * Node's check. */ + bool is_connect = req->method->len == 7 && memcmp(req->method->data, "CONNECT", 7) == 0; + bool takes_upgrade = has_upgrade_hdr && conn_upgrade_token && conn->srv->upgrade_ls.n > 0; + if (!http10 && conn->srv->require_host_header && !is_connect && !takes_upgrade) { + bool has_host = false; + for (size_t i = 0; i < req->nheaders; i++) { + const ScrStr *name = req->hnames[i]; + if (name->len == 4 && memcmp(name->data, "host", 4) == 0) { + has_host = true; + break; + } + } + if (!has_host) { + scr_http_req_release(req); + scr_http_conn_missing_host(conn); + return true; + } + } + /* A CONNECT request: 'connect' fires INSTEAD of 'request' — the * upgrade machinery's exact shape ((req, socket, head); the parser * steps aside; no listener destroys the socket, Node's default). The * h2 compat server's HTTP/1.1 arm lands here too; its HTTP/2 arm is * dispatched separately with the compatibility response handle. */ - if (req->method->len == 7 && memcmp(req->method->data, "CONNECT", 7) == 0) { + if (is_connect) { memmove(conn->buf, conn->buf + head_len, conn->len - head_len); conn->len -= head_len; if (!scr_net_ls_has_live(&conn->srv->connect_ls)) { diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 4e21fa397..68d91b26c 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -1814,6 +1814,55 @@ static void scr_builtin_object_prototype_cleanup(void) { scr_dyn_release(prototype); } +/* The error constructors as values (an error's `constructor`, and the + * globals in JavaScript): one interned function per constructor and thread, + * indexed like scr_error_vts, with the constructor's name and length. + * Calling one through the value is not supported yet, as for the other + * interned constructors (scr_dyn_global_constructor). */ +static const char *const scr_error_ctor_names[] = { + "Error", "TypeError", "RangeError", "SyntaxError", NULL, + "ReferenceError", "EvalError", "URIError", "AggregateError", +}; +#define SCR_ERROR_CTORS (sizeof scr_error_ctor_names / sizeof scr_error_ctor_names[0]) +static SCR_TL ScrDyn *scr_error_ctor_values[SCR_ERROR_CTORS]; +static SCR_TL bool scr_error_ctors_registered; + +static void scr_error_ctors_cleanup(void) { + for (size_t i = 0; i < SCR_ERROR_CTORS; i++) { + scr_dyn_release(scr_error_ctor_values[i]); + scr_error_ctor_values[i] = NULL; + } +} + +static ScrDyn *scr_error_ctor_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)args; + (void)argc; + char message[128]; + int length = snprintf(message, sizeof message, + "calling the %s constructor through a stored value is not supported yet", + (const char *)closure->fn); + scr_throw_error_msg_code(SCR_ERR_ERROR, message, (size_t)length, "SC2020"); + return NULL; +} + +ScrDyn *scr_dyn_error_constructor(const char *name) { + for (size_t i = 0; i < SCR_ERROR_CTORS; i++) { + if (!scr_error_ctor_names[i] || strcmp(scr_error_ctor_names[i], name) != 0) continue; + if (!scr_error_ctors_registered) { + scr_error_ctors_registered = true; + scr_atexit(scr_error_ctors_cleanup); + } + if (!scr_error_ctor_values[i]) { + uint32_t length = strcmp(name, "AggregateError") == 0 ? 2 : 1; + scr_error_ctor_values[i] = + scr_dyn_new_func(scr_closure_new((void *)scr_error_ctor_names[i], 0), scr_error_ctor_call, length, + "native:constructor", scr_error_ctor_names[i]); + } + return scr_dyn_retain(scr_error_ctor_values[i]); + } + return NULL; +} + ScrDyn *scr_dyn_object_prototype(void) { if (!scr_builtin_object_prototype) { scr_builtin_object_prototype = scr_dyn_alloc(SCR_DYN_OBJ); @@ -3542,6 +3591,10 @@ static const ScrDynTypedRecordOps *scr_typed_ref_record_ops(const ScrDyn *d) { return (const ScrDynTypedRecordOps *)(const void *)d->v.typed_ref.array; } +const ScrDynTypedRecordOps *scr_dyn_typed_ref_live_record(const ScrDyn *d) { + return d->kind == SCR_DYN_TYPED_REF ? scr_typed_ref_record_ops(d) : NULL; +} + static bool scr_typed_ref_record_field(const ScrDynTypedRecordOps *ops, const ScrStr *key, size_t *out) { /* Compiled reads pass the interned literal the field names also use. */ @@ -3920,7 +3973,10 @@ static int scr_dyn_global_constructor_instanceof(const ScrDyn *value, const ScrD return -1; } -bool scr_bytes_instanceof(const ScrDyn *value, const ScrDyn *callee) { +/* `value instanceof callee` for an untyped constructor value. `refusal`, when + * given, is the site's SC1090 message for a runtime constructor with no + * instance test. */ +bool scr_bytes_instanceof_at(const ScrDyn *value, const ScrDyn *callee, const ScrStr *refusal) { if (!callee || callee->kind != SCR_DYN_FUNC) { static const char message[] = "Right-hand side of 'instanceof' is not callable"; scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); @@ -3962,12 +4018,33 @@ bool scr_bytes_instanceof(const ScrDyn *value, const ScrDyn *callee) { scr_dyn_release(prototype); return found; } + bool undefined_prototype = prototype->kind == SCR_DYN_UNDEF; scr_dyn_release(prototype); + /* A function the program compiled with no prototype (an arrow function + * or a method): false for a primitive, and Node's TypeError for every + * object, typed arrays, native handles, promises and typed references + * included. The runtime's own constructors carry "native:" signatures. */ + if (undefined_prototype && (!callee->v.fn.sig || strncmp(callee->v.fn.sig, "native:", 7) != 0)) { + if (value->kind == SCR_DYN_UNDEF || value->kind == SCR_DYN_NULL || value->kind == SCR_DYN_BOOL || + value->kind == SCR_DYN_NUM || value->kind == SCR_DYN_STR || value->kind == SCR_DYN_BIGINT || + value->kind == SCR_DYN_SYMBOL) + return false; + static const char message[] = "Function has non-object prototype 'undefined' in instanceof check"; + scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); + return false; + } + } + if (refusal) { + scr_throw_error_msg_code(SCR_ERR_ERROR, refusal->data, refusal->len, "SC1090"); + return false; } static const char message[] = "instanceof against this native constructor is not supported yet"; scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC1090"); return false; } +bool scr_bytes_instanceof(const ScrDyn *value, const ScrDyn *callee) { + return scr_bytes_instanceof_at(value, callee, NULL); +} ScrDyn *scr_bytes_construct(const ScrDyn *callee, const ScrDyn *args, const ScrStr *what) { if (callee->kind == SCR_DYN_FUNC && scr_array_ctor && callee->v.fn.clo == scr_array_ctor->v.fn.clo) return scr_array_ctor_call(callee->v.fn.clo, args->v.arr.items, args->v.arr.len); @@ -4073,9 +4150,16 @@ ScrDyn *scr_dyn_new_func(ScrClosure *clo, ScrDynThunk thunk, uint32_t arity, con /* A signature adapter reports the length of the function it adapts. */ d->v.fn.arity = clo->identity != NULL ? clo->identity_length : arity; d->v.fn.class_obj = NULL; + d->v.fn.message = NULL; return d; } +void scr_dyn_func_set_message(ScrDyn *fn, ScrDynThunk thunk, ScrDynMessageListener listener) { + /* scr_dyn_new_func answers an adapted function's original box, whose + * closure has other parameter types. */ + if (fn && fn->kind == SCR_DYN_FUNC && fn->v.fn.thunk == thunk) fn->v.fn.message = listener; +} + /* A class is function-shaped for typeof and identity, but calling it without * new always throws. The closure owns the class object and its lexical boxes. */ static ScrDyn *scr_dyn_class_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) { @@ -7951,6 +8035,17 @@ ScrStr *scr_dyn_to_string(const ScrDyn *d, const ScrStr *enc) { return out; } case SCR_DYN_FUNC: { + /* The interned builtin constructors print with their names, as in Node + * (function Map() { [native code] }); other functions without a source + * text print the anonymous native form. */ + if (d->v.fn.sig && strcmp(d->v.fn.sig, "native:constructor") == 0 && d->v.fn.name) { + ScrJsonBuf b; + scr_jb_init(&b); + scr_jb_puts(&b, "function "); + scr_jb_puts(&b, d->v.fn.name); + scr_jb_puts(&b, "() { [native code] }"); + return scr_jb_finish(&b); + } static const char f[] = "function () { [native code] }"; return scr_str_new(f, sizeof f - 1); } @@ -11943,21 +12038,18 @@ static ScrDyn *scr_dyn_obj_read_receiver(const ScrDyn *d, const char *key, size_ ScrDyn *own = scr_dyn_obj_get(d, key, key_len); if (own) return scr_dyn_retain(own); if (key_len == 11 && memcmp(key, "constructor", 11) == 0) { - static const char *const tokens[] = { - "[builtin Error]", "[builtin TypeError]", - "[builtin RangeError]", "[builtin SyntaxError]", - NULL, "[builtin ReferenceError]", "[builtin EvalError]", "[builtin URIError]", - "[builtin AggregateError]", + /* An error's constructor: the interned function value its global + * reads as (scr_dyn_error_constructor). */ + static const char *const names[] = { + "Error", "TypeError", "RangeError", "SyntaxError", NULL, + "ReferenceError", "EvalError", "URIError", "AggregateError", }; size_t hit = scr_errdyn_find(d, false); const ScrVt *vt = hit == SIZE_MAX ? NULL : scr_errdyn_cache[hit].err->vt; - for (size_t kind = 0; vt && kind < sizeof tokens / sizeof tokens[0]; kind++) { - if (!tokens[kind]) continue; + for (size_t kind = 0; vt && kind < sizeof names / sizeof names[0]; kind++) { + if (!names[kind]) continue; if (vt != &scr_error_vts[kind]) continue; - ScrStr *token = scr_str_new(tokens[kind], strlen(tokens[kind])); - ScrDyn *result = scr_dyn_new_str(token); - scr_str_release(token); - return result; + return scr_dyn_error_constructor(names[kind]); } } return scr_dyn_retain(scr_dyn_undefined()); diff --git a/packages/runtime/src/scr_lib.c b/packages/runtime/src/scr_lib.c index 9bebdec5e..37ca28f16 100644 --- a/packages/runtime/src/scr_lib.c +++ b/packages/runtime/src/scr_lib.c @@ -9225,6 +9225,10 @@ ScrCryptoHash *scr_crypto_hmac_new_dyn(ScrStr *alg, ScrDyn *key, ScrDyn *options if (key_object) { bytes = scr_bytes_new(SCR_BYTES_U8, (double)key_object_len); if (key_object_len) memcpy(bytes->data, key_object, key_object_len); + } else if (scr_crypto_is_key_object(key)) { + /* A private or public KeyObject: scr_pki.c reports Node's error. */ + scr_crypto_reject_asymmetric_secret(key); + return NULL; } else if (key->kind == SCR_DYN_STR) { ScrStr *utf8 = encoding ? NULL : scr_str_new("utf8", 4); bytes = scr_bytes_from_str_checked(key->v.str, encoding ? encoding : utf8); @@ -9856,9 +9860,15 @@ const unsigned char *scr_key_object_bytes(const ScrDyn *value, size_t *len) { return key->data; } -/* `value instanceof KeyObject`. */ +void (*scr_crypto_reject_asymmetric_secret)(const ScrDyn *key) = NULL; + +/* `value instanceof KeyObject`: a secret key, or a private or public key + * (scr_pki.c, matched by its handle tags so this unit does not link it). */ bool scr_crypto_is_key_object(const ScrDyn *value) { size_t len; + if (value->kind == SCR_DYN_HANDLE && + (value->v.handle.tag == SCR_DYNH_PRIVATE_KEY || value->v.handle.tag == SCR_DYNH_PUBLIC_KEY)) + return true; return scr_key_object_bytes(value, &len) != NULL; } @@ -12992,7 +13002,7 @@ ScrArr *scr_set_to_arr_ref(const ScrMap *s, const ScrArrOps *ops) { for (size_t i = 0; i < s->nentries; i++) { if (!scr_map_entry_live(s, i)) continue; ops->retain((void *)(uintptr_t)scr_map_entry_at(s, i)->key); - out->data[out->len] = scr_map_entry_at(s, i)->key; + scr_arr_dense_put(out, out->len, scr_map_entry_at(s, i)->key); *scr_arr_state_in(states, out->len++) = SCR_ARR_VALUE; } return out; diff --git a/packages/runtime/src/scr_mailbox.c b/packages/runtime/src/scr_mailbox.c index a8dc7e5cc..b2af22774 100644 --- a/packages/runtime/src/scr_mailbox.c +++ b/packages/runtime/src/scr_mailbox.c @@ -140,9 +140,11 @@ ScrMailbox *scr_mailbox_retain(ScrMailbox *mailbox) { void scr_mail_event_free(ScrMailEvent *event) { if (!event) return; - scr_message_free(event->message); free(event->data); - free(event); + ScrMessage *message = event->message; + /* An event in its message's block goes with the message. */ + if (!event->in_message) free(event); + scr_message_free(message); } void scr_mailbox_release(ScrMailbox *mailbox) { @@ -460,7 +462,7 @@ void scr_channel_set_owner(ScrChannel *channel, int side, ScrMailbox *owner, uns * messages as ready, so it does not wait while the queue is non-empty. */ bool scr_channel_post(ScrChannel *channel, int side, ScrMessage *message, unsigned message_kind, unsigned wake_kind) { - ScrMailEvent *event = calloc(1, sizeof(*event)); + ScrMailEvent *event = message ? scr_message_mail_event(message) : calloc(1, sizeof(*event)); if (!event) scr_trap("scriptc: out of memory\n"); event->kind = message_kind; event->message = message; diff --git a/packages/runtime/src/scr_mailbox.h b/packages/runtime/src/scr_mailbox.h index 4e5f253b5..a4acfccd0 100644 --- a/packages/runtime/src/scr_mailbox.h +++ b/packages/runtime/src/scr_mailbox.h @@ -12,8 +12,13 @@ typedef struct ScrMailEvent { unsigned char *data; /* raw bytes (a worker's captured stdout or stderr), owned */ size_t length; struct ScrMailEvent *next; + bool in_message; /* the event is the one message's block holds (scr_message_mail_event) */ } ScrMailEvent; +/* The event that carries `message`, in the message's own block: a queued + * message is then one allocation. Freeing the event frees the message. */ +ScrMailEvent *scr_message_mail_event(ScrMessage *message); + ScrMailbox *scr_mailbox_new(void); ScrMailbox *scr_mailbox_retain(ScrMailbox *mailbox); void scr_mailbox_release(ScrMailbox *mailbox); diff --git a/packages/runtime/src/scr_map.c b/packages/runtime/src/scr_map.c index 99caa31a8..648d1bfc8 100644 --- a/packages/runtime/src/scr_map.c +++ b/packages/runtime/src/scr_map.c @@ -220,47 +220,106 @@ static bool scr_map_key_eq(const ScrMap *m, uint64_t stored, uint64_t probe) { * the value: its hash is the key word's scr_key_mix, which is cheap to * recompute, and its stored keys are normalized (one NaN pattern, +0), so * a dead entry marks its key with a NaN pattern that no stored key has. + * A SCR_MAP_VAL_CREF map (string keys, compact class values) keeps 16-byte + * entries: the key, its 32-bit hash and the value as an address over 8. * Every other map keeps 24-byte entries with the cached hash, zero for a - * dead entry. Both layouts put the key at byte 0 and the value at byte 8. */ + * dead entry. Every layout puts the key at byte 0; the 8-byte layouts put + * the value at byte 8. */ static inline bool scr_map_num(const ScrMap *m) { return m->key_kind == SCR_MAP_KEY_F64; } +static inline bool scr_map_cref(const ScrMap *m) { + return __builtin_expect(m->val_kind == SCR_MAP_VAL_CREF, 0); +} + static inline size_t scr_map_entry_size(const ScrMap *m) { - return scr_map_num(m) ? sizeof(ScrMapNumEntry) : sizeof(ScrMapEntry); + if (scr_map_num(m)) return sizeof(ScrMapNumEntry); + if (scr_map_cref(m)) return sizeof(ScrMapCEntry); + return sizeof(ScrMapEntry); } /* The entry at index e. A number-key entry has no hash field: read its key - * and value only (scr_map_hash_at gives the hash). */ + * and value only (scr_map_hash_at gives the hash); a SCR_MAP_VAL_CREF + * entry's value is read with scr_map_val_at. */ static inline ScrMapEntry *scr_map_at(const ScrMap *m, size_t e) { return (ScrMapEntry *)((char *)m->entries + e * scr_map_entry_size(m)); } +static inline ScrMapCEntry *scr_map_c_at(const ScrMap *m, size_t e) { + return (ScrMapCEntry *)m->entries + e; +} + static inline bool scr_map_live_at(const ScrMap *m, size_t e) { - const ScrMapEntry *x = scr_map_at(m, e); - return scr_map_num(m) ? x->key != SCR_MAP_NUM_DEAD : x->hash != 0; + if (scr_map_num(m)) return ((const ScrMapNumEntry *)m->entries)[e].key != SCR_MAP_NUM_DEAD; + if (scr_map_cref(m)) return scr_map_c_at(m, e)->hash != 0; + return m->entries[e].hash != 0; } /* The entry's hash, nonzero when it is live and zero for a dead entry. */ static inline uint64_t scr_map_hash_at(const ScrMap *m, size_t e) { - const ScrMapEntry *x = scr_map_at(m, e); - if (!scr_map_num(m)) return x->hash; - if (x->key == SCR_MAP_NUM_DEAD) return 0; - uint64_t hash = scr_map_hash_word(x->key); - return hash ? hash : 1; + if (scr_map_num(m)) { + uint64_t key = ((const ScrMapNumEntry *)m->entries)[e].key; + if (key == SCR_MAP_NUM_DEAD) return 0; + uint64_t hash = scr_map_hash_word(key); + return hash ? hash : 1; + } + if (scr_map_cref(m)) { + uint32_t h = scr_map_c_at(m, e)->hash; + return h ? scr_map_hash_widen(h) : 0; + } + return m->entries[e].hash; } static inline void scr_map_kill_at(ScrMap *m, size_t e) { - ScrMapEntry *x = scr_map_at(m, e); - if (scr_map_num(m)) x->key = SCR_MAP_NUM_DEAD; - else x->hash = 0; + if (scr_map_num(m)) ((ScrMapNumEntry *)m->entries)[e].key = SCR_MAP_NUM_DEAD; + else if (scr_map_cref(m)) scr_map_c_at(m, e)->hash = 0; + else m->entries[e].hash = 0; } +/* A compact value's 4-byte form and back (scr_cref_encode in scr_array.h). */ +static inline uint32_t scr_map_cref_encode(uint64_t val) { return (uint32_t)(val >> 3); } +static inline uint64_t scr_map_cref_decode(uint32_t val) { return (uint64_t)val << 3; } + +/* The value of entry e as an 8-byte slot, and its store. */ +static inline uint64_t scr_map_val_at(const ScrMap *m, size_t e) { + if (scr_map_num(m)) return ((const ScrMapNumEntry *)m->entries)[e].val; + if (scr_map_cref(m)) return scr_map_cref_decode(scr_map_c_at(m, e)->val); + return m->entries[e].val; +} + +static inline void scr_map_val_put(ScrMap *m, size_t e, uint64_t val) { + if (scr_map_num(m)) ((ScrMapNumEntry *)m->entries)[e].val = val; + else if (scr_map_cref(m)) scr_map_c_at(m, e)->val = scr_map_cref_encode(val); + else m->entries[e].val = val; +} + +/* A reference value as a +1 result: compact values are never counted. */ +static inline void *scr_map_val_retain(const ScrMap *m, uint64_t val) { + if (scr_map_cref(m)) return scr_map_slot_to_ptr(val); + return m->ops->val_retain(scr_map_slot_to_ptr(val)); +} + +/* A SCR_MAP_VAL_CREF map's keys are strings, whose hashes are widened + * 32-bit values: the entry keeps the low half. */ static inline void scr_map_put_at(ScrMap *m, size_t e, uint64_t key, uint64_t val, uint64_t hash) { - ScrMapEntry *x = scr_map_at(m, e); + if (scr_map_num(m)) { + ScrMapNumEntry *x = (ScrMapNumEntry *)m->entries + e; + x->key = key; + x->val = val; + return; + } + if (scr_map_cref(m)) { + ScrMapCEntry *x = scr_map_c_at(m, e); + x->key = key; + x->hash = (uint32_t)hash; + x->val = scr_map_cref_encode(val); + return; + } + ScrMapEntry *x = m->entries + e; x->key = key; x->val = val; - if (!scr_map_num(m)) x->hash = hash; + x->hash = hash; } /* ── absent-key filter ───────────────────────────────────────────────── @@ -282,17 +341,25 @@ static inline uint64_t scr_map_filter_bits(uint64_t hash) { * before equality and survive table growth without hashing the keys again. * Remap zero to one so every live entry has a nonzero hash. */ /* SCR_MAP_LOOKUP_NUM is a word compare on a number-key map, so the probe - * reads 16-byte entries; every other lookup is on a map with 24-byte - * entries. Knowing the layout from the lookup keeps the entry stride a - * constant in the inlined probes. */ + * reads 16-byte entries; SCR_MAP_LOOKUP_STR_C and SPAN_C are the string and + * span probes of a SCR_MAP_VAL_CREF map, whose 16-byte entries keep a + * 32-bit hash; every other lookup is on a map with 24-byte entries. Knowing + * the layout from the lookup keeps the entry stride a constant in the + * inlined probes. */ typedef enum { SCR_MAP_LOOKUP_STR, SCR_MAP_LOOKUP_SPAN, SCR_MAP_LOOKUP_WORD, SCR_MAP_LOOKUP_NUM, - SCR_MAP_LOOKUP_GENERIC + SCR_MAP_LOOKUP_GENERIC, + SCR_MAP_LOOKUP_STR_C, + SCR_MAP_LOOKUP_SPAN_C } ScrMapLookup; +static inline bool scr_map_lookup_c(ScrMapLookup lookup) { + return lookup == SCR_MAP_LOOKUP_STR_C || lookup == SCR_MAP_LOOKUP_SPAN_C; +} + typedef struct { const char *bytes; size_t length; } ScrMapSpan; /* Choose equality once per operation. Inlining the traversal with a constant @@ -301,12 +368,12 @@ typedef struct { const char *bytes; size_t length; } ScrMapSpan; static inline bool scr_map_lookup_eq(const ScrMap *m, uint64_t stored, uint64_t key, ScrMapLookup lookup) { if (lookup == SCR_MAP_LOOKUP_WORD || lookup == SCR_MAP_LOOKUP_NUM) return stored == key; - if (lookup == SCR_MAP_LOOKUP_SPAN) { + if (lookup == SCR_MAP_LOOKUP_SPAN || lookup == SCR_MAP_LOOKUP_SPAN_C) { const ScrStr *a = scr_map_slot_to_ptr(stored); const ScrMapSpan *b = scr_map_slot_to_ptr(key); return a->len == b->length && scr_key_equal(a->data, b->bytes, a->len); } - if (lookup == SCR_MAP_LOOKUP_STR) { + if (lookup == SCR_MAP_LOOKUP_STR || lookup == SCR_MAP_LOOKUP_STR_C) { const ScrStr *a = scr_map_slot_to_ptr(stored); const ScrStr *b = scr_map_slot_to_ptr(key); return a == b || (a->len == b->len && scr_key_equal(a->data, b->data, a->len)); @@ -318,6 +385,7 @@ static inline bool scr_map_lookup_eq(const ScrMap *m, uint64_t stored, uint64_t static inline __attribute__((always_inline)) ScrMapEntry *scr_map_at_with(const ScrMap *m, size_t e, ScrMapLookup lookup) { if (lookup == SCR_MAP_LOOKUP_NUM) return (ScrMapEntry *)((ScrMapNumEntry *)m->entries + e); + if (scr_map_lookup_c(lookup)) return (ScrMapEntry *)((ScrMapCEntry *)m->entries + e); return &m->entries[e]; } @@ -328,6 +396,8 @@ static inline __attribute__((always_inline)) bool scr_map_match_at( const ScrMap *m, size_t e, uint64_t hash, uint64_t key, ScrMapLookup lookup) { const ScrMapEntry *x = scr_map_at_with(m, e, lookup); if (lookup == SCR_MAP_LOOKUP_NUM) return x->key == key; + if (scr_map_lookup_c(lookup)) + return ((const ScrMapCEntry *)x)->hash == (uint32_t)hash && scr_map_lookup_eq(m, x->key, key, lookup); return x->hash == hash && scr_map_lookup_eq(m, x->key, key, lookup); } @@ -437,7 +507,7 @@ static inline __attribute__((always_inline)) size_t scr_map_find_with( static ScrMapLookup scr_map_lookup_kind(const ScrMap *m) { switch (m->key_kind) { - case SCR_MAP_KEY_STR: return SCR_MAP_LOOKUP_STR; + case SCR_MAP_KEY_STR: return scr_map_cref(m) ? SCR_MAP_LOOKUP_STR_C : SCR_MAP_LOOKUP_STR; case SCR_MAP_KEY_F64: return SCR_MAP_LOOKUP_NUM; case SCR_MAP_KEY_BOOL: case SCR_MAP_KEY_REF: @@ -484,6 +554,7 @@ static size_t scr_map_find(const ScrMap *m, uint64_t hash, uint64_t key) { if (m->ndense) return scr_map_find_dense(m, scr_map_slot_to_f64(key)); switch (scr_map_lookup_kind(m)) { case SCR_MAP_LOOKUP_STR: return scr_map_find_with(m, hash, key, SCR_MAP_LOOKUP_STR); + case SCR_MAP_LOOKUP_STR_C: return scr_map_find_with(m, hash, key, SCR_MAP_LOOKUP_STR_C); case SCR_MAP_LOOKUP_WORD: return scr_map_find_with(m, hash, key, SCR_MAP_LOOKUP_WORD); case SCR_MAP_LOOKUP_NUM: return scr_map_find_with(m, hash, key, SCR_MAP_LOOKUP_NUM); default: return scr_map_find_with(m, hash, key, SCR_MAP_LOOKUP_GENERIC); @@ -504,9 +575,24 @@ const ScrMapEntry *scr_map_entry_f64(const ScrMap *m, double key) { /* The key's hash is usually cached, so even the small linear scan rejects * entries by hash before touching their bytes. */ +/* The probes of a SCR_MAP_VAL_CREF map, out of line so that the inlined + * probes of other maps keep their size. */ +static __attribute__((noinline)) size_t scr_map_find_str_c(const ScrMap *m, const ScrStr *key) { + return scr_map_find_with(m, scr_map_hash_str(key), scr_map_slot_from_ptr((void *)key), + SCR_MAP_LOOKUP_STR_C); +} + +static __attribute__((noinline)) size_t scr_map_find_span_c(const ScrMap *m, const char *bytes, + size_t length) { + ScrMapSpan key = { bytes, length }; + return scr_map_find_with(m, scr_map_hash_bytes(bytes, length), scr_map_slot_from_ptr(&key), + SCR_MAP_LOOKUP_SPAN_C); +} + static inline __attribute__((always_inline)) size_t scr_map_find_str(const ScrMap *m, const ScrStr *key) { if (m->nlive == 0) return SCR_MAP_EMPTY; + if (scr_map_cref(m)) return scr_map_find_str_c(m, key); return scr_map_find_with(m, scr_map_hash_str(key), scr_map_slot_from_ptr((void *)key), SCR_MAP_LOOKUP_STR); } @@ -515,6 +601,7 @@ static inline __attribute__((always_inline)) size_t scr_map_find_str(const ScrMa * rules as a string key without allocating or copying the probe. */ static size_t scr_map_find_span(const ScrMap *m, const char *bytes, size_t length) { if (m->nlive == 0) return SCR_MAP_EMPTY; + if (scr_map_cref(m)) return scr_map_find_span_c(m, bytes, length); ScrMapSpan key = { bytes, length }; return scr_map_find_with(m, scr_map_hash_bytes(bytes, length), scr_map_slot_from_ptr(&key), SCR_MAP_LOOKUP_SPAN); @@ -523,20 +610,20 @@ static size_t scr_map_find_span(const ScrMap *m, const char *bytes, size_t lengt bool scr_map_get_span_f64(const ScrMap *m, const char *bytes, size_t length, double *out) { size_t e = scr_map_find_span(m, bytes, length); if (e == SCR_MAP_EMPTY) return false; - *out = scr_map_slot_to_f64(scr_map_at(m, e)->val); + *out = scr_map_slot_to_f64(scr_map_val_at(m, e)); return true; } bool scr_map_get_span_bool(const ScrMap *m, const char *bytes, size_t length, bool *out) { size_t e = scr_map_find_span(m, bytes, length); if (e == SCR_MAP_EMPTY) return false; - *out = scr_map_at(m, e)->val != 0; + *out = scr_map_val_at(m, e) != 0; return true; } void *scr_map_get_span_ref(const ScrMap *m, const char *bytes, size_t length) { size_t e = scr_map_find_span(m, bytes, length); - return e == SCR_MAP_EMPTY ? NULL : m->ops->val_retain(scr_map_slot_to_ptr(scr_map_at(m, e)->val)); + return e == SCR_MAP_EMPTY ? NULL : scr_map_val_retain(m, scr_map_val_at(m, e)); } /* Rebuild the bucket table (size must be a power of two >= 2 * nentries): @@ -666,7 +753,12 @@ static void scr_map_compact(ScrMap *m, bool reindex) { if (k > top) top = k; } filter |= scr_map_filter_bits(scr_map_hash_at(m, r)); - if (w != r) memcpy(scr_map_at(m, w), scr_map_at(m, r), scr_map_entry_size(m)); + if (w != r) { + /* A constant size per layout keeps the copy inline. */ + if (scr_map_num(m)) memcpy(scr_map_at(m, w), scr_map_at(m, r), sizeof(ScrMapNumEntry)); + else if (scr_map_cref(m)) memcpy(scr_map_at(m, w), scr_map_at(m, r), sizeof(ScrMapCEntry)); + else memcpy(scr_map_at(m, w), scr_map_at(m, r), sizeof(ScrMapEntry)); + } w++; } m->nentries = (uint32_t)w; @@ -778,7 +870,7 @@ static void scr_map_trace(void *o, ScrTraceVisit visit, void *ctx) { for (size_t e = 0; e < m->nentries; e++) { if (!scr_map_live_at(m, e)) continue; if (keys) visit(scr_map_slot_to_ptr(scr_map_at(m, e)->key), ctx); - if (vals) visit(scr_map_slot_to_ptr(scr_map_at(m, e)->val), ctx); + if (vals) visit(scr_map_slot_to_ptr(scr_map_val_at(m, e)), ctx); } } @@ -791,7 +883,7 @@ static void scr_map_gcfree(void *o) { for (size_t e = 0; e < m->nentries; e++) { if (!scr_map_live_at(m, e)) continue; if (!keys) scr_map_release_key(m, scr_map_at(m, e)->key); - if (!vals) scr_map_release_val(m, scr_map_at(m, e)->val); + if (!vals) scr_map_release_val(m, scr_map_val_at(m, e)); } scr_mem_free(m->entries); scr_mem_free(m->buckets); @@ -843,10 +935,15 @@ ScrMap *scr_map_retain(ScrMap *m) { static void scr_map_destroy(void *object) { ScrMap *m = object; scr_weak_dispose_inline(m); - for (size_t e = 0; e < m->nentries; e++) { - if (!scr_map_live_at(m, e)) continue; - scr_map_release_key(m, scr_map_at(m, e)->key); - scr_map_release_val(m, scr_map_at(m, e)->val); + /* Number and boolean keys, and values other than references, own + * nothing: a map of only those releases no entries. */ + bool keys = m->key_kind == SCR_MAP_KEY_STR || scr_map_ref_key(m->key_kind); + if (keys || m->val_kind == SCR_MAP_VAL_REF) { + for (size_t e = 0; e < m->nentries; e++) { + if (!scr_map_live_at(m, e)) continue; + scr_map_release_key(m, scr_map_at(m, e)->key); + scr_map_release_val(m, scr_map_val_at(m, e)); + } } scr_mem_free(m->entries); scr_mem_free(m->buckets); @@ -897,7 +994,7 @@ void scr_map_clear(ScrMap *m) { uint32_t *dense = m->ndense ? m->dense : NULL; for (size_t e = 0; e < walked; e++) { if (!scr_map_live_at(m, e)) continue; - uint64_t key = scr_map_at(m, e)->key, val = scr_map_at(m, e)->val; + uint64_t key = scr_map_at(m, e)->key, val = scr_map_val_at(m, e); scr_map_kill_at(m, e); if (dense) dense[(size_t)scr_map_slot_to_f64(key)] = 0; scr_map_release_key(m, key); @@ -955,7 +1052,7 @@ bool scr_map_has_str(const ScrMap *m, const ScrStr *key) { static bool scr_map_delete_found(ScrMap *m, size_t e) { if (e == SCR_MAP_EMPTY) return false; - uint64_t key = scr_map_at(m, e)->key, val = scr_map_at(m, e)->val; + uint64_t key = scr_map_at(m, e)->key, val = scr_map_val_at(m, e); scr_map_kill_at(m, e); /* bucket slot stays: probe chains intact */ m->nlive--; if (m->ndense) m->dense[(size_t)scr_map_slot_to_f64(key)] = 0; @@ -1060,6 +1157,11 @@ static inline __attribute__((always_inline)) void scr_map_set_with( e = scr_map_bucket(m, slot); } if (e != SCR_MAP_EMPTY) { + if (scr_map_lookup_c(lookup)) { + /* Compact values are never counted: nothing to release. */ + ((ScrMapCEntry *)m->entries)[e].val = scr_map_cref_encode(val); + return; + } ScrMapEntry *x = scr_map_at_with(m, e, lookup); uint64_t old = x->val; x->val = val; /* unlink before releasing (cycle collector) */ @@ -1069,7 +1171,18 @@ static inline __attribute__((always_inline)) void scr_map_set_with( scr_map_append(m, hash, key, val, slot); } +/* The insert of a SCR_MAP_VAL_CREF map, out of line so that the string-key + * insert of other maps keeps its size. */ +static __attribute__((noinline)) void scr_map_set_str_c(ScrMap *m, uint64_t hash, uint64_t key, + uint64_t val) { + scr_map_set_with(m, hash, key, val, SCR_MAP_LOOKUP_STR_C); +} + static void scr_map_set_str_slot(ScrMap *m, uint64_t hash, uint64_t key, uint64_t val) { + if (scr_map_cref(m)) { + scr_map_set_str_c(m, hash, key, val); + return; + } scr_map_set_with(m, hash, key, val, SCR_MAP_LOOKUP_STR); } @@ -1157,6 +1270,7 @@ static void scr_map_set(ScrMap *m, uint64_t hash, uint64_t key, uint64_t val) { } switch (scr_map_lookup_kind(m)) { case SCR_MAP_LOOKUP_STR: scr_map_set_str_slot(m, hash, key, val); return; + case SCR_MAP_LOOKUP_STR_C: scr_map_set_str_c(m, hash, key, val); return; case SCR_MAP_LOOKUP_WORD: scr_map_set_word(m, hash, key, val); return; case SCR_MAP_LOOKUP_NUM: scr_map_set_num(m, hash, key, val); return; default: scr_map_set_with(m, hash, key, val, SCR_MAP_LOOKUP_GENERIC); return; @@ -1246,10 +1360,14 @@ bool scr_map_get_str_bool(const ScrMap *m, const ScrStr *key, bool *out) { return true; } +/* scr_map_entry_str serves maps with 24-byte entries (generated code reads + * a SCR_MAP_VAL_CREF map through scr_map_entry_str_c). */ static __attribute__((noinline)) const ScrMapEntry *scr_map_entry_str_slow(const ScrMap *m, const ScrStr *key) { - size_t e = scr_map_find_str(m, key); - return e == SCR_MAP_EMPTY ? NULL : scr_map_at(m, e); + if (m->nlive == 0) return NULL; + size_t e = scr_map_find_with(m, scr_map_hash_str(key), scr_map_slot_from_ptr((void *)key), + SCR_MAP_LOOKUP_STR); + return e == SCR_MAP_EMPTY ? NULL : &m->entries[e]; } /* A stored key with the probe's hash but another identity: compare bytes. @@ -1302,10 +1420,60 @@ const ScrMapEntry *scr_map_entry_str(const ScrMap *m, const ScrStr *key) { return scr_map_entry_str_slow(m, key); } +static __attribute__((noinline)) const ScrMapCEntry *scr_map_entry_str_c_slow(const ScrMap *m, + const ScrStr *key) { + size_t e = scr_map_find_str(m, key); + return e == SCR_MAP_EMPTY ? NULL : scr_map_c_at(m, e); +} + +static __attribute__((noinline)) const ScrMapCEntry *scr_map_entry_str_c_bytes(const ScrMap *m, + const ScrStr *key, + const ScrMapCEntry *e) { + const ScrStr *stored = scr_map_slot_to_ptr(e->key); + if (stored->len == key->len && scr_key_equal(stored->data, key->data, key->len)) return e; + return scr_map_entry_str_c_slow(m, key); +} + +/* scr_map_entry_str over a SCR_MAP_VAL_CREF map's 16-byte entries, whose + * hash is the low half of the widened one. */ +const ScrMapCEntry *scr_map_entry_str_c(const ScrMap *m, const ScrStr *key) { +#if SCR_STR_HASH_CACHE + uint32_t h = key->hash; + size_t nbuckets = m->nbuckets; + if (__builtin_expect(h != 0 && scr_map_compact_buckets(nbuckets), 1)) { + uint64_t hash = scr_map_hash_widen(h); + uint64_t bits = scr_map_filter_bits(hash); + if ((m->filter & bits) != bits) return NULL; + const ScrMapCEntry *entries = (const ScrMapCEntry *)m->entries; + if (nbuckets == 0) { + for (size_t e = 0, n = m->nentries; e < n; e++) { + if (entries[e].hash != h) continue; + if (entries[e].key == scr_map_slot_from_ptr((void *)key)) return &entries[e]; + return scr_map_entry_str_c_bytes(m, key, &entries[e]); + } + return NULL; + } + const uint32_t *buckets = m->buckets; + uint32_t tag = (uint32_t)(hash >> 32 & UINT32_C(0xff000000)); + size_t mask = nbuckets - 1; + for (size_t i = hash & mask;; i = (i + 1) & mask) { + uint32_t bucket = buckets[i]; + if (!bucket) return NULL; + if ((bucket & ~SCR_MAP_INDEX_MASK) != tag) continue; + const ScrMapCEntry *e = &entries[(bucket & SCR_MAP_INDEX_MASK) - 1]; + if (e->hash != h) continue; + if (e->key == scr_map_slot_from_ptr((void *)key)) return e; + return scr_map_entry_str_c_bytes(m, key, e); + } + } +#endif + return scr_map_entry_str_c_slow(m, key); +} + void *scr_map_get_str_ref(const ScrMap *m, const ScrStr *key) { size_t e = scr_map_find_str(m, key); if (e == SCR_MAP_EMPTY) return NULL; - return m->ops->val_retain(scr_map_slot_to_ptr(scr_map_at(m, e)->val)); /* +1 */ + return scr_map_val_retain(m, scr_map_val_at(m, e)); /* +1 */ } bool scr_map_get_ref_f64(const ScrMap *m, const void *key, double *out) { @@ -1408,6 +1576,12 @@ static const ScrMapEntry *scr_map_iter_at(const ScrMap *m, double i) { return scr_map_at(m, (size_t)i); } +/* The value of the live entry at iteration index i as an 8-byte slot. */ +static uint64_t scr_map_iter_val_slot(const ScrMap *m, double i) { + (void)scr_map_iter_at(m, i); + return scr_map_val_at(m, (size_t)i); +} + double scr_map_iter_key_f64(const ScrMap *m, double i) { return scr_map_slot_to_f64(scr_map_iter_at(m, i)->key); } @@ -1442,7 +1616,7 @@ ScrDyn *scr_map_dyn_key(const ScrMap *map, double index) { } ScrDyn *scr_map_dyn_value(const ScrMap *map, double index) { - uint64_t slot = scr_map_iter_at(map, index)->val; + uint64_t slot = scr_map_iter_val_slot(map, index); return map->dyn_ops ? map->dyn_ops->val_box(slot) : scr_dyn_retain(scr_map_slot_to_ptr(slot)); } @@ -1491,6 +1665,9 @@ ScrMap *scr_map_clone(const ScrMap *source, bool keys_only, const ScrMapOps *ops if (!out->entries) scr_map_oom(); out->ecap = (uint32_t)count; size_t top = 0; + /* A key-only copy of a SCR_MAP_VAL_CREF map has the 24-byte layout: its + * entries are copied field by field. */ + bool same_layout = scr_map_cref(source) == scr_map_cref(out); for (size_t i = 0; i < source->nentries; i++) { if (!scr_map_live_at(source, i)) continue; const ScrMapEntry *entry = scr_map_at(source, i); @@ -1498,11 +1675,13 @@ ScrMap *scr_map_clone(const ScrMap *source, bool keys_only, const ScrMapOps *ops size_t k = (size_t)scr_map_slot_to_f64(entry->key); if (k > top) top = k; } - ScrMapEntry *copy = scr_map_at(out, out->nentries++); - memcpy(copy, entry, entry_size); + size_t e = out->nentries++; + ScrMapEntry *copy = scr_map_at(out, e); + if (__builtin_expect(same_layout, 1)) memcpy(copy, entry, entry_size); + else scr_map_put_at(out, e, entry->key, keys_only ? 0 : scr_map_val_at(source, i), scr_map_hash_at(source, i)); if (out->key_kind == SCR_MAP_KEY_STR) scr_str_retain(scr_map_slot_to_ptr(copy->key)); else if (scr_map_ref_key(out->key_kind)) out->ops->key_retain(scr_map_slot_to_ptr(copy->key)); - if (keys_only) copy->val = 0; + if (keys_only) scr_map_val_put(out, e, 0); else if (out->val_kind == SCR_MAP_VAL_REF) out->ops->val_retain(scr_map_slot_to_ptr(copy->val)); } out->nlive = (uint32_t)count; @@ -1550,7 +1729,7 @@ ScrMap *scr_map_clone_dyn(const ScrMap *source, bool keys_only) { void scr_map_values_into(ScrMap *set, const ScrMap *source, bool checked) { for (size_t i = 0; i < source->nentries; i++) { if (!scr_map_live_at(source, i)) continue; - uint64_t value = checked ? scr_map_slot_from_ptr(scr_map_dyn_value(source, (double)i)) : scr_map_at(source, i)->val; + uint64_t value = checked ? scr_map_slot_from_ptr(scr_map_dyn_value(source, (double)i)) : scr_map_val_at(source, i); if (set->key_kind == SCR_MAP_KEY_F64) value = scr_map_f64_bits(scr_map_slot_to_f64(value)); scr_map_set(set, scr_map_hash_key(set, value), value, 0); if (checked) scr_dyn_release(scr_map_slot_to_ptr(value)); @@ -1567,7 +1746,7 @@ ScrArr *scr_set_to_arr_dyn(const ScrMap *map) { } void *scr_map_iter_val_ref(const ScrMap *m, double i) { - return m->ops->val_retain(scr_map_slot_to_ptr(scr_map_iter_at(m, i)->val)); /* +1 */ + return scr_map_val_retain(m, scr_map_iter_val_slot(m, i)); /* +1 */ } /* A published (immortal) map never changes, so iterating it needs no diff --git a/packages/runtime/src/scr_map.h b/packages/runtime/src/scr_map.h index c61df9a90..1f6e3ee12 100644 --- a/packages/runtime/src/scr_map.h +++ b/packages/runtime/src/scr_map.h @@ -50,7 +50,11 @@ typedef enum { SCR_MAP_KEY_BIGINT, SCR_MAP_KEY_BOOL, SCR_MAP_KEY_UNION_VALUE, SCR_MAP_KEY_NULL, SCR_MAP_KEY_UNDEFINED /* tag-only union arms */ } ScrMapKeyKind; -typedef enum { SCR_MAP_VAL_F64, SCR_MAP_VAL_BOOL, SCR_MAP_VAL_REF } ScrMapValKind; +/* SCR_MAP_VAL_CREF: a string-key map whose values are compact class + * instances in the program-lifetime region (llvm/classes.ts + * LlClassMeta.compact). Its entries are ScrMapCEntry; the values are never + * counted or traced. */ +typedef enum { SCR_MAP_VAL_F64, SCR_MAP_VAL_BOOL, SCR_MAP_VAL_REF, SCR_MAP_VAL_CREF } ScrMapValKind; typedef struct { uint64_t key; /* double bits (normalized), ScrStr* (owned), or ref ptr (owned) */ @@ -65,6 +69,15 @@ typedef struct { uint64_t val; } ScrMapNumEntry; #define SCR_MAP_NUM_DEAD UINT64_C(0x7ff0000000000001) +/* A SCR_MAP_VAL_CREF map's entry: the string key, the key's 32-bit hash + * (its cached 64-bit hash is the same value in both halves, + * scr_map.c scr_map_hash_widen; zero = tombstone), and the value as an + * address over 8 (0 for NULL). */ +typedef struct { + uint64_t key; + uint32_t hash; + uint32_t val; +} ScrMapCEntry; /* Checked views use the original storage. Slot converters borrow on reads * and return owned references on writes, just like typed Map operations. */ @@ -140,16 +153,25 @@ typedef struct ScrMap { * the whole deleted prefix again. */ uint32_t head; } ScrMap; -/* The entry at index i of either layout, and whether it is live (code that - * walks a map's entries outside scr_map.c). Key and value are at bytes 0 - * and 8 in both layouts; a number-key entry has no hash field. */ +/* The entry at index i of any layout, whether it is live, and its value as + * an 8-byte slot (code that walks a map's entries outside scr_map.c). The + * key is at byte 0 in every layout; a number-key entry has no hash field, + * and a SCR_MAP_VAL_CREF entry keeps its value in 4 bytes. */ static inline const ScrMapEntry *scr_map_entry_at(const ScrMap *m, size_t i) { - size_t size = m->key_kind == SCR_MAP_KEY_F64 ? sizeof(ScrMapNumEntry) : sizeof(ScrMapEntry); + size_t size = m->key_kind == SCR_MAP_KEY_F64 ? sizeof(ScrMapNumEntry) + : m->val_kind == SCR_MAP_VAL_CREF ? sizeof(ScrMapCEntry) + : sizeof(ScrMapEntry); return (const ScrMapEntry *)((const char *)m->entries + i * size); } static inline bool scr_map_entry_live(const ScrMap *m, size_t i) { const ScrMapEntry *e = scr_map_entry_at(m, i); - return m->key_kind == SCR_MAP_KEY_F64 ? e->key != SCR_MAP_NUM_DEAD : e->hash != 0; + if (m->key_kind == SCR_MAP_KEY_F64) return e->key != SCR_MAP_NUM_DEAD; + if (m->val_kind == SCR_MAP_VAL_CREF) return ((const ScrMapCEntry *)e)->hash != 0; + return e->hash != 0; +} +static inline uint64_t scr_map_entry_val(const ScrMap *m, const ScrMapEntry *e) { + if (m->val_kind == SCR_MAP_VAL_CREF) return (uint64_t)((const ScrMapCEntry *)e)->val << 3; + return e->val; } /* The LLVM backend reads the header through two types (emitter.ts). Its * inline retain/release test the traced byte as %ScrMapRc field 3, as @@ -162,7 +184,8 @@ static inline bool scr_map_entry_live(const ScrMap *m, size_t i) { * third field (sc_map_iter_entry), or for a number-key map its key against * SCR_MAP_NUM_DEAD (sc_map_iter_entry_num). Iteration over a checked * (unknown-key) Map or Set picks one of the two steps from the key kind, - * %ScrMapIx field 1, compared with SCR_MAP_KEY_F64 (0). */ + * %ScrMapIx field 1, compared with SCR_MAP_KEY_F64 (0). Map.size + * (sc_map_size) reads the live count, field 9, as scr_map_size does. */ _Static_assert(offsetof(ScrMap, key_kind) == sizeof(size_t) && SCR_MAP_KEY_F64 == 0, "inline iteration over checked collections reads ScrMap.key_kind as %ScrMapIx field 1"); _Static_assert(offsetof(ScrMap, traced) == sizeof(size_t) + 2, @@ -193,6 +216,9 @@ _Static_assert(sizeof(ScrMapEntry) == 24 && offsetof(ScrMapEntry, val) == 8, "inline lookups read ScrMapEntry.val at byte 8"); _Static_assert(sizeof(ScrMapNumEntry) == 16 && offsetof(ScrMapNumEntry, val) == 8, "inline number-key lookups and iteration read %ScrMapNumEntry { key, val }"); +_Static_assert(sizeof(ScrMapCEntry) == 16 && offsetof(ScrMapCEntry, hash) == 8 && + offsetof(ScrMapCEntry, val) == 12, + "inline compact-value lookups and iteration read %ScrMapCEntry { key, hash, val }"); _Static_assert(sizeof(ScrMap) <= 11 * sizeof(uint64_t), "the ScrMap header stays within 88 bytes"); /* Entry for a number key, or NULL when absent. The pointer is valid until * the next mutation; generated code reads the value slot through it. */ @@ -201,6 +227,9 @@ const ScrMapEntry *scr_map_entry_f64(const ScrMap *m, double key); * reference reads retain the value with its static type's retain instead * of the collection's adapter. */ const ScrMapEntry *scr_map_entry_str(const ScrMap *m, const ScrStr *key); +/* scr_map_entry_str for a SCR_MAP_VAL_CREF map: its entry is a + * ScrMapCEntry. */ +const ScrMapCEntry *scr_map_entry_str_c(const ScrMap *m, const ScrStr *key); /* String-key lookups borrow an exact byte span. Reference results return * +1; scalar outputs are written only on a hit, like the string getters. */ diff --git a/packages/runtime/src/scr_message.c b/packages/runtime/src/scr_message.c index 5e1bce37f..d4eb263cf 100644 --- a/packages/runtime/src/scr_message.c +++ b/packages/runtime/src/scr_message.c @@ -1,32 +1,9 @@ #include "scr_message.h" +#include "scr_mailbox.h" #include #include -typedef enum { - SCR_MESSAGE_UNDEFINED, - SCR_MESSAGE_NULL, - SCR_MESSAGE_BOOL, - SCR_MESSAGE_NUMBER, - SCR_MESSAGE_STRING, - SCR_MESSAGE_BIGINT, - SCR_MESSAGE_ARRAY, - SCR_MESSAGE_OBJECT, - SCR_MESSAGE_STORAGE, - SCR_MESSAGE_BYTES, - SCR_MESSAGE_ARRAY_BUFFER, - SCR_MESSAGE_ERROR, - /* A deeply frozen plain-data graph shared by pointer (see publication). */ - SCR_MESSAGE_PUBLISHED, - /* A typed value published with @scriptc/threads (immortal), delivered as - * a fresh typed-ref capsule over the same pointer. */ - SCR_MESSAGE_TYPED_REF, - /* A transferred MessagePort: its channel end (+1) and side. */ - SCR_MESSAGE_PORT, - /* A native handle cloned through its unit's ScrDynHandleTransfer. */ - SCR_MESSAGE_HANDLE, -} ScrMessageKind; - const ScrMessagePortOps *scr_message_port_ops = NULL; typedef struct { @@ -95,6 +72,8 @@ struct ScrMessage { size_t count; bool storage; /* has STORAGE nodes */ bool releases; /* has STORAGE, PORT or HANDLE nodes, which own resources */ + bool readable; /* see scr_message_readable */ + ScrMailEvent event; /* the event that carries the message (scr_message_mail_event) */ }; typedef struct { @@ -121,6 +100,10 @@ typedef struct { * and properties allocates only its block. */ #define SCR_MESSAGE_INLINE 8 +/* How deep live records nest before the rest of a graph encodes through + * the visit stack (scr_message_value recurses for them). */ +#define SCR_MESSAGE_RECORD_DEPTH 32 + typedef struct { ScrMessageNode *nodes; ScrMessageVisit *visits; @@ -143,6 +126,9 @@ typedef struct { bool uncaught; /* the next value is an uncaught exception or its cause */ bool storage; bool releases; + bool shared; /* a node was reached twice */ + size_t record_depth; /* live records being encoded from their fields */ + bool adopts; /* has PORT or HANDLE nodes */ ScrMessageNode inline_nodes[SCR_MESSAGE_INLINE]; ScrMessageVisit inline_visits[SCR_MESSAGE_INLINE]; ScrMessagePending inline_properties[SCR_MESSAGE_INLINE]; @@ -168,7 +154,8 @@ static void scr_message_encoder_init(ScrMessageEncoder *encoder, const ScrDyn *t encoder->stack_capacity = SCR_MESSAGE_INLINE; encoder->transfer = transfer; encoder->uncaught = uncaught; - encoder->storage = encoder->releases = false; + encoder->storage = encoder->releases = encoder->shared = encoder->adopts = false; + encoder->record_depth = 0; } static bool scr_message_listed(const ScrMessageEncoder *encoder, const ScrDyn *value) { @@ -290,7 +277,10 @@ static void scr_message_push(ScrMessageEncoder *encoder, size_t node) { static size_t scr_message_storage(ScrMessageEncoder *encoder, const ScrBytes *bytes) { const ScrBytes *owner = bytes->backing ? bytes->backing : bytes; size_t found = scr_message_find(encoder, owner, SCR_MESSAGE_STORAGE + 1); - if (found != SIZE_MAX) return found; + if (found != SIZE_MAX) { + encoder->shared = true; + return found; + } size_t node = scr_message_node(encoder); scr_message_identify(encoder, owner, SCR_MESSAGE_STORAGE + 1, node); ScrMessageNode *storage = &encoder->nodes[node]; @@ -339,6 +329,10 @@ static ScrDyn *scr_message_uncaught_keys(const ScrDyn *view) { return keys; } +static void scr_message_property(ScrMessageEncoder *encoder, size_t id, + const ScrStr *key, size_t value, bool hidden); +static bool scr_message_drain(ScrMessageEncoder *encoder, size_t base); + static size_t scr_message_value(ScrMessageEncoder *encoder, const ScrDyn *input) { bool uncaught = encoder->uncaught; encoder->uncaught = false; @@ -360,7 +354,10 @@ static size_t scr_message_value(ScrMessageEncoder *encoder, const ScrDyn *input) return SIZE_MAX; } size_t found = scr_message_find(encoder, identity, domain); - if (found != SIZE_MAX) return found; + if (found != SIZE_MAX) { + encoder->shared = true; + return found; + } size_t id = scr_message_node(encoder); scr_message_identify(encoder, identity, domain, id); encoder->visits[id].input = scr_dyn_retain((ScrDyn *)input); @@ -397,6 +394,46 @@ static size_t scr_message_value(ScrMessageEncoder *encoder, const ScrDyn *input) node->v.published = (ScrDyn *)input; return id; } + /* A live record capsule (scr_dyn_typed_ref_bind_record) whose view was + * never created encodes its fields from the record: the view would hold + * exactly those keys, in that order, with the values its ops box. Nested + * records encode the same way up to a depth, then through their views + * and the visit stack. */ + const ScrDynTypedRecordOps *record = + input->kind == SCR_DYN_TYPED_REF && encoder->record_depth < SCR_MESSAGE_RECORD_DEPTH + ? scr_dyn_typed_ref_live_record(input) : NULL; + if (record) { + encoder->visits[id].view = (ScrDyn *)input; + encoder->nodes[id].kind = SCR_MESSAGE_OBJECT; + void *rec = input->v.typed_ref.ptr; + encoder->record_depth++; + size_t result = id; + /* A field's value encodes completely, its containers' properties + * included, before the next field's: getters run in Node's order. */ + size_t base = encoder->depth; + for (size_t i = 0; i < record->count; i++) { + if (!record->fixed && !record->has(rec, i)) continue; + ScrDyn *child = record->get(rec, i); + if (!child || scr_exc_pending()) { + scr_dyn_release(child); + result = SIZE_MAX; + break; + } + size_t next = scr_message_value(encoder, child); + scr_dyn_release(child); + if (next == SIZE_MAX) { + result = SIZE_MAX; + break; + } + scr_message_property(encoder, id, record->names[i], next, false); + if (encoder->depth > base && !scr_message_drain(encoder, base)) { + result = SIZE_MAX; + break; + } + } + encoder->record_depth--; + return result; + } ScrDyn *value = input->kind == SCR_DYN_TYPED_REF ? scr_dyn_typed_ref_materialize(input) : (ScrDyn *)input; encoder->visits[id].view = value; @@ -482,7 +519,7 @@ static size_t scr_message_value(ScrMessageEncoder *encoder, const ScrDyn *input) node->kind = SCR_MESSAGE_PORT; node->v.port.channel = channel; node->v.port.side = side; - encoder->releases = true; + encoder->releases = encoder->adopts = true; break; } bool buffer = scr_buffer_storage_is(value); @@ -498,7 +535,7 @@ static size_t scr_message_value(ScrMessageEncoder *encoder, const ScrDyn *input) node->kind = SCR_MESSAGE_HANDLE; node->v.handle.transfer = transfer; node->v.handle.payload = payload; - encoder->releases = true; + encoder->releases = encoder->adopts = true; break; } const ScrBytes *bytes = buffer ? value->v.handle.ptr : value->v.bytes; @@ -678,6 +715,7 @@ static ScrMessage *scr_message_try_publish(const ScrDyn *value) { message = scr_message_alloc(1, sizeof(*message) + sizeof(*message->nodes)); message->nodes = (ScrMessageNode *)(message + 1); message->count = 1; + message->readable = true; message->nodes[0].kind = SCR_MESSAGE_PUBLISHED; message->nodes[0].v.published = (ScrDyn *)value; if (getenv("SCRIPTC_PUBLISH_TRACE")) @@ -770,7 +808,8 @@ static ScrMessage *scr_message_pack(ScrMessageEncoder *encoder) { ScrMessageProperty *slots = (ScrMessageProperty *)(nodes + count); ScrMessageTyped *records = (ScrMessageTyped *)(slots + properties); char *text = (char *)(records + typed); - *message = (ScrMessage){nodes, count, encoder->storage, encoder->releases}; + *message = (ScrMessage){.nodes = nodes, .count = count, .storage = encoder->storage, + .releases = encoder->releases, .readable = !encoder->shared && !encoder->adopts}; memcpy(nodes, encoder->nodes, count * sizeof(*nodes)); if (typed) memcpy(records, encoder->typed, typed * sizeof(*records)); size_t first = 0; @@ -801,10 +840,11 @@ static ScrMessage *scr_message_pack(ScrMessageEncoder *encoder) { return message; } -static ScrMessage *scr_message_encode_run(ScrMessageEncoder *encoder, const ScrDyn *value) { - ScrMessage *message = NULL; - if (scr_message_value(encoder, value) == SIZE_MAX) goto done; - while (encoder->depth && !scr_exc_pending()) { +/* Encodes the properties of the containers on the visit stack above + * `base`, depth first, as Node's serializer visits them (getters run in + * that order). False when a property could not be read or encoded. */ +static bool scr_message_drain(ScrMessageEncoder *encoder, size_t base) { + while (encoder->depth > base && !scr_exc_pending()) { size_t id = encoder->stack[encoder->depth - 1]; ScrMessageVisit *visit = &encoder->visits[id]; if (visit->cursor == visit->keys->v.arr.len) { encoder->depth--; continue; } @@ -812,7 +852,7 @@ static ScrMessage *scr_message_encode_run(ScrMessageEncoder *encoder, const ScrD bool error = encoder->nodes[id].kind == SCR_MESSAGE_ERROR; if (key->kind != SCR_DYN_STR || (!error && !scr_dyn_property_is_enumerable(visit->view, key->v.str))) continue; ScrDyn *child = scr_dyn_bag_get(visit->view, key->v.str, visit->view); - if (!child || scr_exc_pending()) { scr_dyn_release(child); break; } + if (!child || scr_exc_pending()) { scr_dyn_release(child); return false; } bool hidden = error; if (visit->uncaught) { /* Node keeps an uncaught Error's data properties except functions @@ -823,10 +863,15 @@ static ScrMessage *scr_message_encode_run(ScrMessageEncoder *encoder, const ScrD } size_t next = scr_message_value(encoder, child); scr_dyn_release(child); - if (next == SIZE_MAX) break; + if (next == SIZE_MAX) return false; scr_message_property(encoder, id, key->v.str, next, hidden); } -done: + return !scr_exc_pending(); +} + +static ScrMessage *scr_message_encode_run(ScrMessageEncoder *encoder, const ScrDyn *value) { + ScrMessage *message = NULL; + if (scr_message_value(encoder, value) != SIZE_MAX) scr_message_drain(encoder, 0); if (!scr_exc_pending()) message = scr_message_pack(encoder); else for (size_t i = 0; i < encoder->count; i++) scr_message_drop(&encoder->nodes[i]); @@ -1014,6 +1059,33 @@ done:; return result; } +bool scr_message_readable(const ScrMessage *message) { return message->readable; } + +ScrMailEvent *scr_message_mail_event(ScrMessage *message) { + message->event = (ScrMailEvent){.message = message, .in_message = true}; + return &message->event; +} + +int32_t scr_message_kind(const ScrMessage *message, size_t node) { return message->nodes[node].kind; } + +double scr_message_number(const ScrMessage *message, size_t node) { return message->nodes[node].v.number; } + +ScrStr *scr_message_string(const ScrMessage *message, size_t node) { + return scr_str_new(message->nodes[node].v.text.data, message->nodes[node].v.text.length); +} + +bool scr_message_fields(const ScrMessage *message, size_t node, const char *const *names, + const size_t *lengths, size_t count, size_t *values) { + const ScrMessageNode *object = &message->nodes[node]; + if (object->kind != SCR_MESSAGE_OBJECT || object->count != count) return false; + for (size_t i = 0; i < count; i++) { + const ScrMessageProperty *property = &object->properties[i]; + if (property->length != lengths[i] || memcmp(property->key, names[i], lengths[i])) return false; + values[i] = property->value; + } + return true; +} + void scr_message_free(ScrMessage *message) { if (!message) return; if (message->releases) diff --git a/packages/runtime/src/scr_message.test.c b/packages/runtime/src/scr_message.test.c index bdabc4d12..b6adf34c5 100644 --- a/packages/runtime/src/scr_message.test.c +++ b/packages/runtime/src/scr_message.test.c @@ -250,6 +250,52 @@ static void check_large(void) { scr_message_free(message); } +/* The readers compiled 'message' listeners use: node kinds and values, a + * record's keys in order, and which messages they may read. */ +static void check_readers(void) { + ScrDyn *root = scr_dyn_new_obj(); + put_number(root, "id", 7); + ScrStr *text = scr_str_new("seven", 5); + scr_dyn_obj_set(root, "name", 4, scr_dyn_new_str(text)); + scr_str_release(text); + scr_dyn_obj_set(root, "done", 4, scr_dyn_new_bool(true)); + ScrMessage *message = scr_message_encode(root); + assert(message && scr_message_readable(message)); + assert(scr_message_kind(message, 0) == SCR_MESSAGE_OBJECT); + static const char *const names[] = {"id", "name", "done"}; + static const size_t lengths[] = {2, 4, 4}; + size_t values[3]; + assert(scr_message_fields(message, 0, names, lengths, 3, values)); + assert(scr_message_kind(message, values[0]) == SCR_MESSAGE_NUMBER); + assert(scr_message_number(message, values[0]) == 7); + assert(scr_message_kind(message, values[1]) == SCR_MESSAGE_STRING); + ScrStr *name = scr_message_string(message, values[1]); + assert(name->len == 5 && !memcmp(name->data, "seven", 5)); + scr_str_release(name); + assert(scr_message_kind(message, values[2]) == SCR_MESSAGE_BOOL); + assert(scr_message_number(message, values[2]) == 1); + /* Another order, a missing key or an extra key does not match. */ + static const char *const reordered[] = {"name", "id", "done"}; + static const size_t reordered_lengths[] = {4, 2, 4}; + assert(!scr_message_fields(message, 0, reordered, reordered_lengths, 3, values)); + assert(!scr_message_fields(message, 0, names, lengths, 2, values)); + assert(!scr_message_fields(message, values[0], names, lengths, 3, values)); + scr_message_free(message); + put_number(root, "extra", 1); + message = scr_message_encode(root); + assert(!scr_message_fields(message, 0, names, lengths, 3, values)); + scr_message_free(message); + /* A value reached twice makes the message unreadable. */ + ScrDyn *shared = scr_dyn_new_obj(); + scr_dyn_obj_set(root, "first", 5, scr_dyn_retain(shared)); + scr_dyn_obj_set(root, "second", 6, shared); + message = scr_message_encode(root); + assert(message && !scr_message_readable(message)); + scr_message_free(message); + scr_dyn_release(root); + scr_collect_cycles(); +} + /* A link-gated unit's handle crosses threads through the transfer hooks in * its ops: share on the sender, adopt on the receiver, drop with the * message. The test borrows the last capacity slot, which no unit uses, so @@ -376,6 +422,7 @@ int main(void) { check_rejection(); check_documents(); check_large(); + check_readers(); check_handle_transfer(); scr_context_cleanup(); puts("message graphs preserve identity, sparse values, byte aliases and getter order"); diff --git a/packages/runtime/src/scr_net.c b/packages/runtime/src/scr_net.c index f091daede..29a4af393 100644 --- a/packages/runtime/src/scr_net.c +++ b/packages/runtime/src/scr_net.c @@ -517,6 +517,9 @@ struct ScrNetSocket { int remote_port_cache; bool remote_v6_cache; ScrStr *pending_err; /* deferred connect failure */ + /* pending_err's code when its message does not name one (static + * storage; NULL = the code the message maps to) */ + const char *pending_code; /* the caller-lookup dial (net.connect with a lookup option): true while * the lookup decides; dial_ips holds its answered addresses (+1 each), * dialed in order — a connect failure closes the fd and tries @@ -531,6 +534,9 @@ struct ScrNetSocket { size_t dial_n, dial_i; /* a host name the dial resolves first (+1; NULL for IP literals) */ ScrStr *dial_host; + /* the host a client dial was given, Node's socket._host (+1; NULL on + * server sockets): tls.connect({ socket }) verifies against it */ + ScrStr *host_name; /* the in-flight dial chain's LAST failure — surfaced (moved into * pending_err) only when the list exhausts; a later success drops it */ ScrStr *dial_err; @@ -607,6 +613,12 @@ struct ScrNetSocket { * until the proxy answers 200: writes buffer and the transport * handshake waits until the tunnel is up */ struct ScrNetTunnel *tunnel; + /* new tls.TLSSocket(socket) and tls.connect({ socket }): on the wrapped + * socket, the socket that took over its connection (+1; end() and + * destroy() act on it); on that socket, the one it took over (+1; its + * 'close' fires just before this socket's, as in Node) */ + struct ScrNetSocket *adopted_by; + struct ScrNetSocket *adopted_from; }; /* The CONNECT head still being sent from out_off, and the proxy's @@ -616,6 +628,9 @@ typedef struct ScrNetTunnel { size_t out_len, out_off; char *in; size_t in_len; + /* bytes a wrapped socket left unsent, which go out before the + * transport's own; no response head is read */ + bool send_only; } ScrNetTunnel; /* The 'lookup' event's half of the runtime, installed by the first @@ -712,6 +727,7 @@ void scr_net_sock_release(ScrNetSocket *s) { free(s->dial_ips); scr_str_release(s->dial_err); scr_str_release(s->dial_host); + scr_str_release(s->host_name); scr_str_release(s->unix_path); scr_net_ls_drop(&s->lookup_ls); if (s->lookup_evs != NULL) scr_net_lookup_ops->free(s); @@ -721,6 +737,8 @@ void scr_net_sock_release(ScrNetSocket *s) { if (s->pipe_dst) scr_net_sock_release(s->pipe_dst); if (s->conn_pending) scr_net_server_release(s->conn_pending); if (s->server) scr_net_server_release(s->server); + if (s->adopted_by) scr_net_sock_release(s->adopted_by); + if (s->adopted_from) scr_net_sock_release(s->adopted_from); if (s->fd >= 0) scr_net_close_fd_raw(s->fd); #ifdef SCR_RC_AUDIT scr_net_live--; @@ -962,7 +980,7 @@ static ScrNetSocket *scr_net_sock_new(void) { static void scr_net_sock_update_read(ScrNetSocket *s) { bool want = s->fd >= 0 && !s->connecting && !s->rd_eof && !s->user_paused && (s->data_ls.n > 0 || s->pipe_dst != NULL || s->native_data != NULL || - s->tunnel != NULL /* the proxy's CONNECT answer */ || + (s->tunnel != NULL && !s->tunnel->send_only) /* the proxy's CONNECT answer */ || s->flowing /* resume(): flow (and discard sans listeners) */ || s->readable_ls.n > 0 /* paused-mode consumer: bytes buffer */ || s->tops != NULL /* a transport is ALWAYS a consumer: the TLS @@ -977,7 +995,8 @@ static void scr_net_sock_update_read(ScrNetSocket *s) { * finished socket the same way */ || (!s->eof_pending && s->rlen - s->rhead < SCR_NET_READ_AHEAD) /* no consumer yet: read ahead into the buffer, as Node's - * stream does up to its high-water mark */); + * stream does up to its high-water mark */) && + !(s->tunnel != NULL && s->tunnel->send_only); if (want && !s->read_armed) { scr_net_watch_read(s->fd, s, true); s->read_armed = true; @@ -1056,6 +1075,23 @@ static void scr_net_sock_buffer(ScrNetSocket *s, const char *data, size_t len) { * dead or ended write half (Node fires 'error' there; this slice drops — * SEMANTICS.md documents the bound). */ static void scr_net_sock_write_raw(ScrNetSocket *s, const char *data, size_t len) { + if (s->adopted_by != NULL) { + /* Node writes these bytes to the wire as they are, between the TLS + * records of the socket that took over (the peer then fails the + * session). Bytes the kernel cannot take now end that socket. */ + ScrNetSocket *t = s->adopted_by; + while (len > 0 && t->fd >= 0) { + ssize_t sent = scr_net_send(t->fd, data, len); + if (sent < 0 && errno == EINTR) continue; + if (sent <= 0) { + scr_net_sock_destroy(t); + break; + } + data += sent; + len -= (size_t)sent; + } + return; + } /* A socket whose dial hasn't STARTED yet (the agent's deferred dial, * the caller-lookup wait) has no fd but is logically connecting — * bytes buffer and flush at establishment like any mid-connect write. */ @@ -1490,6 +1526,12 @@ static void scr_net_sock_tunnel_pump(ScrNetSocket *s) { } scr_net_sock_update_write(s); if (s->fd < 0 || t->out_off < t->out_len) return; + if (t->send_only) { + scr_net_tunnel_free(t); + s->tunnel = NULL; + scr_net_sock_connected(s); + return; + } for (;;) { char buf[4096]; ssize_t n = read(s->fd, buf, sizeof buf); @@ -1592,6 +1634,10 @@ static void scr_net_sock_transport_pump(ScrNetSocket *s) { if (scr_exc_pending()) return; scr_net_fire0_this(&s->ready_ls, s, SCR_DYNH_NET_SOCKET); if (scr_exc_pending()) return; + if (s->tops->after_established) { + s->tops->after_established(s->tctx); + if (scr_exc_pending()) return; + } scr_net_sock_flush(s); scr_net_sock_update_read(s); scr_net_sock_update_write(s); @@ -2250,6 +2296,8 @@ ScrStr *scr_net_blocking_lookup(ScrStr *host /*borrowed*/) { * autoselect), and any other name is resolved when the dial starts. */ static void scr_net_sock_set_peer(ScrNetSocket *s, ScrStr *host /*borrowed, nullable*/) { const char *h = host && host->len > 0 ? host->data : "localhost"; + scr_str_release(s->host_name); + s->host_name = scr_str_new(h, strlen(h)); if (strcmp(h, "localhost") == 0) { h = "127.0.0.1"; if (scr_net_lookup_ops != NULL) scr_net_lookup_ops->record(s, "localhost", 9, h, 4, NULL); @@ -3024,6 +3072,10 @@ void scr_net_sock_write_bytes(ScrNetSocket *s, ScrBytes *data /*borrowed*/) { } void scr_net_sock_end(ScrNetSocket *s) { + if (s->adopted_by != NULL) { + scr_net_sock_end(s->adopted_by); + return; + } if (s->fd < 0 || s->wr_ending) return; s->wr_ending = true; scr_net_sock_maybe_finish_write(s); @@ -3136,7 +3188,10 @@ void scr_net_sock_on_write_flush(ScrNetSocket *s, ScrClosure *cb /*moves*/) { double scr_net_sock_bytes_written(ScrNetSocket *s) { return (double)s->bytes_written; } /* socket.readable: true until the read half is done (peer FIN / destroy). */ -bool scr_net_sock_readable(ScrNetSocket *s) { return s->fd >= 0 && !s->rd_eof; } +bool scr_net_sock_readable(ScrNetSocket *s) { + if (s->adopted_by != NULL) return scr_net_sock_readable(s->adopted_by); + return s->fd >= 0 && !s->rd_eof; +} /* socket.setTimeout(ms): ms > 0 arms the idle timer NOW (Node starts the * clock at the call — connecting time counts), ms <= 0 disables. */ @@ -3206,6 +3261,7 @@ ScrStr *scr_net_sock_remote_family(ScrNetSocket *s) { /* socket.localAddress / localPort / localFamily: read from the open * socket (undefined while connecting and after close, as in Node). */ static bool scr_net_sock_local(ScrNetSocket *s, char *addr, size_t cap, int *port, bool *v6) { + if (s->adopted_by != NULL) return scr_net_sock_local(s->adopted_by, addr, cap, port, v6); if (s->fd < 0 || s->connecting || s->dial_deferred || s->lookup_wait) return false; struct sockaddr_storage ss; socklen_t len = sizeof ss; @@ -4652,6 +4708,10 @@ bool scr_net_is_ipv4(const ScrDyn *input) { return scr_net_is_ip(input) == 4; } bool scr_net_is_ipv6(const ScrDyn *input) { return scr_net_is_ip(input) == 6; } void scr_net_sock_destroy(ScrNetSocket *s) { + if (s->adopted_by != NULL) { + scr_net_sock_destroy(s->adopted_by); + return; + } if (s->fd < 0 && !s->in_registry) return; s->rd_eof = true; s->wr_done = true; @@ -4833,7 +4893,8 @@ void scr_net_sock_on_error(ScrNetSocket *s, ScrClosure *cb /*moves*/, ScrChildEr } void scr_net_sock_on_connect(ScrNetSocket *s, ScrClosure *cb /*moves*/, bool once) { - if (s->close_emitted || (!s->connecting && s->fd < 0)) { + /* a socket that took over another's connection never emits 'connect' */ + if (s->close_emitted || (!s->connecting && s->fd < 0) || s->adopted_from != NULL) { scr_closure_release(cb); return; } @@ -4939,12 +5000,15 @@ void scr_net_sock_clear_native_reader(ScrNetSocket *s) { /* socket.destroyed — true once the fd is gone (destroy() or full close), * Node's flag for "this stream is done". */ -bool scr_net_sock_destroyed(ScrNetSocket *s) { return s->fd < 0; } +bool scr_net_sock_destroyed(ScrNetSocket *s) { + return s->adopted_by != NULL ? scr_net_sock_destroyed(s->adopted_by) : s->fd < 0; +} /* socket.writable — Node's stream flag: the write half is open (no end() * yet, no FIN sent, fd alive). Connecting sockets answer true — writes * queue, exactly Node. */ bool scr_net_sock_writable(ScrNetSocket *s) { + if (s->adopted_by != NULL) return scr_net_sock_writable(s->adopted_by); return s->fd >= 0 && !s->wr_ending && !s->wr_done; } @@ -4996,8 +5060,78 @@ void scr_net_sock_set_transport(ScrNetSocket *s, const ScrNetTransportOps *ops, scr_net_sock_update_write(s); } +/* A fresh socket takes over raw's open connection: the fd, the bytes it + * buffered either way and the peer identity. raw keeps its listeners and + * its server's accounting but does no I/O of its own; end() and destroy() + * on it act on the new socket, and its 'close' fires just before the new + * socket's. Bytes raw had not sent yet go out first. +1; the caller sets + * the transport, then starts it with scr_net_sock_adopt_start. NULL when + * raw has no open, idle connection to give (connecting, closed, already + * wrapped, or carrying a transport). */ +ScrNetSocket *scr_net_sock_adopt(ScrNetSocket *raw) { + if (raw->fd < 0 || scr_net_sock_connecting(raw) || raw->adopted_by != NULL || raw->tops != NULL) + return NULL; + ScrNetSocket *t = scr_net_sock_new(); + scr_str_release(scr_net_sock_remote_address(raw)); /* cache the peer while the fd is raw's */ + if (raw->remote_cache) { + t->remote_cache = scr_str_retain(raw->remote_cache); + t->remote_port_cache = raw->remote_port_cache; + t->remote_v6_cache = raw->remote_v6_cache; + } + memcpy(t->peer_ip, raw->peer_ip, sizeof t->peer_ip); + t->peer_port = raw->peer_port; + t->unrefd = raw->unrefd; + t->rd_eof = raw->rd_eof; + scr_net_sock_timer_cancel(raw); + if (raw->read_armed) scr_net_watch_read(raw->fd, raw, false); + if (raw->write_armed) scr_net_watch_write(raw->fd, raw, false); + raw->read_armed = raw->write_armed = false; + t->fd = raw->fd; + raw->fd = -1; + if (raw->rlen > raw->rhead) scr_net_sock_append_rbuf(t, raw->rbuf + raw->rhead, raw->rlen - raw->rhead); + raw->rhead = raw->rlen = 0; + while (raw->wlen > raw->whead) { + ssize_t sent = scr_net_send(t->fd, raw->wbuf + raw->whead, raw->wlen - raw->whead); + if (sent < 0 && errno == EINTR) continue; + if (sent <= 0) break; /* EAGAIN, or an error the transport will meet */ + raw->whead += (size_t)sent; + } + if (raw->wlen > raw->whead) { + ScrNetTunnel *tun = calloc(1, sizeof *tun); + if (!tun) scr_net_oom(); + tun->out_len = raw->wlen - raw->whead; + tun->out = malloc(tun->out_len); + if (!tun->out) scr_net_oom(); + memcpy(tun->out, raw->wbuf + raw->whead, tun->out_len); + tun->send_only = true; + t->tunnel = tun; + } + raw->whead = raw->wlen = 0; + raw->adopted_by = scr_net_sock_retain(t); + t->adopted_from = scr_net_sock_retain(raw); + scr_net_sock_register(t); + return t; +} + +/* Starts an adopted socket once its transport is set: the unsent bytes + * drain first, then the handshake runs (over any bytes raw had already + * read). */ +void scr_net_sock_adopt_start(ScrNetSocket *t) { + if (t->tunnel != NULL) { + scr_net_sock_update_write(t); + return; + } + /* the handshake starts from the poller (the socket is writable), never + * the constructing stack — Node's start is asynchronous too */ + scr_net_sock_transport_want_write(t); +} + int scr_net_sock_fd(ScrNetSocket *s) { return s->fd; } +/* The host a client socket was told to dial (Node's socket._host). +1; + * NULL on server sockets. */ +ScrStr *scr_net_sock_dial_name(ScrNetSocket *s) { return scr_str_retain(s->host_name); } + /* Drain up to n bytes of the receive buffer (peeked/unshifted bytes) — * consumers call this BEFORE the fd so re-queued bytes keep their * position in the stream. */ @@ -5050,6 +5184,32 @@ void scr_net_sock_replay(ScrNetSocket *s, const char *data, size_t len) { s->rcap = total; } +/* The peer closed before the handshake finished, on a socket whose + * stream the program already holds (new tls.TLSSocket): 'end', then + * 'close' without an error, as Node reports it. */ +void scr_net_sock_transport_eof(ScrNetSocket *s) { + scr_net_sock_eof(s); + if (s->fd >= 0) scr_net_sock_close_fd(s); +} + +/* A listener for the handshake's completion once TCP is up (a socket + * tls.connect({ socket }) took over is up from the start): it registers + * until the transport establishes. */ +void scr_net_sock_on_established(ScrNetSocket *s, ScrClosure *cb /*moves*/, bool once) { + if (s->close_emitted || s->fd < 0 || s->tops == NULL || s->t_est) { + scr_closure_release(cb); + return; + } + scr_net_ls_add(&s->conn_ls, cb, NULL, once); +} + +/* scr_net_sock_transport_error with the error's code given (static + * storage), for messages whose text does not name it. */ +void scr_net_sock_transport_error_code(ScrNetSocket *s, const char *msg, const char *code) { + if (!s->pending_err) s->pending_code = code; + scr_net_sock_transport_error(s, msg); +} + void scr_net_sock_transport_error(ScrNetSocket *s, const char *msg) { if (msg != NULL) { if (!s->pending_err) s->pending_err = scr_str_new(msg, strlen(msg)); @@ -5141,6 +5301,72 @@ static void scr_net_server_settle(ScrNetServer *srv) { scr_net_server_unregister(srv); } +/* Fires a closed socket's 'close' and settles it: its server's count + * drains, the protocol layer hears it, listeners drop and it leaves the + * registry. A socket that took over another's connection settles that one + * first, so the wrapped socket's 'close' comes just before its own. False + * with an exception pending. */ +static bool scr_net_sock_settle(ScrNetSocket *sock) { + ScrNetSocket *raw = sock->adopted_from; + if (raw != NULL) { + sock->adopted_from = NULL; + if (raw->adopted_by == sock) { + raw->adopted_by = NULL; + scr_net_sock_release(sock); + } + bool ok = raw->close_emitted || scr_net_sock_settle(raw); + scr_net_sock_release(raw); + if (!ok) return false; + } + sock->close_emitted = true; + sock->emit_close = false; + /* The owning server's count drains FIRST, and a now-drained closing + * server settles before this socket's own 'close' fires — Node's + * observed order (differential: the net-echo fixture). */ + if (sock->server) { + ScrNetServer *srv = sock->server; + sock->server = NULL; + srv->nconns--; + if (srv->closing && srv->nconns == 0 && !srv->close_emitted) { + scr_net_server_settle(srv); + } + scr_net_server_release(srv); + if (scr_exc_pending()) return false; + } + if (sock->native_closed) { + sock->native_closed(sock->native_ctx); + if (scr_exc_pending()) return false; + } + { + ScrDyn *had_error = scr_dyn_new_bool(sock->had_error); + scr_net_fire_arg_this(&sock->close_ls, had_error, sock, SCR_DYNH_NET_SOCKET); + scr_dyn_release(had_error); + } + /* settle: listeners drop (cycle story) */ + scr_net_ls_drop(&sock->data_ls); + scr_net_ls_drop(&sock->end_ls); + scr_net_ls_drop(&sock->close_ls); + scr_net_ls_drop(&sock->err_ls); + scr_net_ls_drop(&sock->conn_ls); + scr_net_ls_drop(&sock->timeout_ls); + scr_net_ls_drop(&sock->readable_ls); + scr_net_ls_drop(&sock->finish_ls); + scr_net_ls_drop(&sock->wcb_ls); + scr_net_ls_drop(&sock->ready_ls); + scr_net_ls_drop(&sock->lookup_ls); + if (sock->tops && sock->tops->settle) sock->tops->settle(sock->tctx); + if (sock->pipe_dst) { + scr_net_sock_release(sock->pipe_dst); + sock->pipe_dst = NULL; + } + if (sock->conn_pending) { + scr_net_server_release(sock->conn_pending); + sock->conn_pending = NULL; + } + scr_net_sock_unregister(sock); + return !scr_exc_pending(); +} + /* Sockets first (a dying connection may drain its server), servers after. * Every fire checks for a pending exception and bails — the loop surfaces * it as an uncaught throw. */ @@ -5200,7 +5426,19 @@ static void scr_net_sweep(void) { * listeners — and the unhandled-'error' exit */ bool consumed = false; if (sock->native_err) consumed = sock->native_err(sock->native_ctx, msg); - if (!consumed && !scr_exc_pending()) scr_net_fire_err_this(&sock->err_ls, msg, sock, SCR_DYNH_NET_SOCKET); + if (!consumed && !scr_exc_pending()) { + ScrError *coded = NULL; + ScrError *saved = scr_event_error_payload; + if (sock->pending_code != NULL) { + coded = scr_error_new(SCR_ERR_ERROR, msg); + coded->code = scr_str_new(sock->pending_code, strlen(sock->pending_code)); + scr_event_error_payload = coded; + } + scr_net_fire_err_this(&sock->err_ls, msg, sock, SCR_DYNH_NET_SOCKET); + scr_event_error_payload = saved; + if (coded != NULL) scr_error_release(coded); + } + sock->pending_code = NULL; scr_str_release(msg); if (scr_exc_pending()) { scr_net_sock_release(sock); @@ -5208,58 +5446,7 @@ static void scr_net_sweep(void) { } } if (sock->emit_close && !sock->close_emitted && sock->fd < 0) { - sock->close_emitted = true; - sock->emit_close = false; - /* The owning server's count drains FIRST, and a now-drained closing - * server settles before this socket's own 'close' fires — Node's - * observed order (differential: the net-echo fixture). */ - if (sock->server) { - ScrNetServer *srv = sock->server; - sock->server = NULL; - srv->nconns--; - if (srv->closing && srv->nconns == 0 && !srv->close_emitted) { - scr_net_server_settle(srv); - } - scr_net_server_release(srv); - if (scr_exc_pending()) { - scr_net_sock_release(sock); - return; - } - } - if (sock->native_closed) { - sock->native_closed(sock->native_ctx); - if (scr_exc_pending()) { - scr_net_sock_release(sock); - return; - } - } - { - ScrDyn *had_error = scr_dyn_new_bool(sock->had_error); - scr_net_fire_arg_this(&sock->close_ls, had_error, sock, SCR_DYNH_NET_SOCKET); - scr_dyn_release(had_error); - } - /* settle: listeners drop (cycle story) */ - scr_net_ls_drop(&sock->data_ls); - scr_net_ls_drop(&sock->end_ls); - scr_net_ls_drop(&sock->close_ls); - scr_net_ls_drop(&sock->err_ls); - scr_net_ls_drop(&sock->conn_ls); - scr_net_ls_drop(&sock->timeout_ls); - scr_net_ls_drop(&sock->readable_ls); - scr_net_ls_drop(&sock->finish_ls); - scr_net_ls_drop(&sock->wcb_ls); - scr_net_ls_drop(&sock->ready_ls); - scr_net_ls_drop(&sock->lookup_ls); - if (sock->pipe_dst) { - scr_net_sock_release(sock->pipe_dst); - sock->pipe_dst = NULL; - } - if (sock->conn_pending) { - scr_net_server_release(sock->conn_pending); - sock->conn_pending = NULL; - } - scr_net_sock_unregister(sock); - if (scr_exc_pending()) { + if (!scr_net_sock_settle(sock)) { scr_net_sock_release(sock); return; } @@ -5504,6 +5691,17 @@ static void scr_net_cleanup_atexit(void) { scr_net_server_release(s->server); s->server = NULL; } + /* a wrapped socket and the socket that took it over hold each other */ + if (s->adopted_by) { + ScrNetSocket *t = s->adopted_by; + s->adopted_by = NULL; + scr_net_sock_release(t); + } + if (s->adopted_from) { + ScrNetSocket *raw = s->adopted_from; + s->adopted_from = NULL; + scr_net_sock_release(raw); + } scr_net_sock_unregister(s); } while (scr_net_servers) { diff --git a/packages/runtime/src/scr_net.h b/packages/runtime/src/scr_net.h index 33638f699..ffa9b2f72 100644 --- a/packages/runtime/src/scr_net.h +++ b/packages/runtime/src/scr_net.h @@ -351,8 +351,22 @@ typedef struct ScrNetTransportOps { bool (*pending)(void *tctx); void (*shutdown_write)(void *tctx); void (*free)(void *tctx); + /* nullable: runs after the established socket's 'connect' listeners + * (the TLS 'secure' event, which Node emits after secureConnect) */ + void (*after_established)(void *tctx); + /* nullable: runs when the closed socket settles, after its own + * listeners drop; the transport drops the listeners it holds */ + void (*settle)(void *tctx); } ScrNetTransportOps; void scr_net_sock_set_transport(ScrNetSocket *s, const ScrNetTransportOps *ops, void *tctx); +/* new tls.TLSSocket(socket) and tls.connect({ socket }): a fresh socket + * takes over an open connection (fd, buffered bytes, peer identity); the + * old socket stays open without I/O, forwards end() and destroy(), and + * fires 'close' just before the new one. +1, or NULL when the socket has + * no open, idle connection to give. Set the transport on the result, then + * call scr_net_sock_adopt_start. */ +ScrNetSocket *scr_net_sock_adopt(ScrNetSocket *raw); +void scr_net_sock_adopt_start(ScrNetSocket *t); /* Routes a client socket through an HTTP CONNECT proxy. Call it right * after the dial (scr_net_connect to the proxy) and before any transport * is set: once TCP is up, `request` (the CONNECT head, copied) is sent @@ -366,6 +380,9 @@ void scr_net_sock_set_tunnel(ScrNetSocket *s, const char *request, size_t len); int scr_net_tunnel_status(const char *head, size_t len); void scr_net_tunnel_failure(int status, char *msg, size_t cap); int scr_net_sock_fd(ScrNetSocket *s); +/* The host a client socket was told to dial (Node's socket._host). +1; + * NULL on server sockets. */ +ScrStr *scr_net_sock_dial_name(ScrNetSocket *s); /* Drain up to n bytes of the socket's receive buffer (peeked/unshifted * bytes — the demux path); consumers call it before the fd. */ size_t scr_net_sock_take_buffered(ScrNetSocket *s, char *buf, size_t n); @@ -385,6 +402,16 @@ void scr_net_sock_replay(ScrNetSocket *s, const char *data, size_t len); * 'error' + teardown to the sweep; NULL tears down without an 'error' * event, Node's default for an unhandled server-side handshake failure. */ void scr_net_sock_transport_error(ScrNetSocket *s, const char *msg); +/* The same with the error's code given (static storage), for messages + * whose text does not name it. */ +void scr_net_sock_transport_error_code(ScrNetSocket *s, const char *msg, const char *code); +/* The peer closed before the handshake finished on a wrapped socket: + * 'end', then 'close' without an error. */ +void scr_net_sock_transport_eof(ScrNetSocket *s); +/* Registers a listener that fires when the transport establishes, once + * TCP is up (a socket that took over an open connection is up from the + * start). */ +void scr_net_sock_on_established(ScrNetSocket *s, ScrClosure *cb /*moves*/, bool once); /* Defer the server's 'connection' listeners to handshake completion — * tls.createServer's handler is the 'secureConnection' event, fired with * the socket only once the TLS layer stands (Node's timing). */ diff --git a/packages/runtime/src/scr_pki.c b/packages/runtime/src/scr_pki.c new file mode 100644 index 000000000..54aadc06b --- /dev/null +++ b/packages/runtime/src/scr_pki.c @@ -0,0 +1,2144 @@ +/* node:crypto asymmetric keys and signatures over the vendored mbedTLS + * public-key layer: createPrivateKey, createPublicKey, generateKeyPairSync + * and generateKeyPair for "rsa", "ec" and "ed25519" keys, the + * PrivateKeyObject and PublicKeyObject handles (type, asymmetricKeyType, + * asymmetricKeyDetails, export(), equals()), crypto.sign and crypto.verify, + * and the Sign and Verify objects of createSign and createVerify. Keys read + * and write PEM and DER as PKCS#1, SEC1, PKCS#8 and SubjectPublicKeyInfo, + * and JWK; the EC curves are prime256v1, secp384r1, secp521r1 and + * secp256k1, and the Ed25519 arithmetic is scr_ed25519.c. Argument checks + * and errors follow Node 24's lib/internal/crypto/keys.js, keygen.js and + * sig.js. Encrypted keys and other key types throw a coded SC2020 error. + * All arguments are borrowed. */ +#include "scr_runtime.h" + +#include +#include +#include +#include +#include + +#include "mbedtls/asn1.h" +#include "mbedtls/asn1write.h" +#include "mbedtls/base64.h" +#include "mbedtls/bignum.h" +#include "mbedtls/ecdsa.h" +#include "mbedtls/ecp.h" +#include "mbedtls/md.h" +#include "mbedtls/oid.h" +#include "mbedtls/pk.h" +#include "mbedtls/platform_util.h" +#include "mbedtls/rsa.h" +#include "mbedtls/x509_crt.h" + +#include "scr_pki.h" + +/* ── errors ───────────────────────────────────────────────────────────── */ + +static void sp_throw(int kind, const char *code, const char *message) { + scr_throw_error_msg_code(kind, message, strlen(message), code); +} + +void scr_pki_throw_fmt(int kind, const char *code, const char *fmt, ...) { + va_list ap, sized; + va_start(ap, fmt); + va_copy(sized, ap); + int n = vsnprintf(NULL, 0, fmt, sized); + va_end(sized); + if (n < 0) n = 0; + char *message = malloc((size_t)n + 1); + if (!message) scr_trap("scriptc: out of memory\n"); + vsnprintf(message, (size_t)n + 1, fmt, ap); + va_end(ap); + if (code) scr_throw_error_msg_code(kind, message, (size_t)n, code); + else scr_throw_error_msg(kind, message, (size_t)n); + free(message); +} + +/* The "Received" text of Node's ERR_INVALID_ARG_VALUE and ERR_OUT_OF_RANGE + * messages: a string as util.inspect quotes it (cut at 128 UTF-16 units + * plus "..."), a number as ERR_OUT_OF_RANGE prints it when `range`, any + * other value as its sketch. +1. */ +static ScrStr *sp_received(const ScrDyn *got, bool range) { + if (got->kind == SCR_DYN_STR) return scr_str_inspect_quoted(got->v.str, true); + char text[96]; + if (range && got->kind == SCR_DYN_NUM) return scr_str_new(text, scr_num_received(got->v.num, text)); + const char *lite = scr_dyn_inspect_lite(got, text, sizeof text); + return scr_str_new(lite, strlen(lite)); +} + +/* ERR_INVALID_ARG_VALUE for an option ("The property 'options.type' is + * invalid. Received 'spki'"). */ +static void sp_property_invalid(const char *name, const ScrDyn *got) { + ScrStr *received = sp_received(got, false); + scr_pki_throw_fmt(SCR_ERR_TYPE, "ERR_INVALID_ARG_VALUE", "The property '%s' is invalid. Received %.*s", name, + (int)received->len, received->data); + scr_str_release(received); +} + +static void sp_unsupported_input(void) { + sp_throw(SCR_ERR_ERROR, "ERR_OSSL_UNSUPPORTED", "error:1E08010C:DECODER routines::unsupported"); +} + +/* The coded refusal for a form this runtime does not implement. */ +static void sp_not_supported(const char *what) { + scr_pki_throw_fmt(SCR_ERR_ERROR, "SC2020", "'%s' is part of the standard library types but has no scriptc lowering yet", + what); +} + +static int sp_random(void *state, unsigned char *out, size_t len) { + (void)state; + arc4random_buf(out, len); + return 0; +} + +/* ── Ed25519 ────────────────────────────────────────────────────────── + * The arithmetic is scr_ed25519.c, which links only into programs that + * name Ed25519 and installs itself here at startup. Keys are the 32-byte + * seed and the 32-byte encoded public point. */ +const ScrPkiEd25519 *scr_pki_ed25519 = NULL; + +/* The Ed25519 arithmetic, or NULL with a coded SC2020 error pending. */ +static const ScrPkiEd25519 *sp_ed(void) { + if (!scr_pki_ed25519) + sp_throw(SCR_ERR_ERROR, "SC2020", + "Ed25519 keys are supported when a string literal in the program names 'ed25519'"); + return scr_pki_ed25519; +} + +bool scr_pki_ed25519_ready(void) { return sp_ed() != NULL; } + +static bool sp_ed_verify(const ScrPkiEd25519 *ed, const uint8_t pub[32], const uint8_t *m, size_t n, + const uint8_t *sig, size_t sig_len) { + return sig_len == 64 && ed->verify(pub, m, n, sig); +} + +/* ── option bags ──────────────────────────────────────────────────────── */ + +static bool sp_is_object(const ScrDyn *v) { return v != NULL && v->kind == SCR_DYN_OBJ; } + +/* A statically typed object argument (a record or class instance) as a + * plain object: +1, or NULL with the error pending. Other values are + * retained as they are. */ +static ScrDyn *sp_plain(const ScrDyn *v) { + if (v != NULL && v->kind == SCR_DYN_TYPED_REF) return scr_dyn_typed_ref_materialize(v); + return scr_dyn_retain((ScrDyn *)(v ? v : scr_dyn_undefined())); +} + +/* A borrowed option, or the undefined singleton. */ +static const ScrDyn *sp_opt(const ScrDyn *obj, const char *key) { + const ScrDyn *v = sp_is_object(obj) ? scr_dyn_obj_get(obj, key, strlen(key)) : NULL; + return v ? v : scr_dyn_undefined(); +} + +static bool sp_is_str(const ScrDyn *v, const char *text) { + size_t n = strlen(text); + return v->kind == SCR_DYN_STR && v->v.str->len == n && memcmp(v->v.str->data, text, n) == 0; +} + +static bool sp_absent(const ScrDyn *v) { return v->kind == SCR_DYN_UNDEF; } + +/* ── keys ─────────────────────────────────────────────────────────────── */ + +typedef struct SpKey { + size_t rc; + mbedtls_pk_context pk; /* RSA and EC keys */ + bool private_key; + bool ed25519; /* an Ed25519 key: ed_seed (private keys) and ed_pub */ + uint8_t ed_seed[32]; + uint8_t ed_pub[32]; +} SpKey; + +static void *sp_key_retain(void *h) { + SpKey *k = h; + if (k->rc != SIZE_MAX) k->rc++; + return h; +} + +static void sp_key_release(void *h) { + SpKey *k = h; + if (!k || k->rc == SIZE_MAX || --k->rc != 0) return; + mbedtls_pk_free(&k->pk); + mbedtls_platform_zeroize(k, sizeof *k); + free(k); +} + +static SpKey *sp_key_alloc(bool private_key) { + SpKey *k = calloc(1, sizeof *k); + if (!k) scr_trap("scriptc: out of memory\n"); + k->rc = 1; + k->private_key = private_key; + mbedtls_pk_init(&k->pk); + return k; +} + +static void sp_key_install(void); + +/* Boxes the key (consuming the caller's reference). */ +static ScrDyn *sp_key_box(SpKey *k) { + sp_key_install(); + ScrDyn *box = scr_dyn_new_handle(k, k->private_key ? SCR_DYNH_PRIVATE_KEY : SCR_DYNH_PUBLIC_KEY); + sp_key_release(k); + return box; +} + +static SpKey *sp_key_of(const ScrDyn *v) { + if (v == NULL || v->kind != SCR_DYN_HANDLE) return NULL; + if (v->v.handle.tag != SCR_DYNH_PRIVATE_KEY && v->v.handle.tag != SCR_DYNH_PUBLIC_KEY) return NULL; + return v->v.handle.ptr; +} + +bool scr_crypto_is_asymmetric_key(const ScrDyn *value) { return sp_key_of(value) != NULL; } + +/* The type getter of a KeyObject value: "private", "public", or NULL for + * anything else. */ +const char *scr_crypto_asymmetric_key_type(const ScrDyn *value) { + SpKey *k = sp_key_of(value); + return k ? (k->private_key ? "private" : "public") : NULL; +} + +static bool sp_is_rsa(const SpKey *k) { return mbedtls_pk_get_type(&k->pk) == MBEDTLS_PK_RSA; } + +static bool sp_is_ec(const SpKey *k) { return mbedtls_pk_get_type(&k->pk) == MBEDTLS_PK_ECKEY; } + +static bool sp_is_ed(const SpKey *k) { return k->ed25519; } + +typedef struct { + const char *name; /* OpenSSL short name (namedCurve) */ + const char *nist; /* EC_curve_nist2nid alias, or NULL */ + const char *jwk; /* JWK crv */ + mbedtls_ecp_group_id id; +} SpCurve; + +static const SpCurve sp_curves[] = { + {"prime256v1", "P-256", "P-256", MBEDTLS_ECP_DP_SECP256R1}, + {"secp384r1", "P-384", "P-384", MBEDTLS_ECP_DP_SECP384R1}, + {"secp521r1", "P-521", "P-521", MBEDTLS_ECP_DP_SECP521R1}, + {"secp256k1", NULL, "secp256k1", MBEDTLS_ECP_DP_SECP256K1}, +}; + +static const SpCurve *sp_curve_by_id(mbedtls_ecp_group_id id) { + for (size_t i = 0; i < sizeof sp_curves / sizeof sp_curves[0]; i++) + if (sp_curves[i].id == id) return &sp_curves[i]; + return NULL; +} + +static const SpCurve *sp_curve_of(const SpKey *k) { + return sp_is_ec(k) ? sp_curve_by_id(mbedtls_pk_ec(k->pk)->MBEDTLS_PRIVATE(grp).id) : NULL; +} + +/* A parsed key is usable when it is RSA or EC on a curve above. */ +static bool sp_key_supported(const SpKey *k) { + return sp_is_ed(k) || sp_is_rsa(k) || (sp_is_ec(k) && sp_curve_of(k) != NULL); +} + +/* ── DER writing ──────────────────────────────────────────────────────── */ + +/* mbedtls writes DER backwards into the end of a buffer; these helpers + * answer the written length or a negative error. */ +#define SP_DER_MAX 8192 + +static int sp_write_private_der(SpKey *k, unsigned char *buf, size_t size) { + return mbedtls_pk_write_key_der(&k->pk, buf, size); +} + +/* Ed25519's PKCS#8 and SubjectPublicKeyInfo prefixes (RFC 8410): the + * 32-byte seed or public key follows. */ +static const uint8_t sp_ed_pkcs8_prefix[16] = {0x30, 0x2e, 0x02, 0x01, 0x00, 0x30, 0x05, 0x06, + 0x03, 0x2b, 0x65, 0x70, 0x04, 0x22, 0x04, 0x20}; +static const uint8_t sp_ed_spki_prefix[12] = {0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, + 0x2b, 0x65, 0x70, 0x03, 0x21, 0x00}; + +/* Writes prefix || 32 bytes at the end of buf, as the mbedtls writers do. */ +static int sp_write_ed_der(const uint8_t *prefix, size_t prefix_len, const uint8_t key[32], + unsigned char *buf, size_t size) { + size_t n = prefix_len + 32; + if (size < n) return MBEDTLS_ERR_ASN1_BUF_TOO_SMALL; + memcpy(buf + size - n, prefix, prefix_len); + memcpy(buf + size - 32, key, 32); + return (int)n; +} + +static int sp_write_spki_der(SpKey *k, unsigned char *buf, size_t size) { + if (sp_is_ed(k)) return sp_write_ed_der(sp_ed_spki_prefix, sizeof sp_ed_spki_prefix, k->ed_pub, buf, size); + return mbedtls_pk_write_pubkey_der(&k->pk, buf, size); +} + +/* RSAPublicKey (PKCS#1) DER. */ +static int sp_write_pkcs1_public_der(SpKey *k, unsigned char *buf, size_t size) { + unsigned char *p = buf + size; + return mbedtls_pk_write_pubkey(&p, buf, &k->pk); +} + +/* PKCS#8 PrivateKeyInfo around the PKCS#1 or SEC1 key, as OpenSSL writes + * it: version 0, the AlgorithmIdentifier (rsaEncryption with NULL + * parameters, or id-ecPublicKey with the curve OID), and the inner key, + * which for EC omits the parameters field. */ +static int sp_write_pkcs8_der(SpKey *k, unsigned char *buf, size_t size) { + if (sp_is_ed(k)) + return sp_write_ed_der(sp_ed_pkcs8_prefix, sizeof sp_ed_pkcs8_prefix, k->ed_seed, buf, size); + unsigned char inner[SP_DER_MAX]; + size_t inner_len; + if (sp_is_rsa(k)) { + int n = sp_write_private_der(k, inner, sizeof inner); + if (n < 0) return n; + inner_len = (size_t)n; + memmove(inner, inner + sizeof inner - inner_len, inner_len); + } else { + /* ECPrivateKey ::= SEQUENCE { version 1, privateKey OCTET STRING, + * [1] publicKey BIT STRING } */ + const mbedtls_ecp_keypair *ec = mbedtls_pk_ec(k->pk); + unsigned char *c = inner + sizeof inner; + size_t len = 0; + int r; + unsigned char point[160]; + size_t point_len; + if ((r = mbedtls_ecp_point_write_binary(&ec->MBEDTLS_PRIVATE(grp), &ec->MBEDTLS_PRIVATE(Q), + MBEDTLS_ECP_PF_UNCOMPRESSED, &point_len, point, + sizeof point)) != 0) + return r; + size_t pub_len = 0; + if ((r = mbedtls_asn1_write_bitstring(&c, inner, point, point_len * 8)) < 0) return r; + pub_len += (size_t)r; + if ((r = mbedtls_asn1_write_len(&c, inner, pub_len)) < 0) return r; + pub_len += (size_t)r; + if ((r = mbedtls_asn1_write_tag(&c, inner, MBEDTLS_ASN1_CONTEXT_SPECIFIC | MBEDTLS_ASN1_CONSTRUCTED | 1)) < 0) + return r; + pub_len += (size_t)r; + len += pub_len; + size_t d_len = (ec->MBEDTLS_PRIVATE(grp).nbits + 7) / 8; + unsigned char d[72]; + if ((r = mbedtls_mpi_write_binary(&ec->MBEDTLS_PRIVATE(d), d, d_len)) != 0) return r; + if ((r = mbedtls_asn1_write_octet_string(&c, inner, d, d_len)) < 0) return r; + len += (size_t)r; + mbedtls_platform_zeroize(d, sizeof d); + if ((r = mbedtls_asn1_write_int(&c, inner, 1)) < 0) return r; + len += (size_t)r; + if ((r = mbedtls_asn1_write_len(&c, inner, len)) < 0) return r; + len += (size_t)r; + if ((r = mbedtls_asn1_write_tag(&c, inner, MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE)) < 0) + return r; + len += (size_t)r; + inner_len = len; + memmove(inner, c, inner_len); + } + unsigned char *c = buf + size; + size_t len = 0; + int r; + if ((r = mbedtls_asn1_write_octet_string(&c, buf, inner, inner_len)) < 0) goto done; + len += (size_t)r; + { + const char *oid; + size_t oid_len; + size_t alg_len = 0; + if (sp_is_rsa(k)) { + if ((r = mbedtls_asn1_write_null(&c, buf)) < 0) goto done; + alg_len += (size_t)r; + oid = MBEDTLS_OID_PKCS1_RSA; + oid_len = MBEDTLS_OID_SIZE(MBEDTLS_OID_PKCS1_RSA); + } else { + const char *curve_oid; + size_t curve_oid_len; + if ((r = mbedtls_oid_get_oid_by_ec_grp(mbedtls_pk_ec(k->pk)->MBEDTLS_PRIVATE(grp).id, &curve_oid, + &curve_oid_len)) != 0) + goto done; + if ((r = mbedtls_asn1_write_oid(&c, buf, curve_oid, curve_oid_len)) < 0) goto done; + alg_len += (size_t)r; + oid = MBEDTLS_OID_EC_ALG_UNRESTRICTED; + oid_len = MBEDTLS_OID_SIZE(MBEDTLS_OID_EC_ALG_UNRESTRICTED); + } + if ((r = mbedtls_asn1_write_oid(&c, buf, oid, oid_len)) < 0) goto done; + alg_len += (size_t)r; + if ((r = mbedtls_asn1_write_len(&c, buf, alg_len)) < 0) goto done; + alg_len += (size_t)r; + if ((r = mbedtls_asn1_write_tag(&c, buf, MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE)) < 0) + goto done; + alg_len += (size_t)r; + len += alg_len; + } + if ((r = mbedtls_asn1_write_int(&c, buf, 0)) < 0) goto done; + len += (size_t)r; + if ((r = mbedtls_asn1_write_len(&c, buf, len)) < 0) goto done; + len += (size_t)r; + if ((r = mbedtls_asn1_write_tag(&c, buf, MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE)) < 0) + goto done; + len += (size_t)r; + r = (int)len; +done: + mbedtls_platform_zeroize(inner, sizeof inner); + return r; +} + +/* PEM armor with 64-character base64 lines and a final newline. */ +static ScrStr *sp_pem(const char *label, const unsigned char *der, size_t len) { + size_t b64_len = 0; + mbedtls_base64_encode(NULL, 0, &b64_len, der, len); + unsigned char *b64 = malloc(b64_len + 1); + char *out = malloc(b64_len + b64_len / 64 + 2 * strlen(label) + 64); + if (!b64 || !out) scr_trap("scriptc: out of memory\n"); + mbedtls_base64_encode(b64, b64_len + 1, &b64_len, der, len); + size_t o = (size_t)sprintf(out, "-----BEGIN %s-----\n", label); + for (size_t i = 0; i < b64_len; i += 64) { + size_t n = b64_len - i < 64 ? b64_len - i : 64; + memcpy(out + o, b64 + i, n); + o += n; + out[o++] = '\n'; + } + o += (size_t)sprintf(out + o, "-----END %s-----\n", label); + ScrStr *s = scr_str_new(out, o); + mbedtls_platform_zeroize(b64, b64_len); + mbedtls_platform_zeroize(out, o); + free(b64); + free(out); + return s; +} + +static ScrDyn *sp_dyn_buffer(const unsigned char *data, size_t len) { + ScrBytes *bytes = scr_bytes_new(SCR_BYTES_U8, (double)len); + bytes->is_buffer = true; + if (len) memcpy(bytes->data, data, len); + ScrDyn *d = scr_dyn_new_buffer(bytes); + scr_bytes_release(bytes); + return d; +} + +static ScrDyn *sp_dyn_str(ScrStr *s) { + ScrDyn *d = scr_dyn_new_str(s); + scr_str_release(s); + return d; +} + +/* ── JWK ──────────────────────────────────────────────────────────────── */ + +/* base64url without padding of the big-endian integer `x` in `len` bytes + * (0 = its minimal length). */ +static ScrDyn *sp_jwk_int(const mbedtls_mpi *x, size_t len) { + if (len == 0) len = mbedtls_mpi_size(x); + if (len == 0) len = 1; + unsigned char *raw = malloc(len); + size_t b64_len = 0; + if (!raw) scr_trap("scriptc: out of memory\n"); + mbedtls_mpi_write_binary(x, raw, len); + mbedtls_base64_encode(NULL, 0, &b64_len, raw, len); + unsigned char *b64 = malloc(b64_len + 1); + if (!b64) scr_trap("scriptc: out of memory\n"); + mbedtls_base64_encode(b64, b64_len + 1, &b64_len, raw, len); + size_t o = 0; + for (size_t i = 0; i < b64_len; i++) { + unsigned char c = b64[i]; + if (c == '=') break; + b64[o++] = c == '+' ? '-' : c == '/' ? '_' : c; + } + ScrDyn *d = sp_dyn_str(scr_str_new((const char *)b64, o)); + mbedtls_platform_zeroize(raw, len); + free(raw); + free(b64); + return d; +} + +static void sp_set(ScrDyn *obj, const char *key, ScrDyn *value) { + scr_dyn_obj_set(obj, key, strlen(key), value); +} + +/* base64url without padding of raw bytes. */ +static ScrDyn *sp_b64url(const uint8_t *raw, size_t len) { + size_t b64_len = 0; + mbedtls_base64_encode(NULL, 0, &b64_len, raw, len); + unsigned char *b64 = malloc(b64_len + 1); + if (!b64) scr_trap("scriptc: out of memory\n"); + mbedtls_base64_encode(b64, b64_len + 1, &b64_len, raw, len); + size_t o = 0; + for (size_t i = 0; i < b64_len; i++) { + unsigned char c = b64[i]; + if (c == '=') break; + b64[o++] = c == '+' ? '-' : c == '/' ? '_' : c; + } + ScrDyn *d = sp_dyn_str(scr_str_new((const char *)b64, o)); + mbedtls_platform_zeroize(b64, b64_len); + free(b64); + return d; +} + +static ScrDyn *sp_export_jwk(SpKey *k) { + ScrDyn *jwk = scr_dyn_new_obj(); + if (sp_is_ed(k)) { + sp_set(jwk, "kty", sp_dyn_str(scr_str_new("OKP", 3))); + sp_set(jwk, "crv", sp_dyn_str(scr_str_new("Ed25519", 7))); + sp_set(jwk, "x", sp_b64url(k->ed_pub, 32)); + if (k->private_key) sp_set(jwk, "d", sp_b64url(k->ed_seed, 32)); + return jwk; + } + if (sp_is_rsa(k)) { + mbedtls_rsa_context *rsa = mbedtls_pk_rsa(k->pk); + mbedtls_mpi n, e, d, p, q, dp, dq, qi; + mbedtls_mpi *all[] = {&n, &e, &d, &p, &q, &dp, &dq, &qi}; + for (size_t i = 0; i < 8; i++) mbedtls_mpi_init(all[i]); + mbedtls_rsa_export(rsa, &n, k->private_key ? &p : NULL, k->private_key ? &q : NULL, + k->private_key ? &d : NULL, &e); + sp_set(jwk, "kty", sp_dyn_str(scr_str_new("RSA", 3))); + sp_set(jwk, "n", sp_jwk_int(&n, 0)); + sp_set(jwk, "e", sp_jwk_int(&e, 0)); + if (k->private_key) { + mbedtls_rsa_export_crt(rsa, &dp, &dq, &qi); + sp_set(jwk, "d", sp_jwk_int(&d, 0)); + sp_set(jwk, "p", sp_jwk_int(&p, 0)); + sp_set(jwk, "q", sp_jwk_int(&q, 0)); + sp_set(jwk, "dp", sp_jwk_int(&dp, 0)); + sp_set(jwk, "dq", sp_jwk_int(&dq, 0)); + sp_set(jwk, "qi", sp_jwk_int(&qi, 0)); + } + for (size_t i = 0; i < 8; i++) mbedtls_mpi_free(all[i]); + } else { + const mbedtls_ecp_keypair *ec = mbedtls_pk_ec(k->pk); + const SpCurve *curve = sp_curve_of(k); + size_t size = (ec->MBEDTLS_PRIVATE(grp).nbits + 7) / 8; + sp_set(jwk, "kty", sp_dyn_str(scr_str_new("EC", 2))); + sp_set(jwk, "x", sp_jwk_int(&ec->MBEDTLS_PRIVATE(Q).MBEDTLS_PRIVATE(X), size)); + sp_set(jwk, "y", sp_jwk_int(&ec->MBEDTLS_PRIVATE(Q).MBEDTLS_PRIVATE(Y), size)); + sp_set(jwk, "crv", sp_dyn_str(scr_str_new(curve->jwk, strlen(curve->jwk)))); + if (k->private_key) sp_set(jwk, "d", sp_jwk_int(&ec->MBEDTLS_PRIVATE(d), size)); + } + return jwk; +} + +/* Decodes a JWK base64url member into `x`; false when it is missing or + * not a string. */ +static bool sp_jwk_read(const ScrDyn *jwk, const char *key, mbedtls_mpi *x) { + const ScrDyn *v = sp_opt(jwk, key); + if (v->kind != SCR_DYN_STR) return false; + size_t n = v->v.str->len; + unsigned char *b64 = malloc(n + 4); + unsigned char *raw = malloc(n + 4); + if (!b64 || !raw) scr_trap("scriptc: out of memory\n"); + for (size_t i = 0; i < n; i++) { + char c = v->v.str->data[i]; + b64[i] = (unsigned char)(c == '-' ? '+' : c == '_' ? '/' : c); + } + while (n % 4) b64[n++] = '='; + size_t raw_len = 0; + bool ok = mbedtls_base64_decode(raw, n, &raw_len, b64, n) == 0 && + mbedtls_mpi_read_binary(x, raw, raw_len) == 0; + mbedtls_platform_zeroize(raw, n); + free(b64); + free(raw); + return ok; +} + +/* Decodes a 32-byte JWK base64url member; false when it is missing, not a + * string, or another length. */ +static bool sp_jwk_bytes(const ScrDyn *jwk, const char *key, uint8_t out[32]) { + const ScrDyn *v = sp_opt(jwk, key); + if (v->kind != SCR_DYN_STR || v->v.str->len != 43) return false; + unsigned char b64[44]; + for (size_t i = 0; i < 43; i++) { + char c = v->v.str->data[i]; + b64[i] = (unsigned char)(c == '-' ? '+' : c == '_' ? '/' : c); + } + b64[43] = '='; + size_t n = 0; + return mbedtls_base64_decode(out, 32, &n, b64, 44) == 0 && n == 32; +} + +static void sp_invalid_jwk(void) { + sp_throw(SCR_ERR_TYPE, "ERR_CRYPTO_INVALID_JWK", "Invalid JWK data"); +} + +/* A key from a JWK object: RSA (n, e, and d, p, q for a private key) or EC + * (crv, x, y, and d). NULL with the error pending. */ +static SpKey *sp_key_from_jwk_plain(const ScrDyn *jwk, bool want_private); + +static SpKey *sp_key_from_jwk(const ScrDyn *jwk, bool want_private) { + ScrDyn *plain = sp_plain(jwk); + if (!plain) return NULL; + SpKey *k = sp_key_from_jwk_plain(plain, want_private); + scr_dyn_release(plain); + return k; +} + +static SpKey *sp_key_from_jwk_plain(const ScrDyn *jwk, bool want_private) { + if (!sp_is_object(jwk)) { + scr_dyn_prop_type_fail("key.key", "of type object", jwk); + return NULL; + } + const ScrDyn *kty = sp_opt(jwk, "kty"); + if (kty->kind != SCR_DYN_STR) { + scr_dyn_prop_type_fail("key.kty", "of type string", kty); + return NULL; + } + bool has_d = !sp_absent(sp_opt(jwk, "d")); + if (want_private && !has_d) { + sp_invalid_jwk(); + return NULL; + } + bool private_key = want_private; + SpKey *k = sp_key_alloc(private_key); + bool ok = false; + if (sp_is_str(kty, "OKP")) { + const ScrDyn *crv = sp_opt(jwk, "crv"); + if (!sp_is_str(crv, "Ed25519")) { + sp_key_release(k); + if (crv->kind != SCR_DYN_STR) { + scr_dyn_prop_type_fail("key.crv", "of type string", crv); + return NULL; + } + sp_not_supported("JWK keys of this crv"); + return NULL; + } + k->ed25519 = true; + if (private_key) { + ok = sp_jwk_bytes(jwk, "d", k->ed_seed); + if (ok) { + const ScrPkiEd25519 *ed = sp_ed(); + if (!ed) { + sp_key_release(k); + return NULL; + } + ed->public_key(k->ed_pub, k->ed_seed); + } + } else { + ok = sp_jwk_bytes(jwk, "x", k->ed_pub); + } + } else if (sp_is_str(kty, "RSA")) { + mbedtls_mpi n, e, d, p, q; + mbedtls_mpi_init(&n); + mbedtls_mpi_init(&e); + mbedtls_mpi_init(&d); + mbedtls_mpi_init(&p); + mbedtls_mpi_init(&q); + if (sp_jwk_read(jwk, "n", &n) && sp_jwk_read(jwk, "e", &e) && + mbedtls_pk_setup(&k->pk, mbedtls_pk_info_from_type(MBEDTLS_PK_RSA)) == 0) { + mbedtls_rsa_context *rsa = mbedtls_pk_rsa(k->pk); + if (private_key) { + ok = sp_jwk_read(jwk, "d", &d) && sp_jwk_read(jwk, "p", &p) && sp_jwk_read(jwk, "q", &q) && + mbedtls_rsa_import(rsa, &n, &p, &q, &d, &e) == 0 && mbedtls_rsa_complete(rsa) == 0 && + mbedtls_rsa_check_privkey(rsa) == 0; + } else { + ok = mbedtls_rsa_import(rsa, &n, NULL, NULL, NULL, &e) == 0 && + mbedtls_rsa_complete(rsa) == 0 && mbedtls_rsa_check_pubkey(rsa) == 0; + } + } + mbedtls_mpi_free(&n); + mbedtls_mpi_free(&e); + mbedtls_mpi_free(&d); + mbedtls_mpi_free(&p); + mbedtls_mpi_free(&q); + } else if (sp_is_str(kty, "EC")) { + const ScrDyn *crv = sp_opt(jwk, "crv"); + if (crv->kind != SCR_DYN_STR) { + sp_key_release(k); + scr_dyn_prop_type_fail("key.crv", "of type string", crv); + return NULL; + } + const SpCurve *curve = NULL; + for (size_t i = 0; i < sizeof sp_curves / sizeof sp_curves[0]; i++) + if (sp_is_str(crv, sp_curves[i].jwk)) curve = &sp_curves[i]; + if (!curve) { + sp_key_release(k); + sp_throw(SCR_ERR_TYPE, "ERR_CRYPTO_INVALID_CURVE", "Invalid EC curve name"); + return NULL; + } + if (mbedtls_pk_setup(&k->pk, mbedtls_pk_info_from_type(MBEDTLS_PK_ECKEY)) == 0) { + mbedtls_ecp_keypair *ec = mbedtls_pk_ec(k->pk); + mbedtls_ecp_point *Q = &ec->MBEDTLS_PRIVATE(Q); + ok = mbedtls_ecp_group_load(&ec->MBEDTLS_PRIVATE(grp), curve->id) == 0 && + sp_jwk_read(jwk, "x", &Q->MBEDTLS_PRIVATE(X)) && + sp_jwk_read(jwk, "y", &Q->MBEDTLS_PRIVATE(Y)) && + mbedtls_mpi_lset(&Q->MBEDTLS_PRIVATE(Z), 1) == 0 && + mbedtls_ecp_check_pubkey(&ec->MBEDTLS_PRIVATE(grp), Q) == 0; + if (ok && private_key) + ok = sp_jwk_read(jwk, "d", &ec->MBEDTLS_PRIVATE(d)) && + mbedtls_ecp_check_privkey(&ec->MBEDTLS_PRIVATE(grp), &ec->MBEDTLS_PRIVATE(d)) == 0; + } + } else { + sp_key_release(k); + sp_not_supported("JWK keys of this kty"); + return NULL; + } + if (!ok) { + sp_key_release(k); + sp_invalid_jwk(); + return NULL; + } + return k; +} + +/* ── parsing ──────────────────────────────────────────────────────────── */ + +/* The private key of a public SpKey derived from `k`. */ +static SpKey *sp_public_of(SpKey *k) { + if (sp_is_ed(k)) { + SpKey *pub = sp_key_alloc(false); + pub->ed25519 = true; + memcpy(pub->ed_pub, k->ed_pub, 32); + return pub; + } + unsigned char der[SP_DER_MAX]; + int n = sp_write_spki_der(k, der, sizeof der); + SpKey *pub = sp_key_alloc(false); + if (n < 0 || mbedtls_pk_parse_public_key(&pub->pk, der + sizeof der - n, (size_t)n) != 0) { + sp_key_release(pub); + return NULL; + } + return pub; +} + +/* Parses key material (PEM or DER bytes). `private_key` selects + * createPrivateKey's parse (PKCS#1, SEC1, PKCS#8) or createPublicKey's + * (SubjectPublicKeyInfo, PKCS#1 RSAPublicKey, then a private key or an + * X.509 certificate, whose public key it takes). NULL with Node's + * decoder error pending. */ +/* The DER of the first PEM block in data (+1 malloc'd in *out), or the + * data itself when it has no PEM armor (*out NULL). */ +static bool sp_der_of(const unsigned char *data, size_t len, unsigned char **out, const unsigned char **der, + size_t *der_len) { + *out = NULL; + const unsigned char *begin = NULL; + for (size_t i = 0; i + 11 <= len; i++) { + if (memcmp(data + i, "-----BEGIN ", 11) == 0) { + begin = data + i; + break; + } + } + if (!begin) { + *der = data; + *der_len = len; + return true; + } + const unsigned char *end = data + len; + const unsigned char *body = memchr(begin, '\n', (size_t)(end - begin)); + if (!body) return false; + body++; + unsigned char *b64 = malloc((size_t)(end - body) + 1); + unsigned char *raw = malloc((size_t)(end - body) + 1); + if (!b64 || !raw) scr_trap("scriptc: out of memory\n"); + size_t n = 0; + for (const unsigned char *p = body; p < end && *p != '-'; p++) + if (*p != '\n' && *p != '\r' && *p != ' ' && *p != '\t') b64[n++] = *p; + size_t raw_len = 0; + bool ok = mbedtls_base64_decode(raw, (size_t)(end - body) + 1, &raw_len, b64, n) == 0; + free(b64); + if (!ok) { + free(raw); + return false; + } + *out = raw; + *der = raw; + *der_len = raw_len; + return true; +} + +/* The SubjectPublicKeyInfo element of an X.509 certificate's DER. */ +static bool sp_cert_spki(const unsigned char *der, size_t len, const unsigned char **spki, size_t *spki_len) { + unsigned char *p = (unsigned char *)der; + const unsigned char *end = der + len; + size_t n; + if (mbedtls_asn1_get_tag(&p, end, &n, MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE) != 0) return false; + if (mbedtls_asn1_get_tag(&p, end, &n, MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE) != 0) return false; + const unsigned char *tbs_end = p + n; + /* [0] version, serialNumber, signature, issuer, validity, subject */ + if (mbedtls_asn1_get_tag(&p, tbs_end, &n, MBEDTLS_ASN1_CONTEXT_SPECIFIC | MBEDTLS_ASN1_CONSTRUCTED | 0) == 0) + p += n; + int tags[] = {MBEDTLS_ASN1_INTEGER, MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE, + MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE, MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE, + MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE}; + for (size_t i = 0; i < 5; i++) { + if (mbedtls_asn1_get_tag(&p, tbs_end, &n, tags[i]) != 0) return false; + p += n; + } + const unsigned char *start = p; + if (mbedtls_asn1_get_tag(&p, tbs_end, &n, MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE) != 0) return false; + *spki = start; + *spki_len = (size_t)(p + n - start); + return true; +} + +/* An Ed25519 key from PKCS#8 or SubjectPublicKeyInfo DER, or (for a public + * key) the key of an X.509 certificate; NULL when the DER is none of + * these, or with *failed set and an error pending. A private key wanted + * as public answers its public key. */ +static SpKey *sp_ed_from_der(const unsigned char *der, size_t len, bool private_key, bool *failed) { + *failed = false; + if (len == 48 && memcmp(der, sp_ed_pkcs8_prefix, 16) == 0) { + const ScrPkiEd25519 *ed = sp_ed(); + if (!ed) { + *failed = true; + return NULL; + } + SpKey *k = sp_key_alloc(private_key); + k->ed25519 = true; + ed->public_key(k->ed_pub, der + 16); + if (private_key) memcpy(k->ed_seed, der + 16, 32); + return k; + } + if (private_key) return NULL; + const unsigned char *spki = der; + size_t spki_len = len; + if (!(len == 44 && memcmp(der, sp_ed_spki_prefix, 12) == 0) && !sp_cert_spki(der, len, &spki, &spki_len)) + return NULL; + if (spki_len != 44 || memcmp(spki, sp_ed_spki_prefix, 12) != 0) return NULL; + SpKey *k = sp_key_alloc(false); + k->ed25519 = true; + memcpy(k->ed_pub, spki + 12, 32); + return k; +} + +static SpKey *sp_parse(const unsigned char *data, size_t len, bool private_key) { + { + unsigned char *decoded; + const unsigned char *der; + size_t der_len; + if (sp_der_of(data, len, &decoded, &der, &der_len)) { + bool failed; + SpKey *ed = sp_ed_from_der(der, der_len, private_key, &failed); + if (decoded) { + mbedtls_platform_zeroize(decoded, der_len); + free(decoded); + } + if (ed || failed) return ed; + } + } + /* mbedtls reads PEM from a NUL-terminated buffer whose length counts the + * NUL. */ + bool pem = false; + for (size_t i = 0; i + 11 <= len; i++) { + if (memcmp(data + i, "-----BEGIN ", 11) == 0) { + pem = true; + break; + } + } + unsigned char *buf = malloc(len + 1); + if (!buf) scr_trap("scriptc: out of memory\n"); + memcpy(buf, data, len); + buf[len] = 0; + size_t n = pem ? len + 1 : len; + SpKey *k = sp_key_alloc(private_key); + bool ok; + if (private_key) { + ok = mbedtls_pk_parse_key(&k->pk, buf, n, NULL, 0, sp_random, NULL) == 0; + } else { + ok = mbedtls_pk_parse_public_key(&k->pk, buf, n) == 0; + if (!ok) { + SpKey *priv = sp_key_alloc(true); + mbedtls_pk_free(&k->pk); + mbedtls_pk_init(&k->pk); + if (mbedtls_pk_parse_key(&priv->pk, buf, n, NULL, 0, sp_random, NULL) == 0) { + sp_key_release(k); + k = sp_public_of(priv); + ok = k != NULL; + if (!k) k = sp_key_alloc(false); + } else { + mbedtls_x509_crt crt; + mbedtls_x509_crt_init(&crt); + if (mbedtls_x509_crt_parse(&crt, buf, n) == 0) { + unsigned char der[SP_DER_MAX]; + int w = mbedtls_pk_write_pubkey_der(&crt.pk, der, sizeof der); + ok = w > 0 && mbedtls_pk_parse_public_key(&k->pk, der + sizeof der - w, (size_t)w) == 0; + } + mbedtls_x509_crt_free(&crt); + } + sp_key_release(priv); + } + } + mbedtls_platform_zeroize(buf, len); + free(buf); + if (!ok) { + sp_key_release(k); + sp_unsupported_input(); + return NULL; + } + if (!sp_key_supported(k)) { + sp_key_release(k); + sp_not_supported("crypto keys of this type"); + return NULL; + } + return k; +} + +/* The bytes of a string or buffer source, +1; NULL when `v` is neither. */ +static ScrBytes *sp_source_bytes(const ScrDyn *v, const ScrDyn *encoding) { + if (v->kind == SCR_DYN_STR) { + bool named = encoding != NULL && encoding->kind == SCR_DYN_STR; + ScrStr *utf8 = named ? NULL : scr_str_new("utf8", 4); + ScrBytes *b = scr_bytes_from_str_checked(v->v.str, named ? encoding->v.str : utf8); + if (utf8) scr_str_release(utf8); + return b; + } + if (v->kind == SCR_DYN_BYTES || scr_buffer_storage_is(v)) return scr_bytes_buffer_source(v); + return NULL; +} + +static const char sp_key_input_types[] = + "of type string or an instance of ArrayBuffer, Buffer, TypedArray, DataView, or URL"; + +/* createPrivateKey / createPublicKey: a PEM string or buffer, or + * { key, format, type, encoding, passphrase }; createPublicKey also takes + * a private KeyObject. */ +static ScrDyn *sp_create_key(ScrDyn *input, bool private_key) { + if (input->kind == SCR_DYN_TYPED_REF) { + ScrDyn *value = scr_dyn_typed_ref_materialize(input); + if (!value) return NULL; + ScrDyn *result = sp_create_key(value, private_key); + scr_dyn_release(value); + return result; + } + SpKey *object = sp_key_of(input); + if (object) { + if (private_key) { + scr_dyn_arg_type_fail("key", sp_key_input_types, input); + return NULL; + } + if (!object->private_key) { + sp_throw(SCR_ERR_TYPE, "ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE", + "Invalid key object type public, expected private."); + return NULL; + } + SpKey *pub = sp_public_of(object); + if (!pub) { + sp_unsupported_input(); + return NULL; + } + return sp_key_box(pub); + } + const ScrDyn *data = input; + const ScrDyn *format = scr_dyn_undefined(); + const ScrDyn *type = scr_dyn_undefined(); + const ScrDyn *encoding = NULL; + if (sp_is_object(input) && !scr_buffer_storage_is(input)) { + data = sp_opt(input, "key"); + format = sp_opt(input, "format"); + type = sp_opt(input, "type"); + encoding = sp_opt(input, "encoding"); + if (!sp_absent(sp_opt(input, "passphrase"))) { + sp_not_supported("encrypted private keys"); + return NULL; + } + SpKey *inner = sp_key_of(data); + if (inner) return sp_create_key((ScrDyn *)data, private_key); + if (sp_is_str(format, "jwk")) { + SpKey *k = sp_key_from_jwk(data, private_key); + return k ? sp_key_box(k) : NULL; + } + if (!sp_absent(format) && !sp_is_str(format, "pem") && !sp_is_str(format, "der")) { + sp_property_invalid("key.format", format); + return NULL; + } + if (sp_is_str(format, "der")) { + bool valid = private_key ? sp_is_str(type, "pkcs1") || sp_is_str(type, "pkcs8") || + sp_is_str(type, "sec1") + : sp_is_str(type, "pkcs1") || sp_is_str(type, "spki"); + if (!valid) { + sp_property_invalid("key.type", type); + return NULL; + } + } + } + ScrBytes *bytes = sp_source_bytes(data, encoding); + if (!bytes) { + if (data->kind == SCR_DYN_STR || data->kind == SCR_DYN_BYTES || scr_buffer_storage_is(data)) + return NULL; /* the decoder threw */ + scr_dyn_arg_type_fail(data == input ? "key" : "key.key", sp_key_input_types, data); + return NULL; + } + SpKey *k = sp_parse(bytes->data, (size_t)scr_bytes_byte_len(bytes), private_key); + scr_bytes_release(bytes); + return k ? sp_key_box(k) : NULL; +} + +ScrDyn *scr_crypto_create_private_key(ScrDyn *key) { return sp_create_key(key, true); } + +ScrDyn *scr_crypto_create_public_key(ScrDyn *key) { return sp_create_key(key, false); } + +/* ── export ───────────────────────────────────────────────────────────── */ + +/* export({ format, type }) per Node's parseKeyEncoding: format "pem", + * "der" or "jwk"; type pkcs1/pkcs8/sec1 for private keys and pkcs1/spki + * for public keys, where pkcs1 needs an RSA key and sec1 an EC key. */ +static ScrDyn *sp_export_plain(SpKey *k, const ScrDyn *options); + +static ScrDyn *sp_export(SpKey *k, const ScrDyn *options) { + ScrDyn *plain = sp_plain(options); + if (!plain) return NULL; + ScrDyn *result = sp_export_plain(k, plain); + scr_dyn_release(plain); + return result; +} + +static ScrDyn *sp_export_plain(SpKey *k, const ScrDyn *options) { + if (options == NULL || !sp_is_object(options)) { + scr_dyn_arg_type_fail("options", "of type object", options ? options : scr_dyn_undefined()); + return NULL; + } + const ScrDyn *format = sp_opt(options, "format"); + if (sp_is_str(format, "jwk")) return sp_export_jwk(k); + bool pem = sp_is_str(format, "pem"); + if (!pem && !sp_is_str(format, "der")) { + sp_property_invalid("options.format", format); + return NULL; + } + const ScrDyn *type = sp_opt(options, "type"); + bool pkcs1 = sp_is_str(type, "pkcs1"); + bool pkcs8 = k->private_key && sp_is_str(type, "pkcs8"); + bool sec1 = k->private_key && sp_is_str(type, "sec1"); + bool spki = !k->private_key && sp_is_str(type, "spki"); + if (!pkcs1 && !pkcs8 && !sec1 && !spki) { + sp_property_invalid("options.type", type); + return NULL; + } + if (!sp_absent(sp_opt(options, "cipher")) || !sp_absent(sp_opt(options, "passphrase"))) { + sp_not_supported("encrypted private key export"); + return NULL; + } + if (pkcs1 && !sp_is_rsa(k)) { + sp_throw(SCR_ERR_ERROR, "ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS", + "The selected key encoding pkcs1 can only be used for RSA keys."); + return NULL; + } + if (sec1 && !sp_is_ec(k)) { + sp_throw(SCR_ERR_ERROR, "ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS", + "The selected key encoding sec1 can only be used for EC keys."); + return NULL; + } + unsigned char *der = malloc(SP_DER_MAX); + if (!der) scr_trap("scriptc: out of memory\n"); + int n; + const char *label; + if (pkcs8) { + n = sp_write_pkcs8_der(k, der, SP_DER_MAX); + label = "PRIVATE KEY"; + } else if (spki) { + n = sp_write_spki_der(k, der, SP_DER_MAX); + label = "PUBLIC KEY"; + } else if (k->private_key) { + n = sp_write_private_der(k, der, SP_DER_MAX); + label = pkcs1 ? "RSA PRIVATE KEY" : "EC PRIVATE KEY"; + } else { + n = sp_write_pkcs1_public_der(k, der, SP_DER_MAX); + label = "RSA PUBLIC KEY"; + } + ScrDyn *result = NULL; + if (n < 0) { + sp_unsupported_input(); + } else { + const unsigned char *start = der + SP_DER_MAX - n; + result = pem ? sp_dyn_str(sp_pem(label, start, (size_t)n)) : sp_dyn_buffer(start, (size_t)n); + } + mbedtls_platform_zeroize(der, SP_DER_MAX); + free(der); + return result; +} + +/* ── the KeyObject handles ────────────────────────────────────────────── */ + +static ScrDyn *sp_key_details(SpKey *k) { + ScrDyn *details = scr_dyn_new_obj(); + if (sp_is_ed(k)) return details; + if (sp_is_rsa(k)) { + mbedtls_rsa_context *rsa = mbedtls_pk_rsa(k->pk); + mbedtls_mpi e; + mbedtls_mpi_init(&e); + mbedtls_rsa_export(rsa, NULL, NULL, NULL, NULL, &e); + uint64_t exponent = 0; + size_t bytes = mbedtls_mpi_size(&e); + unsigned char raw[8] = {0}; + if (bytes <= 8) mbedtls_mpi_write_binary(&e, raw, 8); + for (size_t i = 0; i < 8; i++) exponent = (exponent << 8) | raw[i]; + mbedtls_mpi_free(&e); + sp_set(details, "modulusLength", scr_dyn_new_num((double)mbedtls_pk_get_bitlen(&k->pk))); + ScrBigInt *big = scr_bigint_from_u64(exponent); + sp_set(details, "publicExponent", scr_dyn_new_bigint(big)); + scr_bigint_release(big); + } else { + const SpCurve *curve = sp_curve_of(k); + sp_set(details, "namedCurve", sp_dyn_str(scr_str_new(curve->name, strlen(curve->name)))); + } + return details; +} + +/* The DER that identifies a key for equals(): its private encoding for a + * private key, its SubjectPublicKeyInfo otherwise. */ +static int sp_identity_der(SpKey *k, unsigned char *buf, size_t size) { + if (sp_is_ed(k)) return k->private_key ? sp_write_pkcs8_der(k, buf, size) : sp_write_spki_der(k, buf, size); + return k->private_key ? sp_write_private_der(k, buf, size) : sp_write_spki_der(k, buf, size); +} + +static ScrDyn *sp_key_invoke(void *h, ScrDyn *self, const char *method, ScrDyn *const *args, + size_t argc, const char *what) { + (void)self; + SpKey *k = h; + if (strcmp(method, "export") == 0) return sp_export(k, argc ? args[0] : NULL); + if (strcmp(method, "equals") == 0) { + const ScrDyn *other_value = argc ? args[0] : scr_dyn_undefined(); + SpKey *other = sp_key_of(other_value); + if (!other) { + size_t len; + if (scr_key_object_bytes(other_value, &len)) return scr_dyn_new_bool(false); + scr_dyn_arg_type_fail("otherKeyObject", "an instance of KeyObject", other_value); + return NULL; + } + if (other->private_key != k->private_key || + mbedtls_pk_get_type(&other->pk) != mbedtls_pk_get_type(&k->pk)) + return scr_dyn_new_bool(false); + unsigned char *a = malloc(SP_DER_MAX), *b = malloc(SP_DER_MAX); + if (!a || !b) scr_trap("scriptc: out of memory\n"); + int na = sp_identity_der(k, a, SP_DER_MAX); + int nb = sp_identity_der(other, b, SP_DER_MAX); + bool same = na > 0 && na == nb && memcmp(a + SP_DER_MAX - na, b + SP_DER_MAX - nb, (size_t)na) == 0; + mbedtls_platform_zeroize(a, SP_DER_MAX); + mbedtls_platform_zeroize(b, SP_DER_MAX); + free(a); + free(b); + return scr_dyn_new_bool(same); + } + if (strcmp(method, "toCryptoKey") == 0) { + sp_not_supported("KeyObject.toCryptoKey"); + return NULL; + } + scr_pki_throw_fmt(SCR_ERR_TYPE, NULL, "%s is not a function", what); + return NULL; +} + +static ScrDyn *sp_key_get(void *h, const char *name, size_t len) { + SpKey *k = h; + if (len == 4 && memcmp(name, "type", 4) == 0) + return k->private_key ? sp_dyn_str(scr_str_new("private", 7)) : sp_dyn_str(scr_str_new("public", 6)); + if (len == 17 && memcmp(name, "asymmetricKeyType", 17) == 0) + return sp_is_ed(k) ? sp_dyn_str(scr_str_new("ed25519", 7)) + : sp_is_rsa(k) ? sp_dyn_str(scr_str_new("rsa", 3)) + : sp_dyn_str(scr_str_new("ec", 2)); + if (len == 20 && memcmp(name, "asymmetricKeyDetails", 20) == 0) return sp_key_details(k); + return NULL; +} + +static bool sp_key_set(void *h, const char *name, size_t len, const ScrDyn *value) { + (void)h; + (void)value; + const char *holder = len == 4 && memcmp(name, "type", 4) == 0 ? "KeyObject" + : (len == 17 && memcmp(name, "asymmetricKeyType", 17) == 0) || + (len == 20 && memcmp(name, "asymmetricKeyDetails", 20) == 0) + ? "AsymmetricKeyObject" + : NULL; + if (!holder) return false; + scr_pki_throw_fmt(SCR_ERR_TYPE, NULL, "Cannot set property %.*s of #<%s> which has only a getter", (int)len, name, + holder); + return true; +} + +static ScrDyn *sp_key_inspect_view(void *h) { + (void)h; + return scr_dyn_new_obj(); +} + +static const ScrDynHandleOps sp_private_ops = { + .cls = "PrivateKeyObject", + .retain = sp_key_retain, + .release = sp_key_release, + .invoke = sp_key_invoke, + .get = sp_key_get, + .set = sp_key_set, + .inspect_view = sp_key_inspect_view, + .json_empty_object = true, + .to_string_tag = "KeyObject", + .inspect_view_empty = true, +}; + +static const ScrDynHandleOps sp_public_ops = { + .cls = "PublicKeyObject", + .retain = sp_key_retain, + .release = sp_key_release, + .invoke = sp_key_invoke, + .get = sp_key_get, + .set = sp_key_set, + .inspect_view = sp_key_inspect_view, + .json_empty_object = true, + .to_string_tag = "KeyObject", + .inspect_view_empty = true, +}; + +/* createHmac with a private or public key (scr_crypto_reject_asymmetric_secret). */ +static void sp_reject_as_secret(const ScrDyn *key) { + bool private_key = key->v.handle.tag == SCR_DYNH_PRIVATE_KEY; + scr_throw_error_msg_code(SCR_ERR_TYPE, + private_key ? "Invalid key object type private, expected secret." + : "Invalid key object type public, expected secret.", + private_key ? 49 : 48, "ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE"); +} + +static void sp_key_install(void) { + static SCR_TL bool installed; + if (installed) return; + installed = true; + scr_dyn_handle_install(SCR_DYNH_PRIVATE_KEY, &sp_private_ops); + scr_dyn_handle_install(SCR_DYNH_PUBLIC_KEY, &sp_public_ops); + scr_crypto_reject_asymmetric_secret = sp_reject_as_secret; +} + +/* ── generateKeyPairSync ──────────────────────────────────────────────── */ + +/* A key in the encoding an options.publicKeyEncoding/privateKeyEncoding + * object asks for, or the KeyObject when it is undefined. Consumes `k`. */ +static ScrDyn *sp_encoded(SpKey *k, const ScrDyn *encoding) { + if (sp_absent(encoding)) return sp_key_box(k); + ScrDyn *result = sp_export(k, encoding); + sp_key_release(k); + return result; +} + +static bool sp_check_encoding_plain(const ScrDyn *encoding, const char *name, bool private_key); + +static bool sp_check_encoding(const ScrDyn *options, const char *name, bool private_key) { + ScrDyn *encoding = sp_plain(sp_opt(options, name)); + if (!encoding) return false; + bool ok = sp_check_encoding_plain(encoding, name, private_key); + scr_dyn_release(encoding); + return ok; +} + +static bool sp_check_encoding_plain(const ScrDyn *encoding, const char *name, bool private_key) { + if (sp_absent(encoding)) return true; + char full[64]; + snprintf(full, sizeof full, "options.%s", name); + if (!sp_is_object(encoding)) { + sp_property_invalid(full, encoding); + return false; + } + const ScrDyn *format = sp_opt(encoding, "format"); + if (!sp_is_str(format, "pem") && !sp_is_str(format, "der") && !sp_is_str(format, "jwk")) { + snprintf(full, sizeof full, "options.%s.format", name); + sp_property_invalid(full, format); + return false; + } + if (sp_is_str(format, "jwk")) return true; + const ScrDyn *type = sp_opt(encoding, "type"); + bool valid = private_key ? sp_is_str(type, "pkcs1") || sp_is_str(type, "pkcs8") || sp_is_str(type, "sec1") + : sp_is_str(type, "pkcs1") || sp_is_str(type, "spki"); + if (!valid) { + snprintf(full, sizeof full, "options.%s.type", name); + sp_property_invalid(full, type); + return false; + } + return true; +} + +/* Generates the private key for (type, options), validated in Node's + * order; NULL with the error pending. */ +static SpKey *sp_generate(const ScrDyn *type, const ScrDyn *options) { + if (type->kind != SCR_DYN_STR) { + scr_dyn_arg_type_fail("type", "of type string", type); + return NULL; + } + bool rsa = sp_is_str(type, "rsa"); + bool ec = sp_is_str(type, "ec"); + bool ed = sp_is_str(type, "ed25519"); + static const char *const known[] = {"rsa-pss", "dsa", "ed448", "x25519", "x448", "dh"}; + bool other = false; + for (size_t i = 0; i < sizeof known / sizeof known[0]; i++) other = other || sp_is_str(type, known[i]); + if (!rsa && !ec && !ed && !other) { + ScrStr *received = sp_received(type, false); + scr_pki_throw_fmt(SCR_ERR_TYPE, "ERR_INVALID_ARG_VALUE", + "The argument 'type' must be a supported key type. Received %.*s", (int)received->len, + received->data); + scr_str_release(received); + return NULL; + } + /* The ed25519 options are optional. */ + if (!sp_is_object(options) && !(ed && sp_absent(options))) { + scr_dyn_arg_type_fail("options", "of type object", options); + return NULL; + } + if (other) { + sp_not_supported("generateKeyPairSync for this key type"); + return NULL; + } + SpKey *k = sp_key_alloc(true); + if (ed) { + if (!sp_check_encoding(options, "publicKeyEncoding", false) || + !sp_check_encoding(options, "privateKeyEncoding", true)) { + sp_key_release(k); + return NULL; + } + const ScrPkiEd25519 *ed = sp_ed(); + if (!ed) { + sp_key_release(k); + return NULL; + } + k->ed25519 = true; + sp_random(NULL, k->ed_seed, 32); + ed->public_key(k->ed_pub, k->ed_seed); + return k; + } + if (rsa) { + const ScrDyn *bits = sp_opt(options, "modulusLength"); + if (bits->kind != SCR_DYN_NUM) { + sp_key_release(k); + scr_dyn_prop_type_fail("options.modulusLength", "of type number", bits); + return NULL; + } + double b = bits->v.num; + /* validateUint32: an integer first, then the range. */ + bool integer = isfinite(b) && trunc(b) == b; + if (!integer || b < 0 || b > 4294967295.0) { + sp_key_release(k); + ScrStr *received = sp_received(bits, true); + scr_pki_throw_fmt(SCR_ERR_RANGE, "ERR_OUT_OF_RANGE", + "The value of \"options.modulusLength\" is out of range. It must be %s. Received %.*s", + integer ? ">= 0 && <= 4294967295" : "an integer", (int)received->len, received->data); + scr_str_release(received); + return NULL; + } + const ScrDyn *exp = sp_opt(options, "publicExponent"); + double e = 65537; + if (!sp_absent(exp)) { + if (exp->kind != SCR_DYN_NUM) { + sp_key_release(k); + scr_dyn_prop_type_fail("options.publicExponent", "of type number", exp); + return NULL; + } + e = exp->v.num; + } + if (!sp_check_encoding(options, "publicKeyEncoding", false) || + !sp_check_encoding(options, "privateKeyEncoding", true)) { + sp_key_release(k); + return NULL; + } + if (b < 512 || b > 16384 || e < 3 || e > 2147483647.0 || e != (double)(int64_t)e || ((int64_t)e & 1) == 0 || + mbedtls_pk_setup(&k->pk, mbedtls_pk_info_from_type(MBEDTLS_PK_RSA)) != 0 || + mbedtls_rsa_gen_key(mbedtls_pk_rsa(k->pk), sp_random, NULL, (unsigned)b, (int)e) != 0) { + sp_key_release(k); + sp_not_supported("generateKeyPairSync with this RSA modulus length or exponent"); + return NULL; + } + return k; + } + const ScrDyn *curve_name = sp_opt(options, "namedCurve"); + if (curve_name->kind != SCR_DYN_STR) { + sp_key_release(k); + scr_dyn_prop_type_fail("options.namedCurve", "of type string", curve_name); + return NULL; + } + if (!sp_check_encoding(options, "publicKeyEncoding", false) || + !sp_check_encoding(options, "privateKeyEncoding", true)) { + sp_key_release(k); + return NULL; + } + const SpCurve *curve = NULL; + for (size_t i = 0; i < sizeof sp_curves / sizeof sp_curves[0]; i++) + if (sp_is_str(curve_name, sp_curves[i].name) || + (sp_curves[i].nist && sp_is_str(curve_name, sp_curves[i].nist))) + curve = &sp_curves[i]; + if (!curve) { + sp_key_release(k); + sp_throw(SCR_ERR_TYPE, "ERR_CRYPTO_INVALID_CURVE", "Invalid EC curve name"); + return NULL; + } + if (mbedtls_pk_setup(&k->pk, mbedtls_pk_info_from_type(MBEDTLS_PK_ECKEY)) != 0 || + mbedtls_ecp_gen_key(curve->id, mbedtls_pk_ec(k->pk), sp_random, NULL) != 0) { + sp_key_release(k); + sp_unsupported_input(); + return NULL; + } + return k; +} + +/* generateKeyPairSync(type, options): { publicKey, privateKey }, each a + * KeyObject or the encoding the options ask for. */ +ScrDyn *scr_crypto_generate_key_pair_sync(ScrDyn *type, ScrDyn *options) { + if (options->kind == SCR_DYN_TYPED_REF) { + ScrDyn *value = scr_dyn_typed_ref_materialize(options); + if (!value) return NULL; + ScrDyn *result = scr_crypto_generate_key_pair_sync(type, value); + scr_dyn_release(value); + return result; + } + SpKey *k = sp_generate(type, options); + if (!k) return NULL; + SpKey *pub = sp_public_of(k); + if (!pub) { + sp_key_release(k); + sp_unsupported_input(); + return NULL; + } + ScrDyn *public_value = sp_encoded(pub, sp_opt(options, "publicKeyEncoding")); + if (!public_value) { + sp_key_release(k); + return NULL; + } + ScrDyn *private_value = sp_encoded(k, sp_opt(options, "privateKeyEncoding")); + if (!private_value) { + scr_dyn_release(public_value); + return NULL; + } + ScrDyn *pair = scr_dyn_new_obj(); + sp_set(pair, "publicKey", public_value); + sp_set(pair, "privateKey", private_value); + return pair; +} + +/* ── sign and verify ──────────────────────────────────────────────────── */ + +typedef struct { + const char *name; + mbedtls_md_type_t md; +} SpDigest; + +/* Digest names as OpenSSL's EVP_get_digestbyname takes them for + * signatures, matched without regard to case. */ +static const SpDigest sp_digests[] = { + {"sha1", MBEDTLS_MD_SHA1}, {"rsa-sha1", MBEDTLS_MD_SHA1}, + {"sha1withrsaencryption", MBEDTLS_MD_SHA1}, {"ecdsa-with-sha1", MBEDTLS_MD_SHA1}, + {"sha224", MBEDTLS_MD_SHA224}, {"rsa-sha224", MBEDTLS_MD_SHA224}, + {"sha224withrsaencryption", MBEDTLS_MD_SHA224}, + {"sha256", MBEDTLS_MD_SHA256}, {"rsa-sha256", MBEDTLS_MD_SHA256}, + {"sha256withrsaencryption", MBEDTLS_MD_SHA256}, + {"sha384", MBEDTLS_MD_SHA384}, {"rsa-sha384", MBEDTLS_MD_SHA384}, + {"sha384withrsaencryption", MBEDTLS_MD_SHA384}, + {"sha512", MBEDTLS_MD_SHA512}, {"rsa-sha512", MBEDTLS_MD_SHA512}, + {"sha512withrsaencryption", MBEDTLS_MD_SHA512}, + {"sha2-256", MBEDTLS_MD_SHA256}, {"sha2-384", MBEDTLS_MD_SHA384}, + {"sha2-512", MBEDTLS_MD_SHA512}, {"sha2-224", MBEDTLS_MD_SHA224}, +}; + +static bool sp_digest_of(const ScrStr *name, mbedtls_md_type_t *md) { + for (size_t i = 0; i < sizeof sp_digests / sizeof sp_digests[0]; i++) { + size_t n = strlen(sp_digests[i].name); + if (name->len != n) continue; + bool same = true; + for (size_t j = 0; j < n && same; j++) { + char c = name->data[j]; + if (c >= 'A' && c <= 'Z') c = (char)(c - 'A' + 'a'); + same = c == sp_digests[i].name[j]; + } + if (same) { + *md = sp_digests[i].md; + return true; + } + } + return false; +} + +/* The algorithm argument of crypto.sign/verify: a digest name, or + * null/undefined for the key type's default (SHA-256). */ +static bool sp_algorithm(const ScrDyn *alg, mbedtls_md_type_t *md) { + if (alg->kind == SCR_DYN_UNDEF || alg->kind == SCR_DYN_NULL) { + *md = MBEDTLS_MD_SHA256; + return true; + } + if (alg->kind != SCR_DYN_STR) { + scr_dyn_arg_type_fail("algorithm", "of type string", alg); + return false; + } + if (!sp_digest_of(alg->v.str, md)) { + /* The whole name, at any length and with any bytes, as in Node. */ + static const char prefix[] = "Invalid digest: "; + size_t len = sizeof prefix - 1 + alg->v.str->len; + char *message = malloc(len); + if (!message) scr_trap("scriptc: out of memory\n"); + memcpy(message, prefix, sizeof prefix - 1); + memcpy(message + sizeof prefix - 1, alg->v.str->data, alg->v.str->len); + scr_throw_error_msg_code(SCR_ERR_TYPE, message, len, "ERR_CRYPTO_INVALID_DIGEST"); + free(message); + return false; + } + return true; +} + +typedef struct { + SpKey *key; /* +1 */ + int padding; /* RSA_PKCS1_PADDING (1) or RSA_PKCS1_PSS_PADDING (6) */ + int salt; /* -1 digest length, -2 maximum (sign) or auto (verify) */ + bool p1363; /* dsaEncoding: "ieee-p1363" */ +} SpSigKey; + +/* The key argument of a signing operation: a KeyObject, key material, or + * { key, dsaEncoding, padding, saltLength, ...createKey options }. */ +static bool sp_sig_key_plain(const ScrDyn *value, bool sign, SpSigKey *out); + +static bool sp_sig_key(const ScrDyn *value, bool sign, SpSigKey *out) { + out->key = NULL; + ScrDyn *plain = sp_plain(value); + if (!plain) return false; + bool ok = sp_sig_key_plain(plain, sign, out); + scr_dyn_release(plain); + return ok; +} + +static bool sp_sig_key_plain(const ScrDyn *value, bool sign, SpSigKey *out) { + out->key = NULL; + out->padding = 1; + out->salt = sign ? -2 : -2; + out->p1363 = false; + const ScrDyn *options = NULL; + if (sp_is_object(value) && !sp_key_of(value) && !scr_buffer_storage_is(value)) { + options = value; + const ScrDyn *dsa = sp_opt(value, "dsaEncoding"); + if (sp_is_str(dsa, "ieee-p1363")) out->p1363 = true; + else if (!sp_absent(dsa) && !sp_is_str(dsa, "der")) { + sp_property_invalid("options.dsaEncoding", dsa); + return false; + } + const ScrDyn *padding = sp_opt(value, "padding"); + if (!sp_absent(padding)) { + if (padding->kind != SCR_DYN_NUM || (padding->v.num != 1 && padding->v.num != 6)) { + sp_not_supported("signatures with this padding"); + return false; + } + out->padding = (int)padding->v.num; + } + const ScrDyn *salt = sp_opt(value, "saltLength"); + if (!sp_absent(salt)) { + if (salt->kind != SCR_DYN_NUM || salt->v.num != (double)(int)salt->v.num || salt->v.num < -2) { + sp_not_supported("signatures with this saltLength"); + return false; + } + out->salt = (int)salt->v.num; + } + } + SpKey *direct = sp_key_of(options ? sp_opt(options, "key") : value); + if (direct) { + if (sign && !direct->private_key) { + sp_throw(SCR_ERR_TYPE, "ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE", + "Invalid key object type public, expected private."); + return false; + } + sp_key_retain(direct); + out->key = direct; + return true; + } + ScrDyn *created = sp_create_key((ScrDyn *)value, sign); + if (!created) return false; + out->key = sp_key_retain(sp_key_of(created)); + scr_dyn_release(created); + return true; +} + +/* DER ECDSA-Sig-Value ⇄ IEEE P1363 r || s. */ +static bool sp_der_to_p1363(const unsigned char *der, size_t len, size_t size, unsigned char *out) { + unsigned char *p = (unsigned char *)der; + const unsigned char *end = der + len; + size_t seq; + mbedtls_mpi r, s; + mbedtls_mpi_init(&r); + mbedtls_mpi_init(&s); + bool ok = mbedtls_asn1_get_tag(&p, end, &seq, MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE) == 0 && + mbedtls_asn1_get_mpi(&p, end, &r) == 0 && mbedtls_asn1_get_mpi(&p, end, &s) == 0 && + mbedtls_mpi_write_binary(&r, out, size) == 0 && + mbedtls_mpi_write_binary(&s, out + size, size) == 0; + mbedtls_mpi_free(&r); + mbedtls_mpi_free(&s); + return ok; +} + +static int sp_p1363_to_der(const unsigned char *sig, size_t len, unsigned char *buf, size_t size) { + mbedtls_mpi r, s; + mbedtls_mpi_init(&r); + mbedtls_mpi_init(&s); + int out = -1; + if (len % 2 == 0 && mbedtls_mpi_read_binary(&r, sig, len / 2) == 0 && + mbedtls_mpi_read_binary(&s, sig + len / 2, len / 2) == 0) { + unsigned char *c = buf + size; + size_t n = 0; + int w; + if ((w = mbedtls_asn1_write_mpi(&c, buf, &s)) >= 0) { + n += (size_t)w; + if ((w = mbedtls_asn1_write_mpi(&c, buf, &r)) >= 0) { + n += (size_t)w; + if ((w = mbedtls_asn1_write_len(&c, buf, n)) >= 0) { + n += (size_t)w; + if ((w = mbedtls_asn1_write_tag(&c, buf, MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE)) >= 0) + out = (int)(n + (size_t)w); + } + } + } + } + mbedtls_mpi_free(&r); + mbedtls_mpi_free(&s); + return out; +} + +static size_t sp_ec_size(SpKey *k) { return (mbedtls_pk_ec(k->pk)->MBEDTLS_PRIVATE(grp).nbits + 7) / 8; } + +/* Applies the RSA padding of a signature operation. */ +static bool sp_rsa_padding(SpSigKey *key, mbedtls_md_type_t md) { + if (!sp_is_rsa(key->key)) return true; + mbedtls_rsa_context *rsa = mbedtls_pk_rsa(key->key->pk); + if (key->padding == 6) return mbedtls_rsa_set_padding(rsa, MBEDTLS_RSA_PKCS_V21, md) == 0; + return mbedtls_rsa_set_padding(rsa, MBEDTLS_RSA_PKCS_V15, MBEDTLS_MD_NONE) == 0; +} + +/* Signs a digest: +1 Buffer or NULL with the error pending. */ +static ScrDyn *sp_sign_hash(SpSigKey *key, mbedtls_md_type_t md, const unsigned char *hash, size_t hash_len) { + unsigned char sig[MBEDTLS_PK_SIGNATURE_MAX_SIZE]; + size_t sig_len = 0; + int r; + if (!sp_rsa_padding(key, md)) { + sp_unsupported_input(); + return NULL; + } + if (sp_is_rsa(key->key) && key->padding == 6) { + mbedtls_rsa_context *rsa = mbedtls_pk_rsa(key->key->pk); + int salt = key->salt; + size_t bytes = mbedtls_rsa_get_len(rsa); + size_t bits = mbedtls_pk_get_bitlen(&key->key->pk); + size_t max = bytes - hash_len - 2 - ((bits - 1) % 8 == 0 ? 1 : 0); + if (salt == -1) salt = (int)hash_len; + else if (salt == -2) salt = (int)max; + if (salt < 0 || (size_t)salt > max) { + sp_throw(SCR_ERR_ERROR, "ERR_OSSL_RSA_DATA_TOO_LARGE_FOR_KEY_SIZE", + "error:1C8000A5:Provider routines::data too large for key size"); + return NULL; + } + r = mbedtls_rsa_rsassa_pss_sign_ext(rsa, sp_random, NULL, md, (unsigned)hash_len, hash, salt, sig); + sig_len = bytes; + } else { + r = mbedtls_pk_sign(&key->key->pk, md, hash, hash_len, sig, sizeof sig, &sig_len, sp_random, NULL); + } + if (r != 0) { + sp_unsupported_input(); + return NULL; + } + if (sp_is_ec(key->key) && key->p1363) { + size_t size = sp_ec_size(key->key); + unsigned char raw[2 * 66]; + if (!sp_der_to_p1363(sig, sig_len, size, raw)) { + sp_unsupported_input(); + return NULL; + } + return sp_dyn_buffer(raw, 2 * size); + } + return sp_dyn_buffer(sig, sig_len); +} + +/* Verifies a digest against a signature; false for a signature that does + * not verify or does not parse. */ +static bool sp_verify_hash(SpSigKey *key, mbedtls_md_type_t md, const unsigned char *hash, size_t hash_len, + const unsigned char *sig, size_t sig_len) { + if (!sp_rsa_padding(key, md)) return false; + if (sp_is_rsa(key->key) && key->padding == 6) { + mbedtls_rsa_context *rsa = mbedtls_pk_rsa(key->key->pk); + if (sig_len != mbedtls_rsa_get_len(rsa)) return false; + int salt = key->salt; + if (salt == -1) salt = (int)hash_len; + else if (salt == -2) salt = MBEDTLS_RSA_SALT_LEN_ANY; + return mbedtls_rsa_rsassa_pss_verify_ext(rsa, md, (unsigned)hash_len, hash, md, salt, sig) == 0; + } + if (sp_is_ec(key->key) && key->p1363) { + unsigned char der[2 * 66 + 16]; + if (sig_len != 2 * sp_ec_size(key->key)) return false; + int n = sp_p1363_to_der(sig, sig_len, der, sizeof der); + if (n < 0) return false; + return mbedtls_pk_verify(&key->key->pk, md, hash, hash_len, der + sizeof der - n, (size_t)n) == 0; + } + return mbedtls_pk_verify(&key->key->pk, md, hash, hash_len, sig, sig_len) == 0; +} + +static const char sp_data_types[] = "an instance of Buffer, TypedArray, or DataView"; + +/* Ed25519 signs the message itself: an algorithm other than null or + * undefined is OpenSSL's invalid digest (false, error pending). */ +static bool sp_ed_digest_given(const ScrDyn *algorithm) { + if (algorithm->kind == SCR_DYN_UNDEF || algorithm->kind == SCR_DYN_NULL) return true; + sp_throw(SCR_ERR_ERROR, "ERR_OSSL_INVALID_DIGEST", "error:1C80007A:Provider routines::invalid digest"); + return false; +} + +/* crypto.sign(algorithm, data, key): the signature Buffer. */ +ScrDyn *scr_crypto_sign_oneshot(ScrDyn *algorithm, ScrDyn *data, ScrDyn *key) { + mbedtls_md_type_t md; + if (!sp_algorithm(algorithm, &md)) return NULL; + if (data->kind != SCR_DYN_BYTES) { + scr_dyn_arg_type_fail("data", sp_data_types, data); + return NULL; + } + SpSigKey k; + if (!sp_sig_key(key, true, &k)) return NULL; + ScrBytes *bytes = scr_bytes_buffer_source(data); + if (sp_is_ed(k.key)) { + ScrDyn *result = NULL; + const ScrPkiEd25519 *ed = sp_ed_digest_given(algorithm) ? sp_ed() : NULL; + if (ed) { + uint8_t sig[64]; + ed->sign(sig, k.key->ed_seed, k.key->ed_pub, bytes->data, (size_t)scr_bytes_byte_len(bytes)); + result = sp_dyn_buffer(sig, 64); + } + scr_bytes_release(bytes); + sp_key_release(k.key); + return result; + } + unsigned char hash[MBEDTLS_MD_MAX_SIZE]; + const mbedtls_md_info_t *info = mbedtls_md_info_from_type(md); + mbedtls_md(info, bytes->data, (size_t)scr_bytes_byte_len(bytes), hash); + scr_bytes_release(bytes); + ScrDyn *result = sp_sign_hash(&k, md, hash, mbedtls_md_get_size(info)); + sp_key_release(k.key); + return result; +} + +/* crypto.verify(algorithm, data, key, signature): a boolean. */ +ScrDyn *scr_crypto_verify_oneshot(ScrDyn *algorithm, ScrDyn *data, ScrDyn *key, ScrDyn *signature) { + mbedtls_md_type_t md; + if (!sp_algorithm(algorithm, &md)) return NULL; + if (data->kind != SCR_DYN_BYTES) { + scr_dyn_arg_type_fail("data", sp_data_types, data); + return NULL; + } + SpSigKey k; + if (!sp_sig_key(key, false, &k)) return NULL; + if (signature->kind != SCR_DYN_BYTES) { + sp_key_release(k.key); + scr_dyn_arg_type_fail("signature", sp_data_types, signature); + return NULL; + } + ScrBytes *bytes = scr_bytes_buffer_source(data); + ScrBytes *sig = scr_bytes_buffer_source(signature); + if (sp_is_ed(k.key)) { + ScrDyn *result = NULL; + const ScrPkiEd25519 *ed = sp_ed_digest_given(algorithm) ? sp_ed() : NULL; + if (ed) + result = scr_dyn_new_bool(sp_ed_verify(ed, k.key->ed_pub, bytes->data, + (size_t)scr_bytes_byte_len(bytes), sig->data, + (size_t)scr_bytes_byte_len(sig))); + scr_bytes_release(bytes); + scr_bytes_release(sig); + sp_key_release(k.key); + return result; + } + unsigned char hash[MBEDTLS_MD_MAX_SIZE]; + const mbedtls_md_info_t *info = mbedtls_md_info_from_type(md); + mbedtls_md(info, bytes->data, (size_t)scr_bytes_byte_len(bytes), hash); + bool ok = sp_verify_hash(&k, md, hash, mbedtls_md_get_size(info), sig->data, (size_t)scr_bytes_byte_len(sig)); + scr_bytes_release(bytes); + scr_bytes_release(sig); + sp_key_release(k.key); + return scr_dyn_new_bool(ok); +} + +/* ── Sign and Verify objects ──────────────────────────────────────────── */ + +typedef struct SpSigner { + size_t rc; + mbedtls_md_context_t md; + mbedtls_md_type_t type; + bool done; /* sign()/verify() ran: Node's "Not initialised" afterwards */ +} SpSigner; + +static void *sp_signer_retain(void *h) { + ((SpSigner *)h)->rc++; + return h; +} + +static void sp_signer_release(void *h) { + SpSigner *s = h; + if (--s->rc) return; + mbedtls_md_free(&s->md); + free(s); +} + +static void sp_not_initialised(void) { + sp_throw(SCR_ERR_ERROR, "ERR_CRYPTO_INVALID_STATE", "Not initialised"); +} + +/* update(data[, inputEncoding]): strings encode with the encoding (UTF-8 + * by default). */ +static bool sp_signer_update(SpSigner *s, ScrDyn *const *args, size_t argc) { + if (s->done) { + sp_not_initialised(); + return false; + } + const ScrDyn *data = argc ? args[0] : scr_dyn_undefined(); + if (data->kind != SCR_DYN_STR && data->kind != SCR_DYN_BYTES) { + scr_dyn_arg_type_fail("data", "of type string or an instance of Buffer, TypedArray, or DataView", data); + return false; + } + ScrBytes *bytes = sp_source_bytes(data, argc > 1 ? args[1] : NULL); + if (!bytes) return false; + mbedtls_md_update(&s->md, bytes->data, (size_t)scr_bytes_byte_len(bytes)); + scr_bytes_release(bytes); + return true; +} + +/* An output or signature encoding argument: a string encoding, or the + * Buffer form for undefined, null and "buffer". */ +static const ScrStr *sp_encoding_arg(const ScrDyn *v) { + if (v == NULL || v->kind != SCR_DYN_STR || sp_is_str(v, "buffer")) return NULL; + return v->v.str; +} + +static ScrDyn *sp_signer_invoke(void *h, ScrDyn *self, const char *method, ScrDyn *const *args, size_t argc, + const char *what, bool verifier) { + SpSigner *s = h; + if (strcmp(method, "update") == 0) return sp_signer_update(s, args, argc) ? scr_dyn_retain(self) : NULL; + if (!verifier && strcmp(method, "sign") == 0) { + if (s->done) { + sp_not_initialised(); + return NULL; + } + SpSigKey k; + if (!sp_sig_key(argc ? args[0] : scr_dyn_undefined(), true, &k)) return NULL; + if (sp_is_ed(k.key)) { + sp_key_release(k.key); + sp_throw(SCR_ERR_ERROR, "ERR_CRYPTO_UNSUPPORTED_OPERATION", "Unsupported crypto operation"); + return NULL; + } + unsigned char hash[MBEDTLS_MD_MAX_SIZE]; + mbedtls_md_finish(&s->md, hash); + s->done = true; + ScrDyn *sig = sp_sign_hash(&k, s->type, hash, mbedtls_md_get_size(mbedtls_md_info_from_type(s->type))); + sp_key_release(k.key); + const ScrStr *encoding = sp_encoding_arg(argc > 1 ? args[1] : NULL); + if (!sig || !encoding) return sig; + ScrBytes *bytes = scr_bytes_buffer_source(sig); + ScrStr *text = scr_bytes_to_str_checked(bytes, encoding); + scr_bytes_release(bytes); + scr_dyn_release(sig); + return text ? sp_dyn_str(text) : NULL; + } + if (verifier && strcmp(method, "verify") == 0) { + if (s->done) { + sp_not_initialised(); + return NULL; + } + SpSigKey k; + if (!sp_sig_key(argc ? args[0] : scr_dyn_undefined(), false, &k)) return NULL; + if (sp_is_ed(k.key)) { + sp_key_release(k.key); + sp_throw(SCR_ERR_ERROR, "ERR_CRYPTO_UNSUPPORTED_OPERATION", "Unsupported crypto operation"); + return NULL; + } + const ScrDyn *signature = argc > 1 ? args[1] : scr_dyn_undefined(); + if (signature->kind != SCR_DYN_STR && signature->kind != SCR_DYN_BYTES) { + sp_key_release(k.key); + scr_dyn_arg_type_fail("signature", "of type string or an instance of Buffer, TypedArray, or DataView", + signature); + return NULL; + } + const ScrStr *encoding = sp_encoding_arg(argc > 2 ? args[2] : NULL); + ScrDyn *encoding_value = encoding ? scr_dyn_new_str((ScrStr *)encoding) : NULL; + ScrBytes *sig = sp_source_bytes(signature, encoding_value); + if (encoding_value) scr_dyn_release(encoding_value); + if (!sig) { + sp_key_release(k.key); + return NULL; + } + unsigned char hash[MBEDTLS_MD_MAX_SIZE]; + mbedtls_md_finish(&s->md, hash); + s->done = true; + bool ok = sp_verify_hash(&k, s->type, hash, mbedtls_md_get_size(mbedtls_md_info_from_type(s->type)), + sig->data, (size_t)scr_bytes_byte_len(sig)); + scr_bytes_release(sig); + sp_key_release(k.key); + return scr_dyn_new_bool(ok); + } + scr_pki_throw_fmt(SCR_ERR_TYPE, NULL, "%s is not a function", what); + return NULL; +} + +static ScrDyn *sp_sign_invoke(void *h, ScrDyn *self, const char *method, ScrDyn *const *args, size_t argc, + const char *what) { + return sp_signer_invoke(h, self, method, args, argc, what, false); +} + +static ScrDyn *sp_verify_invoke(void *h, ScrDyn *self, const char *method, ScrDyn *const *args, size_t argc, + const char *what) { + return sp_signer_invoke(h, self, method, args, argc, what, true); +} + +/* Sign and Verify have no modeled properties. */ +static ScrDyn *sp_signer_get(void *h, const char *name, size_t len) { + (void)h; + (void)name; + (void)len; + return NULL; +} + +static bool sp_signer_set(void *h, const char *name, size_t len, const ScrDyn *value) { + (void)h; + (void)name; + (void)len; + (void)value; + return false; +} + +static ScrDyn *sp_signer_inspect_view(void *h) { + (void)h; + return scr_dyn_new_obj(); +} + +static const ScrDynHandleOps sp_sign_ops = { + .cls = "Sign", + .retain = sp_signer_retain, + .release = sp_signer_release, + .invoke = sp_sign_invoke, + .get = sp_signer_get, + .set = sp_signer_set, + .inspect_view = sp_signer_inspect_view, +}; + +static const ScrDynHandleOps sp_verify_ops = { + .cls = "Verify", + .retain = sp_signer_retain, + .release = sp_signer_release, + .invoke = sp_verify_invoke, + .get = sp_signer_get, + .set = sp_signer_set, + .inspect_view = sp_signer_inspect_view, +}; + +/* createSign(algorithm) / createVerify(algorithm). */ +static ScrDyn *sp_signer_new(ScrDyn *algorithm, bool verifier) { + if (algorithm->kind != SCR_DYN_STR) { + scr_dyn_arg_type_fail("algorithm", "of type string", algorithm); + return NULL; + } + mbedtls_md_type_t md; + if (!sp_digest_of(algorithm->v.str, &md)) { + sp_throw(SCR_ERR_TYPE, "ERR_CRYPTO_INVALID_DIGEST", "Invalid digest"); + return NULL; + } + static SCR_TL bool installed; + if (!installed) { + installed = true; + scr_dyn_handle_install(SCR_DYNH_SIGN, &sp_sign_ops); + scr_dyn_handle_install(SCR_DYNH_VERIFY, &sp_verify_ops); + } + SpSigner *s = calloc(1, sizeof *s); + if (!s) scr_trap("scriptc: out of memory\n"); + s->rc = 1; + s->type = md; + mbedtls_md_init(&s->md); + mbedtls_md_setup(&s->md, mbedtls_md_info_from_type(md), 0); + mbedtls_md_starts(&s->md); + ScrDyn *box = scr_dyn_new_handle(s, verifier ? SCR_DYNH_VERIFY : SCR_DYNH_SIGN); + sp_signer_release(s); + return box; +} + +ScrDyn *scr_crypto_create_sign(ScrDyn *algorithm) { return sp_signer_new(algorithm, false); } + +ScrDyn *scr_crypto_create_verify(ScrDyn *algorithm) { return sp_signer_new(algorithm, true); } + +/* ── generateKeyPair ──────────────────────────────────────────────────── */ + +static ScrDyn *sp_cap(ScrClosure *c, size_t i) { return scr_box_get_ref(c->caps[i]); } + +static void sp_key_pair_deliver(ScrClosure *self) { + ScrDyn *callback = sp_cap(self, 0); + ScrDyn *pair = sp_cap(self, 1); + ScrDyn *args[3] = {scr_dyn_new_null(), scr_dyn_retain(scr_dyn_obj_get(pair, "publicKey", 9)), + scr_dyn_retain(scr_dyn_obj_get(pair, "privateKey", 10))}; + ScrDyn *result = scr_dyn_call(callback, args, 3, "callback"); + if (result) scr_dyn_release(result); + for (size_t i = 0; i < 3; i++) scr_dyn_release(args[i]); + scr_dyn_release(pair); + scr_dyn_release(callback); +} + +/* generateKeyPair(type, options, callback): argument errors throw, and the + * callback receives (null, publicKey, privateKey) in a later macrotask. */ +void scr_crypto_generate_key_pair_async(ScrDyn *type, ScrDyn *options, ScrDyn *callback) { + ScrDyn *pair = scr_crypto_generate_key_pair_sync(type, options); + if (!pair) return; + ScrClosure *c = scr_closure_new((void *)sp_key_pair_deliver, 2); + c->caps[0] = scr_box_new_obj(scr_dyn_retain_v, scr_dyn_release_v, NULL); + scr_box_set_ref(c->caps[0], scr_dyn_retain(callback)); + c->caps[1] = scr_box_new_obj(scr_dyn_retain_v, scr_dyn_release_v, NULL); + scr_box_set_ref(c->caps[1], pair); + scr_loop_defer(c); +} + +/* ── the core for the WebCrypto layer (scr_pki.h) ─────────────────────── */ + +ScrPkiKey *scr_pki_key_retain(ScrPkiKey *key) { return sp_key_retain(key); } + +void scr_pki_key_release(ScrPkiKey *key) { sp_key_release(key); } + +bool scr_pki_key_is_private(const ScrPkiKey *key) { return key->private_key; } + +bool scr_pki_key_is_rsa(const ScrPkiKey *key) { return sp_is_rsa(key); } + +bool scr_pki_key_is_ed25519(const ScrPkiKey *key) { return sp_is_ed(key); } + +const char *scr_pki_key_curve(const ScrPkiKey *key) { + const SpCurve *curve = sp_curve_of(key); + return curve ? curve->jwk : NULL; +} + +size_t scr_pki_key_bits(const ScrPkiKey *key) { return mbedtls_pk_get_bitlen(&key->pk); } + +ScrBytes *scr_pki_key_exponent(const ScrPkiKey *key) { + mbedtls_mpi e; + mbedtls_mpi_init(&e); + mbedtls_rsa_export(mbedtls_pk_rsa(key->pk), NULL, NULL, NULL, NULL, &e); + size_t n = mbedtls_mpi_size(&e); + ScrBytes *out = scr_bytes_new(SCR_BYTES_U8, (double)n); + if (n) mbedtls_mpi_write_binary(&e, out->data, n); + mbedtls_mpi_free(&e); + return out; +} + +ScrPkiKey *scr_pki_key_parse_der(const uint8_t *der, size_t len, bool private_key) { + if ((private_key && len == 48 && memcmp(der, sp_ed_pkcs8_prefix, 16) == 0) || + (!private_key && len == 44 && memcmp(der, sp_ed_spki_prefix, 12) == 0)) + { + bool failed; + return sp_ed_from_der(der, len, private_key, &failed); + } + SpKey *k = sp_key_alloc(private_key); + int r = private_key ? mbedtls_pk_parse_key(&k->pk, der, len, NULL, 0, sp_random, NULL) + : mbedtls_pk_parse_public_key(&k->pk, der, len); + if (r != 0 || !sp_key_supported(k)) { + sp_key_release(k); + return NULL; + } + return k; +} + +ScrPkiKey *scr_pki_key_from_jwk(const ScrDyn *jwk, bool private_key) { + SpKey *k = sp_key_from_jwk(jwk, private_key); + if (!k) scr_exc_clear(); + return k; +} + +ScrPkiKey *scr_pki_key_from_point(const char *curve_name, const uint8_t *point, size_t len) { + const SpCurve *curve = NULL; + for (size_t i = 0; i < sizeof sp_curves / sizeof sp_curves[0]; i++) + if (strcmp(sp_curves[i].jwk, curve_name) == 0) curve = &sp_curves[i]; + if (!curve) return NULL; + SpKey *k = sp_key_alloc(false); + bool ok = false; + if (mbedtls_pk_setup(&k->pk, mbedtls_pk_info_from_type(MBEDTLS_PK_ECKEY)) == 0) { + mbedtls_ecp_keypair *ec = mbedtls_pk_ec(k->pk); + ok = mbedtls_ecp_group_load(&ec->MBEDTLS_PRIVATE(grp), curve->id) == 0 && + mbedtls_ecp_point_read_binary(&ec->MBEDTLS_PRIVATE(grp), &ec->MBEDTLS_PRIVATE(Q), point, len) == 0 && + mbedtls_ecp_check_pubkey(&ec->MBEDTLS_PRIVATE(grp), &ec->MBEDTLS_PRIVATE(Q)) == 0; + } + if (!ok) { + sp_key_release(k); + return NULL; + } + return k; +} + +ScrPkiKey *scr_pki_key_public_of(ScrPkiKey *key) { return sp_public_of(key); } + +ScrPkiKey *scr_pki_generate_rsa(unsigned bits, uint64_t exponent) { + SpKey *k = sp_key_alloc(true); + if (exponent > 2147483647 || mbedtls_pk_setup(&k->pk, mbedtls_pk_info_from_type(MBEDTLS_PK_RSA)) != 0 || + mbedtls_rsa_gen_key(mbedtls_pk_rsa(k->pk), sp_random, NULL, bits, (int)exponent) != 0) { + sp_key_release(k); + return NULL; + } + return k; +} + +ScrPkiKey *scr_pki_generate_ec(const char *curve_name) { + const SpCurve *curve = NULL; + for (size_t i = 0; i < sizeof sp_curves / sizeof sp_curves[0]; i++) + if (strcmp(sp_curves[i].jwk, curve_name) == 0) curve = &sp_curves[i]; + SpKey *k = sp_key_alloc(true); + if (!curve || mbedtls_pk_setup(&k->pk, mbedtls_pk_info_from_type(MBEDTLS_PK_ECKEY)) != 0 || + mbedtls_ecp_gen_key(curve->id, mbedtls_pk_ec(k->pk), sp_random, NULL) != 0) { + sp_key_release(k); + return NULL; + } + return k; +} + +ScrPkiKey *scr_pki_generate_ed25519(void) { + const ScrPkiEd25519 *ed = sp_ed(); + if (!ed) return NULL; + SpKey *k = sp_key_alloc(true); + k->ed25519 = true; + sp_random(NULL, k->ed_seed, 32); + ed->public_key(k->ed_pub, k->ed_seed); + return k; +} + +ScrPkiKey *scr_pki_key_from_ed25519_raw(const uint8_t *raw, size_t len) { + if (len != 32) return NULL; + SpKey *k = sp_key_alloc(false); + k->ed25519 = true; + memcpy(k->ed_pub, raw, 32); + return k; +} + +ScrBytes *scr_pki_export_ed25519_raw(ScrPkiKey *key) { + ScrBytes *out = scr_bytes_new(SCR_BYTES_U8, 32); + memcpy(out->data, key->ed_pub, 32); + return out; +} + +ScrBytes *scr_pki_export_der(ScrPkiKey *key) { + unsigned char *der = malloc(SP_DER_MAX); + if (!der) scr_trap("scriptc: out of memory\n"); + int n = key->private_key ? sp_write_pkcs8_der(key, der, SP_DER_MAX) : sp_write_spki_der(key, der, SP_DER_MAX); + ScrBytes *out = NULL; + if (n > 0) { + out = scr_bytes_new(SCR_BYTES_U8, (double)n); + memcpy(out->data, der + SP_DER_MAX - n, (size_t)n); + } + mbedtls_platform_zeroize(der, SP_DER_MAX); + free(der); + return out; +} + +ScrBytes *scr_pki_export_point(ScrPkiKey *key) { + const mbedtls_ecp_keypair *ec = mbedtls_pk_ec(key->pk); + unsigned char point[160]; + size_t n; + if (mbedtls_ecp_point_write_binary(&ec->MBEDTLS_PRIVATE(grp), &ec->MBEDTLS_PRIVATE(Q), + MBEDTLS_ECP_PF_UNCOMPRESSED, &n, point, sizeof point) != 0) + return NULL; + ScrBytes *out = scr_bytes_new(SCR_BYTES_U8, (double)n); + memcpy(out->data, point, n); + return out; +} + +ScrDyn *scr_pki_export_jwk(ScrPkiKey *key) { return sp_export_jwk(key); } + +static bool sp_webcrypto_md(const char *hash, mbedtls_md_type_t *md) { + if (strcmp(hash, "SHA-1") == 0) *md = MBEDTLS_MD_SHA1; + else if (strcmp(hash, "SHA-256") == 0) *md = MBEDTLS_MD_SHA256; + else if (strcmp(hash, "SHA-384") == 0) *md = MBEDTLS_MD_SHA384; + else if (strcmp(hash, "SHA-512") == 0) *md = MBEDTLS_MD_SHA512; + else return false; + return true; +} + +ScrBytes *scr_pki_sign(ScrPkiKey *key, const char *hash, bool pss, int salt, const uint8_t *data, + size_t len) { + if (sp_is_ed(key)) { + const ScrPkiEd25519 *ed = sp_ed(); + if (!ed) return NULL; + ScrBytes *out = scr_bytes_new(SCR_BYTES_U8, 64); + ed->sign(out->data, key->ed_seed, key->ed_pub, data, len); + return out; + } + mbedtls_md_type_t md; + if (!sp_webcrypto_md(hash, &md)) return NULL; + const mbedtls_md_info_t *info = mbedtls_md_info_from_type(md); + unsigned char digest[MBEDTLS_MD_MAX_SIZE]; + mbedtls_md(info, data, len, digest); + SpSigKey k = {.key = key, .padding = pss ? 6 : 1, .salt = salt, .p1363 = true}; + ScrDyn *sig = sp_sign_hash(&k, md, digest, mbedtls_md_get_size(info)); + if (!sig) { + scr_exc_clear(); + return NULL; + } + ScrBytes *out = scr_bytes_buffer_source(sig); + scr_dyn_release(sig); + return out; +} + +bool scr_pki_verify(ScrPkiKey *key, const char *hash, bool pss, int salt, const uint8_t *data, + size_t len, const uint8_t *signature, size_t signature_len) { + if (sp_is_ed(key)) { + const ScrPkiEd25519 *ed = sp_ed(); + return ed && sp_ed_verify(ed, key->ed_pub, data, len, signature, signature_len); + } + mbedtls_md_type_t md; + if (!sp_webcrypto_md(hash, &md)) return false; + const mbedtls_md_info_t *info = mbedtls_md_info_from_type(md); + unsigned char digest[MBEDTLS_MD_MAX_SIZE]; + mbedtls_md(info, data, len, digest); + SpSigKey k = {.key = key, .padding = pss ? 6 : 1, .salt = salt, .p1363 = true}; + return sp_verify_hash(&k, md, digest, mbedtls_md_get_size(info), signature, signature_len); +} diff --git a/packages/runtime/src/scr_pki.h b/packages/runtime/src/scr_pki.h new file mode 100644 index 000000000..71be3c1df --- /dev/null +++ b/packages/runtime/src/scr_pki.h @@ -0,0 +1,79 @@ +/* The key and signature core of scr_pki.c, for the WebCrypto layer + * (scr_webcrypto_pki.c) and the Ed25519 unit (scr_ed25519.c). Keys are + * reference-counted RSA keys, EC keys on P-256, P-384, P-521 or secp256k1, + * or Ed25519 keys. Functions answering NULL or false set no error unless + * they say so; byte results are +1. */ +#ifndef SCR_PKI_H +#define SCR_PKI_H + +#include +#include +#include + +typedef struct SpKey ScrPkiKey; + +/* Throws an error of `kind` (with `code`, or none for NULL) whose message + * is the printf format's whole output, at any length. */ +void scr_pki_throw_fmt(int kind, const char *code, const char *fmt, ...) __attribute__((format(printf, 3, 4))); + +/* The Ed25519 arithmetic (scr_ed25519.c), installed at startup in + * programs that name Ed25519; NULL otherwise. Keys are the 32-byte seed + * and the 32-byte encoded public point. */ +typedef struct { + void (*public_key)(uint8_t pub[32], const uint8_t seed[32]); + void (*sign)(uint8_t sig[64], const uint8_t seed[32], const uint8_t pub[32], const uint8_t *m, size_t n); + bool (*verify)(const uint8_t pub[32], const uint8_t *m, size_t n, const uint8_t sig[64]); +} ScrPkiEd25519; +extern const ScrPkiEd25519 *scr_pki_ed25519; + +/* Whether the Ed25519 arithmetic is linked; false with a coded SC2020 + * error pending otherwise. */ +bool scr_pki_ed25519_ready(void); + +ScrPkiKey *scr_pki_key_retain(ScrPkiKey *key); +void scr_pki_key_release(ScrPkiKey *key); +bool scr_pki_key_is_private(const ScrPkiKey *key); +bool scr_pki_key_is_rsa(const ScrPkiKey *key); +bool scr_pki_key_is_ed25519(const ScrPkiKey *key); +/* The EC key's JWK curve name ("P-256", ...), or NULL for RSA keys. */ +const char *scr_pki_key_curve(const ScrPkiKey *key); +/* The RSA modulus length in bits. */ +size_t scr_pki_key_bits(const ScrPkiKey *key); +/* The RSA public exponent's big-endian bytes, without leading zeros. */ +ScrBytes *scr_pki_key_exponent(const ScrPkiKey *key); + +/* A key from SubjectPublicKeyInfo (public) or PKCS#8 (private) DER. */ +ScrPkiKey *scr_pki_key_parse_der(const uint8_t *der, size_t len, bool private_key); +/* A key from a JWK object (borrowed; a plain object). */ +ScrPkiKey *scr_pki_key_from_jwk(const ScrDyn *jwk, bool private_key); +/* A public EC key from an uncompressed or compressed point. */ +ScrPkiKey *scr_pki_key_from_point(const char *curve, const uint8_t *point, size_t len); +ScrPkiKey *scr_pki_key_public_of(ScrPkiKey *key); +ScrPkiKey *scr_pki_generate_rsa(unsigned bits, uint64_t exponent); +ScrPkiKey *scr_pki_generate_ec(const char *curve); +/* NULL with a coded SC2020 error pending when the program does not link + * the Ed25519 unit. */ +ScrPkiKey *scr_pki_generate_ed25519(void); +/* A public Ed25519 key from its 32-byte encoding. */ +ScrPkiKey *scr_pki_key_from_ed25519_raw(const uint8_t *raw, size_t len); +/* The Ed25519 public key's 32-byte encoding. */ +ScrBytes *scr_pki_export_ed25519_raw(ScrPkiKey *key); + +/* SubjectPublicKeyInfo (public keys) or PKCS#8 (private keys) DER. */ +ScrBytes *scr_pki_export_der(ScrPkiKey *key); +/* The EC public key's uncompressed point. */ +ScrBytes *scr_pki_export_point(ScrPkiKey *key); +/* The JWK members in Node's order (kty, n, e, ... or kty, x, y, crv, d), + * +1. */ +ScrDyn *scr_pki_export_jwk(ScrPkiKey *key); + +/* Signs `data` with the digest named by its WebCrypto name ("SHA-256"): + * RSA PKCS#1 v1.5 (pss false), RSA-PSS with `salt` bytes, or ECDSA as + * IEEE P1363 r || s. Ed25519 keys sign the message itself (hash + * ignored). */ +ScrBytes *scr_pki_sign(ScrPkiKey *key, const char *hash, bool pss, int salt, const uint8_t *data, + size_t len); +bool scr_pki_verify(ScrPkiKey *key, const char *hash, bool pss, int salt, const uint8_t *data, + size_t len, const uint8_t *signature, size_t signature_len); + +#endif diff --git a/packages/runtime/src/scr_publish.c b/packages/runtime/src/scr_publish.c index 675a13ca9..44f248b5b 100644 --- a/packages/runtime/src/scr_publish.c +++ b/packages/runtime/src/scr_publish.c @@ -199,6 +199,19 @@ static void scr_pub_slot(ScrPub *ctx, uint64_t slot, ScrPubFn fn) { if (fn) scr_pub_push(ctx, p, fn); } +/* The elements of a compact reference array (SCR_ELEM_CREF): 4-byte dense + * slots, 8-byte pointers in the sparse entries. */ +static __attribute__((noinline)) void scr_pub_cref_elems(ScrArr *a, ScrPub *ctx, ScrPubFn fn) { + size_t dense = a->len < a->cap ? a->len : a->cap; + const uint32_t *refs = (const uint32_t *)a->data; + const uint8_t *states = scr_arr_states_end_w(a, sizeof(uint32_t)); + for (size_t i = 0; i < dense; i++) + if (!states || *scr_arr_state_in(states, i)) scr_pub_slot(ctx, scr_cref_decode(refs[i]), fn); + if (scr_arr_ext_of(a)) + for (size_t i = 0; i < scr_arr_ext_of(a)->sparse_len; i++) + if (scr_arr_ext_of(a)->sparse[i].state) scr_pub_slot(ctx, scr_arr_ext_of(a)->sparse[i].slot, fn); +} + void scr_pub_arr(void *v, ScrPub *ctx, ScrPubFn elem) { ScrArr *a = v; if (scr_arr_ext_of(a) && (scr_arr_ext_of(a)->props || scr_arr_ext_of(a)->metadata)) { @@ -206,10 +219,15 @@ void scr_pub_arr(void *v, ScrPub *ctx, ScrPubFn elem) { return; } if (!scr_pub_mark(ctx, a, a->cyc != 0, SCR_PUB_KIND_ARRAY)) return; + if (__builtin_expect(a->elem == SCR_ELEM_CREF, 0)) { + scr_pub_cref_elems(a, ctx, elem); + return; + } ScrPubFn fn = a->elem == SCR_ELEM_STR ? scr_pub_str_fn : a->elem == SCR_ELEM_REF ? elem : NULL; if (!fn) return; + /* String and reference elements have 8-byte slots. */ size_t dense = a->len < a->cap ? a->len : a->cap; - const uint8_t *states = scr_arr_states_end(a); + const uint8_t *states = scr_arr_states_end_w(a, sizeof(uint64_t)); for (size_t i = 0; i < dense; i++) if (!states || *scr_arr_state_in(states, i)) scr_pub_slot(ctx, a->data[i], fn); if (scr_arr_ext_of(a)) @@ -231,12 +249,12 @@ void scr_pub_map(void *v, ScrPub *ctx, ScrPubFn key, ScrPubFn val) { m->key_kind == SCR_MAP_KEY_BIGINT || m->key_kind == SCR_MAP_KEY_UNION_VALUE ? key : NULL; - ScrPubFn vfn = m->val_kind == SCR_MAP_VAL_REF ? val : NULL; + ScrPubFn vfn = m->val_kind == SCR_MAP_VAL_REF || m->val_kind == SCR_MAP_VAL_CREF ? val : NULL; for (size_t i = 0; i < m->nentries; i++) { if (!scr_map_entry_live(m, i)) continue; /* tombstone */ const ScrMapEntry *e = scr_map_entry_at(m, i); if (kfn) scr_pub_slot(ctx, e->key, kfn); - if (vfn) scr_pub_slot(ctx, e->val, vfn); + if (vfn) scr_pub_slot(ctx, scr_map_entry_val(m, e), vfn); } } diff --git a/packages/runtime/src/scr_stream.c b/packages/runtime/src/scr_stream.c index 271937f2e..be5ccb910 100644 --- a/packages/runtime/src/scr_stream.c +++ b/packages/runtime/src/scr_stream.c @@ -80,8 +80,11 @@ static ScrBytes *scr_stream_buffer(ScrBytes *b) { /* ── state ────────────────────────────────────────────────────────────── */ typedef struct ScrWEntry { - ScrBytes *chunk; /* owned */ + ScrBytes *chunk; /* owned; a ScrDyn* value on an object-mode writable side */ ScrClosure *cb; /* owned or NULL (the user's write(chunk, cb) callback) */ + ScrDyn *enc; /* owned or NULL = undefined: the encoding argument an + * object-mode write passed (Node hands it to _write + * unchanged; null when the callback took its place) */ } ScrWEntry; struct ScrStreamState { @@ -132,6 +135,14 @@ struct ScrStreamState { bool object_entries; /* An operator output's entry kind is decided by its first push. */ bool object_kind_set; + /* An object-mode buffer that holds a value other than a string or a + * Buffer, or both kinds: every entry is an owned ScrDyn* + * (scr_stream_add_value). Takes precedence over `encoded`. */ + bool dyn_entries; + /* The objectMode or readableObjectMode option made the buffer hold + * object entries (a stream without _read is not a seeded + * Readable.from source). */ + bool object_mode_opt; /* The parked for-await next() promise (at most one — the loop awaits * each chunk before asking again). next_dyn picks the resolution * shape: a dyn boxed by tag (the JS lane), or bytes. */ @@ -146,6 +157,11 @@ struct ScrStreamState { * nextTick and then the resumed `await new Promise(next)` * (next_wake: that tick is queued). */ bool next_parked, next_wake; + /* Node's iterator ends through eos, which waits for 'close' when the + * stream will emit it (willEmitClose, fixed when the iterator starts: + * a readable-only stream with autoDestroy and emitClose, not yet + * closed). next_eof_close: the ended waiter is held for 'close'. */ + bool next_started, next_close_wait, next_eof_close; ScrClosure *read_cb; ScrStreamReadInv read_inv; } r; @@ -157,9 +173,23 @@ struct ScrStreamState { size_t n, cap, head; int corked; bool writing, wsync, need_drain, after_scheduled; + /* Deferred afterWrites Node also defers (a user callback, a 'drain' + * or a destroyed stream): its pendingcb, which holds the finish until + * they ran — afterWrite's finishMaybe then puts 'finish' on a tick + * of its own. */ + size_t after_holds; + /* clearBuffer is running its loop: a write completing inside it + * leaves the next buffered write to the loop (Node's + * kBufferProcessing), so completions keep their order */ + bool buffer_processing; + /* objectMode writable side: queued chunks, and the chunk the write + * and transform steps receive, are ScrDyn* values (in the ScrBytes* + * slots), each counting 1 toward length. */ + bool object_mode; bool ending, finished, prefinished, final_called, finish_scheduled; bool final_pending; /* _final called, its callback not yet run */ ScrClosure *inflight_cb; /* owned or NULL */ + ScrDyn *cur_enc; /* borrowed: the running write's encoding argument */ ScrClosure *write_cb; ScrStreamChunkInv write_inv; ScrClosure *final_cb; @@ -267,31 +297,79 @@ static bool scr_stream_is(const ScrEmitter *em) { scr_vt_within(vt, &scr_passthrough_vt); } -/* Readable-buffer entry accessors: bytes chunks, or strings once an +/* Readable-buffer entry accessors: bytes chunks, strings once an * encoding is set (lengths then count JS string units, Node's encoded - * accounting). */ + * accounting), or checked-dynamic values in an object-mode buffer that + * holds other values. */ +typedef enum { SCR_ENTRY_BYTES, SCR_ENTRY_STR, SCR_ENTRY_DYN } ScrEntryKind; + +static ScrEntryKind scr_stream_rkind(const ScrStreamState *st) { + return st->r.dyn_entries ? SCR_ENTRY_DYN : st->r.encoded ? SCR_ENTRY_STR : SCR_ENTRY_BYTES; +} + static size_t scr_stream_entry_len(const ScrStreamState *st, void *e) { if (st->r.object_entries) return 1; /* objectMode-style accounting */ return st->r.encoded ? (size_t)scr_str_utf16_len((ScrStr *)e) : ((ScrBytes *)e)->len; } -static void scr_stream_entry_release(const ScrStreamState *st, void *e) { - if (st->r.encoded) scr_str_release((ScrStr *)e); +/* Releases an entry by the kind it had when it left the buffer: a 'data' + * listener may call setEncoding() or push another kind of value while the + * entry is out. */ +static void scr_stream_entry_release_kind(void *e, ScrEntryKind kind) { + if (kind == SCR_ENTRY_DYN) scr_dyn_release((ScrDyn *)e); + else if (kind == SCR_ENTRY_STR) scr_str_release((ScrStr *)e); else scr_bytes_release((ScrBytes *)e); } -/* Releases an entry by the kind it had when it left the buffer: a 'data' - * listener may call setEncoding() while the entry is out. */ -static void scr_stream_entry_release_kind(void *e, bool is_str) { - if (is_str) scr_str_release((ScrStr *)e); - else scr_bytes_release((ScrBytes *)e); +static void scr_stream_entry_release(const ScrStreamState *st, void *e) { + scr_stream_entry_release_kind(e, scr_stream_rkind(st)); +} + +/* An entry of the given kind as a dyn value (+1; moves entry). */ +static ScrDyn *scr_stream_entry_box(void *entry, ScrEntryKind kind) { + if (kind == SCR_ENTRY_DYN) return entry; + ScrDyn *d; + if (kind == SCR_ENTRY_STR) { + d = scr_dyn_new_str((ScrStr *)entry); + scr_str_release((ScrStr *)entry); + } else { + ScrBytes *b = entry; + d = b->is_buffer ? scr_dyn_new_buffer(b) : scr_dyn_new_bytes(b); + scr_bytes_release(b); + } + return d; +} + +/* Writable-queue chunks: Buffers, or dyn values on an object-mode side. */ +static size_t scr_stream_wchunk_len(const ScrStreamState *st, ScrBytes *chunk) { + return st->w.object_mode ? 1 : chunk->len; +} + +static ScrBytes *scr_stream_wchunk_retain(const ScrStreamState *st, ScrBytes *chunk) { + if (st->w.object_mode) return (ScrBytes *)scr_dyn_retain((ScrDyn *)chunk); + return scr_bytes_retain(chunk); +} + +static void scr_stream_wchunk_release(const ScrStreamState *st, ScrBytes *chunk) { + if (st->w.object_mode) scr_dyn_release((ScrDyn *)chunk); + else scr_bytes_release(chunk); +} + +/* A dyn entry the collector traces, and so frees itself in a collection + * (the kinds that carry a collector header). */ +static bool scr_stream_dyn_entry_traced(const ScrDyn *d) { + return d->kind > SCR_DYN_STR && d->kind != SCR_DYN_UNDEF; } static void scr_stream_state_drop(ScrStreamState *st, bool gc) { if (!st) return; if (st->errored) scr_error_release(st->errored); if (st->native) st->native_release(st->native); - for (size_t i = 0; i < st->r.n; i++) scr_stream_entry_release(st, st->r.buf[i]); + for (size_t i = 0; i < st->r.n; i++) { + /* The collector frees the dyn entries it traced. */ + if (gc && st->r.dyn_entries && scr_stream_dyn_entry_traced(st->r.buf[i])) continue; + scr_stream_entry_release(st, st->r.buf[i]); + } free(st->r.buf); if (st->r.enc) scr_str_release(st->r.enc); if (st->r.push_enc) scr_str_release(st->r.push_enc); @@ -312,8 +390,11 @@ static void scr_stream_state_drop(ScrStreamState *st, bool gc) { for (size_t i = 0; i < st->drain_srcs.n; i++) scr_stream_release(st->drain_srcs.src[i]); } for (size_t i = st->w.head; i < st->w.n; i++) { - scr_bytes_release(st->w.q[i].chunk); + bool traced = st->w.object_mode && scr_stream_dyn_entry_traced((ScrDyn *)st->w.q[i].chunk); + if (!gc || !traced) scr_stream_wchunk_release(st, st->w.q[i].chunk); if (!gc && st->w.q[i].cb) scr_closure_release(st->w.q[i].cb); + ScrDyn *enc = st->w.q[i].enc; + if (enc && (!gc || !scr_stream_dyn_entry_traced(enc))) scr_dyn_release(enc); } free(st->w.q); free(st->w.end_cbs); @@ -335,6 +416,11 @@ void scr_stream_st_gcfree(ScrStreamState *st) { scr_stream_state_drop(st, true); void scr_stream_st_trace(ScrStreamState *st, ScrTraceVisit visit, void *ctx) { if (!st) return; + if (st->r.dyn_entries) { + for (size_t i = 0; i < st->r.n; i++) { + if (scr_stream_dyn_entry_traced(st->r.buf[i])) visit(st->r.buf[i], ctx); + } + } if (st->r.next_waiter) visit(st->r.next_waiter, ctx); if (st->r.read_cb) visit(st->r.read_cb, ctx); if (st->construct_cb) visit(st->construct_cb, ctx); @@ -348,6 +434,9 @@ void scr_stream_st_trace(ScrStreamState *st, ScrTraceVisit visit, void *ctx) { for (size_t i = 0; i < st->fin.n; i++) visit(st->fin.cb[i], ctx); for (size_t i = st->w.head; i < st->w.n; i++) { if (st->w.q[i].cb) visit(st->w.q[i].cb, ctx); + if (st->w.object_mode && scr_stream_dyn_entry_traced((ScrDyn *)st->w.q[i].chunk)) + visit(st->w.q[i].chunk, ctx); + if (st->w.q[i].enc && scr_stream_dyn_entry_traced(st->w.q[i].enc)) visit(st->w.q[i].enc, ctx); } for (size_t i = 0; i < st->pipes.n; i++) visit(st->pipes.dst[i], ctx); for (size_t i = 0; i < st->drain_srcs.n; i++) visit(st->drain_srcs.src[i], ctx); @@ -425,6 +514,7 @@ typedef struct ScrStreamTick { ScrStreamTickOp op; ScrError *err; /* owned or NULL (SCR_ST_ERROR) */ ScrClosure *cb; /* owned or NULL (SCR_ST_AFTER_WRITE / SCR_ST_WCB_ERR / SCR_ST_WRITE_ERR) */ + bool hold; /* SCR_ST_AFTER_WRITE: counted in w.after_holds */ struct ScrStreamTick *next; } ScrStreamTick; @@ -486,10 +576,20 @@ static ScrError *scr_stream_mkerr(const char *code, const char *msg) { return e; } +/* The pending exception as a stream error (+1): Error values as + * themselves, any other thrown value as an internal assertion. */ +static ScrError *scr_stream_take_error(void) { + ScrCaught *caught = scr_exc_take(); + ScrDyn *value = scr_caught_to_dyn(caught); + scr_caught_release(caught); + ScrError *err = value ? scr_errdyn_err_of(value) : NULL; + if (value) scr_dyn_release(value); + return err ? err : scr_stream_mkerr("ERR_INTERNAL_ASSERTION", "a stream operation threw a value that is not an Error"); +} + /* ── forward decls ────────────────────────────────────────────────────── */ static void scr_stream_flow(ScrStream *s); -static int scr_stream_add_entry(ScrStream *s, void *entry /*moves*/, bool is_str); static void scr_stream_readable_listen(ScrStream *s); static void scr_stream_emit_readable_now(ScrStream *s); static void scr_stream_maybe_read_more(ScrStream *s); @@ -498,7 +598,7 @@ static void scr_stream_clear_buffer(ScrStream *s); static void scr_stream_do_destroy(ScrStream *s, ScrError *err /*borrowed*/); static void scr_stream_notify_finished(ScrStream *s); static void *scr_stream_read_n(ScrStream *s, double size); -static void *scr_stream_read_n_kind(ScrStream *s, double size, bool *is_str); +static void *scr_stream_read_n_kind(ScrStream *s, double size, ScrEntryKind *kind); static void scr_stream_settle_next(ScrStream *s); static ScrStream *scr_stream_alloc(const ScrVt *vt, const char *cls, bool has_r, bool has_w, double rhwm, double whwm, bool auto_destroy, @@ -523,10 +623,10 @@ static void scr_stream_error_or_destroy(ScrStream *s, ScrError *err) { /* ── readable internals ───────────────────────────────────────────────── */ -static void scr_stream_rbuf_push(ScrStreamState *st, void *chunk /*moves*/, bool is_str, +static void scr_stream_rbuf_push(ScrStreamState *st, void *chunk /*moves*/, ScrEntryKind kind, bool front) { - if (is_str != st->r.encoded) { - /* every entry has the buffer's representation (encoded: ScrStr) */ + if (kind != scr_stream_rkind(st)) { + /* every entry has the buffer's representation (scr_stream_rkind) */ fputs("scriptc: internal error: stream buffer entry of the wrong kind\n", stderr); abort(); } @@ -549,7 +649,8 @@ static void scr_stream_rbuf_push(ScrStreamState *st, void *chunk /*moves*/, bool } /* Takes n units (n <= length; bytes, or chars once encoded) off the - * buffer head as one +1 Buffer / string. */ + * buffer head as one +1 Buffer / string. An object-mode buffer always + * takes one whole entry (n is 1). */ static void *scr_stream_rbuf_take(ScrStreamState *st, size_t n) { void *first = st->r.n > 0 ? st->r.buf[0] : NULL; if (first && st->r.head_off == 0 && scr_stream_entry_len(st, first) == n) { @@ -610,17 +711,29 @@ static void *scr_stream_rbuf_take(ScrStreamState *st, size_t n) { return out; } -/* emit('data', chunk) + pipe delivery. Borrows chunk (a Buffer, or a - * string once encoded). The emit ABI carries BOTH payload slots — the - * bytes chunk and the string chunk, exactly one non-NULL — and the - * compiler's stream-data listener thunks dispatch on which (typed - * listeners unwrap their declared side; dyn listeners box by tag). */ -static void scr_stream_emit_data(ScrStream *s, void *chunk) { +/* emit('data', chunk) + pipe delivery. Borrows chunk (a Buffer, a string + * once encoded, or a dyn value, by kind). The emit ABI carries three + * payload slots: the bytes chunk, the string chunk, and the value of a + * dyn entry. A dyn entry that is a Buffer or a string also fills that + * slot, so at most one of the first two is non-NULL; the compiler's + * stream-data listener thunks dispatch on them (typed listeners unwrap + * their declared side; dyn listeners box by tag or take the value). */ +static void scr_stream_emit_data(ScrStream *s, void *chunk, ScrEntryKind kind) { ScrStreamState *st = s->st; - ScrBytes *b = st->r.encoded ? NULL : (ScrBytes *)chunk; - ScrStr *str = st->r.encoded ? (ScrStr *)chunk : NULL; + ScrBytes *b = NULL; + ScrStr *str = NULL; + ScrDyn *value = NULL; + if (kind == SCR_ENTRY_DYN) { + value = chunk; + if (value->kind == SCR_DYN_BYTES) b = value->v.bytes; + else if (value->kind == SCR_DYN_STR) str = value->v.str; + } else if (kind == SCR_ENTRY_STR) { + str = chunk; + } else { + b = chunk; + } ScrStr *n = scr_str_new("data", 4); - scr_emitter_emit((ScrEmitter *)s, n, b, str); + scr_emitter_emit((ScrEmitter *)s, n, b, str, value); scr_str_release(n); if (scr_exc_pending()) return; /* pipe delivery, with backpressure: a full destination pauses us until @@ -637,7 +750,9 @@ static void scr_stream_emit_data(ScrStream *s, void *chunk) { if (scr_exc_pending()) return; continue; /* the pipes array shifted down over i */ } - bool ok = b ? scr_stream_write(dst, b, NULL) : scr_stream_write_str(dst, str, NULL); + bool ok = value ? scr_stream_write_dyn(dst, value, NULL) + : b ? scr_stream_write(dst, b, NULL) + : scr_stream_write_str(dst, str, NULL); if (scr_exc_pending()) return; i++; if (!ok && !dst->st->destroyed) { @@ -681,7 +796,7 @@ static void scr_stream_end_readable(ScrStream *s) { * ERR_METHOD_NOT_IMPLEMENTED, Node's contract. */ static void scr_stream_call_read(ScrStream *s) { ScrStreamState *st = s->st; - if (st->r.read_cb == NULL && st->r.object_entries) { + if (st->r.read_cb == NULL && st->r.object_entries && !st->r.object_mode_opt) { /* Readable.from: fully seeded at construction — _read is a no-op */ st->r.in_read_sync = false; return; @@ -716,8 +831,21 @@ static void scr_stream_call_read(ScrStream *s) { * StringDecoder first (Node decodes at push); an empty decode (a partial * multi-byte tail held pending) adds nothing but still clears the * reading flag. */ +static int scr_stream_add_entry(ScrStream *s, void *entry /*moves*/, ScrEntryKind kind); +static void *scr_stream_entry_convert(ScrStreamState *st, void *entry, ScrEntryKind *kind); + static bool scr_stream_add_chunk(ScrStream *s, ScrBytes *chunk, bool front) { ScrStreamState *st = s->st; + if (st->has_r && st->r.object_entries) { + /* object mode: the Buffer is one whole entry */ + if (!front) return scr_stream_add_entry(s, scr_bytes_retain(chunk), SCR_ENTRY_BYTES) > 0; + ScrEntryKind kind = SCR_ENTRY_BYTES; + void *entry = scr_stream_entry_convert(st, scr_bytes_retain(chunk), &kind); + scr_stream_rbuf_push(st, entry, kind, true); + if (st->r.need_readable) scr_stream_emit_readable_nt(s); + scr_stream_settle_next(s); + return !st->r.ended && (st->r.length < st->r.hwm || st->r.length == 0); + } if (!st->has_r) { ScrError *e = scr_stream_mkerr("ERR_STREAM_PUSH_AFTER_EOF", "push on a Writable (no readable side)"); scr_stream_error_or_destroy(s, e); @@ -753,12 +881,12 @@ static bool scr_stream_add_chunk(ScrStream *s, ScrBytes *chunk, bool front) { (scr_emitter_has((ScrEmitter *)s, "data") || st->pipes.n > 0)) { /* the direct-emit fast path: flowing, empty buffer, async push */ if (st->r.emitted_readable) st->r.emitted_readable = false; - bool is_str = st->r.encoded; - scr_stream_emit_data(s, entry); - scr_stream_entry_release_kind(entry, is_str); + ScrEntryKind kind = scr_stream_rkind(st); + scr_stream_emit_data(s, entry, kind); + scr_stream_entry_release_kind(entry, kind); if (scr_exc_pending()) return false; } else { - scr_stream_rbuf_push(st, entry, st->r.encoded, front); + scr_stream_rbuf_push(st, entry, scr_stream_rkind(st), front); if (st->r.need_readable) scr_stream_emit_readable_nt(s); } if (!front) scr_stream_maybe_read_more(s); @@ -781,8 +909,8 @@ static void scr_stream_maybe_read_more(ScrStream *s) { * Answers a +1 Buffer — or a +1 string once encoded (lengths and the * size argument then count JS string units, Node's encoded accounting). */ static void *scr_stream_read_n(ScrStream *s, double size) { - bool is_str; - return scr_stream_read_n_kind(s, size, &is_str); + ScrEntryKind kind; + return scr_stream_read_n_kind(s, size, &kind); } /* read_n, also answering the chunk's kind as it was taken from the buffer @@ -790,9 +918,9 @@ static void *scr_stream_read_n(ScrStream *s, double size) { * inside the read can change the representation, and a 'data' listener * can call setEncoding() before this returns, so callers that need the * kind read it here rather than from st->r.encoded afterwards. */ -static void *scr_stream_read_n_kind(ScrStream *s, double size, bool *is_str) { +static void *scr_stream_read_n_kind(ScrStream *s, double size, ScrEntryKind *kind) { ScrStreamState *st = s->st; - *is_str = false; + *kind = scr_stream_rkind(st); bool absent = size < 0; size_t n = 0; if (!absent) { @@ -852,7 +980,7 @@ static void *scr_stream_read_n_kind(ScrStream *s, double size, bool *is_str) { } } } - *is_str = st->r.encoded; + *kind = scr_stream_rkind(st); void *ret = want > 0 && want <= st->r.length ? scr_stream_rbuf_take(st, want) : NULL; if (ret == NULL) { st->r.need_readable = st->r.length <= st->r.hwm; @@ -865,9 +993,9 @@ static void *scr_stream_read_n_kind(ScrStream *s, double size, bool *is_str) { if (st->r.ended && absent) scr_stream_end_readable(s); } if (ret != NULL && !st->error_emitted && !st->close_emitted) { - scr_stream_emit_data(s, ret); + scr_stream_emit_data(s, ret, *kind); if (scr_exc_pending()) { - scr_stream_entry_release_kind(ret, *is_str); + scr_stream_entry_release_kind(ret, *kind); return NULL; } } @@ -877,10 +1005,10 @@ static void *scr_stream_read_n_kind(ScrStream *s, double size, bool *is_str) { static void scr_stream_flow(ScrStream *s) { ScrStreamState *st = s->st; while (st->r.flowing == 1 && !scr_exc_pending()) { - bool is_str; - void *b = scr_stream_read_n_kind(s, -1, &is_str); + ScrEntryKind kind; + void *b = scr_stream_read_n_kind(s, -1, &kind); if (b == NULL) break; - scr_stream_entry_release_kind(b, is_str); + scr_stream_entry_release_kind(b, kind); } } @@ -950,43 +1078,33 @@ ScrDyn *scr_stream_done_dyn_d(ScrClosure *clo, ScrDyn *const *args, size_t argc) return scr_stream_done_dyn_ret(); } -static void scr_stream_dyn_data(ScrDyn *const *args, size_t argc, - ScrBytes **data, ScrStr **data_str) { - *data = NULL; - *data_str = NULL; - if (argc < 2) return; - const ScrDyn *a = args[1]; - if (a->kind == SCR_DYN_BYTES) *data = scr_bytes_raw_view(a->v.bytes); - else if (a->kind == SCR_DYN_STR) *data_str = scr_str_retain(a->v.str); -} - +/* callback(err, data) of an untyped transform or flush: data is pushed + * as push(data) pushes it. */ ScrDyn *scr_stream_done_dyn_t(ScrClosure *clo, ScrDyn *const *args, size_t argc) { ScrStream *s = scr_stream_dyn_recv(clo); - ScrBytes *data; - ScrStr *data_str; - scr_stream_dyn_data(args, argc, &data, &data_str); - scr_stream_transform_done(s, scr_stream_dyn_err(args, argc), data, data_str); + scr_stream_transform_done_value(s, scr_stream_dyn_err(args, argc), argc > 1 ? args[1] : NULL); scr_stream_release(s); return scr_stream_done_dyn_ret(); } ScrDyn *scr_stream_done_dyn_l(ScrClosure *clo, ScrDyn *const *args, size_t argc) { ScrStream *s = scr_stream_dyn_recv(clo); - ScrBytes *data; - ScrStr *data_str; - scr_stream_dyn_data(args, argc, &data, &data_str); - scr_stream_flush_done(s, scr_stream_dyn_err(args, argc), data, data_str); + scr_stream_flush_done_value(s, scr_stream_dyn_err(args, argc), argc > 1 ? args[1] : NULL); scr_stream_release(s); return scr_stream_done_dyn_ret(); } /* Checked-dynamic chunks (the JS lane's push/write of any-typed values): * dispatch by dyn tag — bytes copy out, strings convert utf8, null takes - * the null path (EOF for push; ERR_STREAM_NULL_VALUES for write). Any - * other write throws Node's ERR_INVALID_ARG_TYPE; any other push throws - * the null-values TypeError (Node reports ERR_INVALID_ARG_TYPE through - * 'error'). Borrow d. */ + * the null path (EOF for push; ERR_STREAM_NULL_VALUES for write). On an + * object-mode side every other value is one entry. Otherwise any other + * write throws Node's ERR_INVALID_ARG_TYPE, push(undefined) adds nothing, + * and any other push reports ERR_INVALID_ARG_TYPE through 'error', as + * readableAddChunkPushByteMode does. Borrow d. */ bool scr_stream_push_dyn(ScrStream *s, const ScrDyn *d) { + ScrStreamState *st = s->st; + if (d->kind != SCR_DYN_NULL && st->has_r && st->r.object_entries) + return scr_stream_push_object(s, d) > 0; switch (d->kind) { case SCR_DYN_BYTES: { ScrBytes *b = scr_bytes_raw_view(d->v.bytes); @@ -998,15 +1116,26 @@ bool scr_stream_push_dyn(ScrStream *s, const ScrDyn *d) { return scr_stream_push_str(s, d->v.str); case SCR_DYN_NULL: return scr_stream_push_null(s); + case SCR_DYN_UNDEF: + if (!st->has_r) return false; + st->r.reading = false; + scr_stream_maybe_read_more(s); + return !st->r.ended && (st->r.length < st->r.hwm || st->r.length == 0); default: { - static const char msg[] = "May not write null values to stream"; - scr_throw_error_msg_code(SCR_ERR_TYPE, msg, sizeof msg - 1, "ERR_STREAM_NULL_VALUES"); + scr_dyn_arg_type_fail("chunk", "of type string or an instance of Buffer, TypedArray, or DataView", d); + ScrError *e = scr_stream_take_error(); + scr_stream_error_or_destroy(s, e); + scr_error_release(e); return false; } } } +static bool scr_stream_write_value(ScrStream *s, ScrDyn *v, ScrClosure *cb); + bool scr_stream_write_dyn(ScrStream *s, const ScrDyn *d, ScrClosure *cb) { + if (d->kind != SCR_DYN_NULL && s->st->has_w && s->st->w.object_mode) + return scr_stream_write_value(s, (ScrDyn *)d, cb); switch (d->kind) { case SCR_DYN_BYTES: { ScrBytes *b = scr_bytes_raw_view(d->v.bytes); @@ -1051,14 +1180,28 @@ static void scr_stream_next_str_fence(void) { scr_throw_error_msg(SCR_ERR_ERROR, msg, sizeof msg - 1); } -/* Fulfills the parked iterator with a chunk taken from the buffer; is_str - * is its kind as taken. The typed lane's chunk is a Buffer, so a string - * chunk rejects with the encoding fence instead. */ +/* Fulfills the parked iterator with a chunk taken from the buffer; kind + * is its kind as taken. The typed lane's chunk is a Buffer, so another + * chunk rejects with the encoding fence instead. The dyn lane's end is + * the done marker (scr_stream_from_done): an object-mode stream can + * yield undefined. */ static void scr_stream_next_fulfill(ScrStreamState *st, ScrPromise *w /*moves*/, - void *chunk /*moves or NULL=EOF*/, bool is_str) { + void *chunk /*moves or NULL=EOF*/, ScrEntryKind kind) { void *payload; bool dyn = st->r.next_dyn; - if (!dyn && chunk != NULL && is_str) { + if (!dyn && chunk != NULL && kind == SCR_ENTRY_DYN) { + ScrDyn *d = chunk; + if (d->kind == SCR_DYN_BYTES) { + chunk = scr_bytes_raw_view(d->v.bytes); + kind = SCR_ENTRY_BYTES; + scr_dyn_release(d); + } else { + chunk = scr_dyn_string_coerce_js(d); + kind = SCR_ENTRY_STR; + scr_dyn_release(d); + } + } + if (!dyn && chunk != NULL && kind == SCR_ENTRY_STR) { scr_str_release((ScrStr *)chunk); scr_stream_next_str_fence(); scr_promise_reject_pending(w); @@ -1066,17 +1209,7 @@ static void scr_stream_next_fulfill(ScrStreamState *st, ScrPromise *w /*moves*/, return; } if (dyn) { - ScrDyn *d; - if (chunk == NULL) { - d = scr_dyn_retain(scr_dyn_undefined()); /* the EOF sentinel */ - } else if (is_str) { - d = scr_dyn_new_str((ScrStr *)chunk); - scr_str_release((ScrStr *)chunk); - } else { - d = scr_dyn_new_buffer((ScrBytes *)chunk); - scr_bytes_release((ScrBytes *)chunk); - } - payload = d; + payload = chunk == NULL ? scr_stream_from_done() : scr_stream_entry_box(chunk, kind); } else { payload = chunk ? (ScrBytes *)chunk : scr_bytes_new(SCR_BYTES_U8, 0); } @@ -1108,11 +1241,11 @@ static void scr_stream_iter_step(ScrStream *s) { if (!st->r.next_waiter) return; if (!st->destroyed && !st->errored) { st->r.next_reading = true; - bool is_str; - void *chunk = scr_stream_read_n_kind(s, -1, &is_str); + ScrEntryKind kind; + void *chunk = scr_stream_read_n_kind(s, -1, &kind); st->r.next_reading = false; if (scr_exc_pending()) { - if (chunk) scr_stream_entry_release_kind(chunk, is_str); + if (chunk) scr_stream_entry_release_kind(chunk, kind); ScrPromise *w = st->r.next_waiter; st->r.next_waiter = NULL; if (w) { @@ -1124,7 +1257,7 @@ static void scr_stream_iter_step(ScrStream *s) { if (chunk) { ScrPromise *w = st->r.next_waiter; st->r.next_waiter = NULL; - scr_stream_next_fulfill(st, w, chunk, is_str); + scr_stream_next_fulfill(st, w, chunk, kind); return; } } @@ -1175,13 +1308,18 @@ static void scr_stream_settle_next(ScrStream *s) { } ScrPromise *w = st->r.next_waiter; st->r.next_waiter = NULL; - bool is_str = st->r.encoded; + ScrEntryKind kind = scr_stream_rkind(st); void *chunk = scr_stream_rbuf_take(st, st->r.object_entries ? 1 : st->r.length); - scr_stream_next_fulfill(st, w, chunk, is_str); + scr_stream_next_fulfill(st, w, chunk, kind); if (st->r.ended && st->r.length == 0) scr_stream_end_readable(s); return; } if (st->r.ended || st->destroyed) { + if (st->r.ended && st->r.next_close_wait && !st->close_emitted) { + if (!st->r.end_emitted) scr_stream_end_readable(s); + st->r.next_eof_close = true; + return; + } if (st->r.ended && !st->r.end_emitted) { /* Node's iterator completes AFTER 'end' (and its autoDestroy): * queue the sentinel BEHIND the pending 'end' tick, so code after @@ -1192,7 +1330,7 @@ static void scr_stream_settle_next(ScrStream *s) { } ScrPromise *w = st->r.next_waiter; st->r.next_waiter = NULL; - scr_stream_next_fulfill(st, w, NULL, false); + scr_stream_next_fulfill(st, w, NULL, SCR_ENTRY_BYTES); } } @@ -1201,7 +1339,7 @@ static ScrPromise *scr_stream_next_chunk_impl(ScrStream *s, bool dyn) { ScrPromise *p = scr_promise_new(); if (!st->has_r) { st->r.next_dyn = dyn; - scr_stream_next_fulfill(st, scr_promise_retain(p), NULL, false); + scr_stream_next_fulfill(st, scr_promise_retain(p), NULL, SCR_ENTRY_BYTES); return p; } if (st->r.next_waiter) { @@ -1214,6 +1352,11 @@ static ScrPromise *scr_stream_next_chunk_impl(ScrStream *s, bool dyn) { st->r.next_dyn = dyn; st->r.next_waiter = scr_promise_retain(p); st->r.next_parked = false; + if (!st->r.next_started) { + st->r.next_started = true; + st->r.next_close_wait = + !st->has_w && st->auto_destroy && st->emit_close && !st->close_scheduled && !st->close_emitted; + } /* createAsyncIterator starts with stream.on('readable', next) */ scr_stream_readable_listen(s); scr_stream_iter_step(s); @@ -1268,25 +1411,11 @@ static void scr_from_src_release(ScrFromSrc *f) { static void *scr_from_src_retain_v(void *f) { return scr_from_src_retain(f); } static void scr_from_src_release_v(void *f) { scr_from_src_release(f); } -/* A Readable.from source whose values no longer fit the buffer's kind - * (strings beside Buffers): destroyed with the "not supported yet" Error. */ -static void scr_from_destroy_mixed(ScrStream *s) { - static const char m[] = "a Readable.from source that mixes strings and Buffers is not supported yet"; - ScrStr *msg = scr_str_new(m, sizeof m - 1); - ScrError *e = scr_error_new(SCR_ERR_ERROR, msg); - scr_str_release(msg); - scr_stream_release(scr_stream_destroy(s, e)); - scr_error_release(e); -} - /* One typed element into the stream: a whole entry in object mode, a * chunk in byte mode. Moves e. Returns push's answer. */ static bool scr_from_push_typed(ScrStream *s, void *e, bool strings) { - if (s->st->r.object_entries) { - int pushed = scr_stream_add_entry(s, e, strings); - if (pushed < 0) scr_from_destroy_mixed(s); - return pushed > 0; - } + if (s->st->r.object_entries) + return scr_stream_add_entry(s, e, strings ? SCR_ENTRY_STR : SCR_ENTRY_BYTES) > 0; bool more = strings ? scr_stream_push_str(s, e) : scr_stream_push(s, e); if (strings) scr_str_release(e); else scr_bytes_release(e); @@ -1447,10 +1576,10 @@ ScrStream *scr_stream_from_arr(ScrArr *arr, double flags, double hwm) { } /* The JS lane: one checked-dynamic value into the stream. null is - * ERR_STREAM_NULL_VALUES; in object mode, values other than strings and - * Buffers (or a mix of the two) are outside the stream model; byte mode - * rejects them with push()'s ERR_INVALID_ARG_TYPE. Each failure destroys - * the stream and answers -1. Borrows v. */ + * ERR_STREAM_NULL_VALUES; in object mode every other value is an entry; + * byte mode rejects values other than strings and Buffers with push()'s + * ERR_INVALID_ARG_TYPE. Each failure destroys the stream and answers -1. + * Borrows v. */ int scr_stream_from_push(ScrStream *s, const ScrDyn *v) { ScrError *e = NULL; if (v->kind == SCR_DYN_NULL) { @@ -1460,14 +1589,7 @@ int scr_stream_from_push(ScrStream *s, const ScrDyn *v) { scr_str_release(msg); scr_error_set_code(e, "ERR_STREAM_NULL_VALUES"); } else if (s->st->r.object_entries) { - int pushed = scr_stream_push_object(s, v); - if (pushed >= 0) return pushed; - static const char mixed[] = "a Readable.from source that mixes strings and Buffers is not supported yet"; - static const char other[] = "Readable.from over values other than strings and Buffers is not supported yet"; - const char *m = pushed == -2 ? mixed : other; - ScrStr *msg = scr_str_new(m, strlen(m)); - e = scr_error_new(SCR_ERR_ERROR, msg); - scr_str_release(msg); + return scr_stream_push_object(s, v); } else if (v->kind == SCR_DYN_STR || v->kind == SCR_DYN_BYTES) { return scr_stream_push_dyn(s, v) ? 1 : 0; } else { @@ -1598,61 +1720,72 @@ ScrStream *scr_stream_new_object_readable(ScrClosure *read, ScrStreamReadInv rea return s; } -/* An object-mode entry in the buffer's representation: a Buffer on a - * stream with a decoder decodes through it (Node's decoder.write in - * readableAddChunkPushObjectMode); an entry of the other kind otherwise - * mixes strings and Buffers, which the buffer cannot hold. Moves entry; - * +1, or NULL (entry released). */ -static void *scr_stream_entry_convert(ScrStreamState *st, void *entry, bool is_str) { - if (is_str == st->r.encoded) return entry; - if (!is_str && st->r.decoding) { +/* The object-mode buffer starts holding dyn values: every buffered entry + * is boxed. */ +static void scr_stream_rbuf_to_dyn(ScrStreamState *st) { + ScrEntryKind kind = scr_stream_rkind(st); + for (size_t i = 0; i < st->r.n; i++) st->r.buf[i] = scr_stream_entry_box(st->r.buf[i], kind); + st->r.dyn_entries = true; + st->r.object_kind_set = true; +} + +/* An object-mode entry in the buffer's representation (moves entry; +1). + * A Buffer on a stream with a decoder decodes through it (Node's + * decoder.write in readableAddChunkPushObjectMode); an entry of another + * kind turns the buffer into dyn entries. */ +static void *scr_stream_entry_convert(ScrStreamState *st, void *entry, ScrEntryKind *kind) { + ScrEntryKind want = scr_stream_rkind(st); + if (*kind == want) return entry; + if (*kind == SCR_ENTRY_BYTES && want == SCR_ENTRY_STR && st->r.decoding) { ScrBytes *b = entry; ScrStr *dec = scr_strdec_write(st->r.enc, st->r.dec_pending, b); st->r.dec_pending = scr_strdec_next(st->r.enc, st->r.dec_pending, b); scr_bytes_release(b); + *kind = SCR_ENTRY_STR; return dec; } - if (is_str) scr_str_release(entry); - else scr_bytes_release(entry); - return NULL; + if (want != SCR_ENTRY_DYN) scr_stream_rbuf_to_dyn(st); + ScrDyn *d = scr_stream_entry_box(entry, *kind); + *kind = SCR_ENTRY_DYN; + return d; } /* readableAddChunk in object mode: every value is one whole entry, empty - * strings and Buffers included. is_str gives entry's kind. Answers push()'s - * result, or -2 (entry released) when the kinds cannot mix. */ -static int scr_stream_add_entry(ScrStream *s, void *entry /*moves*/, bool is_str) { + * strings and Buffers included. kind gives entry's kind. Answers push()'s + * result. */ +static int scr_stream_add_entry(ScrStream *s, void *entry /*moves*/, ScrEntryKind kind) { ScrStreamState *st = s->st; if (!st->r.object_kind_set && st->r.length == 0) { /* the first entry picks the representation */ st->r.object_kind_set = true; - if (is_str && !st->r.encoded) { + if (kind == SCR_ENTRY_STR && !st->r.encoded) { st->r.encoded = true; if (!st->r.enc) st->r.enc = scr_str_new("utf8", 4); + } else if (kind == SCR_ENTRY_DYN) { + st->r.dyn_entries = true; } } - entry = scr_stream_entry_convert(st, entry, is_str); - if (entry == NULL) return -2; + entry = scr_stream_entry_convert(st, entry, &kind); if (st->r.ended) { - scr_stream_entry_release(st, entry); + scr_stream_entry_release_kind(entry, kind); ScrError *e = scr_stream_mkerr("ERR_STREAM_PUSH_AFTER_EOF", "stream.push() after EOF"); scr_stream_error_or_destroy(s, e); scr_error_release(e); return 0; } if (st->destroyed || st->errored) { - scr_stream_entry_release(st, entry); + scr_stream_entry_release_kind(entry, kind); return 0; } st->r.reading = false; if (st->r.flowing == 1 && st->r.length == 0 && !st->r.in_read_sync && (scr_emitter_has((ScrEmitter *)s, "data") || st->pipes.n > 0)) { if (st->r.emitted_readable) st->r.emitted_readable = false; - bool is_str = st->r.encoded; - scr_stream_emit_data(s, entry); - scr_stream_entry_release_kind(entry, is_str); + scr_stream_emit_data(s, entry, kind); + scr_stream_entry_release_kind(entry, kind); if (scr_exc_pending()) return 0; } else { - scr_stream_rbuf_push(st, entry, st->r.encoded, false); + scr_stream_rbuf_push(st, entry, kind, false); if (st->r.need_readable) scr_stream_emit_readable_nt(s); } scr_stream_maybe_read_more(s); @@ -1660,12 +1793,22 @@ static int scr_stream_add_entry(ScrStream *s, void *entry /*moves*/, bool is_str return !st->r.ended && (st->r.length < st->r.hwm || st->r.length == 0); } +/* push(value) on an object-mode stream: a string or a Buffer keeps its + * kind, any other value is a dyn entry. Borrows v. */ int scr_stream_push_object(ScrStream *s, const ScrDyn *v) { - bool is_str = v->kind == SCR_DYN_STR; - if (!is_str && v->kind != SCR_DYN_BYTES) return -1; - void *entry = is_str ? (void *)scr_str_retain(v->v.str) : (void *)scr_bytes_raw_view(v->v.bytes); - int pushed = scr_stream_add_entry(s, entry, is_str); - return pushed < 0 ? pushed : pushed ? 1 : 0; + void *entry; + ScrEntryKind kind; + if (v->kind == SCR_DYN_STR) { + entry = scr_str_retain(v->v.str); + kind = SCR_ENTRY_STR; + } else if (v->kind == SCR_DYN_BYTES) { + entry = scr_bytes_raw_view(v->v.bytes); + kind = SCR_ENTRY_BYTES; + } else { + entry = scr_dyn_retain((ScrDyn *)v); + kind = SCR_ENTRY_DYN; + } + return scr_stream_add_entry(s, entry, kind) ? 1 : 0; } /* ── the registration hook ('data'/'readable' listeners kick flow) ────── */ @@ -1706,7 +1849,8 @@ static void scr_stream_on_listener(ScrEmitter *em, ScrStr *name) { /* ── writable internals ───────────────────────────────────────────────── */ -static void scr_stream_wq_push(ScrStreamState *st, ScrBytes *chunk /*moves*/, ScrClosure *cb /*moves*/) { +static void scr_stream_wq_push(ScrStreamState *st, ScrBytes *chunk /*moves*/, ScrClosure *cb /*moves*/, + ScrDyn *enc /*moves*/) { if (st->w.n == st->w.cap) { st->w.cap = st->w.cap ? st->w.cap * 2 : 4; st->w.q = realloc(st->w.q, st->w.cap * sizeof *st->w.q); @@ -1714,29 +1858,49 @@ static void scr_stream_wq_push(ScrStreamState *st, ScrBytes *chunk /*moves*/, Sc } st->w.q[st->w.n].chunk = chunk; st->w.q[st->w.n].cb = cb; + st->w.q[st->w.n].enc = enc; st->w.n++; } static void scr_stream_transform_callback(ScrStream *s); -static void scr_stream_do_write(ScrStream *s, ScrBytes *chunk /*moves*/, ScrClosure *cb /*moves*/) { +static void scr_stream_do_write_inner(ScrStream *s, ScrBytes *chunk /*moves*/, ScrClosure *cb /*moves*/); + +/* One write reaching _write/_transform; the callback reads the write's + * encoding argument through w.cur_enc while it runs. */ +static void scr_stream_do_write(ScrStream *s, ScrBytes *chunk /*moves*/, ScrClosure *cb /*moves*/, + ScrDyn *enc /*moves*/) { + ScrStreamState *st = s->st; + if (enc == NULL) { + scr_stream_do_write_inner(s, chunk, cb); + return; + } + ScrDyn *outer = st->w.cur_enc; + st->w.cur_enc = enc; + scr_stream_do_write_inner(s, chunk, cb); + st->w.cur_enc = outer; + scr_dyn_release(enc); +} + +static void scr_stream_do_write_inner(ScrStream *s, ScrBytes *chunk /*moves*/, ScrClosure *cb /*moves*/) { ScrStreamState *st = s->st; st->w.writing = true; st->w.wsync = true; - st->w.inflight_len = chunk->len; + st->w.inflight_len = scr_stream_wchunk_len(st, chunk); st->w.inflight_cb = cb; if (st->is_transform || st->passthrough) st->xf_length = st->r.length; if (st->passthrough && st->transform_cb == NULL) { /* identity: push through, complete synchronously */ - scr_stream_add_chunk(s, chunk, false); - scr_bytes_release(chunk); + if (st->w.object_mode) scr_stream_push_dyn(s, (ScrDyn *)chunk); + else scr_stream_add_chunk(s, chunk, false); + scr_stream_wchunk_release(st, chunk); if (!scr_exc_pending()) scr_stream_transform_callback(s); st->w.wsync = false; return; } if (st->is_transform || st->passthrough) { if (st->transform_cb == NULL) { - scr_bytes_release(chunk); + scr_stream_wchunk_release(st, chunk); ScrError *e = scr_stream_mkerr("ERR_METHOD_NOT_IMPLEMENTED", "The _transform() method is not implemented"); st->w.wsync = false; scr_stream_error_or_destroy(s, e); @@ -1744,12 +1908,12 @@ static void scr_stream_do_write(ScrStream *s, ScrBytes *chunk /*moves*/, ScrClos return; } st->transform_inv(st->transform_cb, s, chunk); - scr_bytes_release(chunk); + scr_stream_wchunk_release(st, chunk); st->w.wsync = false; return; } if (st->w.write_cb == NULL) { - scr_bytes_release(chunk); + scr_stream_wchunk_release(st, chunk); ScrError *e = scr_stream_mkerr("ERR_METHOD_NOT_IMPLEMENTED", "The _write() method is not implemented"); st->w.wsync = false; scr_stream_error_or_destroy(s, e); @@ -1757,7 +1921,7 @@ static void scr_stream_do_write(ScrStream *s, ScrBytes *chunk /*moves*/, ScrClos return; } st->w.write_inv(st->w.write_cb, s, chunk); - scr_bytes_release(chunk); + scr_stream_wchunk_release(st, chunk); st->w.wsync = false; } @@ -1770,8 +1934,9 @@ static void scr_stream_error_buffer(ScrStream *s) { if (st->w.writing) return; while (st->w.head < st->w.n && !scr_exc_pending()) { ScrWEntry e = st->w.q[st->w.head++]; - st->w.length -= e.chunk->len; - scr_bytes_release(e.chunk); + st->w.length -= scr_stream_wchunk_len(st, e.chunk); + scr_stream_wchunk_release(st, e.chunk); + if (e.enc) scr_dyn_release(e.enc); if (e.cb) { ((void (*)(ScrClosure *))e.cb->fn)(e.cb); scr_closure_release(e.cb); @@ -1863,12 +2028,14 @@ static SCR_COLD void scr_stream_writev_buffer(ScrStream *s) { } st->w.head = 0; st->w.n = 0; - scr_stream_do_write(s, all, cbs); + scr_stream_do_write(s, all, cbs, NULL); } /* The buffered writes, one at a time (Node's clearBuffer without _writev). */ static void scr_stream_clear_buffer_each(ScrStream *s) { ScrStreamState *st = s->st; + if (st->w.buffer_processing) return; + st->w.buffer_processing = true; while (!st->w.writing && st->w.head < st->w.n && st->w.corked == 0 && !st->destroyed && !scr_exc_pending()) { ScrWEntry e = st->w.q[st->w.head++]; @@ -1876,8 +2043,9 @@ static void scr_stream_clear_buffer_each(ScrStream *s) { st->w.head = 0; st->w.n = 0; } - scr_stream_do_write(s, e.chunk, e.cb); + scr_stream_do_write(s, e.chunk, e.cb, e.enc); } + st->w.buffer_processing = false; } /* The fs streams' clearBuffer: nothing while constructing, and _writev @@ -1947,6 +2115,11 @@ static void scr_stream_finish_maybe(ScrStream *s) { scr_stream_emit0(s, "prefinish"); if (scr_exc_pending()) return; } + /* prefinish ran; without a _final the deferred afterWrites still hold + * 'finish' (Node's pendingcb), and the last of them comes back here. + * A _final's callback puts 'finish' on a tick at once (callFinal's + * onFinish), and a Transform's _final is its flush. */ + if (st->w.after_holds > 0 && !st->w.final_cb && !st->is_transform && !st->passthrough) return; st->w.finish_scheduled = true; scr_st_tick(s, SCR_ST_FINISH, NULL, NULL); } @@ -2327,10 +2500,10 @@ static void scr_stream_dynopt_read_inv(ScrClosure *cb, ScrStream *s, double size static void scr_stream_dynopt_chunk3(ScrClosure *cb, ScrStream *s, ScrBytes *chunk, ScrDynThunk glue, const char *what) { ScrDyn *args[3]; - args[0] = scr_dyn_new_buffer(chunk); - ScrStr *enc = scr_str_new("buffer", 6); - args[1] = scr_dyn_new_str(enc); - scr_str_release(enc); + /* (chunk, encoding, cb): an object-mode side passes the value and the + * write's encoding argument, as Node's Writable does */ + args[0] = scr_stream_chunk_value(s, chunk); + args[1] = scr_stream_chunk_encoding_value(s); args[2] = scr_stream_dynopt_done(s, glue, 1, "(error)"); ScrDyn *fn = scr_stream_dynopt_target(cb); ScrDyn *r = scr_dyn_call(fn, args, 3, what); @@ -2345,10 +2518,8 @@ static void scr_stream_dynopt_write_inv(ScrClosure *cb, ScrStream *s, ScrBytes * static void scr_stream_dynopt_transform_inv(ScrClosure *cb, ScrStream *s, ScrBytes *chunk) { ScrDyn *args[3]; - args[0] = scr_dyn_new_buffer(chunk); - ScrStr *enc = scr_str_new("buffer", 6); - args[1] = scr_dyn_new_str(enc); - scr_str_release(enc); + args[0] = scr_stream_chunk_value(s, chunk); + args[1] = scr_stream_chunk_encoding_value(s); args[2] = scr_stream_dynopt_done(s, &scr_stream_done_dyn_t, 2, "(error,data)"); ScrDyn *fn = scr_stream_dynopt_target(cb); ScrDyn *r = scr_dyn_call(fn, args, 3, "transform"); @@ -2398,6 +2569,7 @@ typedef struct { double rhwm, whwm; bool auto_destroy, emit_close, allow_half_open; bool readable_side, writable_side; + bool readable_object, writable_object; /* objectMode and the sided keys */ ScrStr *enc; /* owned canonical name, or NULL */ /* BORROWED callable fields (owned by the options record). */ ScrDyn *read, *write, *final_fn, *destroy, *transform, *flush; @@ -2474,17 +2646,17 @@ static bool scr_stream_parse_dyn_opts(const ScrDyn *opts, char base, ScrStreamDy out->readable_side = true; out->writable_side = true; out->enc = NULL; + out->readable_object = out->writable_object = false; out->read = out->write = out->final_fn = out->destroy = out->transform = out->flush = NULL; if (opts == NULL || opts->kind != SCR_DYN_OBJ) return true; /* Node's `if (options)` */ const bool duplexish = base == 'd' || base == 't' || base == 'p'; const bool transformish = base == 't' || base == 'p'; const bool r_sided = base != 'w'; - if (scr_stream_dynopt_truthy(opts, "objectMode", 10) || - scr_stream_dynopt_truthy(opts, "readableObjectMode", 18) || - scr_stream_dynopt_truthy(opts, "writableObjectMode", 18)) { - static const char m[] = "objectMode streams are not supported yet (chunks are Buffers or utf8 strings)"; - return scr_stream_dynopt_unsupported(m, sizeof m - 1); + { + bool object = scr_stream_dynopt_truthy(opts, "objectMode", 10); + out->readable_object = object || (duplexish && scr_stream_dynopt_truthy(opts, "readableObjectMode", 18)); + out->writable_object = object || (duplexish && scr_stream_dynopt_truthy(opts, "writableObjectMode", 18)); } if (scr_stream_dynopt_false(scr_dyn_obj_get((ScrDyn *)opts, "decodeStrings", 13))) { static const char m[] = "decodeStrings: false is not supported yet (chunks always decode to Buffers)"; @@ -2527,7 +2699,7 @@ static bool scr_stream_parse_dyn_opts(const ScrDyn *opts, char base, ScrStreamDy /* highWaterMark wins over the sided keys (Node's getHighWaterMark); * the readable side is checked first, as Duplex's Readable constructor - * runs first. */ + * runs first. An object-mode side defaults to 16 entries. */ if (duplexish) { out->rhwm = scr_stream_dynopt_hwm(opts, "readableHighWaterMark", "options.readableHighWaterMark"); if (out->rhwm == -2) return false; @@ -2538,6 +2710,8 @@ static bool scr_stream_parse_dyn_opts(const ScrDyn *opts, char base, ScrStreamDy if (hwm == -2) return false; out->rhwm = out->whwm = hwm; } + if (out->rhwm == -1 && out->readable_object) out->rhwm = 16; + if (out->whwm == -1 && out->writable_object) out->whwm = 16; out->auto_destroy = !scr_stream_dynopt_false(scr_dyn_obj_get((ScrDyn *)opts, "autoDestroy", 11)); out->emit_close = !scr_stream_dynopt_false(scr_dyn_obj_get((ScrDyn *)opts, "emitClose", 9)); if (duplexish) { @@ -2578,6 +2752,10 @@ static ScrClosure *scr_stream_dynopt_pick(ScrDyn *opt, void *dyn_inv, ScrClosure return fb; } +static void scr_stream_dynopt_apply_object(ScrStream *s, const ScrStreamDynOpts *o) { + if (s != NULL && s->st != NULL) scr_stream_set_object_mode(s, o->readable_object, o->writable_object); +} + static void scr_stream_dynopt_apply_enc(ScrStream *s, ScrStr *enc) { if (enc == NULL) return; ScrStream *chain = scr_stream_set_encoding(s, enc); @@ -2593,6 +2771,7 @@ ScrStream *scr_stream_new_readable_dyn(ScrDyn *opts) { ScrClosure *destroy = scr_stream_dynopt_pick(o.destroy, (void *)&scr_stream_dynopt_destroy_inv, NULL, NULL, &dinv); ScrStream *s = scr_stream_new_readable(o.rhwm, o.auto_destroy, o.emit_close, read, (ScrStreamReadInv)rinv, destroy, (ScrStreamErrInv)dinv); + scr_stream_dynopt_apply_object(s, &o); scr_stream_dynopt_apply_enc(s, o.enc); return s; } @@ -2604,8 +2783,10 @@ ScrStream *scr_stream_new_writable_dyn(ScrDyn *opts) { ScrClosure *write = scr_stream_dynopt_pick(o.write, (void *)&scr_stream_dynopt_write_inv, NULL, NULL, &winv); ScrClosure *final_cb = scr_stream_dynopt_pick(o.final_fn, (void *)&scr_stream_dynopt_final_inv, NULL, NULL, &finv); ScrClosure *destroy = scr_stream_dynopt_pick(o.destroy, (void *)&scr_stream_dynopt_destroy_inv, NULL, NULL, &dinv); - return scr_stream_new_writable(o.whwm, o.auto_destroy, o.emit_close, + ScrStream *s = scr_stream_new_writable(o.whwm, o.auto_destroy, o.emit_close, write, (ScrStreamChunkInv)winv, final_cb, (ScrStreamPlainInv)finv, destroy, (ScrStreamErrInv)dinv); + scr_stream_dynopt_apply_object(s, &o); + return s; } ScrStream *scr_stream_new_duplex_dyn(ScrDyn *opts) { @@ -2620,6 +2801,7 @@ ScrStream *scr_stream_new_duplex_dyn(ScrDyn *opts) { o.allow_half_open, o.readable_side, o.writable_side, read, (ScrStreamReadInv)rinv, write, (ScrStreamChunkInv)winv, final_cb, (ScrStreamPlainInv)finv, destroy, (ScrStreamErrInv)dinv); + scr_stream_dynopt_apply_object(s, &o); scr_stream_dynopt_apply_enc(s, o.enc); return s; } @@ -2640,6 +2822,7 @@ static ScrStream *scr_stream_new_transformish_dyn(ScrDyn *opts, char base) { o.allow_half_open, o.readable_side, o.writable_side, transform, (ScrStreamChunkInv)tinv, flush, (ScrStreamPlainInv)linv, destroy, (ScrStreamErrInv)dinv); + scr_stream_dynopt_apply_object(s, &o); scr_stream_dynopt_apply_enc(s, o.enc); return s; } @@ -2666,6 +2849,7 @@ void scr_stream_init_readable_dyn(ScrStream *s, ScrDyn *opts, ScrClosure *destroy = scr_stream_dynopt_pick(o.destroy, (void *)&scr_stream_dynopt_destroy_inv, destroy_fb, (void *)destroy_fb_inv, &dinv); scr_stream_init_readable(s, o.rhwm, o.auto_destroy, o.emit_close, read, (ScrStreamReadInv)rinv, destroy, (ScrStreamErrInv)dinv); + scr_stream_dynopt_apply_object(s, &o); scr_stream_dynopt_apply_enc(s, o.enc); } @@ -2686,6 +2870,7 @@ void scr_stream_init_writable_dyn(ScrStream *s, ScrDyn *opts, ScrClosure *destroy = scr_stream_dynopt_pick(o.destroy, (void *)&scr_stream_dynopt_destroy_inv, destroy_fb, (void *)destroy_fb_inv, &dinv); scr_stream_init_writable(s, o.whwm, o.auto_destroy, o.emit_close, write, (ScrStreamChunkInv)winv, final_cb, (ScrStreamPlainInv)finv, destroy, (ScrStreamErrInv)dinv); + scr_stream_dynopt_apply_object(s, &o); } void scr_stream_init_duplex_dyn(ScrStream *s, ScrDyn *opts, @@ -2710,6 +2895,7 @@ void scr_stream_init_duplex_dyn(ScrStream *s, ScrDyn *opts, o.allow_half_open, o.readable_side, o.writable_side, read, (ScrStreamReadInv)rinv, write, (ScrStreamChunkInv)winv, final_cb, (ScrStreamPlainInv)finv, destroy, (ScrStreamErrInv)dinv); + scr_stream_dynopt_apply_object(s, &o); scr_stream_dynopt_apply_enc(s, o.enc); } @@ -2739,6 +2925,7 @@ static void scr_stream_init_transformish_dyn(ScrStream *s, ScrDyn *opts, char ba transform, (ScrStreamChunkInv)tinv, flush, (ScrStreamPlainInv)linv, destroy, (ScrStreamErrInv)dinv); } + scr_stream_dynopt_apply_object(s, &o); scr_stream_dynopt_apply_enc(s, o.enc); } @@ -3299,6 +3486,12 @@ static void scr_stream_done_marker_free(void) { scr_stream_done_marker = NULL; } +/* readable.chunkDone: whether a dyn for-await step answered the end (the + * done marker; an object-mode stream can yield undefined). */ +bool scr_stream_chunk_done(const ScrDyn *v) { + return v != NULL && v == scr_stream_done_marker; +} + ScrDyn *scr_stream_from_done(void) { if (!scr_stream_done_marker) { scr_stream_done_marker = scr_dyn_new_obj(); @@ -4067,25 +4260,20 @@ static void scr_compose_read_inv(ScrClosure *cb, ScrStream *s, double size) { } /* tail.on('data'): d.push(chunk), pausing the tail when d is full. The - * chunk is the tail's Buffer, or its string when the tail is encoded. */ -static void scr_compose_data_l(ScrClosure *cb, ScrBytes *bytes, ScrStr *str) { + * chunk is the tail's Buffer, its string when the tail is encoded, or an + * object-mode value. */ +static void scr_compose_data_l(ScrClosure *cb, ScrBytes *bytes, ScrStr *str, ScrDyn *value) { ScrCompose *c = scr_box_get_ref(cb->caps[0]); ScrStream *d = c->d; if (d) { ScrStreamState *dst = d->st; bool more; if (dst->r.object_entries) { - int pushed = scr_stream_add_entry( - d, str ? (void *)scr_str_retain(str) : (void *)scr_bytes_retain(bytes), str != NULL); - if (pushed < 0) { - static const char m[] = "a composed stream whose tail mixes strings and Buffers is not supported yet"; - ScrStr *msg = scr_str_new(m, sizeof m - 1); - ScrError *e = scr_error_new(SCR_ERR_ERROR, msg); - scr_str_release(msg); - scr_stream_release(scr_stream_destroy(d, e)); - scr_error_release(e); - } - more = pushed > 0; + more = value ? scr_stream_push_object(d, value) > 0 + : scr_stream_add_entry(d, str ? (void *)scr_str_retain(str) : (void *)scr_bytes_retain(bytes), + str ? SCR_ENTRY_STR : SCR_ENTRY_BYTES) > 0; + } else if (value && !str && !bytes) { + more = scr_stream_push_dyn(d, value); } else if (str) { more = scr_stream_push_str(d, str); } else { @@ -4206,7 +4394,7 @@ ScrStream *scr_stream_compose(double n_d, ScrStream **streams) { c->tail, scr_compose_closure((void *)&scr_compose_tail_eos, c), &scr_compose_tail_eos)); } if (c->readable) { - scr_compose_on(c->tail, "data", &c->data_l, (void *)&scr_compose_data_l, c, scr_ee_inv_fixed2); + scr_compose_on(c->tail, "data", &c->data_l, (void *)&scr_compose_data_l, c, scr_ee_inv_fixed3); scr_compose_on(c->tail, "end", &c->end_l, (void *)&scr_compose_end_l, c, scr_ee_inv_fixed0); } static SCR_TL bool registered = false; @@ -4379,7 +4567,21 @@ static void scr_sc_settle_ok(ScrClosure *cb, int kind) { /* The 'data' accumulation (the emit ABI's two payload slots — exactly * one non-NULL). */ -static void scr_sc_data(ScrClosure *cb, void *b, void *str) { +/* An object-mode value other than a string or a Buffer reaching a + * runtime consumer that reads text or bytes (stream/consumers, readline). */ +static void scr_stream_value_consumer_fence(const char *consumer) { + char msg[160]; + int n = snprintf(msg, sizeof msg, + "%s over an object-mode stream value that is not a string or Buffer is not supported yet", + consumer); + scr_throw_error_msg_code(SCR_ERR_ERROR, msg, (size_t)n, "SC2020"); +} + +static void scr_sc_data(ScrClosure *cb, void *b, void *str, void *value) { + if (b == NULL && str == NULL && value != NULL) { + scr_stream_value_consumer_fence("stream/consumers"); + return; + } ScrArr *chunks = scr_box_get_ref(cb->caps[1]); /* +1 */ if (b != NULL) { scr_arr_push_ref(chunks, scr_bytes_retain((ScrBytes *)b)); @@ -4452,7 +4654,7 @@ static ScrPromise *scr_sc_consume(ScrStream *s, int kind) { ScrStr *dn = scr_str_new("data", 4); scr_emitter_release(scr_emitter_on((ScrEmitter *)s, dn, scr_sc_closure((void *)&scr_sc_data, p, chunks), - scr_ee_inv_fixed2, false, false)); + scr_ee_inv_fixed3, false, false)); scr_str_release(dn); /* resume() rather than the 'data' hook alone: Node's consumer pulls * through the iterator, which drains a PAUSED stream too. */ @@ -4643,7 +4845,11 @@ static SCR_TL ScrRlHook *scr_rl_hooks = NULL; static double scr_stream_rl_id(ScrClosure *cb) { return scr_box_get_f64(cb->caps[0]); } -static void scr_stream_rl_data(ScrClosure *cb, void *b, void *str) { +static void scr_stream_rl_data(ScrClosure *cb, void *b, void *str, void *value) { + if (b == NULL && str == NULL && value != NULL) { + scr_stream_value_consumer_fence("readline"); + return; + } if (b != NULL) scr_rl_input_bytes(scr_stream_rl_id(cb), (ScrBytes *)b); else if (str != NULL) scr_rl_input_str(scr_stream_rl_id(cb), (ScrStr *)str); } @@ -4797,7 +5003,7 @@ double scr_stream_rl_open(ScrStream *in, ScrStream *out, bool out_stdout, double h->next = scr_rl_hooks; scr_rl_hooks = h; scr_stream_rl_on(in, "error", h->error, scr_ee_inv_fixed1); - scr_stream_rl_on(in, "data", h->data, scr_ee_inv_fixed2); + scr_stream_rl_on(in, "data", h->data, scr_ee_inv_fixed3); scr_stream_rl_on(in, "end", h->end, scr_ee_inv_fixed0); scr_stream_rl_resume(in); return id; @@ -5323,6 +5529,85 @@ void scr_console_bind(ScrDyn *console, double which, ScrStream *stream) { inst->stream_drop = &scr_console_stream_drop; } +/* ── object mode ──────────────────────────────────────────────────────── */ + +/* The objectMode option (both sides) or a Duplex's readableObjectMode and + * writableObjectMode, applied right after construction: the readable + * side's entries are whole values (each counts 1, and the first push picks + * the representation), and the writable side queues values. The caller + * passes the object-mode high-water marks (16 by default). */ +void scr_stream_set_object_mode(ScrStream *s, bool readable, bool writable) { + ScrStreamState *st = s->st; + if (readable && st->has_r) { + st->r.object_entries = true; + st->r.object_mode_opt = true; + } + if (writable && st->has_w) st->w.object_mode = true; +} + +/* stream.objectMode: scr_stream_set_object_mode, answering the receiver + * (+1). */ +ScrStream *scr_stream_object_mode(ScrStream *s, bool readable, bool writable) { + scr_stream_set_object_mode(s, readable, writable); + return scr_stream_retain(s); +} + +/* Whether the write and transform steps receive dyn values (an + * object-mode writable side): the compiler's chunk thunks read the chunk + * by this. */ +bool scr_stream_writes_values(ScrStream *s) { + return s->st->has_w && s->st->w.object_mode; +} + +/* emit('data', value) with a checked-dynamic chunk: the 'data' slots by + * the value's tag (scr_stream_emit_data's ABI). Borrows its arguments. */ +bool scr_stream_emit_value(ScrStream *s, ScrStr *name, ScrDyn *v) { + ScrBytes *b = v->kind == SCR_DYN_BYTES ? v->v.bytes : NULL; + ScrStr *str = v->kind == SCR_DYN_STR ? v->v.str : NULL; + return scr_emitter_emit((ScrEmitter *)s, name, b, str, v); +} + +/* The chunk a compiled write or transform callback receives, by its + * declared type: as a Buffer (+1; on an object-mode side the value must + * be a Buffer, else a TypeError is thrown and NULL answered), as a dyn + * value (+1), and the encoding argument: 'buffer' on a byte-mode side, + * else what the write passed (undefined, null or a name). A parameter + * typed string receives the name, or "" for undefined and null; one typed + * otherwise receives the value (+1 each). */ +ScrBytes *scr_stream_chunk_bytes(ScrStream *s, ScrBytes *chunk) { + if (!scr_stream_writes_values(s)) return scr_bytes_retain(chunk); + ScrDyn *v = (ScrDyn *)chunk; + if (v->kind == SCR_DYN_BYTES) return scr_bytes_raw_view(v->v.bytes); + static const char msg[] = + "a stream callback declaring a Buffer chunk received an object-mode value that is not a Buffer"; + scr_throw_error_msg(SCR_ERR_TYPE, msg, sizeof msg - 1); + return NULL; +} + +ScrDyn *scr_stream_chunk_value(ScrStream *s, ScrBytes *chunk) { + if (scr_stream_writes_values(s)) return scr_dyn_retain((ScrDyn *)chunk); + return scr_dyn_new_buffer(chunk); +} + +ScrStr *scr_stream_chunk_encoding(ScrStream *s) { + if (!scr_stream_writes_values(s)) return scr_str_new("buffer", 6); + ScrDyn *enc = s->st->w.cur_enc; + if (enc && enc->kind == SCR_DYN_STR) return scr_str_retain(enc->v.str); + if (enc == NULL || enc->kind == SCR_DYN_NULL) return scr_str_new("", 0); + return scr_dyn_string_coerce_js(enc); +} + +ScrDyn *scr_stream_chunk_encoding_value(ScrStream *s) { + if (!scr_stream_writes_values(s)) { + ScrStr *name = scr_str_new("buffer", 6); + ScrDyn *d = scr_dyn_new_str(name); + scr_str_release(name); + return d; + } + ScrDyn *enc = s->st->w.cur_enc; + return enc ? scr_dyn_retain(enc) : scr_dyn_undefined(); +} + /* ── the readable surface ─────────────────────────────────────────────── */ bool scr_stream_push(ScrStream *s, ScrBytes *chunk) { @@ -5330,6 +5615,9 @@ bool scr_stream_push(ScrStream *s, ScrBytes *chunk) { } bool scr_stream_push_str(ScrStream *s, ScrStr *str) { + /* object mode: the string is one whole entry, not converted */ + if (s->st->has_r && s->st->r.object_entries) + return scr_stream_add_entry(s, scr_str_retain(str), SCR_ENTRY_STR) > 0; ScrStr *enc = s->st->has_r ? s->st->r.push_enc : NULL; ScrBytes *b; if (enc != NULL) { @@ -5349,6 +5637,8 @@ bool scr_stream_push_str(ScrStream *s, ScrStr *str) { /* push(chunk, encoding) with an explicit non-utf8 literal: the per-call * encoding overrides the stream's default. Borrows both strings. */ bool scr_stream_push_str_enc(ScrStream *s, ScrStr *str, ScrStr *enc) { + /* object mode: the string is the chunk and the encoding is not used */ + if (s->st->has_r && s->st->r.object_entries) return scr_stream_push_str(s, str); ScrBytes *b = scr_stream_buffer(scr_bytes_from_str(str, enc)); bool ret = scr_stream_add_chunk(s, b, false); scr_bytes_release(b); @@ -5374,11 +5664,11 @@ bool scr_stream_push_null(ScrStream *s) { if (!st->r.ended) { /* onEofChunk flushes the decoder's held partial first (utf8's * replacement chars et al — Node's decoder.end() push). */ - if (st->r.encoded) { + if (st->r.encoded && !st->r.dyn_entries) { ScrStr *tail = scr_strdec_end(st->r.enc, st->r.dec_pending); st->r.dec_pending = 0; if (scr_str_utf16_len(tail) > 0) { - scr_stream_rbuf_push(st, tail, true, false); + scr_stream_rbuf_push(st, tail, SCR_ENTRY_STR, false); if (st->r.need_readable) scr_stream_emit_readable_nt(s); } else { scr_str_release(tail); @@ -5408,12 +5698,19 @@ void scr_stream_unshift(ScrStream *s, ScrBytes *chunk) { if (st->r.head_off > 0) { /* materialize the part-read head so the front slot is whole */ void *rest = scr_stream_rbuf_take(st, scr_stream_entry_len(st, st->r.buf[0]) - st->r.head_off); - scr_stream_rbuf_push(st, rest, st->r.encoded, true); + scr_stream_rbuf_push(st, rest, scr_stream_rkind(st), true); } scr_stream_add_chunk(s, chunk, true); } void scr_stream_unshift_str(ScrStream *s, ScrStr *str) { + if (s->st->has_r && s->st->r.object_entries) { + /* object mode: the string itself goes to the front */ + ScrDyn *v = scr_dyn_new_str(str); + scr_stream_unshift_dyn(s, v); + scr_dyn_release(v); + return; + } ScrBytes *b = scr_bytes_new(SCR_BYTES_U8, (double)str->len); memcpy(b->data, str->data, str->len); if (!str->ascii) scr_wtf8_to_utf8((char *)b->data, str->len); @@ -5422,6 +5719,46 @@ void scr_stream_unshift_str(ScrStream *s, ScrStr *str) { scr_bytes_release(b); } +/* unshift(value): null is the end of the stream (Node's onEofChunk); an + * object-mode stream takes any other value as one entry at the front; a + * byte-mode stream takes Buffers and strings, ignores undefined and + * reports another value as push does. Borrows d. */ +void scr_stream_unshift_dyn(ScrStream *s, const ScrDyn *d) { + ScrStreamState *st = s->st; + if (d->kind == SCR_DYN_NULL) { + scr_stream_push_null(s); + return; + } + if (st->has_r && st->r.object_entries) { + void *entry; + ScrEntryKind kind; + if (d->kind == SCR_DYN_STR) { + entry = scr_str_retain(d->v.str); + kind = SCR_ENTRY_STR; + } else if (d->kind == SCR_DYN_BYTES) { + entry = scr_bytes_raw_view(d->v.bytes); + kind = SCR_ENTRY_BYTES; + } else { + entry = scr_dyn_retain((ScrDyn *)d); + kind = SCR_ENTRY_DYN; + } + entry = scr_stream_entry_convert(st, entry, &kind); + scr_stream_rbuf_push(st, entry, kind, true); + if (st->r.need_readable) scr_stream_emit_readable_nt(s); + scr_stream_settle_next(s); + return; + } + if (d->kind == SCR_DYN_BYTES) { + ScrBytes *b = scr_bytes_raw_view(d->v.bytes); + scr_stream_unshift(s, b); + scr_bytes_release(b); + } else if (d->kind == SCR_DYN_STR) { + scr_stream_unshift_str(s, d->v.str); + } else { + scr_stream_push_dyn(s, d); + } +} + static void scr_stream_listener_error(ScrClosure *adapter, ScrError *error) { ScrDyn *fn = scr_dyn_listener_fn(adapter); ScrDyn *arg = scr_dyn_from_error(error); @@ -5431,9 +5768,9 @@ static void scr_stream_listener_error(ScrClosure *adapter, ScrError *error) { scr_dyn_release(fn); } -static void scr_stream_listener_data(ScrClosure *adapter, ScrBytes *bytes, ScrStr *string) { +static void scr_stream_listener_data(ScrClosure *adapter, ScrBytes *bytes, ScrStr *string, ScrDyn *value) { ScrDyn *fn = scr_dyn_listener_fn(adapter); - ScrDyn *arg = string ? scr_dyn_new_str(string) : scr_dyn_new_bytes(bytes); + ScrDyn *arg = value ? scr_dyn_retain(value) : string ? scr_dyn_new_str(string) : scr_dyn_new_bytes(bytes); ScrDyn *answer = scr_dyn_call(fn, &arg, 1, "listener"); scr_dyn_release(answer); scr_dyn_release(arg); @@ -5444,7 +5781,7 @@ ScrStream *scr_stream_on_dyn(ScrStream *s, ScrStr *name, ScrDyn *cb, bool once, if (name->len == 4 && !memcmp(name->data, "data", 4)) { return (ScrStream *)scr_emitter_on_dyn((ScrEmitter *)s, name, cb, scr_dyn_listener_closure_fn(cb, (void *)scr_stream_listener_data), - scr_ee_inv_fixed2, once, prepend); + scr_ee_inv_fixed3, once, prepend); } bool error = name->len == 5 && memcmp(name->data, "error", 5) == 0; bool empty = (name->len == 8 && memcmp(name->data, "readable", 8) == 0) || @@ -5468,13 +5805,15 @@ ScrStream *scr_stream_on_dyn(ScrStream *s, ScrStr *name, ScrDyn *cb, bool once, ScrDyn *scr_stream_read_dyn(ScrStream *s, ScrDyn *size) { double n = size && size->kind != SCR_DYN_UNDEF ? scr_dyn_number_coerce(size) : -1; if (scr_exc_pending()) return scr_dyn_undefined(); - bool is_str = false; - void *chunk = s->st->has_r ? scr_stream_read_n_kind(s, n, &is_str) : NULL; + ScrEntryKind kind = SCR_ENTRY_BYTES; + void *chunk = s->st->has_r ? scr_stream_read_n_kind(s, n, &kind) : NULL; if (!chunk) return scr_dyn_new_null(); - ScrDyn *result = is_str ? scr_dyn_new_str(chunk) : scr_dyn_new_buffer(chunk); - if (is_str) scr_str_release(chunk); - else scr_bytes_release(chunk); - return result; + if (kind == SCR_ENTRY_BYTES) { + ScrDyn *result = scr_dyn_new_buffer(chunk); + scr_bytes_release(chunk); + return result; + } + return scr_stream_entry_box(chunk, kind); } /* read() answers STRINGS once encoded: the static result type here is @@ -5491,13 +5830,28 @@ ScrBytes *scr_stream_read(ScrStream *s, double size) { /* a destroyed readable still DRAINS its buffer (Node: destroy leaves * buffered chunks readable; only _read refills are blocked) */ if (!s->st->has_r) return NULL; - if (s->st->r.encoded) { + if (s->st->r.encoded && !s->st->r.dyn_entries) { scr_stream_read_str_fence(); return NULL; } - bool is_str; - void *chunk = scr_stream_read_n_kind(s, size, &is_str); - if (chunk && is_str) { + ScrEntryKind kind; + void *chunk = scr_stream_read_n_kind(s, size, &kind); + if (chunk && kind == SCR_ENTRY_DYN) { + /* an object-mode value: a Buffer is the result, anything else is not */ + ScrDyn *d = chunk; + if (d->kind == SCR_DYN_BYTES) { + ScrBytes *b = scr_bytes_raw_view(d->v.bytes); + scr_dyn_release(d); + return b; + } + scr_dyn_release(d); + static const char msg[] = + "read() of an object-mode stream value that is not a Buffer is not supported yet in typed " + "code (consume 'data' events or for await)"; + scr_throw_error_msg(SCR_ERR_ERROR, msg, sizeof msg - 1); + return NULL; + } + if (chunk && kind == SCR_ENTRY_STR) { /* the read itself made the stream hold strings (a lazy Readable.from * pull's first string, or setEncoding() inside _read): the same * fence, never a string in the Buffer result */ @@ -5515,11 +5869,19 @@ ScrBytes *scr_stream_read(ScrStream *s, double size) { ScrStream *scr_stream_set_encoding(ScrStream *s, ScrStr *enc) { ScrStreamState *st = s->st; if (!st->has_r) return scr_stream_retain(s); + if (st->r.dyn_entries) { + /* an object-mode buffer of values: Buffers pushed later decode */ + if (st->r.enc) scr_str_release(st->r.enc); + st->r.enc = scr_str_retain(enc); + st->r.dec_pending = 0; + st->r.decoding = true; + return scr_stream_retain(s); + } if (st->r.encoded) { /* switching encodings mid-stream: fold the old decoder's pending * tail into the buffer first (rare; Node re-creates the decoder) */ ScrStr *tail = scr_strdec_end(st->r.enc, st->r.dec_pending); - if (scr_str_utf16_len(tail) > 0) scr_stream_rbuf_push(st, tail, true, false); + if (scr_str_utf16_len(tail) > 0) scr_stream_rbuf_push(st, tail, SCR_ENTRY_STR, false); else scr_str_release(tail); scr_str_release(st->r.enc); st->r.enc = scr_str_retain(enc); @@ -5575,7 +5937,7 @@ ScrStream *scr_stream_set_encoding(ScrStream *s, ScrStr *enc) { st->r.decoding = true; st->r.object_kind_set = true; if (content != NULL) { - if (scr_str_utf16_len(content) > 0) scr_stream_rbuf_push(st, content, true, false); + if (scr_str_utf16_len(content) > 0) scr_stream_rbuf_push(st, content, SCR_ENTRY_STR, false); else scr_str_release(content); } return scr_stream_retain(s); @@ -5647,7 +6009,8 @@ ScrStream *scr_stream_unpipe(ScrStream *src, ScrStream *dst) { /* ── the writable surface ─────────────────────────────────────────────── */ -static bool scr_stream_write_chunk(ScrStream *s, ScrBytes *chunk /*borrowed*/, ScrClosure *cb /*moves*/) { +static bool scr_stream_write_chunk_enc(ScrStream *s, ScrBytes *chunk /*borrowed*/, + ScrClosure *cb /*moves*/, ScrDyn *enc /*borrowed*/) { ScrStreamState *st = s->st; if (!st->has_w || st->destroyed || st->w.ending) { ScrError *e = st->has_w && !st->destroyed @@ -5658,12 +6021,12 @@ static bool scr_stream_write_chunk(ScrStream *s, ScrBytes *chunk /*borrowed*/, S scr_error_release(e); return false; } - size_t len = chunk->len; - st->w.length += len; + st->w.length += scr_stream_wchunk_len(st, chunk); + if (enc) enc = scr_dyn_retain(enc); if (st->w.writing || st->w.corked > 0 || st->constructing) { - scr_stream_wq_push(st, scr_bytes_retain(chunk), cb); + scr_stream_wq_push(st, scr_stream_wchunk_retain(st, chunk), cb, enc); } else { - scr_stream_do_write(s, scr_bytes_retain(chunk), cb); + scr_stream_do_write(s, scr_stream_wchunk_retain(st, chunk), cb, enc); } /* Node computes the answer AFTER the (possibly synchronous) write: * a sync completion has already drained its bytes from length. */ @@ -5672,7 +6035,28 @@ static bool scr_stream_write_chunk(ScrStream *s, ScrBytes *chunk /*borrowed*/, S return ret; } +static bool scr_stream_write_chunk(ScrStream *s, ScrBytes *chunk /*borrowed*/, ScrClosure *cb /*moves*/) { + return scr_stream_write_chunk_enc(s, chunk, cb, NULL); +} + +/* The encoding argument of a compiled write(chunk[, cb]) on an object-mode + * side: undefined, or null when the callback took its place (Node's + * write and end shift it there). */ +static ScrDyn *scr_stream_cb_enc(ScrClosure *cb) { return cb ? scr_dyn_new_null() : NULL; } + +/* write(value) on an object-mode writable side: the value is the chunk. + * Borrows v. */ +static bool scr_stream_write_value(ScrStream *s, ScrDyn *v, ScrClosure *cb) { + return scr_stream_write_chunk_enc(s, (ScrBytes *)v, cb, scr_stream_cb_enc(cb)); +} + bool scr_stream_write(ScrStream *s, ScrBytes *chunk, ScrClosure *cb) { + if (s->st->has_w && s->st->w.object_mode) { + ScrDyn *v = chunk->is_buffer ? scr_dyn_new_buffer(chunk) : scr_dyn_new_bytes(chunk); + bool ret = scr_stream_write_value(s, v, cb); + scr_dyn_release(v); + return ret; + } return scr_stream_write_chunk(s, chunk, cb); } @@ -5686,6 +6070,13 @@ bool scr_stream_write_null(ScrStream *s) { } bool scr_stream_write_str(ScrStream *s, ScrStr *str, ScrClosure *cb) { + if (s->st->has_w && s->st->w.object_mode) { + /* object mode: the string itself, not converted */ + ScrDyn *v = scr_dyn_new_str(str); + bool ret = scr_stream_write_value(s, v, cb); + scr_dyn_release(v); + return ret; + } ScrBytes *b = scr_bytes_new(SCR_BYTES_U8, (double)str->len); memcpy(b->data, str->data, str->len); if (!str->ascii) scr_wtf8_to_utf8((char *)b->data, str->len); @@ -5695,10 +6086,65 @@ bool scr_stream_write_str(ScrStream *s, ScrStr *str, ScrClosure *cb) { return ret; } +/* write(chunk, encoding, cb) with the encoding argument as passed (NULL + * or undefined when absent): an object-mode side hands it to _write + * unchanged; a byte-mode side encodes a string chunk with it. Borrows + * chunk and enc. */ +bool scr_stream_write_enc(ScrStream *s, const ScrDyn *chunk, ScrDyn *enc, ScrClosure *cb) { + if (enc && enc->kind == SCR_DYN_UNDEF) enc = NULL; + ScrStreamState *st = s->st; + if (chunk->kind != SCR_DYN_NULL && st->has_w && st->w.object_mode) + return scr_stream_write_chunk_enc(s, (ScrBytes *)chunk, cb, enc); + /* Node's _write: an encoding other than 'buffer' must be a known one + * (ERR_UNKNOWN_ENCODING, thrown), and a string chunk is encoded with + * it; an empty or nullish encoding is the default */ + ScrStr *name = NULL; + if (enc && enc->kind != SCR_DYN_NULL && !(enc->kind == SCR_DYN_STR && enc->v.str->len == 0)) + name = scr_dyn_string_coerce_js(enc); + if (name && !(name->len == 6 && memcmp(name->data, "buffer", 6) == 0)) { + ScrStr *canonical = scr_bytes_normalize_encoding(name); + scr_str_release(name); + if (canonical == NULL) { + if (cb) scr_closure_release(cb); + return false; + } + if (chunk->kind != SCR_DYN_STR) { + scr_str_release(canonical); + return scr_stream_write_dyn(s, chunk, cb); + } + ScrBytes *b = scr_bytes_from_str(chunk->v.str, canonical); + scr_str_release(canonical); + scr_stream_buffer(b); + bool ret = scr_stream_write_chunk(s, b, cb); + scr_bytes_release(b); + return ret; + } + if (name && chunk->kind == SCR_DYN_STR) { + /* Buffer.from(string, 'buffer') */ + scr_str_release(name); + if (cb) scr_closure_release(cb); + static const char msg[] = "Unknown encoding: buffer"; + scr_throw_error_msg_code(SCR_ERR_TYPE, msg, sizeof msg - 1, "ERR_UNKNOWN_ENCODING"); + return false; + } + if (name) scr_str_release(name); + return scr_stream_write_dyn(s, chunk, cb); +} + ScrStream *scr_stream_end(ScrStream *s, ScrBytes *chunk_b, ScrStr *chunk_s, ScrClosure *cb) { ScrStreamState *st = s->st; - if (chunk_b) scr_stream_write(s, chunk_b, NULL); - else if (chunk_s) scr_stream_write_str(s, chunk_s, NULL); + if ((chunk_b || chunk_s) && st->has_w && st->w.object_mode) { + /* end(chunk[, cb]) writes with the encoding Node's end leaves: + * undefined, or null when the callback took its place */ + ScrDyn *v = chunk_s ? scr_dyn_new_str(chunk_s) + : chunk_b->is_buffer ? scr_dyn_new_buffer(chunk_b) : scr_dyn_new_bytes(chunk_b); + scr_stream_write_chunk_enc(s, (ScrBytes *)v, NULL, scr_stream_cb_enc(cb)); + scr_dyn_release(v); + } else if (chunk_b) { + scr_stream_write(s, chunk_b, NULL); + } else if (chunk_s) { + scr_stream_write_str(s, chunk_s, NULL); + } if (!st->has_w || st->destroyed) { if (cb) scr_closure_release(cb); return scr_stream_retain(s); @@ -5778,7 +6224,12 @@ void scr_stream_write_done(ScrStream *s, ScrError *err /*moves*/) { return; } if (was_sync) { + bool hold = ucb != NULL || (st->w.need_drain && st->w.length == 0) || st->destroyed; scr_st_tick(s, SCR_ST_AFTER_WRITE, NULL, ucb); + if (hold) { + scr_st_tail->hold = true; + st->w.after_holds++; + } } else { scr_stream_after_write(s, ucb); } @@ -5837,6 +6288,8 @@ void scr_stream_transform_done(ScrStream *s, ScrError *err /*moves*/, ScrBytes * if (!err) { if (data) { scr_stream_add_chunk(s, data, false); + } else if (data_str && s->st->r.object_entries) { + scr_stream_push_str(s, data_str); /* object mode: the string itself */ } else if (data_str) { ScrBytes *b = scr_bytes_new(SCR_BYTES_U8, (double)data_str->len); memcpy(b->data, data_str->data, data_str->len); @@ -5870,6 +6323,9 @@ void scr_stream_flush_done(ScrStream *s, ScrError *err /*moves*/, ScrBytes *data if (data) { scr_stream_add_chunk(s, data, false); scr_bytes_release(data); + } else if (data_str && st->r.object_entries) { + scr_stream_push_str(s, data_str); /* object mode: the string itself */ + scr_str_release(data_str); } else if (data_str) { ScrBytes *b = scr_bytes_new(SCR_BYTES_U8, (double)data_str->len); memcpy(b->data, data_str->data, data_str->len); @@ -5885,6 +6341,38 @@ void scr_stream_flush_done(ScrStream *s, ScrError *err /*moves*/, ScrBytes *data scr_stream_finish_maybe(s); } +/* The transform and flush completions with a checked-dynamic value (an + * object-mode Transform's data, or an untyped callback's): null and + * undefined push nothing (Transform's `val != null`), and any other value + * is pushed as push(value) pushes it. v borrowed (NULL: absent). */ +static bool scr_stream_done_pushes(const ScrDyn *v) { + return v && v->kind != SCR_DYN_NULL && v->kind != SCR_DYN_UNDEF; +} + +void scr_stream_transform_done_value(ScrStream *s, ScrError *err /*moves*/, ScrDyn *v) { + if (!err && scr_stream_done_pushes(v)) scr_stream_push_dyn(s, v); + if (scr_exc_pending()) { + if (err) scr_error_release(err); + return; + } + if (err) scr_stream_write_done(s, err); + else scr_stream_transform_callback(s); +} + +void scr_stream_flush_done_value(ScrStream *s, ScrError *err /*moves*/, ScrDyn *v) { + s->st->flush_pending = false; + if (err) { + scr_stream_error_or_destroy(s, err); + scr_error_release(err); + return; + } + if (scr_stream_done_pushes(v)) scr_stream_push_dyn(s, v); + if (scr_exc_pending()) return; + scr_stream_push_null(s); + if (scr_exc_pending()) return; + scr_stream_finish_maybe(s); +} + /* ── destroy ──────────────────────────────────────────────────────────── */ /* Node's _destroy step: the destroy hook, or 'error'/'close' on ticks. */ @@ -5970,6 +6458,7 @@ double scr_stream_prop(ScrStream *s, const char *name) { if (strcmp(name, "readableLength") == 0) return (double)st->r.length; if (strcmp(name, "readableHighWaterMark") == 0) return (double)st->r.hwm; if (strcmp(name, "readableObjectMode") == 0) return st->r.object_entries; + if (strcmp(name, "writableObjectMode") == 0) return st->w.object_mode; if (strcmp(name, "writable") == 0) { return st->has_w && !st->destroyed && !st->w.ending && !st->errored; } @@ -6011,7 +6500,7 @@ double scr_stream_prop(ScrStream *s, const char *name) { if (strcmp(p, "destroyed") == 0) return st->destroyed; if (strcmp(p, "errorEmitted") == 0) return st->error_emitted; if (strcmp(p, "needDrain") == 0) return st->w.need_drain; - if (strcmp(p, "objectMode") == 0) return 0; + if (strcmp(p, "objectMode") == 0) return st->w.object_mode; if (strcmp(p, "constructed") == 0) return 1; if (strcmp(p, "closed") == 0) return st->close_emitted; if (strcmp(p, "length") == 0) return (double)st->w.length; @@ -6209,6 +6698,7 @@ static void scr_stream_run_tick(ScrStreamTick *t) { break; case SCR_ST_AFTER_WRITE: { st->w.after_scheduled = false; + if (t->hold) st->w.after_holds--; ScrClosure *cb = t->cb; t->cb = NULL; if (!st->destroyed) { @@ -6272,6 +6762,13 @@ static void scr_stream_run_tick(ScrStreamTick *t) { if (!st->close_emitted) { st->close_emitted = true; if (st->emit_close) scr_stream_emit0(s, "close"); + if (st->r.next_eof_close && !scr_exc_pending()) { + /* the iterator's eos 'close' listener completes the loop */ + st->r.next_eof_close = false; + ScrPromise *w = st->r.next_waiter; + st->r.next_waiter = NULL; + if (w) scr_stream_next_fulfill(st, w, NULL, SCR_ENTRY_BYTES); + } /* eos ordering: the finished()/pipeline watchers fire right after * 'close' (Node's willEmitClose stance). */ scr_stream_notify_finished(s); @@ -6318,9 +6815,9 @@ static void scr_stream_run_tick(ScrStreamTick *t) { } case SCR_ST_READ0: { if (st->has_r) { - bool is_str; - void *chunk = scr_stream_read_n_kind(s, 0, &is_str); - if (chunk) scr_stream_entry_release_kind(chunk, is_str); + ScrEntryKind kind; + void *chunk = scr_stream_read_n_kind(s, 0, &kind); + if (chunk) scr_stream_entry_release_kind(chunk, kind); } break; } @@ -6347,10 +6844,13 @@ static void scr_stream_run_tick(ScrStreamTick *t) { break; } case SCR_ST_NEXT_EOF: { - if (st->r.next_waiter && st->r.length == 0 && (st->r.ended || st->destroyed)) { + if (st->r.next_waiter && st->r.length == 0 && st->r.ended && st->r.next_close_wait && + !st->close_emitted) { + st->r.next_eof_close = true; + } else if (st->r.next_waiter && st->r.length == 0 && (st->r.ended || st->destroyed)) { ScrPromise *w = st->r.next_waiter; st->r.next_waiter = NULL; - scr_stream_next_fulfill(st, w, NULL, false); + scr_stream_next_fulfill(st, w, NULL, SCR_ENTRY_BYTES); } else if (st->r.next_waiter) { scr_stream_settle_next(s); /* content raced in: deliver it */ } @@ -6475,16 +6975,6 @@ static const ScrDynHandleTransform *scr_handle_step_ops(ScrClosure *cb) { return scr_handle_step_handle_ops(cb)->transform; } -/* The pending exception as a stream error (+1). */ -static ScrError *scr_handle_step_caught(void) { - ScrCaught *caught = scr_exc_take(); - ScrDyn *value = scr_caught_to_dyn(caught); - scr_caught_release(caught); - ScrError *err = value ? scr_errdyn_err_of(value) : NULL; - if (value) scr_dyn_release(value); - return err ? err : scr_stream_mkerr("ERR_INTERNAL_ASSERTION", "a Transform handle threw a value that is not an Error"); -} - static void scr_handle_step_transform(ScrClosure *cb, ScrStream *s, ScrBytes *chunk) { /* A throwing update() leaves the exception pending: it throws out of * the write, as Node's _transform does. */ @@ -6494,7 +6984,7 @@ static void scr_handle_step_transform(ScrClosure *cb, ScrStream *s, ScrBytes *ch static void scr_handle_step_flush(ScrClosure *cb, ScrStream *s) { ScrBytes *out = scr_handle_step_ops(cb)->final(scr_handle_step_handle(cb)); - scr_stream_flush_done(s, out ? NULL : scr_handle_step_caught(), out, NULL); + scr_stream_flush_done(s, out ? NULL : scr_stream_take_error(), out, NULL); } static void scr_handle_step_destroy(ScrClosure *cb, ScrStream *s, ScrError *err) { diff --git a/packages/runtime/src/scr_stream.h b/packages/runtime/src/scr_stream.h index 1e5b789e7..8b8a72406 100644 --- a/packages/runtime/src/scr_stream.h +++ b/packages/runtime/src/scr_stream.h @@ -272,6 +272,7 @@ ScrStream *scr_stream_set_push_encoding(ScrStream *s, ScrStr *enc); bool scr_stream_push_null(ScrStream *s); void scr_stream_unshift(ScrStream *s, ScrBytes *chunk); void scr_stream_unshift_str(ScrStream *s, ScrStr *str); +void scr_stream_unshift_dyn(ScrStream *s, const ScrDyn *d); ScrBytes *scr_stream_read(ScrStream *s, double size); ScrStream *scr_stream_on_dyn(ScrStream *s, ScrStr *name, ScrDyn *cb, bool once, bool prepend); ScrDyn *scr_stream_read_dyn(ScrStream *s, ScrDyn *size); /* +1 or NULL; size < 0 = all/first; throws once encoded (the static type is Buffer|null) */ @@ -291,6 +292,8 @@ typedef struct ScrPromise ScrPromise; /* full declaration below (C11 repeat) */ * each chunk before asking again). */ bool scr_stream_push_dyn(ScrStream *s, const ScrDyn *d); /* borrows d; tag dispatch */ bool scr_stream_write_dyn(ScrStream *s, const ScrDyn *d, ScrClosure *cb); /* cb moves */ +/* write(chunk, encoding, cb): enc borrowed, NULL or undefined when absent */ +bool scr_stream_write_enc(ScrStream *s, const ScrDyn *chunk, ScrDyn *enc, ScrClosure *cb); ScrPromise *scr_stream_next_chunk(ScrStream *s); ScrPromise *scr_stream_next_chunk_dyn(ScrStream *s); /* node:stream/promises — the promise forms over the finished/pipeline @@ -475,6 +478,28 @@ void scr_stream_final_done(ScrStream *s, ScrError *err); void scr_stream_destroy_done(ScrStream *s, ScrError *err); void scr_stream_transform_done(ScrStream *s, ScrError *err, ScrBytes *data, ScrStr *data_str); void scr_stream_flush_done(ScrStream *s, ScrError *err, ScrBytes *data, ScrStr *data_str); +/* The same completions with a checked-dynamic data value (borrowed; NULL, + * null and undefined push nothing): an object-mode Transform's data, or an + * untyped callback's. err moves. */ +/* objectMode: scr_stream_set_object_mode applies the option's sides right + * after construction; scr_stream_writes_values answers whether the write + * and transform steps receive dyn values instead of Buffers. */ +void scr_stream_set_object_mode(ScrStream *s, bool readable, bool writable); +ScrStream *scr_stream_object_mode(ScrStream *s, bool readable, bool writable); +bool scr_stream_writes_values(ScrStream *s); +/* A compiled write or transform callback's chunk by its declared type: a + * Buffer (+1, or NULL with a TypeError for another object-mode value), a + * dyn value (+1), and the encoding argument (+1). */ +ScrBytes *scr_stream_chunk_bytes(ScrStream *s, ScrBytes *chunk); +struct ScrDyn *scr_stream_chunk_value(ScrStream *s, ScrBytes *chunk); +ScrStr *scr_stream_chunk_encoding(ScrStream *s); +ScrDyn *scr_stream_chunk_encoding_value(ScrStream *s); +/* emit('data', value) with a checked-dynamic chunk (borrowed). */ +bool scr_stream_emit_value(ScrStream *s, ScrStr *name, struct ScrDyn *v); +/* Whether a dyn for-await step (readable.nextChunkDyn) answered the end. */ +bool scr_stream_chunk_done(const struct ScrDyn *v); +void scr_stream_transform_done_value(ScrStream *s, ScrError *err, struct ScrDyn *v); +void scr_stream_flush_done_value(ScrStream *s, ScrError *err, struct ScrDyn *v); /* Runtime-backed streams (the fs streams in scr_fs_stream.c). * - construct: Node's _construct. The hook runs on a tick after diff --git a/packages/runtime/src/scr_stream_ops.c b/packages/runtime/src/scr_stream_ops.c index d32dd5d67..8fd220168 100644 --- a/packages/runtime/src/scr_stream_ops.c +++ b/packages/runtime/src/scr_stream_ops.c @@ -305,7 +305,7 @@ static ScrPromise *src_next(OpSrc *src) { } static bool src_done(const OpSrc *src, const ScrDyn *v) { - return src->s ? v->kind == SCR_DYN_UNDEF : v == op_k_done; + return src->s ? scr_stream_chunk_done(v) : v == op_k_done; } /* The Readable iterator's return(): the iterator generator resumes at its @@ -1492,16 +1492,6 @@ static void op_from_step_k(void *self, ScrDyn *v) { } int pushed = scr_stream_push_object(out, v); scr_dyn_release(v); - if (pushed == -2) { - op_from_destroy_with(out, op_unsupported( - "a stream operator output that mixes strings and Buffers")); - return; - } - if (pushed < 0) { - op_from_destroy_with(out, op_unsupported( - "a stream operator value that is not a string or Buffer (object-mode streams of other values)")); - return; - } if (pushed) { from_next(f, out); return; diff --git a/packages/runtime/src/scr_tls.c b/packages/runtime/src/scr_tls.c index b4d125cb2..e7436ab69 100644 --- a/packages/runtime/src/scr_tls.c +++ b/packages/runtime/src/scr_tls.c @@ -368,6 +368,10 @@ typedef struct ScrTlsSrv { /* maxVersion TLSv1.2: the ClientHello is pre-parsed so a hello that * offers only later versions gets OpenSSL's protocol_version alert. */ bool max_tls12; + /* new tls.TLSSocket(socket, { secureContext }): the context whose + * certificate and key this config serves (+1; NULL when cert/key are the + * config's own) */ + ScrSecureCtx *ctx; } ScrTlsSrv; /* The ALPN answers (static storage — mbedTLS keeps the pointers). Plain @@ -405,6 +409,28 @@ static ScrTlsSrv *scr_tls_srv_new(const char *cert, size_t cert_len, const char return s; } +/* A server config that serves a SecureContext's certificate and key. */ +static ScrTlsSrv *scr_tls_srv_from_ctx(ScrSecureCtx *ctx /*borrowed*/) { + scr_tls_rng_init(); + ScrTlsSrv *s = calloc(1, sizeof *s); + if (!s) scr_tls_oom(); + s->rc = 1; + mbedtls_x509_crt_init(&s->cert); + mbedtls_pk_init(&s->pk); + mbedtls_ssl_config_init(&s->conf); + if (mbedtls_ssl_config_defaults(&s->conf, MBEDTLS_SSL_IS_SERVER, MBEDTLS_SSL_TRANSPORT_STREAM, + MBEDTLS_SSL_PRESET_DEFAULT) != 0) { + scr_tls_oom(); + } + mbedtls_ssl_conf_rng(&s->conf, mbedtls_ctr_drbg_random, &scr_tls_drbg); + mbedtls_ssl_conf_authmode(&s->conf, MBEDTLS_SSL_VERIFY_NONE); + s->ctx = scr_secure_ctx_retain(ctx); + if (mbedtls_ssl_conf_own_cert(&s->conf, &ctx->cert, &ctx->pk) != 0) { + scr_tls_bad_material("certificate/key pair", 0); + } + return s; +} + static ScrTlsSrv *scr_tls_srv_retain(ScrTlsSrv *s) { s->rc++; return s; @@ -419,6 +445,7 @@ static void scr_tls_srv_release(ScrTlsSrv *s) { mbedtls_x509_crt_free(&s->cert); mbedtls_pk_free(&s->pk); scr_tls_alpn_free(s->alpn); + scr_secure_ctx_release(s->ctx); free(s); } @@ -629,6 +656,15 @@ typedef struct ScrTlsSock { unsigned char *hello; /* the raw ClientHello flight, replayed at DONE */ size_t hello_len, hello_cap; ScrSecureCtx *sni_ctx; /* owned; the answer (NULL = default cert) */ + /* new tls.TLSSocket(socket, { isServer: true }): the program holds this + * socket before the handshake, so a failed handshake is its 'error' + * (tls.createServer's sockets fail without one) */ + bool wrapped; + unsigned char head[5]; /* the first bytes the peer sent (the error's wording) */ + size_t head_len; + /* the 'secure' event's listeners */ + ScrNetLs secure_ls; + bool settled; /* the socket closed: listeners added later never fire */ } ScrTlsSock; /* checkServerIdentity for an IP literal: the address must be one of the @@ -703,6 +739,16 @@ static int scr_tls_bio_send(void *ctx, const unsigned char *buf, size_t len) { return MBEDTLS_ERR_NET_SEND_FAILED; } +/* Keeps the first bytes a wrapped socket's peer sends: a handshake that + * fails on them reports what they looked like, as OpenSSL does. */ +static void scr_tls_note_head(ScrTlsSock *t, const unsigned char *buf, size_t n) { + if (!t->wrapped || t->head_len == sizeof t->head) return; + size_t take = sizeof t->head - t->head_len; + if (take > n) take = n; + memcpy(t->head + t->head_len, buf, take); + t->head_len += take; +} + static int scr_tls_bio_recv(void *ctx, unsigned char *buf, size_t len) { ScrTlsSock *t = (ScrTlsSock *)ctx; int fd = scr_net_sock_fd(t->sock); @@ -710,8 +756,12 @@ static int scr_tls_bio_recv(void *ctx, unsigned char *buf, size_t len) { /* bytes a consumer peeked and unshifted (the demux path) re-enter the * stream here, ahead of the kernel's */ size_t took = scr_net_sock_take_buffered(t->sock, (char *)buf, len); - if (took > 0) return (int)took; + if (took > 0) { + scr_tls_note_head(t, buf, took); + return (int)took; + } ssize_t n = read(fd, buf, len); + if (n > 0) scr_tls_note_head(t, buf, (size_t)n); if (n >= 0) return (int)n; /* 0 = EOF; mbedTLS maps it to CONN_EOF */ if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR) { return MBEDTLS_ERR_SSL_WANT_READ; @@ -1026,6 +1076,43 @@ void scr_tls_sni_answer(ScrClosure *self, bool has_err, ScrSecureCtx *ctx /*move scr_net_sock_release(sock); } +/* A wrapped server socket's failed handshake, reported as Node reports it + * on the TLSSocket: a peer that closed first ends the stream, a reset is + * ECONNRESET, and bytes that are not a TLS record name what they were + * (OpenSSL's reasons without the per-process prefix and source location, + * SEMANTICS.md divergence 55). */ +static void scr_tls_wrapped_failure(ScrTlsSock *t, int r) { + if (r == MBEDTLS_ERR_SSL_CONN_EOF) { + scr_net_sock_transport_eof(t->sock); + return; + } + if (r == MBEDTLS_ERR_NET_CONN_RESET) { + scr_net_sock_transport_error(t->sock, "read ECONNRESET"); + return; + } + const unsigned char *h = t->head; + bool record = t->head_len >= 3 && h[0] >= 20 && h[0] <= 23 && h[1] == 3; + if (t->head_len >= 5 && !record) { + static const char *const http[] = { "GET ", "POST ", "HEAD ", "PATCH", "OPTIO", + "DELET", "TRACE", "PUT ", NULL }; + const char *reason = "error:0A00010B:SSL routines:tls_validate_record_header:wrong version number"; + const char *code = "ERR_SSL_WRONG_VERSION_NUMBER"; + for (size_t i = 0; http[i] != NULL; i++) { + if (memcmp(h, http[i], strlen(http[i])) == 0) { + reason = "error:0A00009C:SSL routines:tls_validate_record_header:http request"; + code = "ERR_SSL_HTTP_REQUEST"; + } + } + if (memcmp(h, "CONNE", 5) == 0) { + reason = "error:0A00009B:SSL routines:tls_validate_record_header:https proxy request"; + code = "ERR_SSL_HTTPS_PROXY_REQUEST"; + } + scr_net_sock_transport_error_code(t->sock, reason, code); + return; + } + scr_net_sock_transport_error(t->sock, "server TLS handshake failed"); +} + static int scr_tls_handshake(void *tctx) { ScrTlsSock *t = (ScrTlsSock *)tctx; if (t->cli != NULL && t->cli->no_protocols) { @@ -1046,6 +1133,10 @@ static int scr_tls_handshake(void *tctx) { scr_tls_current = NULL; if (r == 0) return 1; if (r == MBEDTLS_ERR_SSL_WANT_READ || r == MBEDTLS_ERR_SSL_WANT_WRITE) return 0; + if (t->srv != NULL && t->wrapped) { + scr_tls_wrapped_failure(t, r); + return -1; + } if (t->srv != NULL) { /* Node's server default: 'tlsClientError' with no listener destroys * the socket without an error event — exactly what portless (no @@ -1133,11 +1224,31 @@ static void scr_tls_free(void *tctx) { if (t->srv) scr_tls_srv_release(t->srv); if (t->cli) scr_tls_cli_free(t->cli); scr_net_ls_drop(&t->session_ls); + scr_net_ls_drop(&t->secure_ls); free(t->hello); scr_secure_ctx_release(t->sni_ctx); free(t); } +/* The 'secure' event: after a client's secureConnect listeners, as Node + * emits it. */ +static void scr_tls_after_established(void *tctx) { + ScrTlsSock *t = (ScrTlsSock *)tctx; + scr_net_fire0_this(&t->secure_ls, t->sock, SCR_DYNH_NET_SOCKET); + scr_net_ls_drop(&t->secure_ls); +} + +/* The closed socket settles: its 'secure' and 'session' listeners can no + * longer fire, and a listener that captures the socket would otherwise + * keep it, and this transport, alive (a handshake that fails never fires + * 'secure'). */ +static void scr_tls_settle(void *tctx) { + ScrTlsSock *t = (ScrTlsSock *)tctx; + t->settled = true; + scr_net_ls_drop(&t->secure_ls); + scr_net_ls_drop(&t->session_ls); +} + static const ScrNetTransportOps SCR_TLS_OPS = { .xread = scr_tls_xread, .xwrite = scr_tls_xwrite, @@ -1146,6 +1257,8 @@ static const ScrNetTransportOps SCR_TLS_OPS = { .pending = scr_tls_pending, .shutdown_write = scr_tls_shutdown_write, .free = scr_tls_free, + .settle = scr_tls_settle, + .after_established = scr_tls_after_established, }; /* ── the server surface ──────────────────────────────────────────────── */ @@ -2088,6 +2201,129 @@ ScrNetServer *scr_https_create_server_dyn(const ScrDyn *opts /*borrowed*/, return s; } +/* ── new tls.TLSSocket(socket, options) ───────────────────────────────── */ + +/* The server-mode wrap: a fresh TLS socket takes over raw's connection + * (scr_net_sock_adopt) and runs the server handshake over it. It serves + * `ctx` (options.secureContext, which the lowering passes apart from the + * record; borrowed, nullable) or the record's cert and key. The record + * reads at runtime like tls.createServer's: ALPNProtocols, minVersion and + * maxVersion apply, and options without a lowering throw the fence. Node + * never starts the handshake of a client-mode TLSSocket (no isServer), so + * that form throws the fence too. +1, or NULL with the exception pending. */ +static const char *const SCR_TLS_WRAP_FENCED_OPTIONS[] = { + "server", "session", "requestOCSP", "pskCallback", "enableTrace", NULL, +}; + +/* The socket-level options of a wrap that serves a secureContext, which + * the server-options walk would otherwise check: requestCert (client + * certificates are not modeled), SNICallback and ALPNCallback fence. + * False with the exception pending. */ +static bool scr_tls_wrap_socket_opts_check(const ScrDyn *record, const char *api) { + for (size_t i = 0; record != NULL && i < record->v.obj.len; i++) { + const ScrDynEntry *e = &record->v.obj.entries[i]; + if (e->value->kind == SCR_DYN_UNDEF) continue; + if (strcmp(e->key, "requestCert") == 0) { + if (!scr_tls_dyn_truthy(e->value)) continue; + scr_tls_opt_fence(api, "requestCert", + "client-certificate handshakes are not modeled (SEMANTICS.md divergence 55) — requestCert stays at Node's false default"); + return false; + } + if (strcmp(e->key, "SNICallback") == 0 || strcmp(e->key, "ALPNCallback") == 0) { + scr_tls_opt_fence(api, e->key, + "isServer, cert, key, secureContext, ALPNProtocols, minVersion and maxVersion are the implemented members"); + return false; + } + } + return true; +} + +ScrNetSocket *scr_tls_wrap_socket(ScrNetSocket *raw, const ScrDyn *opts /*borrowed*/, + ScrSecureCtx *ctx /*borrowed, nullable*/) { + static const char api[] = "new tls.TLSSocket"; + bool is_server = false; + const ScrDyn *record = opts != NULL && opts->kind == SCR_DYN_OBJ ? opts : NULL; + for (size_t i = 0; record != NULL && i < record->v.obj.len; i++) { + const ScrDynEntry *e = &record->v.obj.entries[i]; + if (e->value->kind == SCR_DYN_UNDEF) continue; + if (strcmp(e->key, "isServer") == 0) { + is_server = scr_tls_dyn_truthy(e->value); + continue; + } + if (strcmp(e->key, "secureContext") == 0) { + scr_tls_opt_fence(api, "secureContext", + "secureContext lowers as a member of an options object literal"); + return NULL; + } + for (size_t k = 0; SCR_TLS_WRAP_FENCED_OPTIONS[k] != NULL; k++) { + if (strcmp(e->key, SCR_TLS_WRAP_FENCED_OPTIONS[k]) == 0) { + scr_tls_opt_fence(api, e->key, + "isServer, cert, key, secureContext, ALPNProtocols, minVersion and maxVersion are the implemented members"); + return NULL; + } + } + } + if (!is_server) { + scr_tls_opt_fence(api, "isServer", + "Node starts no handshake for a client-mode TLSSocket; tls.connect({ socket }) runs the client handshake over an open socket"); + return NULL; + } + /* Node serves a secureContext as it is: the record's cert, key and + * other context options are not read then. The socket's own options + * still apply, so the ones without a lowering fence either way: + * requestCert asks for a client certificate this runtime cannot + * verify. */ + int min_version = 12, max_version = 13; + ScrBytes *cert = NULL, *key = NULL; + if (ctx == NULL) { + if (!scr_tls_versions_dyn(record, &min_version, &max_version)) return NULL; + if (!scr_tls_srv_opts_walk_ex(record, api, &cert, &key, true)) return NULL; + } else if (!scr_tls_wrap_socket_opts_check(record, api)) { + return NULL; + } + char **alpn = NULL; + const ScrDyn *protocols = record != NULL ? scr_dyn_obj_get(record, "ALPNProtocols", 13) : NULL; + if (protocols != NULL && protocols->kind != SCR_DYN_UNDEF && !scr_tls_alpn_dyn(protocols, &alpn)) { + scr_bytes_release(cert); + scr_bytes_release(key); + return NULL; + } + ScrTlsSrv *srv = ctx != NULL ? scr_tls_srv_from_ctx(ctx) + : scr_tls_srv_new((const char *)cert->data, cert->len, + (const char *)key->data, key->len, false); + scr_bytes_release(cert); + scr_bytes_release(key); + srv->alpn = alpn; /* moves */ + if (alpn != NULL) mbedtls_ssl_conf_alpn_protocols(&srv->conf, (const char **)srv->alpn); + if (!scr_tls_conf_versions(&srv->conf, min_version, max_version)) { + scr_tls_srv_release(srv); + scr_tls_opt_fence(api, "maxVersion", + "a version range with neither TLSv1.2 nor TLSv1.3 has no lowering"); + return NULL; + } + srv->max_tls12 = max_version == 12; + ScrNetSocket *t = scr_net_sock_adopt(raw); + if (t == NULL) { + scr_tls_srv_release(srv); + static const char msg[] = + "new tls.TLSSocket over a socket that is not connected, or is already wrapped, is not supported yet"; + scr_throw_error_msg(SCR_ERR_ERROR, msg, sizeof msg - 1); + return NULL; + } + ScrTlsSock *ts = calloc(1, sizeof *ts); + if (!ts) scr_tls_oom(); + mbedtls_ssl_init(&ts->ssl); + if (mbedtls_ssl_setup(&ts->ssl, &srv->conf) != 0) scr_tls_oom(); + ts->sock = t; + ts->srv = srv; /* moves */ + ts->wrapped = true; + if (srv->max_tls12) ts->sni_state = SCR_SNI_COLLECT; /* pre-parse the hello */ + mbedtls_ssl_set_bio(&ts->ssl, ts, scr_tls_bio_send, scr_tls_bio_recv, NULL); + scr_net_sock_set_transport(t, &SCR_TLS_OPS, ts); + scr_net_sock_adopt_start(t); + return t; +} + /* ── tls.connect (the TLS client socket) ──────────────────────────────── */ /* Node's inspect of a Buffer inside ERR_INVALID_ARG_VALUE's message: @@ -2239,7 +2475,7 @@ static const char *const SCR_TLS_CONNECT_FENCED_OPTIONS[] = { "localAddress", "localPort", "lookup", "minDHSize", "noDelay", "onread", "passphrase", "path", "pfx", "privateKeyEngine", "privateKeyIdentifier", "pskCallback", "readable", "secureContext", "secureOptions", "secureProtocol", "session", - "sessionIdContext", "sigalgs", "signal", "socket", "ticketKeys", + "sessionIdContext", "sigalgs", "signal", "ticketKeys", "writable", NULL, }; @@ -2255,6 +2491,7 @@ ScrNetSocket *scr_tls_connect_dyn(double port, ScrStr *host /*borrowed, nullable const ScrDyn *alpn_value = NULL; const ScrDyn *timeout_value = NULL; int min_version = 12, max_version = 13; + ScrNetSocket *wrap = NULL; /* options.socket: the open socket to take over */ bool ok = true; if (opts != NULL && opts->kind != SCR_DYN_UNDEF && opts->kind != SCR_DYN_NULL) { if (opts->kind != SCR_DYN_OBJ) { @@ -2322,6 +2559,15 @@ ScrNetSocket *scr_tls_connect_dyn(double port, ScrStr *host /*borrowed, nullable timeout_value = scr_tls_dyn_truthy(v) ? v : NULL; continue; } + if (strcmp(e->key, "socket") == 0) { + if (v->kind == SCR_DYN_HANDLE && v->v.handle.tag == SCR_DYNH_NET_SOCKET) { + wrap = (ScrNetSocket *)v->v.handle.ptr; + } else { + scr_tls_opt_fence("tls.connect", "socket", "a net.Socket is the implemented socket value"); + ok = false; + } + continue; + } if (strcmp(e->key, "servername") == 0) { if (v->kind == SCR_DYN_STR) servername = v->v.str; else { @@ -2333,7 +2579,7 @@ ScrNetSocket *scr_tls_connect_dyn(double port, ScrStr *host /*borrowed, nullable for (size_t k = 0; SCR_TLS_CONNECT_FENCED_OPTIONS[k] != NULL; k++) { if (strcmp(e->key, SCR_TLS_CONNECT_FENCED_OPTIONS[k]) == 0) { scr_tls_opt_fence("tls.connect", e->key, - "port/host/rejectUnauthorized/ca/servername/ALPNProtocols/minVersion/maxVersion/timeout are the implemented members of a runtime options record"); + "port/host/socket/rejectUnauthorized/ca/servername/ALPNProtocols/minVersion/maxVersion/timeout are the implemented members of a runtime options record"); ok = false; break; } @@ -2361,7 +2607,7 @@ ScrNetSocket *scr_tls_connect_dyn(double port, ScrStr *host /*borrowed, nullable timeout_ms = timeout_value->v.num; } } - if (ok && port < 0) { + if (ok && port < 0 && wrap == NULL) { scr_dyn_arg_type_fail("options.port", "of type number", scr_dyn_undefined()); ok = false; } @@ -2377,6 +2623,9 @@ ScrNetSocket *scr_tls_connect_dyn(double port, ScrStr *host /*borrowed, nullable * name otherwise (Node's servername default); "" sends no SNI. */ ScrStr *verify_host = dial_host; ScrStr *localhost_fallback = NULL; + /* over options.socket, the host that socket dialed (Node's socket._host) */ + ScrStr *socket_host = verify_host == NULL && wrap != NULL ? scr_net_sock_dial_name(wrap) : NULL; + if (socket_host != NULL) verify_host = socket_host; if (verify_host == NULL) { localhost_fallback = scr_str_new("localhost", 9); verify_host = localhost_fallback; @@ -2384,9 +2633,25 @@ ScrNetSocket *scr_tls_connect_dyn(double port, ScrStr *host /*borrowed, nullable /* Resolve for the dial only (the http client's rule): verify_host above * already holds the NAME, which is what SNI and certificate verification * must see. A null dial_host stays null — scr_net_connect's own arm. */ - ScrStr *dial_addr = dial_host != NULL ? scr_net_blocking_lookup(dial_host) : NULL; - ScrNetSocket *s = scr_net_connect(port, dial_addr, cb); /* cb fires post-handshake — secureConnect */ - if (dial_addr) scr_str_release(dial_addr); + ScrNetSocket *s; + if (wrap != NULL) { + s = scr_net_sock_adopt(wrap); + if (s == NULL) { + scr_tls_alpn_free(alpn); + scr_bytes_release(ca); + scr_closure_release(cb); + scr_str_release(localhost_fallback); + scr_str_release(socket_host); + static const char msg[] = + "tls.connect over a socket that is not connected, or is already wrapped, is not supported yet"; + scr_throw_error_msg(SCR_ERR_ERROR, msg, sizeof msg - 1); + return NULL; + } + } else { + ScrStr *dial_addr = dial_host != NULL ? scr_net_blocking_lookup(dial_host) : NULL; + s = scr_net_connect(port, dial_addr, cb); /* cb fires post-handshake — secureConnect */ + if (dial_addr) scr_str_release(dial_addr); + } ScrTlsCli *cli = scr_tls_cli_new2(verify_host, reject, ca != NULL ? (const char *)ca->data : NULL, ca != NULL ? ca->len : 0, !reject); @@ -2397,8 +2662,13 @@ ScrNetSocket *scr_tls_connect_dyn(double port, ScrStr *host /*borrowed, nullable } cli->no_protocols = !scr_tls_conf_versions(&cli->conf, min_version, max_version); scr_tls_cli_wrap(s, cli); + if (wrap != NULL) { + if (cb != NULL) scr_net_sock_on_established(s, cb, true); /* secureConnect */ + scr_net_sock_adopt_start(s); + } if (timeout_ms > 0) scr_net_sock_set_timeout(s, timeout_ms); scr_str_release(localhost_fallback); + scr_str_release(socket_host); scr_bytes_release(ca); return s; } @@ -2610,12 +2880,39 @@ ScrDyn *scr_tls_sock_alpn_protocol(ScrNetSocket *s) { return d; } +/* secureConnect: a client socket's 'connect' list fires at establishment + * (scr_net.c's transport pump), Node's secureConnect timing. Once TCP is up + * (a socket tls.connect({ socket }) took over is up from the start) the + * listener waits for the handshake instead. Server-side sockets never emit + * it. */ +static void scr_tls_add_secure_connect(ScrNetSocket *s, ScrTlsSock *t, ScrClosure *cb, bool once) { + if (t->srv != NULL) { + scr_closure_release(cb); + return; + } + if (scr_net_sock_connecting(s)) scr_net_sock_on_connect(s, cb, once); + else scr_net_sock_on_established(s, cb, once); +} + void scr_tls_sock_on_secure_connect(ScrNetSocket *s, ScrClosure *cb /*moves*/, bool once) { - if (scr_tls_sock_of(s) == NULL) { + ScrTlsSock *t = scr_tls_sock_of(s); + if (t == NULL) { scr_closure_release(cb); /* a plain socket never fires it, like Node */ return; } - scr_net_sock_on_connect(s, cb, once); /* fires at establishment — secureConnect timing */ + scr_tls_add_secure_connect(s, t, cb, once); +} + +/* The 'secure' event: the handshake completed (after secureConnect on a + * client). A plain socket never emits it, and a registration after the + * handshake comes too late, as in Node. */ +void scr_tls_sock_on_secure(ScrNetSocket *s, ScrClosure *cb /*moves*/, bool once) { + ScrTlsSock *t = scr_tls_sock_of(s); + if (t == NULL || t->established || t->settled) { + scr_closure_release(cb); + return; + } + scr_net_ls_add(&t->secure_ls, cb, NULL, once); } void scr_tls_sock_on_session(ScrNetSocket *s, ScrClosure *cb /*moves*/, void *fn, bool once) { @@ -2624,6 +2921,10 @@ void scr_tls_sock_on_session(ScrNetSocket *s, ScrClosure *cb /*moves*/, void *fn scr_closure_release(cb); /* a plain socket never fires it, like Node */ return; } + if (t->settled) { + scr_closure_release(cb); + return; + } scr_net_ls_add(&t->session_ls, cb, fn, once); if (t->established) scr_tls_fire_session(t); /* the ticket already arrived */ } @@ -2660,9 +2961,11 @@ static bool scr_tls_dynh_on(ScrNetSocket *s, const char *event, const ScrDyn *cb ScrTlsSock *t = scr_tls_sock_of(s); if (t == NULL) return false; if (strcmp(event, "secureConnect") == 0) { - /* a TLS socket's 'connect' list fires at establishment — exactly - * Node's secureConnect timing (scr_net.c's transport pump) */ - scr_net_sock_on_connect(s, scr_dyn_listener_closure0(cb), once); + scr_tls_add_secure_connect(s, t, scr_dyn_listener_closure0(cb), once); + return true; + } + if (strcmp(event, "secure") == 0) { + scr_tls_sock_on_secure(s, scr_dyn_listener_closure0(cb), once); return true; } if (strcmp(event, "session") == 0) { diff --git a/packages/runtime/src/scr_tls.h b/packages/runtime/src/scr_tls.h index fcb221b42..cc8e3336e 100644 --- a/packages/runtime/src/scr_tls.h +++ b/packages/runtime/src/scr_tls.h @@ -62,6 +62,10 @@ ScrDyn *scr_tls_sock_get_cipher(ScrNetSocket *s); /* +1: object, undefined or ScrDyn *scr_tls_sock_alpn_protocol(ScrNetSocket *s); /* +1: string, false or null */ void scr_tls_sock_on_secure_connect(ScrNetSocket *s, ScrClosure *cb /*moves*/, bool once); void scr_tls_sock_on_session(ScrNetSocket *s, ScrClosure *cb /*moves*/, void *fn, bool once); +/* The 'secure' event: fires when the handshake completes (after the + * secureConnect listeners on a client); released unread on a plain socket + * or after the handshake. */ +void scr_tls_sock_on_secure(ScrNetSocket *s, ScrClosure *cb /*moves*/, bool once); /* ── h2 over TLS (scr_tls.c ↔ scr_http2.c): the ALPN=h2 server wrap and * the https-authority client wrap. scr_http2.c calls both directly — @@ -93,6 +97,11 @@ ScrSecureCtx *scr_tls_create_secure_context(const char *cert, size_t cert_len, c * validations first, then the pem walk (+1, or NULL with the exception * pending). Borrowed. */ ScrSecureCtx *scr_tls_create_secure_context_dyn(const ScrDyn *opts); +/* new tls.TLSSocket(socket, options) in server mode: a fresh TLS socket + * takes over socket's connection and runs the server handshake, serving + * ctx (options.secureContext; borrowed, nullable) or the options record's + * cert and key. +1, or NULL with the exception pending. */ +ScrNetSocket *scr_tls_wrap_socket(ScrNetSocket *raw, const ScrDyn *opts /*borrowed*/, ScrSecureCtx *ctx /*borrowed, nullable*/); /* tls.getCACertificates(type): validateString + the documented name set, * then the compiler-rendered fence — always leaves an exception pending. */ void scr_tls_ca_certs_chk(const ScrDyn *type, const ScrStr *fence); diff --git a/packages/runtime/src/scr_webcrypto.c b/packages/runtime/src/scr_webcrypto.c index 4d2c27be4..1b4207349 100644 --- a/packages/runtime/src/scr_webcrypto.c +++ b/packages/runtime/src/scr_webcrypto.c @@ -23,6 +23,7 @@ * compiler links the unit only into programs that call these methods * (the webcrypto feature). */ #include "scr_runtime.h" +#include "scr_webcrypto.h" #include #include @@ -58,13 +59,12 @@ static const char *const scr_wc_key_names[] = { "MLKEM768-X25519", "MLKEM1024-P384", "RSA-OAEP", "RSA-PSS", "RSASSA-PKCS1-v1_5", "X25519", "X448", }; -static const char *const scr_wc_digest_names[] = { +const char *const scr_wc_digest_names[] = { "SHA-1", "SHA-256", "SHA-384", "SHA-512", "SHA3-256", "SHA3-384", "SHA3-512", "cSHAKE128", "cSHAKE256", "KT128", "KT256", "TurboSHAKE128", "TurboSHAKE256", }; -#define SCR_WC_COUNT(names) (sizeof(names) / sizeof((names)[0])) /* The implemented hashes: WebCrypto name and node:crypto digest name. */ static const struct { @@ -90,7 +90,7 @@ static bool scr_wc_ascii_ieq(const ScrStr *s, const char *name) { } /* The canonical registered spelling of `name`, or NULL. */ -static const char *scr_wc_lookup(const char *const *names, size_t count, const ScrStr *name) { +const char *scr_wc_lookup(const char *const *names, size_t count, const ScrStr *name) { for (size_t i = 0; i < count; i++) if (scr_wc_ascii_ieq(name, names[i])) return names[i]; return NULL; @@ -104,11 +104,17 @@ static const char *scr_wc_native_hash(const char *name) { /* ── errors ────────────────────────────────────────────────────────── */ -static const char *const scr_wc_ordinals[] = {"1st", "2nd", "3rd", "4th", "5th"}; +const char *const scr_wc_ordinals[] = {"1st", "2nd", "3rd", "4th", "5th"}; -static void scr_wc_type_error(const char *code, const char *fmt, ...) - __attribute__((format(printf, 2, 3))); -static void scr_wc_type_error(const char *code, const char *fmt, ...) { +/* An argument's ordinal: unwrapKey's seven arguments run past the shared + * table. */ +static const char *scr_wc_ordinal(size_t index) { + static const char *const later[] = {"6th", "7th"}; + size_t shared = SCR_WC_COUNT(scr_wc_ordinals); + return index < shared ? scr_wc_ordinals[index] : later[index - shared]; +} + +void scr_wc_type_error(const char *code, const char *fmt, ...) { char message[512]; va_list ap; va_start(ap, fmt); @@ -120,7 +126,7 @@ static void scr_wc_type_error(const char *code, const char *fmt, ...) { } /* An operation or algorithm Node supports that this slice does not. */ -static void scr_wc_unsupported(const char *method, const char *what) { +void scr_wc_unsupported(const char *method, const char *what) { char message[256]; int n = snprintf(message, sizeof message, "crypto.subtle.%s with %s is not supported yet", method, what); @@ -128,7 +134,7 @@ static void scr_wc_unsupported(const char *method, const char *what) { scr_throw_error_msg_code(SCR_ERR_ERROR, message, (size_t)n, "SC2020"); } -static void scr_wc_unsupported_algorithm(const char *method, const char *name) { +void scr_wc_unsupported_algorithm(const char *method, const char *name) { char what[96]; snprintf(what, sizeof what, "the %s algorithm", name); scr_wc_unsupported(method, what); @@ -138,7 +144,7 @@ static void scr_wc_unsupported_algorithm(const char *method, const char *name) { * Each answers NULL/false with the TypeError pending. `prefix` is * "Failed to execute '' on 'SubtleCrypto'". */ -static bool scr_wc_is_object(const ScrDyn *v) { +bool scr_wc_is_object(const ScrDyn *v) { switch (v->kind) { case SCR_DYN_UNDEF: case SCR_DYN_NULL: @@ -154,14 +160,14 @@ static bool scr_wc_is_object(const ScrDyn *v) { } /* A typed reference capsule as its plain view; other values retained. */ -static ScrDyn *scr_wc_plain(ScrDyn *v) { +ScrDyn *scr_wc_plain(ScrDyn *v) { if (v->kind == SCR_DYN_TYPED_REF) return scr_dyn_typed_ref_materialize(v); return scr_dyn_retain(v); } /* BufferSource: the bytes of a TypedArray, DataView, Buffer or * ArrayBuffer as a u8 view (+1). */ -static ScrBytes *scr_wc_buffer_source(const char *prefix, size_t index, ScrDyn *value) { +ScrBytes *scr_wc_buffer_source(const char *prefix, size_t index, ScrDyn *value) { ScrDyn *plain = scr_wc_plain(value); if (!plain) return NULL; ScrBytes *view = NULL; @@ -171,21 +177,21 @@ static ScrBytes *scr_wc_buffer_source(const char *prefix, size_t index, ScrDyn * scr_wc_type_error("ERR_INVALID_ARG_TYPE", "%s: %s argument is not instance of ArrayBuffer, Buffer, TypedArray, or " "DataView.", - prefix, scr_wc_ordinals[index]); + prefix, scr_wc_ordinal(index)); } scr_dyn_release(plain); return view; } /* DOMString conversion (+1). */ -static ScrStr *scr_wc_dom_string(const ScrDyn *value) { return scr_dyn_string_coerce_js(value); } +ScrStr *scr_wc_dom_string(const ScrDyn *value) { return scr_dyn_string_coerce_js(value); } static const char *const scr_wc_key_formats[] = { "raw", "raw-public", "raw-seed", "raw-secret", "raw-private", "pkcs8", "spki", "jwk", }; /* KeyFormat: the canonical enum spelling, or NULL. */ -static const char *scr_wc_key_format(const char *prefix, ScrDyn *value) { +const char *scr_wc_key_format(const char *prefix, ScrDyn *value) { ScrStr *s = scr_wc_dom_string(value); if (!s) return NULL; for (size_t i = 0; i < SCR_WC_COUNT(scr_wc_key_formats); i++) { @@ -203,22 +209,11 @@ static const char *scr_wc_key_format(const char *prefix, ScrDyn *value) { } /* Node's canonical usage order; the bit is the index. */ -static const char *const scr_wc_usages[] = { +const char *const scr_wc_usages[] = { "encrypt", "decrypt", "sign", "verify", "deriveKey", "deriveBits", "wrapKey", "unwrapKey", "encapsulateKey", "encapsulateBits", "decapsulateKey", "decapsulateBits", }; -enum { - SCR_WC_USAGE_ENCRYPT = 1u << 0, - SCR_WC_USAGE_DECRYPT = 1u << 1, - SCR_WC_USAGE_SIGN = 1u << 2, - SCR_WC_USAGE_VERIFY = 1u << 3, - SCR_WC_USAGE_DERIVE_KEY = 1u << 4, - SCR_WC_USAGE_DERIVE_BITS = 1u << 5, - SCR_WC_USAGE_WRAP_KEY = 1u << 6, - SCR_WC_USAGE_UNWRAP_KEY = 1u << 7, -}; - /* The usage index of a KeyUsage string, or -1. */ static int scr_wc_usage_index(const ScrStr *s) { for (size_t k = 0; k < SCR_WC_COUNT(scr_wc_usages); k++) @@ -228,12 +223,12 @@ static int scr_wc_usage_index(const ScrStr *s) { } /* sequence over an array: the usage mask (-1 on failure). */ -static long scr_wc_key_usages(const char *prefix, size_t index, ScrDyn *value) { +long scr_wc_key_usages(const char *prefix, size_t index, ScrDyn *value) { ScrDyn *plain = scr_wc_plain(value); if (!plain) return -1; if (plain->kind != SCR_DYN_ARR) { scr_wc_type_error("ERR_INVALID_ARG_TYPE", "%s: %s argument cannot be converted to sequence.", - prefix, scr_wc_ordinals[index]); + prefix, scr_wc_ordinal(index)); scr_dyn_release(plain); return -1; } @@ -251,7 +246,7 @@ static long scr_wc_key_usages(const char *prefix, size_t index, ScrDyn *value) { if (k < 0) { scr_wc_type_error("ERR_INVALID_ARG_VALUE", "%s: %s argument[%.0f] '%.*s' is not a valid enum value of type KeyUsage.", - prefix, scr_wc_ordinals[index], i, (int)(s->len > 200 ? 200 : s->len), + prefix, scr_wc_ordinal(index), i, (int)(s->len > 200 ? 200 : s->len), s->data); scr_str_release(s); scr_dyn_release(plain); @@ -274,9 +269,9 @@ typedef struct { double length; } ScrWcAlgorithm; -static const char scr_wc_normalize_prefix[] = "Failed to normalize algorithm"; +const char scr_wc_normalize_prefix[] = "Failed to normalize algorithm"; -static void scr_wc_missing_member(const char *dict, const char *member) { +void scr_wc_missing_member(const char *dict, const char *member) { scr_wc_type_error("ERR_MISSING_OPTION", "%s: passed algorithm cannot be converted to '%s' because '%s' is required in " "'%s'.", @@ -284,7 +279,7 @@ static void scr_wc_missing_member(const char *dict, const char *member) { } /* Reads `key` off an algorithm dictionary (+1; undefined when absent). */ -static ScrDyn *scr_wc_member(const ScrDyn *dict, const char *key) { +ScrDyn *scr_wc_member(const ScrDyn *dict, const char *key) { if (dict->kind != SCR_DYN_OBJ) return scr_dyn_retain(scr_dyn_undefined()); ScrDyn *v = scr_dyn_obj_read(dict, key, strlen(key)); return v ? v : scr_dyn_retain(scr_dyn_undefined()); @@ -315,7 +310,7 @@ static bool scr_wc_enforce_range(const char *prefix, const char *context, const /* The name an AlgorithmIdentifier carries (+1), with the Algorithm * dictionary's missing-name TypeError. `dict` names the dictionary the * value converts to. */ -static ScrStr *scr_wc_algorithm_name(ScrDyn *identifier, const char *dict) { +ScrStr *scr_wc_algorithm_name(ScrDyn *identifier, const char *dict) { if (!scr_wc_is_object(identifier)) return scr_wc_dom_string(identifier); ScrDyn *plain = scr_wc_plain(identifier); if (!plain) return NULL; @@ -330,7 +325,7 @@ static ScrStr *scr_wc_algorithm_name(ScrDyn *identifier, const char *dict) { /* The digest a HashAlgorithmIdentifier names: canonical name, or NULL * with NotSupportedError / the SC2020 refusal pending. */ -static const char *scr_wc_digest_algorithm(ScrDyn *identifier, const char *method) { +const char *scr_wc_digest_algorithm(ScrDyn *identifier, const char *method) { ScrStr *name = scr_wc_algorithm_name(identifier, "Algorithm"); if (!name) return NULL; const char *canonical = scr_wc_lookup(scr_wc_digest_names, SCR_WC_COUNT(scr_wc_digest_names), name); @@ -528,10 +523,10 @@ static ScrCryptoKey *scr_crypto_key_of(const ScrDyn *value) { bool scr_webcrypto_is_crypto_key(const ScrDyn *value) { if (value->kind == SCR_DYN_TYPED_REF) return false; - return scr_crypto_key_of(value) != NULL; + return scr_crypto_key_of(value) != NULL || (scr_wc_asymmetric && scr_wc_asymmetric->is_key(value)); } -static ScrDyn *scr_wc_cstr(const char *s) { +ScrDyn *scr_wc_cstr(const char *s) { ScrStr *str = scr_str_new(s, strlen(s)); ScrDyn *out = scr_dyn_new_str(str); scr_str_release(str); @@ -871,7 +866,55 @@ static ScrBytes *scr_wc_jwk_secret(const ScrWcJwk *jwk, bool extractable, long u * Each takes its argument vector as one dyn array (borrowed) and answers * a +1 promise. */ -static ScrPromise *scr_wc_rejected(void) { +const ScrWcAsymmetric *scr_wc_asymmetric = NULL; + +static const char *const scr_wc_asymmetric_names[] = {"ECDSA", "Ed25519", "RSA-PSS", "RSASSA-PKCS1-v1_5"}; + +/* Whether the AlgorithmIdentifier at args[index] names ECDSA, + * RSASSA-PKCS1-v1_5, RSA-PSS or Ed25519, read without side effects or errors: a + * string, or the name member of an object (a statically typed one through + * its plain view). A value that does not read cleanly takes the HMAC path, + * which reports its errors. */ +static const char *scr_wc_asymmetric_name(const ScrDyn *args, size_t index) { + if ((size_t)scr_dyn_arr_len(args) <= index) return NULL; + ScrDyn *identifier = scr_dyn_arr_at(args, (double)index); + ScrDyn *plain = scr_wc_plain(identifier); + if (!plain) scr_exc_clear(); + const char *found = NULL; + if (plain) { + const ScrDyn *name = plain; + if (plain->kind == SCR_DYN_OBJ) name = scr_dyn_obj_get(plain, "name", 4); + if (name && name->kind == SCR_DYN_STR) + found = scr_wc_lookup(scr_wc_asymmetric_names, SCR_WC_COUNT(scr_wc_asymmetric_names), + name->v.str); + scr_dyn_release(plain); + } + scr_dyn_release(identifier); + return found; +} + +/* Whether `value` is an ECDSA, Ed25519 or RSA CryptoKey, which + * scr_webcrypto_pki.c holds; wrapKey and unwrapKey wrap secret keys only. */ +static bool scr_wc_is_asymmetric_key(const ScrDyn *value) { + return scr_wc_asymmetric && scr_wc_asymmetric->is_key(value); +} + +static bool scr_wc_is_asymmetric_algorithm(const char *name) { + for (size_t i = 0; i < SCR_WC_COUNT(scr_wc_asymmetric_names); i++) + if (strcmp(name, scr_wc_asymmetric_names[i]) == 0) return true; + return false; +} + +/* The asymmetric algorithms run in scr_webcrypto_pki.c, which the compiler + * links when a string literal in the program names one of them. */ +static ScrPromise *scr_wc_asymmetric_missing(const char *method, const char *name) { + char what[160]; + snprintf(what, sizeof what, "the %s algorithm named only by a computed string", name); + scr_wc_unsupported(method, what); + return scr_wc_rejected(); +} + +ScrPromise *scr_wc_rejected(void) { ScrPromise *p = scr_promise_new(); scr_promise_reject_pending(p); return p; @@ -892,7 +935,7 @@ static ScrPromise *scr_wc_deferred(ScrDyn *value) { /* The argument count check every method starts with (after the receiver * check the compiler's call shape already guarantees). */ -static bool scr_wc_required(const char *prefix, const ScrDyn *args, size_t required) { +bool scr_wc_required(const char *prefix, const ScrDyn *args, size_t required) { size_t argc = (size_t)scr_dyn_arr_len(args); if (argc >= required) return true; scr_wc_type_error("ERR_MISSING_ARGS", "%s: %zu arguments required, but only %zu present.", prefix, @@ -900,15 +943,12 @@ static bool scr_wc_required(const char *prefix, const ScrDyn *args, size_t requi return false; } -typedef struct { - ScrDyn *v[5]; -} ScrWcArgs; -static void scr_wc_args_take(ScrWcArgs *a, const ScrDyn *args, size_t n) { +void scr_wc_args_take(ScrWcArgs *a, const ScrDyn *args, size_t n) { for (size_t i = 0; i < n; i++) a->v[i] = scr_dyn_arr_at(args, (double)i); } -static void scr_wc_args_drop(ScrWcArgs *a, size_t n) { +void scr_wc_args_drop(ScrWcArgs *a, size_t n) { for (size_t i = 0; i < n; i++) scr_dyn_release(a->v[i]); } @@ -1098,6 +1138,11 @@ done: } ScrPromise *scr_webcrypto_import_key(ScrDyn *args) { + const char *asymmetric = scr_wc_asymmetric_name(args, 2); + if (asymmetric) { + return scr_wc_asymmetric ? scr_wc_asymmetric->import_key(args) + : scr_wc_asymmetric_missing("importKey", asymmetric); + } ScrDyn *key = scr_wc_import(args); if (!key) return scr_wc_rejected(); return scr_wc_resolved(key); @@ -1106,8 +1151,14 @@ ScrPromise *scr_webcrypto_import_key(ScrDyn *args) { /* generateKey(algorithm, extractable, keyUsages) for HMAC and AES keys: * random bytes of the requested length (an HMAC key defaults to its * hash's block size; a length that is not a whole number of bytes clears - * the final byte's unused low bits). */ + * the final byte's unused low bits). ECDSA, Ed25519, RSASSA-PKCS1-v1_5 and + * RSA-PSS key pairs generate in scr_webcrypto_pki.c. */ ScrPromise *scr_webcrypto_generate_key(ScrDyn *args) { + const char *asymmetric = scr_wc_asymmetric_name(args, 0); + if (asymmetric) { + return scr_wc_asymmetric ? scr_wc_asymmetric->generate_key(args) + : scr_wc_asymmetric_missing("generateKey", asymmetric); + } static const char prefix[] = "Failed to execute 'generateKey' on 'SubtleCrypto'"; if (!scr_wc_required(prefix, args, 3)) return scr_wc_rejected(); ScrWcArgs a; @@ -1151,9 +1202,52 @@ done: return scr_wc_deferred(key); } +/* exportKeySync's checks for a secret key: false with Node's error + * pending. raw, raw-secret and jwk are the formats a secret key exports. */ +static bool scr_wc_exportable(const char *format, const ScrCryptoKey *key) { + if (scr_wc_is_kdf(key->name)) { + char message[64]; + snprintf(message, sizeof message, "%s key export is not supported", key->name); + scr_throw_domex("NotSupportedError", message); + return false; + } + if (!key->extractable) { + scr_throw_domex("InvalidAccessError", "key is not extractable"); + return false; + } + if (strcmp(format, "raw") != 0 && strcmp(format, "raw-secret") != 0 && + strcmp(format, "jwk") != 0) { + char message[96]; + snprintf(message, sizeof message, "Unable to export %s secret key using %s format", key->name, + format); + scr_throw_domex("NotSupportedError", message); + return false; + } + return true; +} + +/* The key bytes as a fresh u8 view (+1). */ +static ScrBytes *scr_wc_key_bytes(const ScrCryptoKey *key) { + ScrBytes *bytes = scr_bytes_new(SCR_BYTES_U8, (double)key->len); + if (key->len) memcpy(bytes->data, key->data, key->len); + return bytes; +} + +/* A JWK's k: the key bytes in unpadded base64url (+1). */ +static ScrStr *scr_wc_jwk_k(const ScrCryptoKey *key) { + ScrBytes *bytes = scr_wc_key_bytes(key); + ScrStr *encoding = scr_str_new("base64url", 9); + ScrStr *k = scr_bytes_to_str(bytes, encoding); + scr_str_release(encoding); + scr_bytes_release(bytes); + return k; +} + /* exportKeySync for secret keys: raw and raw-secret answer an ArrayBuffer - * of the key bytes, jwk an object { key_ops, ext, alg, kty, k }. */ -static ScrDyn *scr_wc_export(const ScrDyn *args) { + * of the key bytes, jwk an object { key_ops, ext, alg, kty, k }. An ECDSA, + * Ed25519 or RSA key sets *asymmetric after the format check instead + * (scr_webcrypto_pki.c exports it). */ +static ScrDyn *scr_wc_export(const ScrDyn *args, bool *asymmetric) { static const char prefix[] = "Failed to execute 'exportKey' on 'SubtleCrypto'"; if (!scr_wc_required(prefix, args, 2)) return NULL; ScrWcArgs a; @@ -1162,34 +1256,19 @@ static ScrDyn *scr_wc_export(const ScrDyn *args) { const char *format = scr_wc_key_format(prefix, a.v[0]); if (!format) goto done; ScrCryptoKey *key = scr_crypto_key_of(a.v[1]); + if (!key && scr_wc_asymmetric && scr_wc_asymmetric->is_key(a.v[1])) { + *asymmetric = true; + goto done; + } if (!key) { scr_wc_type_error("ERR_INVALID_ARG_TYPE", "%s: 2nd argument is not of type CryptoKey.", prefix); goto done; } - if (scr_wc_is_kdf(key->name)) { - char message[64]; - snprintf(message, sizeof message, "%s key export is not supported", key->name); - scr_throw_domex("NotSupportedError", message); - goto done; - } - if (!key->extractable) { - scr_throw_domex("InvalidAccessError", "key is not extractable"); - goto done; - } - if (strcmp(format, "raw") == 0 || strcmp(format, "raw-secret") == 0) { - ScrBytes *bytes = scr_bytes_new(SCR_BYTES_U8, (double)key->len); - if (key->len) memcpy(bytes->data, key->data, key->len); - result = scr_array_buffer_from_bytes(bytes); - scr_bytes_release(bytes); - } else if (strcmp(format, "jwk") == 0) { + if (!scr_wc_exportable(format, key)) goto done; + if (strcmp(format, "jwk") == 0) { char buf[16]; const char *alg = scr_wc_jwk_alg(key->name, key->hash, key->length, buf, sizeof buf); - ScrBytes *bytes = scr_bytes_new(SCR_BYTES_U8, (double)key->len); - if (key->len) memcpy(bytes->data, key->data, key->len); - ScrStr *encoding = scr_str_new("base64url", 9); - ScrStr *k = scr_bytes_to_str(bytes, encoding); - scr_str_release(encoding); - scr_bytes_release(bytes); + ScrStr *k = scr_wc_jwk_k(key); result = scr_dyn_new_obj(); scr_dyn_obj_set(result, "key_ops", 7, scr_wc_usage_array(key->usages)); scr_dyn_obj_set(result, "ext", 3, scr_dyn_new_bool(key->extractable)); @@ -1198,10 +1277,9 @@ static ScrDyn *scr_wc_export(const ScrDyn *args) { scr_dyn_obj_set(result, "k", 1, scr_dyn_new_str(k)); scr_str_release(k); } else { - char message[96]; - snprintf(message, sizeof message, "Unable to export %s secret key using %s format", key->name, - format); - scr_throw_domex("NotSupportedError", message); + ScrBytes *bytes = scr_wc_key_bytes(key); + result = scr_array_buffer_from_bytes(bytes); + scr_bytes_release(bytes); } done: scr_wc_args_drop(&a, 2); @@ -1209,7 +1287,9 @@ done: } ScrPromise *scr_webcrypto_export_key(ScrDyn *args) { - ScrDyn *result = scr_wc_export(args); + bool asymmetric = false; + ScrDyn *result = scr_wc_export(args, &asymmetric); + if (asymmetric) return scr_wc_asymmetric->export_key(args); if (!result) return scr_wc_rejected(); return scr_wc_resolved(result); } @@ -1234,7 +1314,8 @@ static bool scr_wc_sign_verify(const ScrDyn *args, bool verify, unsigned char ma scr_str_release(s); } ScrCryptoKey *key = scr_crypto_key_of(a.v[1]); - if (!key) { + bool asymmetric_key = !key && scr_wc_asymmetric && scr_wc_asymmetric->is_key(a.v[1]); + if (!key && !asymmetric_key) { scr_wc_type_error("ERR_INVALID_ARG_TYPE", "%s: 2nd argument is not of type CryptoKey.", prefix); goto done; } @@ -1252,7 +1333,7 @@ static bool scr_wc_sign_verify(const ScrDyn *args, bool verify, unsigned char ma scr_wc_unsupported_algorithm(method, algorithm.name); goto done; } - if (strcmp(key->name, algorithm.name) != 0) { + if (asymmetric_key || strcmp(key->name, algorithm.name) != 0) { scr_throw_domex("InvalidAccessError", "Key algorithm mismatch"); goto done; } @@ -1276,6 +1357,11 @@ done: } ScrPromise *scr_webcrypto_sign(ScrDyn *args) { + const char *asymmetric = scr_wc_asymmetric_name(args, 0); + if (asymmetric) { + return scr_wc_asymmetric ? scr_wc_asymmetric->sign(args) + : scr_wc_asymmetric_missing("sign", asymmetric); + } unsigned char mac[64]; size_t mac_len = 0; if (!scr_wc_sign_verify(args, false, mac, &mac_len, NULL)) return scr_wc_rejected(); @@ -1298,6 +1384,11 @@ static void scr_wc_settle_verified(ScrClosure *cb, ScrError *error, ScrBytes *va } ScrPromise *scr_webcrypto_verify(ScrDyn *args) { + const char *asymmetric = scr_wc_asymmetric_name(args, 0); + if (asymmetric) { + return scr_wc_asymmetric ? scr_wc_asymmetric->verify(args) + : scr_wc_asymmetric_missing("verify", asymmetric); + } unsigned char mac[64]; size_t mac_len = 0; ScrBytes *signature = NULL; @@ -1548,6 +1639,43 @@ static ScrPromise *scr_wc_deferred_buffer(ScrBytes *bytes) { return p; } +/* The cipher over `data` (cipherOrWrap's job): +1 output bytes, or NULL + * with Node's OperationError pending. */ +static ScrBytes *scr_wc_cipher_bytes(ScrWebCryptoAesFn aes, const char *name, bool decrypt, + const ScrCryptoKey *key, ScrWcCipherParams *params, + ScrBytes *data) { + if (!params->iv) params->iv = scr_bytes_new(SCR_BYTES_U8, 0); + if (!params->aad) params->aad = scr_bytes_new(SCR_BYTES_U8, 0); + ScrBytes *view = data; + SCR_BYTES_SNAPSHOT(view); + ScrBytes *iv = params->iv; + SCR_BYTES_SNAPSHOT(iv); + ScrBytes *aad = params->aad; + SCR_BYTES_SNAPSHOT(aad); + ScrWebCryptoAesParams run = { + .iv = iv->data, + .iv_len = (size_t)scr_bytes_byte_len(iv), + .aad = aad->data, + .aad_len = (size_t)scr_bytes_byte_len(aad), + .tag_len = (size_t)(params->tag_bits / 8), + .counter_bits = (unsigned)params->counter_bits, + }; + ScrBytes *out = + aes(name, decrypt, key->data, key->len, &run, view->data, (size_t)scr_bytes_byte_len(view)); + if (!out) + scr_throw_domex("OperationError", "The operation failed for an operation-specific reason"); + return out; +} + +/* The cipher's promise: an ArrayBuffer of the output, or the job's + * OperationError, on a later loop turn. */ +static ScrPromise *scr_wc_run_cipher(ScrWebCryptoAesFn aes, const char *name, bool decrypt, + const ScrCryptoKey *key, ScrWcCipherParams *params, + ScrBytes *data) { + ScrBytes *out = scr_wc_cipher_bytes(aes, name, decrypt, key, params, data); + return out ? scr_wc_deferred_buffer(out) : scr_wc_deferred_rejection(); +} + ScrPromise *scr_webcrypto_cipher(ScrDyn *args, bool decrypt, ScrWebCryptoAesFn aes) { const char *method = decrypt ? "decrypt" : "encrypt"; const char *prefix = decrypt ? "Failed to execute 'decrypt' on 'SubtleCrypto'" @@ -1561,7 +1689,9 @@ ScrPromise *scr_webcrypto_cipher(ScrDyn *args, bool decrypt, ScrWebCryptoAesFn a ScrPromise *result = NULL; if (!scr_wc_algorithm_identifier(a.v[0])) goto done; ScrCryptoKey *key = scr_crypto_key_of(a.v[1]); - if (!key) { + /* an ECDSA, Ed25519 or RSA key (scr_webcrypto_pki.c) is no cipher key */ + bool asymmetric_key = !key && scr_wc_asymmetric && scr_wc_asymmetric->is_key(a.v[1]); + if (!key && !asymmetric_key) { scr_wc_type_error("ERR_INVALID_ARG_TYPE", "%s: 2nd argument is not of type CryptoKey.", prefix); goto done; } @@ -1569,7 +1699,7 @@ ScrPromise *scr_webcrypto_cipher(ScrDyn *args, bool decrypt, ScrWebCryptoAesFn a if (!data) goto done; const char *name = scr_wc_normalize_cipher(a.v[0], method, ¶ms); if (!name) goto done; - if (strcmp(key->name, name) != 0) { + if (asymmetric_key || strcmp(key->name, name) != 0) { scr_throw_domex("InvalidAccessError", "Key algorithm mismatch"); goto done; } @@ -1580,31 +1710,7 @@ ScrPromise *scr_webcrypto_cipher(ScrDyn *args, bool decrypt, ScrWebCryptoAesFn a goto done; } if (!scr_wc_max_length(data, "data")) goto done; - { - ScrBytes *view = data; - SCR_BYTES_SNAPSHOT(view); - ScrBytes *iv = params.iv; - SCR_BYTES_SNAPSHOT(iv); - if (!params.aad) params.aad = scr_bytes_new(SCR_BYTES_U8, 0); - ScrBytes *aad = params.aad; - SCR_BYTES_SNAPSHOT(aad); - ScrWebCryptoAesParams run = { - .iv = iv->data, - .iv_len = (size_t)scr_bytes_byte_len(iv), - .aad = aad->data, - .aad_len = (size_t)scr_bytes_byte_len(aad), - .tag_len = (size_t)(params.tag_bits / 8), - .counter_bits = (unsigned)params.counter_bits, - }; - ScrBytes *out = aes(name, decrypt, key->data, key->len, &run, view->data, - (size_t)scr_bytes_byte_len(view)); - if (out) { - result = scr_wc_deferred_buffer(out); - } else { - scr_throw_domex("OperationError", "The operation failed for an operation-specific reason"); - result = scr_wc_deferred_rejection(); - } - } + result = scr_wc_run_cipher(aes, name, decrypt, key, ¶ms, data); done: if (data) scr_bytes_release(data); scr_wc_cipher_params_drop(¶ms); @@ -1612,6 +1718,194 @@ done: return result ? result : scr_wc_rejected(); } +/* ── wrapKey and unwrapKey ──────────────────────────────────────────── + * wrapKey is exportKey followed by encrypt (or AES-KW's wrap), and + * unwrapKey is decrypt (or AES-KW's unwrap) followed by importKey, as in + * Node's webcrypto.js. The cipher runs in scr_webcrypto_aes.c. */ + +/* normalizeAlgorithm(algorithm, 'wrapKey' / 'unwrapKey'), falling back to + * the 'encrypt' / 'decrypt' registry as Node does: AES-KW (no + * parameters), or the AES ciphers with their dictionaries. */ +static const char *scr_wc_normalize_wrap(ScrDyn *identifier, const char *method, + ScrWcCipherParams *params) { + memset(params, 0, sizeof *params); + ScrStr *name = scr_wc_algorithm_name(identifier, "Algorithm"); + if (!name) return NULL; + bool kw = scr_wc_ascii_ieq(name, "AES-KW"); + scr_str_release(name); + if (kw) return "AES-KW"; + return scr_wc_normalize_cipher(identifier, method, params); +} + +/* The bytes wrapKey encrypts (+1): the raw key, or the JWK's JSON text, + * padded with spaces to a multiple of 8 bytes for AES-KW. */ +static ScrBytes *scr_wc_wrap_payload(const char *format, const ScrCryptoKey *key, bool kw) { + if (strcmp(format, "jwk") != 0) return scr_wc_key_bytes(key); + char buf[16]; + const char *alg = scr_wc_jwk_alg(key->name, key->hash, key->length, buf, sizeof buf); + ScrStr *k = scr_wc_jwk_k(key); + ScrJsonBuf b; + scr_jb_init(&b); + scr_jb_puts(&b, "{\"key_ops\":["); + bool first = true; + for (size_t i = 0; i < SCR_WC_COUNT(scr_wc_usages); i++) { + if (!(key->usages & (1u << i))) continue; + if (!first) scr_jb_puts(&b, ","); + first = false; + scr_jb_puts(&b, "\""); + scr_jb_puts(&b, scr_wc_usages[i]); + scr_jb_puts(&b, "\""); + } + scr_jb_puts(&b, key->extractable ? "],\"ext\":true" : "],\"ext\":false"); + if (alg) { + scr_jb_puts(&b, ",\"alg\":\""); + scr_jb_puts(&b, alg); + scr_jb_puts(&b, "\""); + } + scr_jb_puts(&b, ",\"kty\":\"oct\",\"k\":\""); + ScrStr *json_prefix = scr_jb_finish(&b); + size_t len = json_prefix->len + k->len + 2; + size_t padded = kw && len % 8 ? len + 8 - len % 8 : len; + ScrBytes *out = scr_bytes_new(SCR_BYTES_U8, (double)padded); + memcpy(out->data, json_prefix->data, json_prefix->len); + memcpy(out->data + json_prefix->len, k->data, k->len); + memcpy(out->data + json_prefix->len + k->len, "\"}", 2); + memset(out->data + len, ' ', padded - len); + scr_str_release(json_prefix); + scr_str_release(k); + return out; +} + +/* wrapKey(format, key, wrappingKey, wrapAlgorithm). */ +ScrPromise *scr_webcrypto_wrap(ScrDyn *args, ScrWebCryptoAesFn aes) { + static const char prefix[] = "Failed to execute 'wrapKey' on 'SubtleCrypto'"; + if (!scr_wc_required(prefix, args, 4)) return scr_wc_rejected(); + ScrWcArgs a; + scr_wc_args_take(&a, args, 4); + ScrWcCipherParams params; + memset(¶ms, 0, sizeof params); + ScrBytes *payload = NULL; + ScrPromise *result = NULL; + const char *format = scr_wc_key_format(prefix, a.v[0]); + if (!format) goto done; + ScrCryptoKey *key = scr_crypto_key_of(a.v[1]); + if (!key && !scr_wc_is_asymmetric_key(a.v[1])) { + scr_wc_type_error("ERR_INVALID_ARG_TYPE", "%s: 2nd argument is not of type CryptoKey.", prefix); + goto done; + } + ScrCryptoKey *wrapping = scr_crypto_key_of(a.v[2]); + if (!wrapping && !scr_wc_is_asymmetric_key(a.v[2])) { + scr_wc_type_error("ERR_INVALID_ARG_TYPE", "%s: 3rd argument is not of type CryptoKey.", prefix); + goto done; + } + if (!scr_wc_algorithm_identifier(a.v[3])) goto done; + const char *name = scr_wc_normalize_wrap(a.v[3], "wrapKey", ¶ms); + if (!name) goto done; + /* An ECDSA, Ed25519 or RSA key never has an AES algorithm. */ + if (!wrapping || strcmp(wrapping->name, name) != 0) { + scr_throw_domex("InvalidAccessError", "Key algorithm mismatch"); + goto done; + } + if (!(wrapping->usages & SCR_WC_USAGE_WRAP_KEY)) { + scr_throw_domex("InvalidAccessError", "Unable to use this key to wrapKey"); + goto done; + } + if (!key) { + scr_wc_unsupported("wrapKey", "an ECDSA, Ed25519 or RSA key"); + goto done; + } + if (!scr_wc_exportable(format, key)) goto done; + payload = scr_wc_wrap_payload(format, key, strcmp(name, "AES-KW") == 0); + result = scr_wc_run_cipher(aes, name, false, wrapping, ¶ms, payload); +done: + if (payload) scr_bytes_release(payload); + scr_wc_cipher_params_drop(¶ms); + scr_wc_args_drop(&a, 4); + return result ? result : scr_wc_rejected(); +} + +/* parseJwk: the unwrapped bytes as UTF-8 JSON converted to a JsonWebKey + * with a kty member, or DataError "Invalid wrapped JWK key". */ +static bool scr_wc_parse_wrapped_jwk(ScrBytes *bytes, ScrWcJwk *jwk) { + static const char prefix[] = "Failed to execute 'unwrapKey' on 'SubtleCrypto'"; + ScrStr *text = scr_text_decode_options(bytes, true, false); + ScrDyn *parsed = text ? scr_json_parse(text) : NULL; + if (text) scr_str_release(text); + bool ok = parsed && scr_wc_is_object(parsed) && scr_wc_jwk_convert(prefix, parsed, jwk) && jwk->kty; + if (parsed) scr_dyn_release(parsed); + if (!ok) { + if (scr_exc_pending()) scr_caught_release(scr_exc_take()); + scr_wc_jwk_drop(jwk); + scr_throw_domex("DataError", "Invalid wrapped JWK key"); + } + return ok; +} + +/* unwrapKey(format, wrappedKey, unwrappingKey, unwrapAlgorithm, + * unwrappedKeyAlgorithm, extractable, keyUsages): argument and key errors + * reject at once; the cipher's failure and the import's errors reject on + * a later loop turn, after Node's job. */ +ScrPromise *scr_webcrypto_unwrap(ScrDyn *args, ScrWebCryptoAesFn aes) { + static const char prefix[] = "Failed to execute 'unwrapKey' on 'SubtleCrypto'"; + if (!scr_wc_required(prefix, args, 7)) return scr_wc_rejected(); + ScrDyn *v[7]; + for (size_t i = 0; i < 7; i++) v[i] = scr_dyn_arr_at(args, (double)i); + ScrWcCipherParams params; + memset(¶ms, 0, sizeof params); + ScrBytes *wrapped = NULL; + ScrPromise *result = NULL; + const char *format = scr_wc_key_format(prefix, v[0]); + if (!format) goto done; + wrapped = scr_wc_buffer_source(prefix, 1, v[1]); + if (!wrapped) goto done; + ScrCryptoKey *unwrapping = scr_crypto_key_of(v[2]); + if (!unwrapping && !scr_wc_is_asymmetric_key(v[2])) { + scr_wc_type_error("ERR_INVALID_ARG_TYPE", "%s: 3rd argument is not of type CryptoKey.", prefix); + goto done; + } + if (!scr_wc_algorithm_identifier(v[3]) || !scr_wc_algorithm_identifier(v[4])) goto done; + bool extractable = scr_dyn_truthy(v[5]); + long usages = scr_wc_key_usages(prefix, 6, v[6]); + if (usages < 0) goto done; + const char *name = scr_wc_normalize_wrap(v[3], "unwrapKey", ¶ms); + if (!name) goto done; + ScrWcAlgorithm imported; + if (!scr_wc_normalize(v[4], scr_wc_import_names, SCR_WC_COUNT(scr_wc_import_names), + SCR_WC_OP_IMPORT, &imported)) + goto done; + if (!unwrapping || strcmp(unwrapping->name, name) != 0) { + scr_throw_domex("InvalidAccessError", "Key algorithm mismatch"); + goto done; + } + if (!(unwrapping->usages & SCR_WC_USAGE_UNWRAP_KEY)) { + scr_throw_domex("InvalidAccessError", "Unable to use this key to unwrapKey"); + goto done; + } + if (scr_wc_is_asymmetric_algorithm(imported.name)) { + scr_wc_unsupported_algorithm("unwrapKey", imported.name); + goto done; + } + if (!scr_wc_max_length(wrapped, "data")) goto done; + ScrBytes *bytes = scr_wc_cipher_bytes(aes, name, true, unwrapping, ¶ms, wrapped); + if (!bytes) { + result = scr_wc_deferred_rejection(); + goto done; + } + ScrWcJwk jwk; + memset(&jwk, 0, sizeof jwk); + ScrDyn *key = NULL; + if (strcmp(format, "jwk") != 0 || scr_wc_parse_wrapped_jwk(bytes, &jwk)) + key = scr_wc_import_key_sync(format, bytes, &jwk, &imported, extractable, usages); + scr_wc_jwk_drop(&jwk); + scr_bytes_release(bytes); + result = key ? scr_wc_deferred(key) : scr_wc_deferred_rejection(); +done: + if (wrapped) scr_bytes_release(wrapped); + scr_wc_cipher_params_drop(¶ms); + for (size_t i = 0; i < 7; i++) scr_dyn_release(v[i]); + return result ? result : scr_wc_rejected(); +} + /* ── deriveBits and deriveKey ───────────────────────────────────────── * PBKDF2 and HKDF over node:crypto's KDF core (scr_crypto_pbkdf2 and * scr_crypto_hkdf_bytes in scr_lib.c), with Node's Pbkdf2Params and @@ -1789,7 +2083,8 @@ static ScrBytes *scr_wc_kdf_derive(const ScrWcKdfParams *params, const ScrCrypto /* The base key checks deriveBits and deriveKey share. */ static bool scr_wc_check_base_key(const ScrCryptoKey *key, const char *algorithm, unsigned usage, const char *usage_name) { - if (!(key->usages & usage)) { + /* NULL: an asymmetric key, which never has a derive usage */ + if (key == NULL || !(key->usages & usage)) { char message[64]; snprintf(message, sizeof message, "baseKey does not have %s usage", usage_name); scr_throw_domex("InvalidAccessError", message); @@ -1815,7 +2110,9 @@ ScrPromise *scr_webcrypto_derive_bits(ScrDyn *args) { ScrPromise *result = NULL; if (!scr_wc_algorithm_identifier(a.v[0])) goto done; ScrCryptoKey *key = scr_crypto_key_of(a.v[1]); - if (!key) { + /* an ECDSA, Ed25519 or RSA key (scr_webcrypto_pki.c) has no derive usage */ + bool asymmetric_key = !key && scr_wc_asymmetric && scr_wc_asymmetric->is_key(a.v[1]); + if (!key && !asymmetric_key) { scr_wc_type_error("ERR_INVALID_ARG_TYPE", "%s: 2nd argument is not of type CryptoKey.", prefix); goto done; } @@ -1825,7 +2122,9 @@ ScrPromise *scr_webcrypto_derive_bits(ScrDyn *args) { if (!null_length && !scr_wc_enforce_range(prefix, "3rd argument", a.v[2], 4294967295.0, &length)) goto done; if (!scr_wc_normalize_kdf(a.v[0], "deriveBits", ¶ms)) goto done; - if (!scr_wc_check_base_key(key, params.name, SCR_WC_USAGE_DERIVE_BITS, "deriveBits")) goto done; + if (!scr_wc_check_base_key(asymmetric_key ? NULL : key, params.name, SCR_WC_USAGE_DERIVE_BITS, + "deriveBits")) + goto done; if (!scr_wc_kdf_length(¶ms, null_length, length)) goto done; if (length == 0) { ScrBytes *empty = scr_bytes_new(SCR_BYTES_U8, 0); @@ -1886,7 +2185,9 @@ ScrPromise *scr_webcrypto_derive_key(ScrDyn *args) { ScrPromise *result = NULL; if (!scr_wc_algorithm_identifier(a.v[0])) goto done; ScrCryptoKey *key = scr_crypto_key_of(a.v[1]); - if (!key) { + /* an ECDSA, Ed25519 or RSA key (scr_webcrypto_pki.c) has no derive usage */ + bool asymmetric_key = !key && scr_wc_asymmetric && scr_wc_asymmetric->is_key(a.v[1]); + if (!key && !asymmetric_key) { scr_wc_type_error("ERR_INVALID_ARG_TYPE", "%s: 2nd argument is not of type CryptoKey.", prefix); goto done; } @@ -1907,7 +2208,9 @@ ScrPromise *scr_webcrypto_derive_key(ScrDyn *args) { scr_wc_unsupported_algorithm("deriveKey", imported.name); goto done; } - if (!scr_wc_check_base_key(key, params.name, SCR_WC_USAGE_DERIVE_KEY, "deriveKey")) goto done; + if (!scr_wc_check_base_key(asymmetric_key ? NULL : key, params.name, SCR_WC_USAGE_DERIVE_KEY, + "deriveKey")) + goto done; if (!scr_wc_kdf_length(¶ms, null_length, length)) goto done; ScrBytes *bits = scr_wc_kdf_derive(¶ms, key, length); if (!bits) goto done; diff --git a/packages/runtime/src/scr_webcrypto.h b/packages/runtime/src/scr_webcrypto.h new file mode 100644 index 000000000..1eb84af9b --- /dev/null +++ b/packages/runtime/src/scr_webcrypto.h @@ -0,0 +1,67 @@ +/* The Web IDL and error helpers of scr_webcrypto.c that the asymmetric + * algorithms (scr_webcrypto_pki.c) share, and the table through which + * scr_webcrypto.c reaches them when that unit is linked. */ +#ifndef SCR_WEBCRYPTO_H +#define SCR_WEBCRYPTO_H + +#include +#include + +#define SCR_WC_COUNT(names) (sizeof(names) / sizeof((names)[0])) + +enum { + SCR_WC_USAGE_ENCRYPT = 1u << 0, + SCR_WC_USAGE_DECRYPT = 1u << 1, + SCR_WC_USAGE_SIGN = 1u << 2, + SCR_WC_USAGE_VERIFY = 1u << 3, + SCR_WC_USAGE_DERIVE_KEY = 1u << 4, + SCR_WC_USAGE_DERIVE_BITS = 1u << 5, + SCR_WC_USAGE_WRAP_KEY = 1u << 6, + SCR_WC_USAGE_UNWRAP_KEY = 1u << 7, +}; + +/* Node's canonical usage order; the bit is the index. */ +extern const char *const scr_wc_usages[12]; +extern const char *const scr_wc_ordinals[5]; +extern const char scr_wc_normalize_prefix[]; +extern const char *const scr_wc_digest_names[13]; + +typedef struct { + ScrDyn *v[5]; +} ScrWcArgs; + +void scr_wc_type_error(const char *code, const char *fmt, ...) __attribute__((format(printf, 2, 3))); +void scr_wc_unsupported(const char *method, const char *what); +void scr_wc_unsupported_algorithm(const char *method, const char *name); +bool scr_wc_is_object(const ScrDyn *v); +ScrDyn *scr_wc_plain(ScrDyn *v); +ScrBytes *scr_wc_buffer_source(const char *prefix, size_t index, ScrDyn *value); +ScrStr *scr_wc_dom_string(const ScrDyn *value); +const char *scr_wc_key_format(const char *prefix, ScrDyn *value); +long scr_wc_key_usages(const char *prefix, size_t index, ScrDyn *value); +void scr_wc_missing_member(const char *dict, const char *member); +ScrDyn *scr_wc_member(const ScrDyn *dict, const char *key); +ScrStr *scr_wc_algorithm_name(ScrDyn *identifier, const char *dict); +const char *scr_wc_digest_algorithm(ScrDyn *identifier, const char *method); +const char *scr_wc_lookup(const char *const *names, size_t count, const ScrStr *name); +ScrDyn *scr_wc_cstr(const char *s); +ScrPromise *scr_wc_rejected(void); +bool scr_wc_required(const char *prefix, const ScrDyn *args, size_t required); +void scr_wc_args_take(ScrWcArgs *a, const ScrDyn *args, size_t n); +void scr_wc_args_drop(ScrWcArgs *a, size_t n); + +/* ECDSA, RSASSA-PKCS1-v1_5 and RSA-PSS (scr_webcrypto_pki.c, linked with + * the pki unit): each takes the method's argument vector (borrowed) and + * answers a +1 promise. NULL until that unit installs it. */ +typedef struct { + ScrPromise *(*generate_key)(const ScrDyn *args); + ScrPromise *(*import_key)(const ScrDyn *args); + ScrPromise *(*export_key)(const ScrDyn *args); + ScrPromise *(*sign)(const ScrDyn *args); + ScrPromise *(*verify)(const ScrDyn *args); + bool (*is_key)(const ScrDyn *value); +} ScrWcAsymmetric; + +extern const ScrWcAsymmetric *scr_wc_asymmetric; + +#endif diff --git a/packages/runtime/src/scr_webcrypto_aes.c b/packages/runtime/src/scr_webcrypto_aes.c index 7cb63f561..016ac57af 100644 --- a/packages/runtime/src/scr_webcrypto_aes.c +++ b/packages/runtime/src/scr_webcrypto_aes.c @@ -122,11 +122,74 @@ static ScrBytes *scr_wc_aes_ctr(const unsigned char *key, size_t key_len, return out; } +/* RFC 3394 AES key wrap with the default IV: wrapping takes a multiple of + * 8 bytes, at least 16; unwrapping a multiple of 8, at least 24, and fails + * when the integrity check value does not match (compared in constant + * time). */ +static ScrBytes *scr_wc_aes_kw(bool unwrap, const unsigned char *key, size_t key_len, + const unsigned char *in, size_t len) { + static const unsigned char iv[8] = {0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6}; + if (len % 8 != 0 || len < (unwrap ? 24u : 16u)) return NULL; + size_t n = unwrap ? len / 8 - 1 : len / 8; + mbedtls_aes_context aes; + mbedtls_aes_init(&aes); + int rc = unwrap ? mbedtls_aes_setkey_dec(&aes, key, (unsigned)(key_len * 8)) + : mbedtls_aes_setkey_enc(&aes, key, (unsigned)(key_len * 8)); + if (rc != 0) { + mbedtls_aes_free(&aes); + return NULL; + } + ScrBytes *out = scr_bytes_new(SCR_BYTES_U8, (double)(unwrap ? len - 8 : len + 8)); + unsigned char a[8], block[16]; + unsigned char *r = unwrap ? out->data : out->data + 8; + if (unwrap) { + memcpy(a, in, 8); + memcpy(r, in + 8, len - 8); + for (int j = 5; j >= 0; j--) { + for (size_t i = n; i >= 1; i--) { + uint64_t t = (uint64_t)n * (uint64_t)j + i; + for (int k = 0; k < 8; k++) block[k] = (unsigned char)(a[k] ^ (t >> (56 - 8 * k))); + memcpy(block + 8, r + 8 * (i - 1), 8); + mbedtls_aes_crypt_ecb(&aes, MBEDTLS_AES_DECRYPT, block, block); + memcpy(a, block, 8); + memcpy(r + 8 * (i - 1), block + 8, 8); + } + } + } else { + memcpy(a, iv, 8); + memcpy(r, in, len); + for (int j = 0; j <= 5; j++) { + for (size_t i = 1; i <= n; i++) { + memcpy(block, a, 8); + memcpy(block + 8, r + 8 * (i - 1), 8); + mbedtls_aes_crypt_ecb(&aes, MBEDTLS_AES_ENCRYPT, block, block); + uint64_t t = (uint64_t)n * (uint64_t)j + i; + for (int k = 0; k < 8; k++) a[k] = (unsigned char)(block[k] ^ (t >> (56 - 8 * k))); + memcpy(r + 8 * (i - 1), block + 8, 8); + } + } + memcpy(out->data, a, 8); + } + mbedtls_platform_zeroize(block, sizeof block); + mbedtls_aes_free(&aes); + if (unwrap) { + unsigned char diff = 0; + for (int k = 0; k < 8; k++) diff |= (unsigned char)(a[k] ^ iv[k]); + if (diff) { + mbedtls_platform_zeroize(out->data, len - 8); + scr_bytes_release(out); + return NULL; + } + } + return out; +} + /* The cipher scr_webcrypto_cipher runs: +1 output bytes, or NULL when the * operation fails (Node's OperationError). */ static ScrBytes *scr_wc_aes(const char *name, bool decrypt, const unsigned char *key, size_t key_len, const ScrWebCryptoAesParams *p, const unsigned char *in, size_t len) { + if (strcmp(name, "AES-KW") == 0) return scr_wc_aes_kw(decrypt, key, key_len, in, len); if (strcmp(name, "AES-GCM") == 0) return scr_wc_aes_gcm(decrypt, key, key_len, p, in, len); if (strcmp(name, "AES-CBC") == 0) return scr_wc_aes_cbc(decrypt, key, key_len, p, in, len); return scr_wc_aes_ctr(key, key_len, p, in, len); @@ -135,3 +198,7 @@ static ScrBytes *scr_wc_aes(const char *name, bool decrypt, const unsigned char ScrPromise *scr_webcrypto_encrypt(ScrDyn *args) { return scr_webcrypto_cipher(args, false, scr_wc_aes); } ScrPromise *scr_webcrypto_decrypt(ScrDyn *args) { return scr_webcrypto_cipher(args, true, scr_wc_aes); } + +ScrPromise *scr_webcrypto_wrap_key(ScrDyn *args) { return scr_webcrypto_wrap(args, scr_wc_aes); } + +ScrPromise *scr_webcrypto_unwrap_key(ScrDyn *args) { return scr_webcrypto_unwrap(args, scr_wc_aes); } diff --git a/packages/runtime/src/scr_webcrypto_pki.c b/packages/runtime/src/scr_webcrypto_pki.c new file mode 100644 index 000000000..7439a12db --- /dev/null +++ b/packages/runtime/src/scr_webcrypto_pki.c @@ -0,0 +1,841 @@ +/* WebCrypto's ECDSA, RSASSA-PKCS1-v1_5, RSA-PSS and Ed25519 algorithms: + * crypto.subtle.generateKey, importKey (spki, pkcs8, jwk, and raw for ECDSA + * and Ed25519 public keys), exportKey, sign and verify, and the CryptoKey + * values of these keys. ECDSA keys are on P-256, P-384 and P-521; ECDSA + * signatures are IEEE P1363 r || s. + * + * Argument conversion, algorithm normalization and the DOMException + * shapes follow Node 24's lib/internal/crypto/webcrypto.js, ec.js and + * rsa.js, through the Web IDL helpers of scr_webcrypto.c. The keys and + * signatures come from the pki unit (scr_pki.c). This unit installs + * itself into scr_webcrypto.c's dispatch table at startup; the compiler + * links it when a program uses WebCrypto and names one of these + * algorithms. All arguments are borrowed. */ +#include "scr_runtime.h" + +#include +#include +#include +#include + +#include "scr_pki.h" +#include "scr_webcrypto.h" + +typedef enum { SW_ECDSA, SW_RSASSA, SW_PSS, SW_ED25519 } SwKind; + +static const char *const sw_names[] = {"ECDSA", "RSASSA-PKCS1-v1_5", "RSA-PSS", "Ed25519"}; + +static const char *const sw_curves[] = {"P-256", "P-384", "P-521"}; + +/* ── CryptoKey ────────────────────────────────────────────────────────── */ + +typedef struct SwKey { + size_t rc; + ScrPkiKey *key; + SwKind kind; + const char *hash; /* RSA keys: the WebCrypto hash name */ + unsigned usages; + bool extractable; + ScrDyn *algorithm; /* cached getter results, or NULL */ + ScrDyn *usage_list; +} SwKey; + +static void *sw_key_retain(void *p) { + ((SwKey *)p)->rc++; + return p; +} + +static void sw_key_release(void *p) { + SwKey *k = p; + if (!k || --k->rc != 0) return; + scr_pki_key_release(k->key); + if (k->algorithm) scr_dyn_release(k->algorithm); + if (k->usage_list) scr_dyn_release(k->usage_list); + free(k); +} + +static SwKey *sw_key_of(const ScrDyn *value) { + if (value->kind != SCR_DYN_HANDLE || value->v.handle.tag != SCR_DYNH_ASYMMETRIC_CRYPTO_KEY) return NULL; + return value->v.handle.ptr; +} + +static bool sw_is_key(const ScrDyn *value) { return sw_key_of(value) != NULL; } + +static ScrDyn *sw_key_algorithm(SwKey *k) { + if (!k->algorithm) { + ScrDyn *algorithm = scr_dyn_new_obj(); + scr_dyn_obj_set(algorithm, "name", 4, scr_wc_cstr(sw_names[k->kind])); + if (k->kind == SW_ED25519) { + /* { name } only */ + } else if (k->kind == SW_ECDSA) { + scr_dyn_obj_set(algorithm, "namedCurve", 10, scr_wc_cstr(scr_pki_key_curve(k->key))); + } else { + scr_dyn_obj_set(algorithm, "modulusLength", 13, scr_dyn_new_num((double)scr_pki_key_bits(k->key))); + ScrBytes *exponent = scr_pki_key_exponent(k->key); + scr_dyn_obj_set(algorithm, "publicExponent", 14, scr_dyn_new_bytes(exponent)); + scr_bytes_release(exponent); + ScrDyn *hash = scr_dyn_new_obj(); + scr_dyn_obj_set(hash, "name", 4, scr_wc_cstr(k->hash)); + scr_dyn_obj_set(algorithm, "hash", 4, hash); + } + k->algorithm = algorithm; + } + return scr_dyn_retain(k->algorithm); +} + +static ScrDyn *sw_usage_array(unsigned usages) { + ScrDyn *list = scr_dyn_new_arr(); + for (size_t i = 0; i < SCR_WC_COUNT(scr_wc_usages); i++) + if (usages & (1u << i)) scr_dyn_arr_push(list, scr_wc_cstr(scr_wc_usages[i])); + return list; +} + +static ScrDyn *sw_key_usages(SwKey *k) { + if (!k->usage_list) k->usage_list = sw_usage_array(k->usages); + return scr_dyn_retain(k->usage_list); +} + +static bool sw_name_is(const char *name, size_t len, const char *literal) { + return len == strlen(literal) && memcmp(name, literal, len) == 0; +} + +static ScrDyn *sw_key_get(void *h, const char *name, size_t len) { + SwKey *k = h; + if (sw_name_is(name, len, "type")) + return scr_wc_cstr(scr_pki_key_is_private(k->key) ? "private" : "public"); + if (sw_name_is(name, len, "extractable")) return scr_dyn_new_bool(k->extractable); + if (sw_name_is(name, len, "algorithm")) return sw_key_algorithm(k); + if (sw_name_is(name, len, "usages")) return sw_key_usages(k); + return NULL; +} + +static bool sw_key_set(void *h, const char *name, size_t len, const ScrDyn *value) { + (void)h; + (void)value; + if (!sw_name_is(name, len, "type") && !sw_name_is(name, len, "extractable") && + !sw_name_is(name, len, "algorithm") && !sw_name_is(name, len, "usages")) + return false; + scr_pki_throw_fmt(SCR_ERR_TYPE, NULL, "Cannot set property %.*s of # which has only a getter", (int)len, + name); + return true; +} + +static ScrDyn *sw_key_invoke(void *h, ScrDyn *self, const char *method, ScrDyn *const *args, size_t argc, + const char *what) { + (void)h; + (void)self; + (void)args; + (void)argc; + if (strcmp(method, "toString") == 0) return scr_wc_cstr("[object CryptoKey]"); + scr_pki_throw_fmt(SCR_ERR_TYPE, NULL, "%s is not a function", what); + return NULL; +} + +static ScrDyn *sw_key_symbol_get(void *h, const ScrDyn *key) { + (void)h; + if (key->kind != SCR_DYN_SYMBOL || !key->v.symbol.description) return NULL; + ScrStr *description = key->v.symbol.description(key->v.symbol.value); + bool tag = description && description->len == 18 && memcmp(description->data, "Symbol.toStringTag", 18) == 0; + if (description) scr_str_release(description); + return tag ? scr_wc_cstr("CryptoKey") : NULL; +} + +/* As for HMAC CryptoKeys, util.inspect refuses these keys rather than + * printing a different shape than Node's custom inspector. */ +static const ScrDynHandleOps sw_key_ops = { + .cls = "CryptoKey", + .retain = sw_key_retain, + .release = sw_key_release, + .invoke = sw_key_invoke, + .get = sw_key_get, + .set = sw_key_set, + .json_empty_object = true, + .to_string_tag = "CryptoKey", + .symbol_get = sw_key_symbol_get, +}; + +/* Boxes a CryptoKey over `key` (consumed). */ +static ScrDyn *sw_key_new(ScrPkiKey *key, SwKind kind, const char *hash, unsigned usages, bool extractable) { + static SCR_TL bool installed; + if (!installed) { + installed = true; + scr_dyn_handle_install(SCR_DYNH_ASYMMETRIC_CRYPTO_KEY, &sw_key_ops); + } + SwKey *k = calloc(1, sizeof *k); + if (!k) scr_trap("scriptc: out of memory\n"); + k->rc = 1; + k->key = key; + k->kind = kind; + k->hash = hash; + k->usages = usages; + k->extractable = extractable; + ScrDyn *box = scr_dyn_new_handle(k, SCR_DYNH_ASYMMETRIC_CRYPTO_KEY); + sw_key_release(k); + return box; +} + +/* ── algorithm parameters ─────────────────────────────────────────────── */ + +static SwKind sw_kind_of(const char *name) { + if (strcmp(name, "ECDSA") == 0) return SW_ECDSA; + if (strcmp(name, "Ed25519") == 0) return SW_ED25519; + return strcmp(name, "RSA-PSS") == 0 ? SW_PSS : SW_RSASSA; +} + +static bool sw_is_rsa_kind(SwKind kind) { return kind == SW_RSASSA || kind == SW_PSS; } + +/* The canonical algorithm name of an AlgorithmIdentifier the dispatcher + * already recognized as one of this unit's algorithms, or NULL with the + * error pending. */ +static const char *sw_algorithm_name(ScrDyn *identifier) { + ScrStr *name = scr_wc_algorithm_name(identifier, "Algorithm"); + if (!name) return NULL; + const char *found = scr_wc_lookup(sw_names, SCR_WC_COUNT(sw_names), name); + scr_str_release(name); + if (!found) scr_throw_domex("NotSupportedError", "Unrecognized algorithm name"); + return found; +} + +/* A required dictionary member (+1), or NULL with ERR_MISSING_OPTION. */ +static ScrDyn *sw_required(ScrDyn *plain, const char *dict, const char *member) { + ScrDyn *v = scr_wc_member(plain, member); + if (v->kind == SCR_DYN_UNDEF) { + scr_dyn_release(v); + scr_wc_missing_member(dict, member); + return NULL; + } + return v; +} + +/* namedCurve (DOMString, required): one of the implemented curves, or + * NULL with the error pending. */ +static const char *sw_named_curve(ScrDyn *plain, const char *dict) { + ScrDyn *value = sw_required(plain, dict, "namedCurve"); + if (!value) return NULL; + ScrStr *s = scr_wc_dom_string(value); + scr_dyn_release(value); + if (!s) return NULL; + const char *curve = NULL; + for (size_t i = 0; i < SCR_WC_COUNT(sw_curves); i++) + if (s->len == strlen(sw_curves[i]) && memcmp(s->data, sw_curves[i], s->len) == 0) curve = sw_curves[i]; + scr_str_release(s); + if (!curve) scr_throw_domex("NotSupportedError", "Unrecognized namedCurve"); + return curve; +} + +/* hash (HashAlgorithmIdentifier, required): with `sha_only`, Node's + * ensureSHA check first ("Only SHA hashes are supported in "). */ +static const char *sw_hash(ScrDyn *plain, const char *dict, bool sha_only, const char *method) { + ScrDyn *hash = sw_required(plain, dict, "hash"); + if (!hash) return NULL; + if (sha_only) { + ScrStr *name = NULL; + if (scr_wc_is_object(hash)) { + ScrDyn *hp = scr_wc_plain(hash); + ScrDyn *member = hp ? scr_wc_member(hp, "name") : NULL; + if (hp) scr_dyn_release(hp); + if (member && member->kind == SCR_DYN_STR) name = scr_str_retain(member->v.str); + if (member) scr_dyn_release(member); + } else { + name = scr_wc_dom_string(hash); + if (!name) { + scr_dyn_release(hash); + return NULL; + } + } + bool sha = name && name->len >= 3 && (name->data[0] | 32) == 's' && (name->data[1] | 32) == 'h' && + (name->data[2] | 32) == 'a'; + if (name) scr_str_release(name); + if (!sha) { + char message[96]; + snprintf(message, sizeof message, "Only SHA hashes are supported in %s", dict); + scr_throw_domex("NotSupportedError", message); + scr_dyn_release(hash); + return NULL; + } + } + const char *canonical = scr_wc_digest_algorithm(hash, method); + scr_dyn_release(hash); + return canonical; +} + +/* An [EnforceRange] unsigned long member; false with the TypeError + * pending. */ +static bool sw_unsigned_long(ScrDyn *value, const char *member, double *out) { + double x; + if (!scr_dyn_number_coerce_js(value, &x)) return false; + if (x != x || x == 1.0 / 0.0 || x == -1.0 / 0.0) { + scr_wc_type_error("ERR_INVALID_ARG_TYPE", "%s: %s in passed algorithm is not a finite number.", + scr_wc_normalize_prefix, member); + return false; + } + x = trunc(x); + if (x < 0 || x > 4294967295.0) { + scr_wc_type_error("ERR_OUT_OF_RANGE", + "%s: %s in passed algorithm is outside the expected range of 0 to 4294967295.", + scr_wc_normalize_prefix, member); + return false; + } + *out = x == 0 ? 0 : x; + return true; +} + +static void sw_unsupported_usage(SwKind kind) { + char message[96]; + snprintf(message, sizeof message, "Unsupported key usage for %s key", sw_names[kind]); + scr_throw_domex("SyntaxError", message); +} + +/* ── generateKey ──────────────────────────────────────────────────────── */ + +static ScrPromise *sw_generate_key(const ScrDyn *args) { + static const char prefix[] = "Failed to execute 'generateKey' on 'SubtleCrypto'"; + if (!scr_wc_required(prefix, args, 3)) return scr_wc_rejected(); + ScrWcArgs a; + scr_wc_args_take(&a, args, 3); + ScrDyn *pair = NULL; + ScrDyn *plain = NULL; + if (!scr_wc_is_object(a.v[0])) { + ScrStr *s = scr_wc_dom_string(a.v[0]); + if (!s) goto done; + scr_str_release(s); + } + bool extractable = scr_dyn_truthy(a.v[1]); + long usages = scr_wc_key_usages(prefix, 2, a.v[2]); + if (usages < 0) goto done; + const char *name = sw_algorithm_name(a.v[0]); + if (!name) goto done; + SwKind kind = sw_kind_of(name); + plain = scr_wc_plain(a.v[0]); + if (!plain) goto done; + const char *curve = NULL; + const char *hash = NULL; + double bits = 0; + ScrBytes *exponent = NULL; + if (kind == SW_ED25519) { + /* no parameters */ + } else if (kind == SW_ECDSA) { + if (!scr_wc_is_object(plain)) { + scr_wc_missing_member("EcKeyGenParams", "namedCurve"); + goto done; + } + curve = sw_named_curve(plain, "EcKeyGenParams"); + if (!curve) goto done; + } else { + static const char dict[] = "RsaHashedKeyGenParams"; + if (!scr_wc_is_object(plain)) { + scr_wc_missing_member(dict, "hash"); + goto done; + } + /* Dictionary members convert in name order: hash, modulusLength, + * publicExponent. */ + hash = sw_hash(plain, dict, true, "generateKey"); + if (!hash) goto done; + ScrDyn *length = sw_required(plain, dict, "modulusLength"); + if (!length) goto done; + bool ok = sw_unsigned_long(length, "modulusLength", &bits); + scr_dyn_release(length); + if (!ok) goto done; + ScrDyn *e = sw_required(plain, dict, "publicExponent"); + if (!e) goto done; + if (e->kind != SCR_DYN_BYTES || e->v.bytes->elem != SCR_BYTES_U8) { + scr_wc_type_error("ERR_INVALID_ARG_TYPE", "%s: publicExponent in passed algorithm is not instance of Uint8Array.", + scr_wc_normalize_prefix); + scr_dyn_release(e); + goto done; + } + exponent = scr_bytes_buffer_source(e); + scr_dyn_release(e); + } + if (usages & ~(long)(SCR_WC_USAGE_SIGN | SCR_WC_USAGE_VERIFY)) { + sw_unsupported_usage(kind); + if (exponent) scr_bytes_release(exponent); + goto done; + } + ScrPkiKey *key; + if (kind == SW_ED25519) { + key = scr_pki_generate_ed25519(); + if (!key) goto done; + } else if (kind == SW_ECDSA) { + key = scr_pki_generate_ec(curve); + } else { + /* publicExponent: big-endian, at most a 32-bit value. */ + size_t n = (size_t)scr_bytes_byte_len(exponent); + uint64_t value = 0; + bool fits = true; + for (size_t i = 0; i < n; i++) { + if (value > 0xffffffu && fits) fits = false; + value = (value << 8) | exponent->data[i]; + } + scr_bytes_release(exponent); + if (!fits || value > 0xffffffffu) { + scr_throw_domex("OperationError", "The publicExponent must be equivalent to an unsigned 32-bit value"); + goto done; + } + if (bits < 512) { + scr_throw_domex("OperationError", "algorithm.modulusLength must be at least 512"); + goto done; + } + if ((value & 1) == 0) { + scr_throw_domex("OperationError", "algorithm.publicExponent must be odd"); + goto done; + } + if (bits > 16384 || value < 3) { + scr_wc_unsupported("generateKey", "this RSA modulus length or public exponent"); + goto done; + } + key = scr_pki_generate_rsa((unsigned)bits, value); + } + if (!key) { + scr_throw_domex("OperationError", "Key generation failed"); + goto done; + } + unsigned private_usages = (unsigned)usages & SCR_WC_USAGE_SIGN; + unsigned public_usages = (unsigned)usages & SCR_WC_USAGE_VERIFY; + if (private_usages == 0) { + scr_pki_key_release(key); + scr_throw_domex("SyntaxError", "Usages cannot be empty when creating a key."); + goto done; + } + ScrPkiKey *public_key = scr_pki_key_public_of(key); + if (!public_key) { + scr_pki_key_release(key); + scr_throw_domex("OperationError", "Key generation failed"); + goto done; + } + pair = scr_dyn_new_obj(); + scr_dyn_obj_set(pair, "publicKey", 9, sw_key_new(public_key, kind, hash, public_usages, true)); + scr_dyn_obj_set(pair, "privateKey", 10, sw_key_new(key, kind, hash, private_usages, extractable)); +done: + if (plain) scr_dyn_release(plain); + scr_wc_args_drop(&a, 3); + if (!pair) return scr_wc_rejected(); + ScrPromise *p = scr_promise_new(); + scr_crypto_defer_dyn(pair, NULL, scr_crypto_promise_callback(p), scr_crypto_promise_settle_dyn); + return p; +} + +/* ── importKey ────────────────────────────────────────────────────────── */ + +/* A JWK string member, or NULL. */ +static const ScrDyn *sw_jwk_member(const ScrDyn *jwk, const char *key) { + const ScrDyn *v = scr_dyn_obj_get(jwk, key, strlen(key)); + return v && v->kind != SCR_DYN_UNDEF ? v : NULL; +} + +static bool sw_jwk_is(const ScrDyn *v, const char *text) { + return v && v->kind == SCR_DYN_STR && v->v.str->len == strlen(text) && + memcmp(v->v.str->data, text, v->v.str->len) == 0; +} + +/* The JWK checks Node runs before reading the key: use, key_ops and ext. */ +static bool sw_jwk_checks(const ScrDyn *jwk, unsigned usages, bool extractable) { + const ScrDyn *use = sw_jwk_member(jwk, "use"); + if (use && !sw_jwk_is(use, "sig")) { + scr_throw_domex("DataError", "Invalid JWK \"use\" Parameter"); + return false; + } + const ScrDyn *ops = sw_jwk_member(jwk, "key_ops"); + if (ops) { + if (ops->kind != SCR_DYN_ARR) { + scr_throw_domex("DataError", "Key operations and usage mismatch"); + return false; + } + for (size_t u = 0; u < SCR_WC_COUNT(scr_wc_usages); u++) { + if (!(usages & (1u << u))) continue; + bool listed = false; + double n = scr_dyn_arr_len(ops); + for (double i = 0; i < n && !listed; i++) { + ScrDyn *item = scr_dyn_arr_at(ops, i); + listed = sw_jwk_is(item, scr_wc_usages[u]); + scr_dyn_release(item); + } + if (!listed) { + scr_throw_domex("DataError", "Key operations and usage mismatch"); + return false; + } + } + } + const ScrDyn *ext = sw_jwk_member(jwk, "ext"); + if (ext && ext->kind == SCR_DYN_BOOL && !ext->v.b && extractable) { + scr_throw_domex("DataError", "JWK \"ext\" Parameter and extractable mismatch"); + return false; + } + return true; +} + +/* The JWK "alg" an RSA key with this hash carries ("RS256", "PS384"). */ +static void sw_rsa_jwk_alg(SwKind kind, const char *hash, char out[8]) { + const char *bits = strcmp(hash, "SHA-1") == 0 ? "1" : hash + 4; + snprintf(out, 8, "%s%s", kind == SW_PSS ? "PS" : "RS", bits); +} + +static ScrDyn *sw_import(const ScrDyn *args) { + static const char prefix[] = "Failed to execute 'importKey' on 'SubtleCrypto'"; + if (!scr_wc_required(prefix, args, 5)) return NULL; + ScrWcArgs a; + scr_wc_args_take(&a, args, 5); + ScrDyn *result = NULL; + ScrBytes *data = NULL; + ScrDyn *jwk = NULL; + ScrDyn *plain = NULL; + const char *format = scr_wc_key_format(prefix, a.v[0]); + if (!format) goto done; + bool is_jwk = strcmp(format, "jwk") == 0; + if (is_jwk) { + if (!scr_wc_is_object(a.v[1])) { + scr_wc_type_error("ERR_INVALID_ARG_TYPE", "%s: 2nd argument is not of type JsonWebKey.", prefix); + goto done; + } + jwk = scr_wc_plain(a.v[1]); + if (!jwk) goto done; + } else { + data = scr_wc_buffer_source(prefix, 1, a.v[1]); + if (!data) goto done; + } + if (!scr_wc_is_object(a.v[2])) { + ScrStr *s = scr_wc_dom_string(a.v[2]); + if (!s) goto done; + scr_str_release(s); + } + bool extractable = scr_dyn_truthy(a.v[3]); + long usages = scr_wc_key_usages(prefix, 4, a.v[4]); + if (usages < 0) goto done; + const char *name = sw_algorithm_name(a.v[2]); + if (!name) goto done; + SwKind kind = sw_kind_of(name); + plain = scr_wc_plain(a.v[2]); + if (!plain) goto done; + const char *curve = NULL; + const char *hash = NULL; + if (kind == SW_ED25519) { + /* no parameters */ + } else if (kind == SW_ECDSA) { + if (!scr_wc_is_object(plain)) { + scr_wc_missing_member("EcKeyImportParams", "namedCurve"); + goto done; + } + curve = sw_named_curve(plain, "EcKeyImportParams"); + if (!curve) goto done; + } else { + if (!scr_wc_is_object(plain)) { + scr_wc_missing_member("RsaHashedImportParams", "hash"); + goto done; + } + hash = sw_hash(plain, "RsaHashedImportParams", true, "importKey"); + if (!hash) goto done; + } + bool private_key; + if (is_jwk) { + private_key = sw_jwk_member(jwk, "d") != NULL; + } else if (strcmp(format, "pkcs8") == 0) { + private_key = true; + } else if (strcmp(format, "spki") == 0 || + ((kind == SW_ECDSA || kind == SW_ED25519) && strcmp(format, "raw") == 0)) { + private_key = false; + } else { + char message[96]; + snprintf(message, sizeof message, "Unable to import %s using %s format", sw_names[kind], format); + scr_throw_domex("NotSupportedError", message); + goto done; + } + unsigned allowed = private_key ? SCR_WC_USAGE_SIGN : SCR_WC_USAGE_VERIFY; + ScrPkiKey *key = NULL; + /* An Ed25519 private key derives its public key in the Ed25519 unit. */ + if (kind == SW_ED25519 && private_key && !scr_pki_ed25519_ready()) goto done; + if (is_jwk) { + const ScrDyn *kty = sw_jwk_member(jwk, "kty"); + if (!kty) { + scr_throw_domex("DataError", "Invalid keyData"); + goto done; + } + if (!sw_jwk_is(kty, kind == SW_ECDSA ? "EC" : kind == SW_ED25519 ? "OKP" : "RSA")) { + scr_throw_domex("DataError", "Invalid JWK \"kty\" Parameter"); + goto done; + } + if (kind == SW_ECDSA && !sw_jwk_is(sw_jwk_member(jwk, "crv"), curve)) { + scr_throw_domex("DataError", "JWK \"crv\" does not match the requested algorithm"); + goto done; + } + if (kind == SW_ED25519 && !sw_jwk_is(sw_jwk_member(jwk, "crv"), "Ed25519")) { + scr_throw_domex("DataError", "JWK \"crv\" Parameter and algorithm name mismatch"); + goto done; + } + if ((unsigned)usages & ~allowed) { + sw_unsupported_usage(kind); + goto done; + } + if (!sw_jwk_checks(jwk, (unsigned)usages, extractable)) goto done; + const ScrDyn *alg = sw_jwk_member(jwk, "alg"); + if (kind == SW_ED25519 && alg && !sw_jwk_is(alg, "Ed25519") && !sw_jwk_is(alg, "EdDSA")) { + scr_throw_domex("DataError", "JWK \"alg\" does not match the requested algorithm"); + goto done; + } + if (sw_is_rsa_kind(kind) && alg) { + char expected[8]; + sw_rsa_jwk_alg(kind, hash, expected); + if (!sw_jwk_is(alg, expected)) { + scr_throw_domex("DataError", "JWK \"alg\" does not match the requested algorithm"); + goto done; + } + } + key = scr_pki_key_from_jwk(jwk, private_key); + } else { + if ((unsigned)usages & ~allowed) { + sw_unsupported_usage(kind); + goto done; + } + ScrBytes *view = data; + SCR_BYTES_SNAPSHOT(view); + size_t len = (size_t)scr_bytes_byte_len(view); + if (strcmp(format, "raw") == 0) + key = kind == SW_ED25519 ? scr_pki_key_from_ed25519_raw(view->data, len) + : scr_pki_key_from_point(curve, view->data, len); + else key = scr_pki_key_parse_der(view->data, len, private_key); + } + if (!key) { + scr_throw_domex("DataError", "Invalid keyData"); + goto done; + } + bool key_matches = kind == SW_ED25519 ? scr_pki_key_is_ed25519(key) + : kind == SW_ECDSA ? !scr_pki_key_is_rsa(key) && !scr_pki_key_is_ed25519(key) + : scr_pki_key_is_rsa(key); + if (!key_matches) { + scr_pki_key_release(key); + scr_throw_domex("DataError", "Invalid key type"); + goto done; + } + if (kind == SW_ECDSA && strcmp(scr_pki_key_curve(key), curve) != 0) { + scr_pki_key_release(key); + scr_throw_domex("DataError", "Named curve mismatch"); + goto done; + } + if (private_key && usages == 0) { + scr_pki_key_release(key); + scr_throw_domex("SyntaxError", "Usages cannot be empty when importing a private key."); + goto done; + } + result = sw_key_new(key, kind, hash, (unsigned)usages, extractable); +done: + if (data) scr_bytes_release(data); + if (jwk) scr_dyn_release(jwk); + if (plain) scr_dyn_release(plain); + scr_wc_args_drop(&a, 5); + return result; +} + +static ScrPromise *sw_import_key(const ScrDyn *args) { + ScrDyn *key = sw_import(args); + if (!key) return scr_wc_rejected(); + return scr_promise_settled_ref(key, &scr_dyn_retain_v, &scr_dyn_release_v, NULL); +} + +/* ── exportKey ────────────────────────────────────────────────────────── */ + +static ScrDyn *sw_array_buffer(ScrBytes *bytes) { + ScrDyn *out = scr_array_buffer_from_bytes(bytes); + scr_bytes_release(bytes); + return out; +} + +static ScrDyn *sw_export(const ScrDyn *args) { + ScrWcArgs a; + scr_wc_args_take(&a, args, 2); + ScrDyn *result = NULL; + const char *format = scr_wc_key_format("Failed to execute 'exportKey' on 'SubtleCrypto'", a.v[0]); + SwKey *k = sw_key_of(a.v[1]); + bool private_key = scr_pki_key_is_private(k->key); + if (!format) goto done; + if (!k->extractable) { + scr_throw_domex("InvalidAccessError", "key is not extractable"); + goto done; + } + if (strcmp(format, "spki") == 0 || strcmp(format, "pkcs8") == 0) { + bool want_private = strcmp(format, "pkcs8") == 0; + if (want_private != private_key) { + scr_throw_domex("InvalidAccessError", want_private ? "Key must be a private key" : "Key must be a public key"); + goto done; + } + ScrBytes *der = scr_pki_export_der(k->key); + if (!der) { + scr_throw_domex("OperationError", "Failed to export key"); + goto done; + } + result = sw_array_buffer(der); + } else if (strcmp(format, "raw") == 0 && (k->kind == SW_ECDSA || k->kind == SW_ED25519) && + !private_key) { + ScrBytes *point = k->kind == SW_ED25519 ? scr_pki_export_ed25519_raw(k->key) : scr_pki_export_point(k->key); + if (!point) { + scr_throw_domex("OperationError", "Failed to export key"); + goto done; + } + result = sw_array_buffer(point); + } else if (strcmp(format, "jwk") == 0) { + ScrDyn *members = scr_pki_export_jwk(k->key); + ScrDyn *jwk = scr_dyn_new_obj(); + scr_dyn_obj_set(jwk, "key_ops", 7, sw_usage_array(k->usages)); + scr_dyn_obj_set(jwk, "ext", 3, scr_dyn_new_bool(k->extractable)); + if (sw_is_rsa_kind(k->kind)) { + char alg[8]; + sw_rsa_jwk_alg(k->kind, k->hash, alg); + scr_dyn_obj_set(jwk, "alg", 3, scr_wc_cstr(alg)); + } else if (k->kind == SW_ED25519) { + scr_dyn_obj_set(jwk, "alg", 3, scr_wc_cstr("Ed25519")); + } + /* The key members in Node's order for each key type. */ + static const char *const rsa_names[] = {"kty", "n", "e", "d", "p", "q", "dp", "dq", "qi"}; + static const char *const ec_names[] = {"kty", "x", "y", "crv", "d"}; + static const char *const okp_names[] = {"kty", "crv", "x", "d"}; + const char *const *names = sw_is_rsa_kind(k->kind) ? rsa_names : k->kind == SW_ED25519 ? okp_names : ec_names; + size_t count = sw_is_rsa_kind(k->kind) ? SCR_WC_COUNT(rsa_names) + : k->kind == SW_ED25519 ? SCR_WC_COUNT(okp_names) + : SCR_WC_COUNT(ec_names); + for (size_t i = 0; i < count; i++) { + const ScrDyn *v = scr_dyn_obj_get(members, names[i], strlen(names[i])); + if (v) scr_dyn_obj_set(jwk, names[i], strlen(names[i]), scr_dyn_retain((ScrDyn *)v)); + } + scr_dyn_release(members); + result = jwk; + } else { + char message[128]; + snprintf(message, sizeof message, "Unable to export %s %s key using %s format", sw_names[k->kind], + private_key ? "private" : "public", format); + scr_throw_domex("NotSupportedError", message); + } +done: + scr_wc_args_drop(&a, 2); + return result; +} + +static ScrPromise *sw_export_key(const ScrDyn *args) { + ScrDyn *value = sw_export(args); + if (!value) return scr_wc_rejected(); + return scr_promise_settled_ref(value, &scr_dyn_retain_v, &scr_dyn_release_v, NULL); +} + +/* ── sign and verify ──────────────────────────────────────────────────── */ + +/* The deferred verify result: a dyn boolean. */ +static void sw_settle_verified(ScrClosure *cb, ScrError *error, ScrBytes *value) { + (void)error; + ScrPromise *p; + memcpy(&p, &cb->caps[0]->slot, sizeof p); + ScrDyn *verified = (ScrDyn *)value; + scr_promise_fulfill_bool(p, scr_dyn_truthy(verified)); + scr_dyn_release(verified); +} + +static ScrPromise *sw_sign_verify(const ScrDyn *args, bool verify) { + const char *prefix = verify ? "Failed to execute 'verify' on 'SubtleCrypto'" + : "Failed to execute 'sign' on 'SubtleCrypto'"; + const char *method = verify ? "verify" : "sign"; + size_t n = verify ? 4 : 3; + if (!scr_wc_required(prefix, args, n)) return scr_wc_rejected(); + ScrWcArgs a; + scr_wc_args_take(&a, args, n); + ScrPromise *result = NULL; + ScrBytes *data = NULL; + ScrBytes *signature = NULL; + ScrDyn *plain = NULL; + if (!scr_wc_is_object(a.v[0])) { + ScrStr *s = scr_wc_dom_string(a.v[0]); + if (!s) goto done; + scr_str_release(s); + } + SwKey *k = sw_key_of(a.v[1]); + if (!k && !scr_webcrypto_is_crypto_key(a.v[1])) { + scr_wc_type_error("ERR_INVALID_ARG_TYPE", "%s: 2nd argument is not of type CryptoKey.", prefix); + goto done; + } + if (verify) { + signature = scr_wc_buffer_source(prefix, 2, a.v[2]); + if (!signature) goto done; + } + data = scr_wc_buffer_source(prefix, verify ? 3 : 2, a.v[verify ? 3 : 2]); + if (!data) goto done; + const char *name = sw_algorithm_name(a.v[0]); + if (!name) goto done; + SwKind kind = sw_kind_of(name); + plain = scr_wc_plain(a.v[0]); + if (!plain) goto done; + const char *hash = NULL; + int salt = 0; + if (kind == SW_ED25519) { + /* no parameters */ + } else if (kind == SW_ECDSA) { + if (!scr_wc_is_object(plain)) { + scr_wc_missing_member("EcdsaParams", "hash"); + goto done; + } + hash = sw_hash(plain, "EcdsaParams", false, method); + if (!hash) goto done; + } else if (kind == SW_PSS) { + if (!scr_wc_is_object(plain)) { + scr_wc_missing_member("RsaPssParams", "saltLength"); + goto done; + } + ScrDyn *value = sw_required(plain, "RsaPssParams", "saltLength"); + if (!value) goto done; + double x; + bool ok = sw_unsigned_long(value, "saltLength", &x); + scr_dyn_release(value); + if (!ok) goto done; + salt = x > 65536 ? 65536 : (int)x; + } + if (!k || k->kind != kind) { + scr_throw_domex("InvalidAccessError", "Key algorithm mismatch"); + goto done; + } + if (!(k->usages & (verify ? SCR_WC_USAGE_VERIFY : SCR_WC_USAGE_SIGN))) { + char message[64]; + snprintf(message, sizeof message, "Unable to use this key to %s", method); + scr_throw_domex("InvalidAccessError", message); + goto done; + } + if (sw_is_rsa_kind(kind)) hash = k->hash; + if (kind == SW_ED25519 && !scr_pki_ed25519_ready()) goto done; + { + ScrBytes *view = data; + SCR_BYTES_SNAPSHOT(view); + size_t len = (size_t)scr_bytes_byte_len(view); + if (verify) { + ScrBytes *sig = signature; + SCR_BYTES_SNAPSHOT(sig); + bool ok = scr_pki_verify(k->key, hash, kind == SW_PSS, salt, view->data, len, sig->data, + (size_t)scr_bytes_byte_len(sig)); + result = scr_promise_new(); + scr_crypto_defer_dyn(scr_dyn_new_bool(ok), NULL, scr_crypto_promise_callback(result), sw_settle_verified); + } else { + ScrBytes *out = scr_pki_sign(k->key, hash, kind == SW_PSS, salt, view->data, len); + if (!out) { + scr_throw_domex("OperationError", "Failed to sign"); + goto done; + } + result = scr_promise_new(); + scr_crypto_defer_bytes(out, NULL, scr_crypto_promise_callback(result), scr_crypto_promise_settle_array_buffer); + } + } +done: + if (data) scr_bytes_release(data); + if (signature) scr_bytes_release(signature); + if (plain) scr_dyn_release(plain); + scr_wc_args_drop(&a, n); + return result ? result : scr_wc_rejected(); +} + +static ScrPromise *sw_sign(const ScrDyn *args) { return sw_sign_verify(args, false); } + +static ScrPromise *sw_verify(const ScrDyn *args) { return sw_sign_verify(args, true); } + +static const ScrWcAsymmetric sw_table = { + .generate_key = sw_generate_key, + .import_key = sw_import_key, + .export_key = sw_export_key, + .sign = sw_sign, + .verify = sw_verify, + .is_key = sw_is_key, +}; + +/* Installed before main: scr_webcrypto.c dispatches these algorithms + * here. */ +__attribute__((constructor)) static void sw_install(void) { scr_wc_asymmetric = &sw_table; } diff --git a/packages/runtime/src/scr_worker.c b/packages/runtime/src/scr_worker.c index 67f4ec084..59e245ea0 100644 --- a/packages/runtime/src/scr_worker.c +++ b/packages/runtime/src/scr_worker.c @@ -99,9 +99,10 @@ static bool scr_worker_stats(void) { static ScrMailEvent *scr_worker_event(unsigned kind, uint64_t source, int code, ScrMessage *message) { - ScrMailEvent *event = calloc(1, sizeof(*event)); + ScrMailEvent *event = message ? scr_message_mail_event(message) : calloc(1, sizeof(*event)); if (!event) scr_trap("scriptc: out of memory\n"); - *event = (ScrMailEvent){.kind = kind, .source = source, .code = code, .message = message}; + *event = (ScrMailEvent){ + .kind = kind, .source = source, .code = code, .message = message, .in_message = message != NULL}; return event; } diff --git a/packages/runtime/src/scr_worker_events.c b/packages/runtime/src/scr_worker_events.c index 252230372..3d10183f4 100644 --- a/packages/runtime/src/scr_worker_events.c +++ b/packages/runtime/src/scr_worker_events.c @@ -665,6 +665,17 @@ static bool scr_channel_dispatch(void) { continue; } scr_worker_view_retain(view); + if (event->message && !view->broadcast && scr_message_readable(event->message)) { + /* Compiled listeners with a typed message entry read the message. */ + ScrStr *name = scr_str_new("message", 7); + scr_dyn_this_push(view, SCR_DYNH_MESSAGE_PORT); + scr_emitter_emit_message(view->events, name, event->message, scr_message_decode); + scr_dyn_this_pop(); + scr_str_release(name); + scr_mail_event_free(event); + scr_worker_view_release(view); + return true; + } ScrDyn *argument = event->message ? scr_message_decode(event->message) : NULL; if (argument && view->broadcast) { ScrDyn *self = scr_dyn_new_handle(view, SCR_DYNH_BROADCAST_CHANNEL); @@ -815,6 +826,19 @@ static void scr_worker_dispatch(void) { } } scr_worker_view_retain(view); + if (event->kind == SCR_WORKER_MESSAGE && event->message && scr_message_readable(event->message)) { + /* Compiled listeners with a typed message entry read the message. */ + if (!scr_exc_pending()) { + ScrStr *name = scr_str_new("message", 7); + scr_dyn_this_push(view, view->port ? SCR_DYNH_MESSAGE_PORT : SCR_DYNH_WORKER); + scr_emitter_emit_message(view->events, name, event->message, scr_message_decode); + scr_dyn_this_pop(); + scr_str_release(name); + } + scr_worker_view_release(view); + scr_mail_event_free(event); + return; + } const char *event_name = event->kind == SCR_WORKER_ONLINE ? "online" : event->kind == SCR_WORKER_EXIT ? "exit" : event->kind == SCR_WORKER_ERROR ? "error" : "message"; ScrDyn *argument = event->message ? scr_message_decode(event->message) : diff --git a/packages/runtime/test/dh.test.ts b/packages/runtime/test/dh.test.ts new file mode 100644 index 000000000..d402d0465 --- /dev/null +++ b/packages/runtime/test/dh.test.ts @@ -0,0 +1,85 @@ +import { execFile } from "node:child_process"; +import { mkdir } from "node:fs/promises"; +import { join } from "node:path"; +import { promisify } from "node:util"; +import { beforeAll, expect, test } from "vitest"; + +const execFileAsync = promisify(execFile); +const testDir = import.meta.dirname; +const bin = join(testDir, "build", "test_dh"); +const mbedtls = join(testDir, "../vendor/mbedtls"); + +const runtimeSources = [ + "scr_shared.c", + "scr_bytes.c", + "scr_bytes.test.c", + "scr_bytes_io.c", + "scr_fs_cp.c", + "scr_zlib.c", + "scr_lib.c", + "scr_hash.c", + "scr_string.c", + "scr_array.c", + "scr_map.c", + "scr_exception.c", + "scr_error.c", + "scr_number.c", + "scr_console.c", + "scr_closure.c", + "scr_ffi.c", + "scr_object.c", + "scr_union.c", + "scr_cycle.c", + "scr_alloc.c", + "scr_json.c", + "scr_inspect.c", + "scr_bigint.c", + "scr_async.c", + "scr_async_dyn.c", + "scr_child.c", + "scr_path.c", + "scr_url.c", + "scr_url_params.c", +]; + +// The bignum modules scr_dh.c uses. +const mbedtlsSources = ["bignum.c", "bignum_core.c", "constant_time.c", "platform_util.c"]; + +// Compiled once with ASan, UBSan and the RC audit. test_dh.c includes +// scr_dh.c and checks the primality tests' Montgomery exponentiation +// against mbedTLS's, the primality test on primes, Carmichael numbers and +// strong pseudoprimes, exponents past mbedTLS's 8192-bit limit, DH_check +// results, and generated safe primes. +beforeAll(async () => { + await mkdir(join(testDir, "build"), { recursive: true }); + await execFileAsync("clang", [ + "-std=c11", + "-O1", + "-Wall", + "-Wextra", + "-Wno-missing-field-initializers", + "-fsanitize=address,undefined", + "-DSCR_RC_AUDIT", + "-I", + join(testDir, "../src"), + "-I", + join(mbedtls, "include"), + "-I", + join(mbedtls, "library"), + "-o", + bin, + join(testDir, "test_dh.c"), + ...runtimeSources.map((source) => join(testDir, "../src", source)), + ...mbedtlsSources.map((source) => join(mbedtls, "library", source)), + "-Wno-deprecated-declarations", + "-lz", + ...(process.platform === "linux" ? ["-D_GNU_SOURCE", "-lm"] : []), + ]); +}, 300_000); + +test("DiffieHellman runtime: exponentiation, primality and safe primes", async () => { + const { stderr } = await execFileAsync(bin, [], { + env: { ...process.env, UBSAN_OPTIONS: "halt_on_error=1" }, + }); + expect(stderr.trim().split("\n").pop()).toMatch(/^(\d+)\/\1 cases passed$/); +}, 300_000); diff --git a/packages/runtime/test/test_dh.c b/packages/runtime/test/test_dh.c new file mode 100644 index 000000000..f459f1047 --- /dev/null +++ b/packages/runtime/test/test_dh.c @@ -0,0 +1,280 @@ +/* Unit tests for the DiffieHellman runtime's arithmetic (scr_dh.c), built + * with ASan and UBSan by dh.test.ts. The unit is included directly so the + * tests can compare the Montgomery exponentiation the primality tests use + * with mbedTLS's, run the primality test on primes, Carmichael numbers and + * strong pseudoprimes to small bases, split exponents past mbedTLS's limit, + * and check generated safe primes and DH_check results. */ +#include "../src/scr_dh.c" + +#include +#include + +static long total = 0, failed = 0; + +static void check(bool ok, const char *what) { + total++; + if (!ok) { + failed++; + fprintf(stderr, "FAIL: %s\n", what); + } +} + +static void from_hex(mbedtls_mpi *x, const char *hex) { + if (mbedtls_mpi_read_string(x, 16, hex) != 0) scr_trap("bad hex\n"); +} + +/* The Montgomery exponentiation, with the base 2 and with general bases, + * against mbedtls_mpi_exp_mod for moduli of one to 64 limbs. */ +static void test_mont_pow(void) { +#if SD_MONT + static const size_t sizes[] = {3, 64, 65, 127, 128, 129, 512, 1000, 2048, 4096}; + mbedtls_mpi n, base, e, two, got, want; + mbedtls_mpi_init(&n); + mbedtls_mpi_init(&base); + mbedtls_mpi_init(&e); + mbedtls_mpi_init(&two); + mbedtls_mpi_init(&got); + mbedtls_mpi_init(&want); + mbedtls_mpi_lset(&two, 2); + for (size_t s = 0; s < sizeof sizes / sizeof sizes[0]; s++) { + for (int trial = 0; trial < 6; trial++) { + size_t bits = sizes[s]; + sd_random_bits(&n, bits, true); + if (mbedtls_mpi_cmp_int(&n, 3) <= 0) mbedtls_mpi_lset(&n, 7); + sd_random_bits(&base, bits > 2 ? bits - 1 : 1, false); + size_t ebits = trial == 0 ? 1 : trial == 1 ? bits + 70 : (bits * (size_t)trial) / 5 + 1; + sd_random_bits(&e, ebits, false); + if (trial == 0) mbedtls_mpi_lset(&e, 0); + ScDhMont m; + bool ready = sd_mont_init(&m, &n); + check(ready, "sd_mont_init"); + char what[96]; + snprintf(what, sizeof what, "base^e mod n for %zu bits, trial %d", bits, trial); + bool ok = ready && sd_mont_pow(&m, &got, &base, &e) && mbedtls_mpi_exp_mod(&want, &base, &e, &n, NULL) == 0; + check(ok && mbedtls_mpi_cmp_mpi(&got, &want) == 0, what); + snprintf(what, sizeof what, "2^e mod n for %zu bits, trial %d", bits, trial); + ok = ready && sd_mont_pow(&m, &got, NULL, &e) && mbedtls_mpi_exp_mod(&want, &two, &e, &n, NULL) == 0; + check(ok && mbedtls_mpi_cmp_mpi(&got, &want) == 0, what); + sd_mont_free(&m); + } + } + mbedtls_mpi_free(&n); + mbedtls_mpi_free(&base); + mbedtls_mpi_free(&e); + mbedtls_mpi_free(&two); + mbedtls_mpi_free(&got); + mbedtls_mpi_free(&want); +#endif +} + +/* BN_check_prime's answers for small values, Mersenne numbers, Carmichael + * numbers and strong pseudoprimes to small prime bases. */ +static void test_is_prime(void) { + static const struct { + const char *hex; + bool prime; + } cases[] = { + {"0", false}, + {"1", false}, + {"2", true}, + {"3", true}, + {"4", false}, + {"5", true}, + {"9", false}, + {"231", false}, /* 561, a Carmichael number */ + {"451", false}, /* 1105, a Carmichael number */ + {"7ff", false}, /* 2047, a strong pseudoprime to the base 2 */ + {"45c5", false}, /* 17861 = 53 · 337 */ + {"45c7", true}, /* 17863, the largest trial-division prime */ + {"bfa17dc7", false}, /* 3215031751, to the bases 2, 3, 5 and 7 */ + {"351591274f9af9fb", false}, /* 3825123056546413051, to the bases up to 23 */ + {"437ae92817f9fc85b7e5", false}, /* to the prime bases up to 37 */ + {"2be6951adc5b22410a5fd", false}, /* to the prime bases up to 41 */ + {"1fffffffffffffff", true}, /* 2^61 - 1 */ + {"1ffffffffffffffffffffff", true}, /* 2^89 - 1 */ + {"7fffffffffffffffffffffffffffffff", true}, /* 2^127 - 1 */ + {"100000000000000000000000000000001", false}, /* 2^128 + 1 */ + }; + ScDhSmallPrimes *small = sd_small_primes(); + mbedtls_mpi x; + mbedtls_mpi_init(&x); + for (size_t i = 0; i < sizeof cases / sizeof cases[0]; i++) { + from_hex(&x, cases[i].hex); + char what[96]; + snprintf(what, sizeof what, "is_prime(0x%s) == %d", cases[i].hex, cases[i].prime); + check(sd_is_prime(small, &x) == cases[i].prime, what); + } + /* The modp groups' primes are safe primes. */ + for (size_t i = 0; i < 4; i++) { + mbedtls_mpi_read_binary(&x, sd_groups[i].p, sd_groups[i].p_len); + check(sd_is_prime(small, &x), "a modp prime is prime"); + mbedtls_mpi_shift_r(&x, 1); + check(sd_is_prime(small, &x), "half a modp prime is prime"); + mbedtls_mpi_add_int(&x, &x, 2); + check(!sd_is_prime(small, &x), "half a modp prime plus 2 is composite"); + } + mbedtls_mpi_free(&x); + free(small); +} + +/* DH_check bits for small and composite primes, the Pocklington path for + * safe primes, and a generator at p - 1. */ +static void test_check_params(void) { + static const struct { + const char *p; + int g; + int codes; + } cases[] = { + {"0", 2, 137}, {"1", 2, 137}, {"2", 2, 139}, {"3", 2, 138}, {"5", 2, 128}, {"5", 5, 136}, + {"7", 2, 128}, {"9", 2, 129}, {"f", 2, 129}, {"11", 2, 130}, {"17", 2, 128}, {"17", 22, 136}, + {"18", 2, 129}, {"231", 2, 129}, {"7ff", 2, 129}, + }; + mbedtls_mpi p, g; + mbedtls_mpi_init(&p); + mbedtls_mpi_init(&g); + for (size_t i = 0; i < sizeof cases / sizeof cases[0]; i++) { + from_hex(&p, cases[i].p); + mbedtls_mpi_lset(&g, cases[i].g); + char what[96]; + int codes = sd_check_params(&p, &g); + snprintf(what, sizeof what, "check(0x%s, %d) == %d (got %d)", cases[i].p, cases[i].g, cases[i].codes, codes); + check(codes == cases[i].codes, what); + } + mbedtls_mpi_read_binary(&p, sd_groups[1].p, sd_groups[1].p_len); + mbedtls_mpi_lset(&g, 2); + check(sd_check_params(&p, &g) == 0, "check(modp2, 2) == 0"); + mbedtls_mpi_sub_int(&g, &p, 1); + check(sd_check_params(&p, &g) == SD_NOT_SUITABLE_GENERATOR, "check(modp2, p - 1) == 8"); + mbedtls_mpi_lset(&g, 2); + mbedtls_mpi_add_int(&p, &p, 2); + check(sd_check_params(&p, &g) == SD_P_NOT_PRIME, "check(modp2 + 2, 2) == 1"); + mbedtls_mpi_free(&p); + mbedtls_mpi_free(&g); +} + +/* Exponents past 8192 bits, split into digits, against the exponent + * reduced modulo p - 1 (Fermat) for a prime p. */ +static void test_long_exponent(void) { + mbedtls_mpi p, p1, base, e, reduced, got, want; + mbedtls_mpi_init(&p); + mbedtls_mpi_init(&p1); + mbedtls_mpi_init(&base); + mbedtls_mpi_init(&e); + mbedtls_mpi_init(&reduced); + mbedtls_mpi_init(&got); + mbedtls_mpi_init(&want); + mbedtls_mpi_read_binary(&p, sd_groups[0].p, sd_groups[0].p_len); + mbedtls_mpi_sub_int(&p1, &p, 1); + mbedtls_mpi_lset(&base, 5); + static const size_t sizes[] = {8192, 8193, 16382, 16383, 20000}; + for (size_t i = 0; i < sizeof sizes / sizeof sizes[0]; i++) { + sd_random_bits(&e, sizes[i], false); + mbedtls_mpi_mod_mpi(&reduced, &e, &p1); + bool ok = sd_exp(&got, &base, &e, &p) && mbedtls_mpi_exp_mod(&want, &base, &reduced, &p, NULL) == 0; + char what[64]; + snprintf(what, sizeof what, "5^e mod p for a %zu-bit e", sizes[i]); + check(ok && mbedtls_mpi_cmp_mpi(&got, &want) == 0, what); + } + mbedtls_mpi_lset(&p, 24); + check(!sd_exp(&got, &base, &e, &p), "an even modulus fails"); + mbedtls_mpi_lset(&p, 1); + check(!sd_exp(&got, &base, &e, &p), "the modulus 1 fails"); + mbedtls_mpi_free(&p); + mbedtls_mpi_free(&p1); + mbedtls_mpi_free(&base); + mbedtls_mpi_free(&e); + mbedtls_mpi_free(&reduced); + mbedtls_mpi_free(&got); + mbedtls_mpi_free(&want); +} + +/* Generated safe primes have the requested length and congruence, and + * mbedTLS's own primality test accepts p and (p - 1) / 2. */ +static void test_generate(void) { + static const unsigned adds[] = {24, 60, 12}; + mbedtls_mpi p, q, g; + mbedtls_mpi_init(&p); + mbedtls_mpi_init(&q); + mbedtls_mpi_init(&g); + mbedtls_mpi_lset(&g, 2); + for (size_t i = 0; i < sizeof adds / sizeof adds[0]; i++) { + for (int trial = 0; trial < 2; trial++) { + bool ok = sd_generate_safe_prime(&p, 512, adds[i], adds[i] - 1, false); + mbedtls_mpi_uint r = 0; + ok = ok && mbedtls_mpi_bitlen(&p) == 512 && mbedtls_mpi_mod_int(&r, &p, adds[i]) == 0 && r == adds[i] - 1; + mbedtls_mpi_copy(&q, &p); + mbedtls_mpi_shift_r(&q, 1); + ok = ok && mbedtls_mpi_is_prime_ext(&p, 40, sd_random, NULL) == 0 && + mbedtls_mpi_is_prime_ext(&q, 40, sd_random, NULL) == 0; + char what[64]; + snprintf(what, sizeof what, "a 512-bit safe prime ≡ %u (mod %u)", adds[i] - 1, adds[i]); + check(ok, what); + check(sd_check_params(&p, &g) == 0, "a generated prime has no DH_check findings"); + } + } + mbedtls_mpi_free(&p); + mbedtls_mpi_free(&q); + mbedtls_mpi_free(&g); +} + +/* generatePrime's search: the sizes and add forms with one possible + * result, top-two-bit safe-prime starts, and checks above OpenSSL's + * round count. */ +static void test_generate_prime(void) { + static const struct { + size_t bits; + bool safe; + int add, rem; + int want; + } cases[] = { + {2, false, 0, 0, 3}, {3, false, 0, 0, 7}, {4, false, 0, 0, 13}, {3, true, 0, 0, 7}, + {6, true, 0, 0, 59}, {7, true, 0, 0, 107}, {8, false, 200, 1, 401}, {8, false, 128, 0, 257}, + {5, false, 16, 3, 19}, + }; + mbedtls_mpi p, add, rem, q; + mbedtls_mpi_init(&p); + mbedtls_mpi_init(&add); + mbedtls_mpi_init(&rem); + mbedtls_mpi_init(&q); + for (size_t i = 0; i < sizeof cases / sizeof cases[0]; i++) { + mbedtls_mpi_lset(&add, cases[i].add); + mbedtls_mpi_lset(&rem, cases[i].rem); + bool ok = sd_generate_prime(&p, cases[i].bits, cases[i].safe, cases[i].add ? &add : NULL, + cases[i].rem ? &rem : NULL) && + mbedtls_mpi_cmp_int(&p, cases[i].want) == 0; + char what[96]; + snprintf(what, sizeof what, "generatePrime(%zu, safe %d, add %d, rem %d) is %d", cases[i].bits, cases[i].safe, + cases[i].add, cases[i].rem, cases[i].want); + check(ok, what); + } + for (int trial = 0; trial < 2; trial++) { + bool ok = sd_generate_safe_prime(&p, 128, 12, 11, true) && mbedtls_mpi_bitlen(&p) == 128 && + mbedtls_mpi_get_bit(&p, 126) == 1; + mbedtls_mpi_copy(&q, &p); + mbedtls_mpi_shift_r(&q, 1); + ok = ok && mbedtls_mpi_is_prime_ext(&p, 40, sd_random, NULL) == 0 && + mbedtls_mpi_is_prime_ext(&q, 40, sd_random, NULL) == 0; + check(ok, "a 128-bit safe prime with its top two bits set"); + } + ScDhSmallPrimes *small = sd_small_primes(); + mbedtls_mpi_read_string(&q, 10, "3215031751"); + check(!sd_is_prime_checks(small, &q, 200), "200 rounds reject a strong pseudoprime to 2, 3, 5 and 7"); + mbedtls_mpi_read_string(&q, 10, "2305843009213693951"); + check(sd_is_prime_checks(small, &q, 200), "200 rounds accept 2^61 - 1"); + free(small); + mbedtls_mpi_free(&p); + mbedtls_mpi_free(&add); + mbedtls_mpi_free(&rem); + mbedtls_mpi_free(&q); +} + +int main(void) { + test_mont_pow(); + test_is_prime(); + test_check_params(); + test_long_exponent(); + test_generate(); + test_generate_prime(); + fprintf(stderr, "%ld/%ld cases passed\n", total - failed, total); + return failed ? 1 : 0; +} diff --git a/packages/runtime/test/test_map.c b/packages/runtime/test/test_map.c index 32bdb7be1..46805da1e 100644 --- a/packages/runtime/test/test_map.c +++ b/packages/runtime/test/test_map.c @@ -826,11 +826,96 @@ static void test_absent_key_filter(void) { for (int i = 0; i < UNIVERSE; i++) scr_str_release(names[i]); } +/* SCR_MAP_VAL_CREF: string keys with compact class values in 16-byte + * entries (a 32-bit hash, the value as an address over 8). The values here + * are stand-in addresses, never dereferenced. */ +static uint64_t cref_val(size_t i) { return (uint64_t)(i + 1) * 8; } + +static void test_compact_values(void) { + ScrMap *m = scr_map_new(SCR_MAP_KEY_STR, SCR_MAP_VAL_CREF, NULL); + enum { N = 2000 }; + char buf[32]; + ScrStr *keys[N]; + for (size_t i = 0; i < N; i++) { + snprintf(buf, sizeof buf, "k%zu", i); + keys[i] = S(buf); + scr_map_set_str_ref(m, keys[i], (void *)(uintptr_t)cref_val(i)); + } + check(scr_map_size(m) == N, "cref: size after inserts"); + bool all = true; + for (size_t i = 0; i < N; i++) { + all &= scr_map_get_str_ref(m, keys[i]) == (void *)(uintptr_t)cref_val(i); + const ScrMapCEntry *e = scr_map_entry_str_c(m, keys[i]); + all &= e && ((uint64_t)e->val << 3) == cref_val(i) && e->key == (uint64_t)(uintptr_t)keys[i]; + } + check(all, "cref: get and entry_str_c find every value"); + ScrStr *missing = S("absent"); + check(!scr_map_get_str_ref(m, missing) && !scr_map_entry_str_c(m, missing), "cref: absent key"); + check(scr_map_get_span_ref(m, "k7", 2) == (void *)(uintptr_t)cref_val(7), "cref: span lookup"); + /* A key object other than the stored one, equal bytes. */ + ScrStr *again = S("k42"); + check(scr_map_entry_str_c(m, again) && scr_map_get_str_ref(m, again) == (void *)(uintptr_t)cref_val(42), + "cref: equal key bytes, other object"); + scr_str_release(again); + for (size_t i = 0; i < N; i += 2) scr_map_set_str_ref(m, keys[i], (void *)(uintptr_t)cref_val(i + N)); + all = true; + for (size_t i = 0; i < N; i++) + all &= scr_map_get_str_ref(m, keys[i]) == (void *)(uintptr_t)cref_val(i % 2 ? i : i + N); + check(all && scr_map_size(m) == N, "cref: overwrite keeps the entry"); + for (size_t i = 0; i < N; i++) + if (i % 4) scr_map_delete_str(m, keys[i]); + check(scr_map_size(m) == N / 4, "cref: deletes"); + all = true; + for (size_t i = 0; i < N; i++) { + bool want = i % 4 == 0; + all &= scr_map_has_str(m, keys[i]) == want; + if (want) all &= scr_map_get_str_ref(m, keys[i]) == (void *)(uintptr_t)cref_val(i + N); + } + check(all, "cref: lookups after compaction"); + /* Iteration order and values. */ + size_t seen = 0; + all = true; + scr_map_iter_enter(m); + for (double i = scr_map_iter_start(m); i < scr_map_iter_count(m); i = scr_map_iter_skip(m, i + 1)) { + ScrStr *k = scr_map_iter_key_str(m, i); + void *v = scr_map_iter_val_ref(m, i); + size_t want = seen * 4; + all &= k->len == keys[want]->len && memcmp(k->data, keys[want]->data, k->len) == 0; + all &= v == (void *)(uintptr_t)cref_val(want + N); + scr_str_release(k); + seen++; + } + scr_map_iter_exit(m); + check(all && seen == N / 4, "cref: iteration order and values"); + /* Copies keep the layout; a key-only copy takes the 24-byte one. */ + ScrMap *copy = scr_map_clone(m, false, NULL); + ScrMap *set = scr_map_clone(m, true, NULL); + all = scr_map_size(copy) == N / 4 && scr_map_size(set) == N / 4; + for (size_t i = 0; i < N; i += 4) { + all &= scr_map_get_str_ref(copy, keys[i]) == (void *)(uintptr_t)cref_val(i + N); + all &= scr_map_has_str(set, keys[i]); + } + all &= !scr_map_has_str(set, keys[1]); + check(all, "cref: clone and key-only clone"); + scr_map_set_str_ref(copy, keys[1], (void *)(uintptr_t)cref_val(1)); + check(!scr_map_has_str(m, keys[1]) && scr_map_size(copy) == N / 4 + 1, "cref: copies are independent"); + scr_map_clear(m); + check(scr_map_size(m) == 0 && !scr_map_has_str(m, keys[0]), "cref: clear"); + scr_map_set_str_ref(m, keys[3], (void *)(uintptr_t)cref_val(3)); + check(scr_map_get_str_ref(m, keys[3]) == (void *)(uintptr_t)cref_val(3), "cref: reuse after clear"); + scr_map_release(copy); + scr_map_release(set); + scr_map_release(m); + scr_str_release(missing); + for (size_t i = 0; i < N; i++) scr_str_release(keys[i]); +} + int main(void) { test_key_bytes(); test_string_bucket_collisions(); test_string_keys(); test_string_entries(); + test_compact_values(); test_same_value_zero(); test_span_keys(); test_scratch_keys(); diff --git a/packages/runtime/vendor/README.md b/packages/runtime/vendor/README.md index 977179609..7ab8cf6b0 100644 --- a/packages/runtime/vendor/README.md +++ b/packages/runtime/vendor/README.md @@ -48,3 +48,15 @@ The objects build lazily on the first zlib-using cross compile into the per-user The TLS provider behind node:tls/node:https (see the design note atop packages/runtime/src/scr_tls.c for why mbedTLS over SecureTransport/BoringSSL/libtls). Only `include/` and `library/` are vendored (LICENSE beside them) — tests, docs, programs, scripts, and CMake machinery are not. No vendored file is modified and no custom config is applied (the stock `mbedtls_config.h` builds every `library/*.c` standalone with clang); to update, re-fetch the release tarball, copy the same two directories, and bump `MBEDTLS_VERSION` in packages/compiler/src/backend/vendor-archives.ts. The archive (libmbedtls.a) is prebuilt best-effort during npm installation (or explicitly by `scriptc cache warm`) and otherwise built lazily on the first TLS-using compile. It lives under the per-user build cache's `vendor/mbedtls----/` directory — one flavor per lane (plain, asan), native platform/architecture or explicit cross target, and compiler environment/identity, the libqjs.a pattern. TLS-free binaries never compile or link any of this. + +## libsodium/ + +- Project: https://github.com/jedisct1/libsodium +- Version: 1.0.20 (release tarball libsodium-1.0.20.tar.gz, sha256 ebb65ef6ca439333c2bb41a0c1990587288da07f6c7fd07cb3a18cc18d30ce19) +- License: ISC (see libsodium/LICENSE) + +The Ed25519 arithmetic behind node:crypto and WebCrypto Ed25519 keys (see packages/runtime/src/scr_ed25519.c). Its verification matches Node 24: it rejects a non-canonical S and the small-order R and public-key encodings, then checks the cofactorless equation. Only the ref10 Ed25519 subset is vendored, flat in one directory: `crypto_core/ed25519/ref10/ed25519_ref10.c` with its `fe_25_5/` headers, `crypto_sign/ed25519/ref10/{sign,open,keypair}.c` and `sign_ed25519_ref10.h`, `crypto_hash/sha512/cp/hash_sha512_cp.c`, `crypto_verify/verify.c`, and the public and private headers they include. The 64-bit `fe_51` field code (used only with `HAVE_TI_MODE`) and the rest of the library are not vendored. + +The only modification to upstream files: `#include "private/x.h"` flattened to `#include "x.h"` in ed25519_ref10.c, sign.c, open.c, keypair.c, hash_sha512_cp.c and ed25519_ref10_fe_25_5.h, so the directory is self-contained. scr_ed25519.c textually includes the .c files and supplies `sodium_memzero`, `sodium_memcmp`, `sodium_is_zero` and `randombytes_buf` (from `arc4random_buf`) in place of libsodium's utils.c and randombytes.c. To update, re-fetch the release tarball, copy the same files, and re-apply that include flattening. + +The unit compiles only into programs that use asymmetric keys and name Ed25519 in a string literal (the `ed25519` runtime feature); other binaries never compile or link any of this. diff --git a/packages/runtime/vendor/libsodium/LICENSE b/packages/runtime/vendor/libsodium/LICENSE new file mode 100644 index 000000000..6bce7f058 --- /dev/null +++ b/packages/runtime/vendor/libsodium/LICENSE @@ -0,0 +1,18 @@ +/* + * ISC License + * + * Copyright (c) 2013-2024 + * Frank Denis + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ diff --git a/packages/runtime/vendor/libsodium/README.md b/packages/runtime/vendor/libsodium/README.md new file mode 100644 index 000000000..5207d8ad6 --- /dev/null +++ b/packages/runtime/vendor/libsodium/README.md @@ -0,0 +1,7 @@ +# libsodium 1.0.20 (Ed25519 subset) + +- Source: https://download.libsodium.org/libsodium/releases/libsodium-1.0.20.tar.gz (the same tarball as the GitHub release 1.0.20-RELEASE) +- sha256: ebb65ef6ca439333c2bb41a0c1990587288da07f6c7fd07cb3a18cc18d30ce19 +- License: ISC, see LICENSE + +See ../README.md for the files vendored, the one modification, and how to update. diff --git a/packages/runtime/vendor/libsodium/common.h b/packages/runtime/vendor/libsodium/common.h new file mode 100644 index 000000000..63f0d2b2d --- /dev/null +++ b/packages/runtime/vendor/libsodium/common.h @@ -0,0 +1,285 @@ +#ifndef common_H +#define common_H 1 + +#if !defined(_MSC_VER) && !defined(DEV_MODE) && 0 +# warning *** This is unstable, untested, development code. +# warning It might not compile. It might not work as expected. +# warning It might be totally insecure. +# warning Do not use this except if you are planning to contribute code. +# warning Use releases available at https://download.libsodium.org/libsodium/releases/ instead. +# warning Alternatively, use the "stable" branch in the git repository. +#endif + +#if !defined(_MSC_VER) && (!defined(CONFIGURED) || CONFIGURED != 1) +# warning *** The library is being compiled using an undocumented method. +# warning This is not supported. It has not been tested, it might not +# warning work as expected, and performance is likely to be suboptimal. +#endif + +#include +#include +#include + +#define COMPILER_ASSERT(X) (void) sizeof(char[(X) ? 1 : -1]) + +#ifdef HAVE_TI_MODE +# if defined(__SIZEOF_INT128__) +typedef unsigned __int128 uint128_t; +# else +typedef unsigned uint128_t __attribute__((mode(TI))); +# endif +#endif + +#define ROTL32(X, B) rotl32((X), (B)) +static inline uint32_t +rotl32(const uint32_t x, const int b) +{ + return (x << b) | (x >> (32 - b)); +} + +#define ROTL64(X, B) rotl64((X), (B)) +static inline uint64_t +rotl64(const uint64_t x, const int b) +{ + return (x << b) | (x >> (64 - b)); +} + +#define ROTR32(X, B) rotr32((X), (B)) +static inline uint32_t +rotr32(const uint32_t x, const int b) +{ + return (x >> b) | (x << (32 - b)); +} + +#define ROTR64(X, B) rotr64((X), (B)) +static inline uint64_t +rotr64(const uint64_t x, const int b) +{ + return (x >> b) | (x << (64 - b)); +} + +#define LOAD64_LE(SRC) load64_le(SRC) +static inline uint64_t +load64_le(const uint8_t src[8]) +{ +#ifdef NATIVE_LITTLE_ENDIAN + uint64_t w; + memcpy(&w, src, sizeof w); + return w; +#else + uint64_t w = (uint64_t) src[0]; + w |= (uint64_t) src[1] << 8; + w |= (uint64_t) src[2] << 16; + w |= (uint64_t) src[3] << 24; + w |= (uint64_t) src[4] << 32; + w |= (uint64_t) src[5] << 40; + w |= (uint64_t) src[6] << 48; + w |= (uint64_t) src[7] << 56; + return w; +#endif +} + +#define STORE64_LE(DST, W) store64_le((DST), (W)) +static inline void +store64_le(uint8_t dst[8], uint64_t w) +{ +#ifdef NATIVE_LITTLE_ENDIAN + memcpy(dst, &w, sizeof w); +#else + dst[0] = (uint8_t) w; w >>= 8; + dst[1] = (uint8_t) w; w >>= 8; + dst[2] = (uint8_t) w; w >>= 8; + dst[3] = (uint8_t) w; w >>= 8; + dst[4] = (uint8_t) w; w >>= 8; + dst[5] = (uint8_t) w; w >>= 8; + dst[6] = (uint8_t) w; w >>= 8; + dst[7] = (uint8_t) w; +#endif +} + +#define LOAD32_LE(SRC) load32_le(SRC) +static inline uint32_t +load32_le(const uint8_t src[4]) +{ +#ifdef NATIVE_LITTLE_ENDIAN + uint32_t w; + memcpy(&w, src, sizeof w); + return w; +#else + uint32_t w = (uint32_t) src[0]; + w |= (uint32_t) src[1] << 8; + w |= (uint32_t) src[2] << 16; + w |= (uint32_t) src[3] << 24; + return w; +#endif +} + +#define STORE32_LE(DST, W) store32_le((DST), (W)) +static inline void +store32_le(uint8_t dst[4], uint32_t w) +{ +#ifdef NATIVE_LITTLE_ENDIAN + memcpy(dst, &w, sizeof w); +#else + dst[0] = (uint8_t) w; w >>= 8; + dst[1] = (uint8_t) w; w >>= 8; + dst[2] = (uint8_t) w; w >>= 8; + dst[3] = (uint8_t) w; +#endif +} + +/* ----- */ + +#define LOAD64_BE(SRC) load64_be(SRC) +static inline uint64_t +load64_be(const uint8_t src[8]) +{ +#ifdef NATIVE_BIG_ENDIAN + uint64_t w; + memcpy(&w, src, sizeof w); + return w; +#else + uint64_t w = (uint64_t) src[7]; + w |= (uint64_t) src[6] << 8; + w |= (uint64_t) src[5] << 16; + w |= (uint64_t) src[4] << 24; + w |= (uint64_t) src[3] << 32; + w |= (uint64_t) src[2] << 40; + w |= (uint64_t) src[1] << 48; + w |= (uint64_t) src[0] << 56; + return w; +#endif +} + +#define STORE64_BE(DST, W) store64_be((DST), (W)) +static inline void +store64_be(uint8_t dst[8], uint64_t w) +{ +#ifdef NATIVE_BIG_ENDIAN + memcpy(dst, &w, sizeof w); +#else + dst[7] = (uint8_t) w; w >>= 8; + dst[6] = (uint8_t) w; w >>= 8; + dst[5] = (uint8_t) w; w >>= 8; + dst[4] = (uint8_t) w; w >>= 8; + dst[3] = (uint8_t) w; w >>= 8; + dst[2] = (uint8_t) w; w >>= 8; + dst[1] = (uint8_t) w; w >>= 8; + dst[0] = (uint8_t) w; +#endif +} + +#define LOAD32_BE(SRC) load32_be(SRC) +static inline uint32_t +load32_be(const uint8_t src[4]) +{ +#ifdef NATIVE_BIG_ENDIAN + uint32_t w; + memcpy(&w, src, sizeof w); + return w; +#else + uint32_t w = (uint32_t) src[3]; + w |= (uint32_t) src[2] << 8; + w |= (uint32_t) src[1] << 16; + w |= (uint32_t) src[0] << 24; + return w; +#endif +} + +#define STORE32_BE(DST, W) store32_be((DST), (W)) +static inline void +store32_be(uint8_t dst[4], uint32_t w) +{ +#ifdef NATIVE_BIG_ENDIAN + memcpy(dst, &w, sizeof w); +#else + dst[3] = (uint8_t) w; w >>= 8; + dst[2] = (uint8_t) w; w >>= 8; + dst[1] = (uint8_t) w; w >>= 8; + dst[0] = (uint8_t) w; +#endif +} + +#define XOR_BUF(OUT, IN, N) xor_buf((OUT), (IN), (N)) +static inline void +xor_buf(unsigned char *out, const unsigned char *in, size_t n) +{ + size_t i; + + for (i = 0; i < n; i++) { + out[i] ^= in[i]; + } +} + +#if !defined(__clang__) && !defined(__GNUC__) +# ifdef __attribute__ +# undef __attribute__ +# endif +# define __attribute__(a) +#endif + +#ifndef CRYPTO_ALIGN +# if defined(__INTEL_COMPILER) || defined(_MSC_VER) +# define CRYPTO_ALIGN(x) __declspec(align(x)) +# else +# define CRYPTO_ALIGN(x) __attribute__ ((aligned(x))) +# endif +#endif + +#ifdef _MSC_VER + +# if defined(_M_X64) || defined(_M_IX86) +# include + +# define HAVE_INTRIN_H 1 +# define HAVE_MMINTRIN_H 1 +# define HAVE_EMMINTRIN_H 1 +# define HAVE_PMMINTRIN_H 1 +# define HAVE_TMMINTRIN_H 1 +# define HAVE_SMMINTRIN_H 1 +# define HAVE_AVXINTRIN_H 1 +# if _MSC_VER >= 1600 +# define HAVE_WMMINTRIN_H 1 +# endif +# if _MSC_VER >= 1700 && defined(_M_X64) +# define HAVE_AVX2INTRIN_H 1 +# endif +# if _MSC_VER >= 1910 && defined(_M_X64) +# define HAVE_AVX512FINTRIN_H 1 +# endif + +# elif defined(_M_ARM64) + +# ifndef __ARM_ARCH +# define __ARM_ARCH 1 +# endif +# ifndef __ARM_NEON +# define __ARM_NEON 1 +# endif +# define HAVE_ARMCRYPTO 1 + +# endif /* _MSC_VER */ + +#elif defined(HAVE_INTRIN_H) +# include +#endif + +#ifdef HAVE_LIBCTGRIND +extern void ct_poison (const void *, size_t); +extern void ct_unpoison(const void *, size_t); +# define POISON(X, L) ct_poison((X), (L)) +# define UNPOISON(X, L) ct_unpoison((X), (L)) +#else +# define POISON(X, L) (void) 0 +# define UNPOISON(X, L) (void) 0 +#endif + +#ifdef HAVE_GCC_MEMORY_FENCES +# define ACQUIRE_FENCE __atomic_thread_fence(__ATOMIC_ACQUIRE) +#elif defined(HAVE_C11_MEMORY_FENCES) +# define ACQUIRE_FENCE atomic_thread_fence(memory_order_acquire) +#else +# define ACQUIRE_FENCE (void) 0 +#endif + +#endif diff --git a/packages/runtime/vendor/libsodium/crypto_hash_sha512.h b/packages/runtime/vendor/libsodium/crypto_hash_sha512.h new file mode 100644 index 000000000..5b690fb25 --- /dev/null +++ b/packages/runtime/vendor/libsodium/crypto_hash_sha512.h @@ -0,0 +1,60 @@ +#ifndef crypto_hash_sha512_H +#define crypto_hash_sha512_H + +/* + * WARNING: Unless you absolutely need to use SHA512 for interoperability, + * purposes, you might want to consider crypto_generichash() instead. + * Unlike SHA512, crypto_generichash() is not vulnerable to length + * extension attacks. + */ + +#include +#include +#include + +#include "export.h" + +#ifdef __cplusplus +# ifdef __GNUC__ +# pragma GCC diagnostic ignored "-Wlong-long" +# endif +extern "C" { +#endif + +typedef struct crypto_hash_sha512_state { + uint64_t state[8]; + uint64_t count[2]; + uint8_t buf[128]; +} crypto_hash_sha512_state; + +SODIUM_EXPORT +size_t crypto_hash_sha512_statebytes(void); + +#define crypto_hash_sha512_BYTES 64U +SODIUM_EXPORT +size_t crypto_hash_sha512_bytes(void); + +SODIUM_EXPORT +int crypto_hash_sha512(unsigned char *out, const unsigned char *in, + unsigned long long inlen) __attribute__ ((nonnull(1))); + +SODIUM_EXPORT +int crypto_hash_sha512_init(crypto_hash_sha512_state *state) + __attribute__ ((nonnull)); + +SODIUM_EXPORT +int crypto_hash_sha512_update(crypto_hash_sha512_state *state, + const unsigned char *in, + unsigned long long inlen) + __attribute__ ((nonnull(1))); + +SODIUM_EXPORT +int crypto_hash_sha512_final(crypto_hash_sha512_state *state, + unsigned char *out) + __attribute__ ((nonnull)); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/packages/runtime/vendor/libsodium/crypto_scalarmult_curve25519.h b/packages/runtime/vendor/libsodium/crypto_scalarmult_curve25519.h new file mode 100644 index 000000000..60e9d0c5a --- /dev/null +++ b/packages/runtime/vendor/libsodium/crypto_scalarmult_curve25519.h @@ -0,0 +1,42 @@ +#ifndef crypto_scalarmult_curve25519_H +#define crypto_scalarmult_curve25519_H + +#include + +#include "export.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define crypto_scalarmult_curve25519_BYTES 32U +SODIUM_EXPORT +size_t crypto_scalarmult_curve25519_bytes(void); + +#define crypto_scalarmult_curve25519_SCALARBYTES 32U +SODIUM_EXPORT +size_t crypto_scalarmult_curve25519_scalarbytes(void); + +/* + * NOTE: Do not use the result of this function directly for key exchange. + * + * Hash the result with the public keys in order to compute a shared + * secret key: H(q || client_pk || server_pk) + * + * Or unless this is not an option, use the crypto_kx() API instead. + */ +SODIUM_EXPORT +int crypto_scalarmult_curve25519(unsigned char *q, const unsigned char *n, + const unsigned char *p) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull)); + +SODIUM_EXPORT +int crypto_scalarmult_curve25519_base(unsigned char *q, + const unsigned char *n) + __attribute__ ((nonnull)); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/packages/runtime/vendor/libsodium/crypto_sign_ed25519.h b/packages/runtime/vendor/libsodium/crypto_sign_ed25519.h new file mode 100644 index 000000000..0fdac42d3 --- /dev/null +++ b/packages/runtime/vendor/libsodium/crypto_sign_ed25519.h @@ -0,0 +1,124 @@ +#ifndef crypto_sign_ed25519_H +#define crypto_sign_ed25519_H + +#include +#include "crypto_hash_sha512.h" +#include "export.h" + +#ifdef __cplusplus +# ifdef __GNUC__ +# pragma GCC diagnostic ignored "-Wlong-long" +# endif +extern "C" { +#endif + +typedef struct crypto_sign_ed25519ph_state { + crypto_hash_sha512_state hs; +} crypto_sign_ed25519ph_state; + +SODIUM_EXPORT +size_t crypto_sign_ed25519ph_statebytes(void); + +#define crypto_sign_ed25519_BYTES 64U +SODIUM_EXPORT +size_t crypto_sign_ed25519_bytes(void); + +#define crypto_sign_ed25519_SEEDBYTES 32U +SODIUM_EXPORT +size_t crypto_sign_ed25519_seedbytes(void); + +#define crypto_sign_ed25519_PUBLICKEYBYTES 32U +SODIUM_EXPORT +size_t crypto_sign_ed25519_publickeybytes(void); + +#define crypto_sign_ed25519_SECRETKEYBYTES (32U + 32U) +SODIUM_EXPORT +size_t crypto_sign_ed25519_secretkeybytes(void); + +#define crypto_sign_ed25519_MESSAGEBYTES_MAX (SODIUM_SIZE_MAX - crypto_sign_ed25519_BYTES) +SODIUM_EXPORT +size_t crypto_sign_ed25519_messagebytes_max(void); + +SODIUM_EXPORT +int crypto_sign_ed25519(unsigned char *sm, unsigned long long *smlen_p, + const unsigned char *m, unsigned long long mlen, + const unsigned char *sk) + __attribute__ ((nonnull(1, 5))); + +SODIUM_EXPORT +int crypto_sign_ed25519_open(unsigned char *m, unsigned long long *mlen_p, + const unsigned char *sm, unsigned long long smlen, + const unsigned char *pk) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(3, 5))); + +SODIUM_EXPORT +int crypto_sign_ed25519_detached(unsigned char *sig, + unsigned long long *siglen_p, + const unsigned char *m, + unsigned long long mlen, + const unsigned char *sk) + __attribute__ ((nonnull(1, 5))); + +SODIUM_EXPORT +int crypto_sign_ed25519_verify_detached(const unsigned char *sig, + const unsigned char *m, + unsigned long long mlen, + const unsigned char *pk) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(1, 4))); + +SODIUM_EXPORT +int crypto_sign_ed25519_keypair(unsigned char *pk, unsigned char *sk) + __attribute__ ((nonnull)); + +SODIUM_EXPORT +int crypto_sign_ed25519_seed_keypair(unsigned char *pk, unsigned char *sk, + const unsigned char *seed) + __attribute__ ((nonnull)); + +SODIUM_EXPORT +int crypto_sign_ed25519_pk_to_curve25519(unsigned char *curve25519_pk, + const unsigned char *ed25519_pk) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull)); + +SODIUM_EXPORT +int crypto_sign_ed25519_sk_to_curve25519(unsigned char *curve25519_sk, + const unsigned char *ed25519_sk) + __attribute__ ((nonnull)); + +SODIUM_EXPORT +int crypto_sign_ed25519_sk_to_seed(unsigned char *seed, + const unsigned char *sk) + __attribute__ ((nonnull)); + +SODIUM_EXPORT +int crypto_sign_ed25519_sk_to_pk(unsigned char *pk, const unsigned char *sk) + __attribute__ ((nonnull)); + +SODIUM_EXPORT +int crypto_sign_ed25519ph_init(crypto_sign_ed25519ph_state *state) + __attribute__ ((nonnull)); + +SODIUM_EXPORT +int crypto_sign_ed25519ph_update(crypto_sign_ed25519ph_state *state, + const unsigned char *m, + unsigned long long mlen) + __attribute__ ((nonnull(1))); + +SODIUM_EXPORT +int crypto_sign_ed25519ph_final_create(crypto_sign_ed25519ph_state *state, + unsigned char *sig, + unsigned long long *siglen_p, + const unsigned char *sk) + __attribute__ ((nonnull(1, 2, 4))); + +SODIUM_EXPORT +int crypto_sign_ed25519ph_final_verify(crypto_sign_ed25519ph_state *state, + const unsigned char *sig, + const unsigned char *pk) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull)); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/packages/runtime/vendor/libsodium/crypto_verify_16.h b/packages/runtime/vendor/libsodium/crypto_verify_16.h new file mode 100644 index 000000000..7b9c8077a --- /dev/null +++ b/packages/runtime/vendor/libsodium/crypto_verify_16.h @@ -0,0 +1,23 @@ +#ifndef crypto_verify_16_H +#define crypto_verify_16_H + +#include +#include "export.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define crypto_verify_16_BYTES 16U +SODIUM_EXPORT +size_t crypto_verify_16_bytes(void); + +SODIUM_EXPORT +int crypto_verify_16(const unsigned char *x, const unsigned char *y) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull)); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/packages/runtime/vendor/libsodium/crypto_verify_32.h b/packages/runtime/vendor/libsodium/crypto_verify_32.h new file mode 100644 index 000000000..9b0f4529f --- /dev/null +++ b/packages/runtime/vendor/libsodium/crypto_verify_32.h @@ -0,0 +1,23 @@ +#ifndef crypto_verify_32_H +#define crypto_verify_32_H + +#include +#include "export.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define crypto_verify_32_BYTES 32U +SODIUM_EXPORT +size_t crypto_verify_32_bytes(void); + +SODIUM_EXPORT +int crypto_verify_32(const unsigned char *x, const unsigned char *y) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull)); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/packages/runtime/vendor/libsodium/crypto_verify_64.h b/packages/runtime/vendor/libsodium/crypto_verify_64.h new file mode 100644 index 000000000..c83b73025 --- /dev/null +++ b/packages/runtime/vendor/libsodium/crypto_verify_64.h @@ -0,0 +1,23 @@ +#ifndef crypto_verify_64_H +#define crypto_verify_64_H + +#include +#include "export.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define crypto_verify_64_BYTES 64U +SODIUM_EXPORT +size_t crypto_verify_64_bytes(void); + +SODIUM_EXPORT +int crypto_verify_64(const unsigned char *x, const unsigned char *y) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull)); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/packages/runtime/vendor/libsodium/ed25519_ref10.c b/packages/runtime/vendor/libsodium/ed25519_ref10.c new file mode 100644 index 000000000..ac7a41492 --- /dev/null +++ b/packages/runtime/vendor/libsodium/ed25519_ref10.c @@ -0,0 +1,2858 @@ +#include +#include +#include +#include + +#include "crypto_verify_32.h" +#include "common.h" +#include "ed25519_ref10.h" +#include "utils.h" + +static inline uint64_t +load_3(const unsigned char *in) +{ + uint64_t result; + + result = (uint64_t) in[0]; + result |= ((uint64_t) in[1]) << 8; + result |= ((uint64_t) in[2]) << 16; + + return result; +} + +static inline uint64_t +load_4(const unsigned char *in) +{ + uint64_t result; + + result = (uint64_t) in[0]; + result |= ((uint64_t) in[1]) << 8; + result |= ((uint64_t) in[2]) << 16; + result |= ((uint64_t) in[3]) << 24; + + return result; +} + +/* + * Field arithmetic: + * Use 5*51 bit limbs on 64-bit systems with support for 128 bit arithmetic, + * and 10*25.5 bit limbs elsewhere. + * + * Functions used elsewhere that are candidates for inlining are defined + * via "private/curve25519_ref10.h". + */ + +#ifdef HAVE_TI_MODE +# include "fe_51/constants.h" +# include "fe_51/fe.h" +#else +# include "fe_25_5/constants.h" +# include "fe_25_5/fe.h" +#endif + +void +fe25519_invert(fe25519 out, const fe25519 z) +{ + fe25519 t0; + fe25519 t1; + fe25519 t2; + fe25519 t3; + int i; + + fe25519_sq(t0, z); + fe25519_sq(t1, t0); + fe25519_sq(t1, t1); + fe25519_mul(t1, z, t1); + fe25519_mul(t0, t0, t1); + fe25519_sq(t2, t0); + fe25519_mul(t1, t1, t2); + fe25519_sq(t2, t1); + for (i = 1; i < 5; ++i) { + fe25519_sq(t2, t2); + } + fe25519_mul(t1, t2, t1); + fe25519_sq(t2, t1); + for (i = 1; i < 10; ++i) { + fe25519_sq(t2, t2); + } + fe25519_mul(t2, t2, t1); + fe25519_sq(t3, t2); + for (i = 1; i < 20; ++i) { + fe25519_sq(t3, t3); + } + fe25519_mul(t2, t3, t2); + for (i = 1; i < 11; ++i) { + fe25519_sq(t2, t2); + } + fe25519_mul(t1, t2, t1); + fe25519_sq(t2, t1); + for (i = 1; i < 50; ++i) { + fe25519_sq(t2, t2); + } + fe25519_mul(t2, t2, t1); + fe25519_sq(t3, t2); + for (i = 1; i < 100; ++i) { + fe25519_sq(t3, t3); + } + fe25519_mul(t2, t3, t2); + for (i = 1; i < 51; ++i) { + fe25519_sq(t2, t2); + } + fe25519_mul(t1, t2, t1); + for (i = 1; i < 6; ++i) { + fe25519_sq(t1, t1); + } + fe25519_mul(out, t1, t0); +} + +static void +fe25519_pow22523(fe25519 out, const fe25519 z) +{ + fe25519 t0; + fe25519 t1; + fe25519 t2; + int i; + + fe25519_sq(t0, z); + fe25519_sq(t1, t0); + fe25519_sq(t1, t1); + fe25519_mul(t1, z, t1); + fe25519_mul(t0, t0, t1); + fe25519_sq(t0, t0); + fe25519_mul(t0, t1, t0); + fe25519_sq(t1, t0); + for (i = 1; i < 5; ++i) { + fe25519_sq(t1, t1); + } + fe25519_mul(t0, t1, t0); + fe25519_sq(t1, t0); + for (i = 1; i < 10; ++i) { + fe25519_sq(t1, t1); + } + fe25519_mul(t1, t1, t0); + fe25519_sq(t2, t1); + for (i = 1; i < 20; ++i) { + fe25519_sq(t2, t2); + } + fe25519_mul(t1, t2, t1); + for (i = 1; i < 11; ++i) { + fe25519_sq(t1, t1); + } + fe25519_mul(t0, t1, t0); + fe25519_sq(t1, t0); + for (i = 1; i < 50; ++i) { + fe25519_sq(t1, t1); + } + fe25519_mul(t1, t1, t0); + fe25519_sq(t2, t1); + for (i = 1; i < 100; ++i) { + fe25519_sq(t2, t2); + } + fe25519_mul(t1, t2, t1); + for (i = 1; i < 51; ++i) { + fe25519_sq(t1, t1); + } + fe25519_mul(t0, t1, t0); + fe25519_sq(t0, t0); + fe25519_sq(t0, t0); + fe25519_mul(out, t0, z); +} + +static inline void +fe25519_cneg(fe25519 h, const fe25519 f, unsigned int b) +{ + fe25519 negf; + + fe25519_neg(negf, f); + fe25519_copy(h, f); + fe25519_cmov(h, negf, b); +} + +static inline void +fe25519_abs(fe25519 h, const fe25519 f) +{ + fe25519_cneg(h, f, fe25519_isnegative(f)); +} + +static inline void +fe25519_sqmul(fe25519 s, const int n, const fe25519 a) +{ + int i; + + for (i = 0; i < n; i++) { + fe25519_sq(s, s); + } + fe25519_mul(s, s, a); +} + +static unsigned int +fe25519_notsquare(const fe25519 x) +{ + fe25519 _10, _11, _1100, _1111, _11110000, _11111111; + fe25519 t, u, v; + unsigned char s[32]; + + /* Jacobi symbol - x^((p-1)/2) */ + fe25519_mul(_10, x, x); + fe25519_mul(_11, x, _10); + fe25519_sq(_1100, _11); + fe25519_sq(_1100, _1100); + fe25519_mul(_1111, _11, _1100); + fe25519_sq(_11110000, _1111); + fe25519_sq(_11110000, _11110000); + fe25519_sq(_11110000, _11110000); + fe25519_sq(_11110000, _11110000); + fe25519_mul(_11111111, _1111, _11110000); + fe25519_copy(t, _11111111); + fe25519_sqmul(t, 2, _11); + fe25519_copy(u, t); + fe25519_sqmul(t, 10, u); + fe25519_sqmul(t, 10, u); + fe25519_copy(v, t); + fe25519_sqmul(t, 30, v); + fe25519_copy(v, t); + fe25519_sqmul(t, 60, v); + fe25519_copy(v, t); + fe25519_sqmul(t, 120, v); + fe25519_sqmul(t, 10, u); + fe25519_sqmul(t, 3, _11); + fe25519_sq(t, t); + + fe25519_tobytes(s, t); + + return s[1] & 1; +} + +/* + r = p + q + */ + +void +ge25519_add(ge25519_p1p1 *r, const ge25519_p3 *p, const ge25519_cached *q) +{ + fe25519 t0; + + fe25519_add(r->X, p->Y, p->X); + fe25519_sub(r->Y, p->Y, p->X); + fe25519_mul(r->Z, r->X, q->YplusX); + fe25519_mul(r->Y, r->Y, q->YminusX); + fe25519_mul(r->T, q->T2d, p->T); + fe25519_mul(r->X, p->Z, q->Z); + fe25519_add(t0, r->X, r->X); + fe25519_sub(r->X, r->Z, r->Y); + fe25519_add(r->Y, r->Z, r->Y); + fe25519_add(r->Z, t0, r->T); + fe25519_sub(r->T, t0, r->T); +} + +static void +slide_vartime(signed char *r, const unsigned char *a) +{ + int i; + int b; + int k; + int ribs; + int cmp; + + for (i = 0; i < 256; ++i) { + r[i] = 1 & (a[i >> 3] >> (i & 7)); + } + for (i = 0; i < 256; ++i) { + if (! r[i]) { + continue; + } + for (b = 1; b <= 6 && i + b < 256; ++b) { + if (! r[i + b]) { + continue; + } + ribs = r[i + b] << b; + cmp = r[i] + ribs; + if (cmp <= 15) { + r[i] = cmp; + r[i + b] = 0; + } else { + cmp = r[i] - ribs; + if (cmp < -15) { + break; + } + r[i] = cmp; + for (k = i + b; k < 256; ++k) { + if (! r[k]) { + r[k] = 1; + break; + } + r[k] = 0; + } + } + } + } +} + +int +ge25519_frombytes(ge25519_p3 *h, const unsigned char *s) +{ + fe25519 u; + fe25519 v; + fe25519 vxx; + fe25519 m_root_check, p_root_check; + fe25519 negx; + fe25519 x_sqrtm1; + int has_m_root, has_p_root; + + fe25519_frombytes(h->Y, s); + fe25519_1(h->Z); + fe25519_sq(u, h->Y); + fe25519_mul(v, u, d); + fe25519_sub(u, u, h->Z); /* u = y^2-1 */ + fe25519_add(v, v, h->Z); /* v = dy^2+1 */ + + fe25519_mul(h->X, u, v); + fe25519_pow22523(h->X, h->X); + fe25519_mul(h->X, u, h->X); /* u((uv)^((q-5)/8)) */ + + fe25519_sq(vxx, h->X); + fe25519_mul(vxx, vxx, v); + fe25519_sub(m_root_check, vxx, u); /* vx^2-u */ + fe25519_add(p_root_check, vxx, u); /* vx^2+u */ + has_m_root = fe25519_iszero(m_root_check); + has_p_root = fe25519_iszero(p_root_check); + fe25519_mul(x_sqrtm1, h->X, sqrtm1); /* x*sqrt(-1) */ + fe25519_cmov(h->X, x_sqrtm1, 1 - has_m_root); + + fe25519_neg(negx, h->X); + fe25519_cmov(h->X, negx, fe25519_isnegative(h->X) ^ (s[31] >> 7)); + fe25519_mul(h->T, h->X, h->Y); + + return (has_m_root | has_p_root) - 1; +} + +int +ge25519_frombytes_negate_vartime(ge25519_p3 *h, const unsigned char *s) +{ + fe25519 u; + fe25519 v; + fe25519 v3; + fe25519 vxx; + fe25519 m_root_check, p_root_check; + + fe25519_frombytes(h->Y, s); + fe25519_1(h->Z); + fe25519_sq(u, h->Y); + fe25519_mul(v, u, d); + fe25519_sub(u, u, h->Z); /* u = y^2-1 */ + fe25519_add(v, v, h->Z); /* v = dy^2+1 */ + + fe25519_sq(v3, v); + fe25519_mul(v3, v3, v); /* v3 = v^3 */ + fe25519_sq(h->X, v3); + fe25519_mul(h->X, h->X, v); + fe25519_mul(h->X, h->X, u); /* x = uv^7 */ + + fe25519_pow22523(h->X, h->X); /* x = (uv^7)^((q-5)/8) */ + fe25519_mul(h->X, h->X, v3); + fe25519_mul(h->X, h->X, u); /* x = uv^3(uv^7)^((q-5)/8) */ + + fe25519_sq(vxx, h->X); + fe25519_mul(vxx, vxx, v); + fe25519_sub(m_root_check, vxx, u); /* vx^2-u */ + if (fe25519_iszero(m_root_check) == 0) { + fe25519_add(p_root_check, vxx, u); /* vx^2+u */ + if (fe25519_iszero(p_root_check) == 0) { + return -1; + } + fe25519_mul(h->X, h->X, sqrtm1); + } + + if (fe25519_isnegative(h->X) == (s[31] >> 7)) { + fe25519_neg(h->X, h->X); + } + fe25519_mul(h->T, h->X, h->Y); + + return 0; +} + +/* + r = p + q + */ + +static void +ge25519_madd(ge25519_p1p1 *r, const ge25519_p3 *p, const ge25519_precomp *q) +{ + fe25519 t0; + + fe25519_add(r->X, p->Y, p->X); + fe25519_sub(r->Y, p->Y, p->X); + fe25519_mul(r->Z, r->X, q->yplusx); + fe25519_mul(r->Y, r->Y, q->yminusx); + fe25519_mul(r->T, q->xy2d, p->T); + fe25519_add(t0, p->Z, p->Z); + fe25519_sub(r->X, r->Z, r->Y); + fe25519_add(r->Y, r->Z, r->Y); + fe25519_add(r->Z, t0, r->T); + fe25519_sub(r->T, t0, r->T); +} + +/* + r = p - q + */ + +static void +ge25519_msub(ge25519_p1p1 *r, const ge25519_p3 *p, const ge25519_precomp *q) +{ + fe25519 t0; + + fe25519_add(r->X, p->Y, p->X); + fe25519_sub(r->Y, p->Y, p->X); + fe25519_mul(r->Z, r->X, q->yminusx); + fe25519_mul(r->Y, r->Y, q->yplusx); + fe25519_mul(r->T, q->xy2d, p->T); + fe25519_add(t0, p->Z, p->Z); + fe25519_sub(r->X, r->Z, r->Y); + fe25519_add(r->Y, r->Z, r->Y); + fe25519_sub(r->Z, t0, r->T); + fe25519_add(r->T, t0, r->T); +} + +/* + r = p + */ + +void +ge25519_p1p1_to_p2(ge25519_p2 *r, const ge25519_p1p1 *p) +{ + fe25519_mul(r->X, p->X, p->T); + fe25519_mul(r->Y, p->Y, p->Z); + fe25519_mul(r->Z, p->Z, p->T); +} + +/* + r = p + */ + +void +ge25519_p1p1_to_p3(ge25519_p3 *r, const ge25519_p1p1 *p) +{ + fe25519_mul(r->X, p->X, p->T); + fe25519_mul(r->Y, p->Y, p->Z); + fe25519_mul(r->Z, p->Z, p->T); + fe25519_mul(r->T, p->X, p->Y); +} + +static void +ge25519_p2_0(ge25519_p2 *h) +{ + fe25519_0(h->X); + fe25519_1(h->Y); + fe25519_1(h->Z); +} + +/* + r = 2 * p + */ + +static void +ge25519_p2_dbl(ge25519_p1p1 *r, const ge25519_p2 *p) +{ + fe25519 t0; + + fe25519_sq(r->X, p->X); + fe25519_sq(r->Z, p->Y); + fe25519_sq2(r->T, p->Z); + fe25519_add(r->Y, p->X, p->Y); + fe25519_sq(t0, r->Y); + fe25519_add(r->Y, r->Z, r->X); + fe25519_sub(r->Z, r->Z, r->X); + fe25519_sub(r->X, t0, r->Y); + fe25519_sub(r->T, r->T, r->Z); +} + +static void +ge25519_p3_0(ge25519_p3 *h) +{ + fe25519_0(h->X); + fe25519_1(h->Y); + fe25519_1(h->Z); + fe25519_0(h->T); +} + +static void +ge25519_cached_0(ge25519_cached *h) +{ + fe25519_1(h->YplusX); + fe25519_1(h->YminusX); + fe25519_1(h->Z); + fe25519_0(h->T2d); +} + +/* + r = p + */ + +void +ge25519_p3_to_cached(ge25519_cached *r, const ge25519_p3 *p) +{ + fe25519_add(r->YplusX, p->Y, p->X); + fe25519_sub(r->YminusX, p->Y, p->X); + fe25519_copy(r->Z, p->Z); + fe25519_mul(r->T2d, p->T, d2); +} + +static void +ge25519_p3_to_precomp(ge25519_precomp *pi, const ge25519_p3 *p) +{ + fe25519 recip; + fe25519 x; + fe25519 y; + fe25519 xy; + + fe25519_invert(recip, p->Z); + fe25519_mul(x, p->X, recip); + fe25519_mul(y, p->Y, recip); + fe25519_add(pi->yplusx, y, x); + fe25519_sub(pi->yminusx, y, x); + fe25519_mul(xy, x, y); + fe25519_mul(pi->xy2d, xy, d2); +} + +/* + r = p + */ + +static void +ge25519_p3_to_p2(ge25519_p2 *r, const ge25519_p3 *p) +{ + fe25519_copy(r->X, p->X); + fe25519_copy(r->Y, p->Y); + fe25519_copy(r->Z, p->Z); +} + +void +ge25519_p3_tobytes(unsigned char *s, const ge25519_p3 *h) +{ + fe25519 recip; + fe25519 x; + fe25519 y; + + fe25519_invert(recip, h->Z); + fe25519_mul(x, h->X, recip); + fe25519_mul(y, h->Y, recip); + fe25519_tobytes(s, y); + s[31] ^= fe25519_isnegative(x) << 7; +} + +/* + r = 2 * p + */ + +static void +ge25519_p3_dbl(ge25519_p1p1 *r, const ge25519_p3 *p) +{ + ge25519_p2 q; + ge25519_p3_to_p2(&q, p); + ge25519_p2_dbl(r, &q); +} + +static void +ge25519_precomp_0(ge25519_precomp *h) +{ + fe25519_1(h->yplusx); + fe25519_1(h->yminusx); + fe25519_0(h->xy2d); +} + +static unsigned char +equal(signed char b, signed char c) +{ + unsigned char ub = b; + unsigned char uc = c; + unsigned char x = ub ^ uc; /* 0: yes; 1..255: no */ + uint32_t y = (uint32_t) x; /* 0: yes; 1..255: no */ + + y -= 1; /* 4294967295: yes; 0..254: no */ + y >>= 31; /* 1: yes; 0: no */ + + return y; +} + +static unsigned char +negative(signed char b) +{ + /* 18446744073709551361..18446744073709551615: yes; 0..255: no */ + uint64_t x = b; + + x >>= 63; /* 1: yes; 0: no */ + + return x; +} + +static void +ge25519_cmov(ge25519_precomp *t, const ge25519_precomp *u, unsigned char b) +{ + fe25519_cmov(t->yplusx, u->yplusx, b); + fe25519_cmov(t->yminusx, u->yminusx, b); + fe25519_cmov(t->xy2d, u->xy2d, b); +} + +static void +ge25519_cmov_cached(ge25519_cached *t, const ge25519_cached *u, unsigned char b) +{ + fe25519_cmov(t->YplusX, u->YplusX, b); + fe25519_cmov(t->YminusX, u->YminusX, b); + fe25519_cmov(t->Z, u->Z, b); + fe25519_cmov(t->T2d, u->T2d, b); +} + +static void +ge25519_cmov8(ge25519_precomp *t, const ge25519_precomp precomp[8], const signed char b) +{ + ge25519_precomp minust; + const unsigned char bnegative = negative(b); + const unsigned char babs = b - (((-bnegative) & b) * ((signed char) 1 << 1)); + + ge25519_precomp_0(t); + ge25519_cmov(t, &precomp[0], equal(babs, 1)); + ge25519_cmov(t, &precomp[1], equal(babs, 2)); + ge25519_cmov(t, &precomp[2], equal(babs, 3)); + ge25519_cmov(t, &precomp[3], equal(babs, 4)); + ge25519_cmov(t, &precomp[4], equal(babs, 5)); + ge25519_cmov(t, &precomp[5], equal(babs, 6)); + ge25519_cmov(t, &precomp[6], equal(babs, 7)); + ge25519_cmov(t, &precomp[7], equal(babs, 8)); + fe25519_copy(minust.yplusx, t->yminusx); + fe25519_copy(minust.yminusx, t->yplusx); + fe25519_neg(minust.xy2d, t->xy2d); + ge25519_cmov(t, &minust, bnegative); +} + +static void +ge25519_cmov8_base(ge25519_precomp *t, const int pos, const signed char b) +{ + static const ge25519_precomp base[32][8] = { /* base[i][j] = (j+1)*256^i*B */ +#ifdef HAVE_TI_MODE +# include "fe_51/base.h" +#else +# include "fe_25_5/base.h" +#endif + }; + ge25519_cmov8(t, base[pos], b); +} + +static void +ge25519_cmov8_cached(ge25519_cached *t, const ge25519_cached cached[8], const signed char b) +{ + ge25519_cached minust; + const unsigned char bnegative = negative(b); + const unsigned char babs = b - (((-bnegative) & b) * ((signed char) 1 << 1)); + + ge25519_cached_0(t); + ge25519_cmov_cached(t, &cached[0], equal(babs, 1)); + ge25519_cmov_cached(t, &cached[1], equal(babs, 2)); + ge25519_cmov_cached(t, &cached[2], equal(babs, 3)); + ge25519_cmov_cached(t, &cached[3], equal(babs, 4)); + ge25519_cmov_cached(t, &cached[4], equal(babs, 5)); + ge25519_cmov_cached(t, &cached[5], equal(babs, 6)); + ge25519_cmov_cached(t, &cached[6], equal(babs, 7)); + ge25519_cmov_cached(t, &cached[7], equal(babs, 8)); + fe25519_copy(minust.YplusX, t->YminusX); + fe25519_copy(minust.YminusX, t->YplusX); + fe25519_copy(minust.Z, t->Z); + fe25519_neg(minust.T2d, t->T2d); + ge25519_cmov_cached(t, &minust, bnegative); +} + +/* + r = p - q + */ + +void +ge25519_sub(ge25519_p1p1 *r, const ge25519_p3 *p, const ge25519_cached *q) +{ + fe25519 t0; + + fe25519_add(r->X, p->Y, p->X); + fe25519_sub(r->Y, p->Y, p->X); + fe25519_mul(r->Z, r->X, q->YminusX); + fe25519_mul(r->Y, r->Y, q->YplusX); + fe25519_mul(r->T, q->T2d, p->T); + fe25519_mul(r->X, p->Z, q->Z); + fe25519_add(t0, r->X, r->X); + fe25519_sub(r->X, r->Z, r->Y); + fe25519_add(r->Y, r->Z, r->Y); + fe25519_sub(r->Z, t0, r->T); + fe25519_add(r->T, t0, r->T); +} + +void +ge25519_tobytes(unsigned char *s, const ge25519_p2 *h) +{ + fe25519 recip; + fe25519 x; + fe25519 y; + + fe25519_invert(recip, h->Z); + fe25519_mul(x, h->X, recip); + fe25519_mul(y, h->Y, recip); + fe25519_tobytes(s, y); + s[31] ^= fe25519_isnegative(x) << 7; +} + +/* + r = a * A + b * B + where a = a[0]+256*a[1]+...+256^31 a[31]. + and b = b[0]+256*b[1]+...+256^31 b[31]. + B is the Ed25519 base point (x,4/5) with x positive. + + Only used for signatures verification. + */ + +void +ge25519_double_scalarmult_vartime(ge25519_p2 *r, const unsigned char *a, + const ge25519_p3 *A, const unsigned char *b) +{ + static const ge25519_precomp Bi[8] = { +#ifdef HAVE_TI_MODE +# include "fe_51/base2.h" +#else +# include "fe_25_5/base2.h" +#endif + }; + signed char aslide[256]; + signed char bslide[256]; + ge25519_cached Ai[8]; /* A,3A,5A,7A,9A,11A,13A,15A */ + ge25519_p1p1 t; + ge25519_p3 u; + ge25519_p3 A2; + int i; + + slide_vartime(aslide, a); + slide_vartime(bslide, b); + + ge25519_p3_to_cached(&Ai[0], A); + + ge25519_p3_dbl(&t, A); + ge25519_p1p1_to_p3(&A2, &t); + + ge25519_add(&t, &A2, &Ai[0]); + ge25519_p1p1_to_p3(&u, &t); + ge25519_p3_to_cached(&Ai[1], &u); + + ge25519_add(&t, &A2, &Ai[1]); + ge25519_p1p1_to_p3(&u, &t); + ge25519_p3_to_cached(&Ai[2], &u); + + ge25519_add(&t, &A2, &Ai[2]); + ge25519_p1p1_to_p3(&u, &t); + ge25519_p3_to_cached(&Ai[3], &u); + + ge25519_add(&t, &A2, &Ai[3]); + ge25519_p1p1_to_p3(&u, &t); + ge25519_p3_to_cached(&Ai[4], &u); + + ge25519_add(&t, &A2, &Ai[4]); + ge25519_p1p1_to_p3(&u, &t); + ge25519_p3_to_cached(&Ai[5], &u); + + ge25519_add(&t, &A2, &Ai[5]); + ge25519_p1p1_to_p3(&u, &t); + ge25519_p3_to_cached(&Ai[6], &u); + + ge25519_add(&t, &A2, &Ai[6]); + ge25519_p1p1_to_p3(&u, &t); + ge25519_p3_to_cached(&Ai[7], &u); + + ge25519_p2_0(r); + + for (i = 255; i >= 0; --i) { + if (aslide[i] || bslide[i]) { + break; + } + } + + for (; i >= 0; --i) { + ge25519_p2_dbl(&t, r); + + if (aslide[i] > 0) { + ge25519_p1p1_to_p3(&u, &t); + ge25519_add(&t, &u, &Ai[aslide[i] / 2]); + } else if (aslide[i] < 0) { + ge25519_p1p1_to_p3(&u, &t); + ge25519_sub(&t, &u, &Ai[(-aslide[i]) / 2]); + } + + if (bslide[i] > 0) { + ge25519_p1p1_to_p3(&u, &t); + ge25519_madd(&t, &u, &Bi[bslide[i] / 2]); + } else if (bslide[i] < 0) { + ge25519_p1p1_to_p3(&u, &t); + ge25519_msub(&t, &u, &Bi[(-bslide[i]) / 2]); + } + + ge25519_p1p1_to_p2(r, &t); + } +} + +/* + h = a * p + where a = a[0]+256*a[1]+...+256^31 a[31] + + Preconditions: + a[31] <= 127 + + p is public + */ + +void +ge25519_scalarmult(ge25519_p3 *h, const unsigned char *a, const ge25519_p3 *p) +{ + signed char e[64]; + signed char carry; + ge25519_p1p1 r; + ge25519_p2 s; + ge25519_p1p1 t2, t3, t4, t5, t6, t7, t8; + ge25519_p3 p2, p3, p4, p5, p6, p7, p8; + ge25519_cached pi[8]; + ge25519_cached t; + int i; + + ge25519_p3_to_cached(&pi[1 - 1], p); /* p */ + + ge25519_p3_dbl(&t2, p); + ge25519_p1p1_to_p3(&p2, &t2); + ge25519_p3_to_cached(&pi[2 - 1], &p2); /* 2p = 2*p */ + + ge25519_add(&t3, p, &pi[2 - 1]); + ge25519_p1p1_to_p3(&p3, &t3); + ge25519_p3_to_cached(&pi[3 - 1], &p3); /* 3p = 2p+p */ + + ge25519_p3_dbl(&t4, &p2); + ge25519_p1p1_to_p3(&p4, &t4); + ge25519_p3_to_cached(&pi[4 - 1], &p4); /* 4p = 2*2p */ + + ge25519_add(&t5, p, &pi[4 - 1]); + ge25519_p1p1_to_p3(&p5, &t5); + ge25519_p3_to_cached(&pi[5 - 1], &p5); /* 5p = 4p+p */ + + ge25519_p3_dbl(&t6, &p3); + ge25519_p1p1_to_p3(&p6, &t6); + ge25519_p3_to_cached(&pi[6 - 1], &p6); /* 6p = 2*3p */ + + ge25519_add(&t7, p, &pi[6 - 1]); + ge25519_p1p1_to_p3(&p7, &t7); + ge25519_p3_to_cached(&pi[7 - 1], &p7); /* 7p = 6p+p */ + + ge25519_p3_dbl(&t8, &p4); + ge25519_p1p1_to_p3(&p8, &t8); + ge25519_p3_to_cached(&pi[8 - 1], &p8); /* 8p = 2*4p */ + + for (i = 0; i < 32; ++i) { + e[2 * i + 0] = (a[i] >> 0) & 15; + e[2 * i + 1] = (a[i] >> 4) & 15; + } + /* each e[i] is between 0 and 15 */ + /* e[63] is between 0 and 7 */ + + carry = 0; + for (i = 0; i < 63; ++i) { + e[i] += carry; + carry = e[i] + 8; + carry >>= 4; + e[i] -= carry * ((signed char) 1 << 4); + } + e[63] += carry; + /* each e[i] is between -8 and 8 */ + + ge25519_p3_0(h); + + for (i = 63; i != 0; i--) { + ge25519_cmov8_cached(&t, pi, e[i]); + ge25519_add(&r, h, &t); + + ge25519_p1p1_to_p2(&s, &r); + ge25519_p2_dbl(&r, &s); + ge25519_p1p1_to_p2(&s, &r); + ge25519_p2_dbl(&r, &s); + ge25519_p1p1_to_p2(&s, &r); + ge25519_p2_dbl(&r, &s); + ge25519_p1p1_to_p2(&s, &r); + ge25519_p2_dbl(&r, &s); + + ge25519_p1p1_to_p3(h, &r); /* *16 */ + } + ge25519_cmov8_cached(&t, pi, e[i]); + ge25519_add(&r, h, &t); + + ge25519_p1p1_to_p3(h, &r); +} + +/* + h = a * B (with precomputation) + where a = a[0]+256*a[1]+...+256^31 a[31] + B is the Ed25519 base point (x,4/5) with x positive + (as bytes: 0x5866666666666666666666666666666666666666666666666666666666666666) + + Preconditions: + a[31] <= 127 + */ + +void +ge25519_scalarmult_base(ge25519_p3 *h, const unsigned char *a) +{ + signed char e[64]; + signed char carry; + ge25519_p1p1 r; + ge25519_p2 s; + ge25519_precomp t; + int i; + + for (i = 0; i < 32; ++i) { + e[2 * i + 0] = (a[i] >> 0) & 15; + e[2 * i + 1] = (a[i] >> 4) & 15; + } + /* each e[i] is between 0 and 15 */ + /* e[63] is between 0 and 7 */ + + carry = 0; + for (i = 0; i < 63; ++i) { + e[i] += carry; + carry = e[i] + 8; + carry >>= 4; + e[i] -= carry * ((signed char) 1 << 4); + } + e[63] += carry; + /* each e[i] is between -8 and 8 */ + + ge25519_p3_0(h); + + for (i = 1; i < 64; i += 2) { + ge25519_cmov8_base(&t, i / 2, e[i]); + ge25519_madd(&r, h, &t); + ge25519_p1p1_to_p3(h, &r); + } + + ge25519_p3_dbl(&r, h); + ge25519_p1p1_to_p2(&s, &r); + ge25519_p2_dbl(&r, &s); + ge25519_p1p1_to_p2(&s, &r); + ge25519_p2_dbl(&r, &s); + ge25519_p1p1_to_p2(&s, &r); + ge25519_p2_dbl(&r, &s); + ge25519_p1p1_to_p3(h, &r); + + for (i = 0; i < 64; i += 2) { + ge25519_cmov8_base(&t, i / 2, e[i]); + ge25519_madd(&r, h, &t); + ge25519_p1p1_to_p3(h, &r); + } +} + +/* multiply by the order of the main subgroup l = 2^252+27742317777372353535851937790883648493 */ +static void +ge25519_mul_l(ge25519_p3 *r, const ge25519_p3 *A) +{ + static const signed char aslide[253] = { + 13, 0, 0, 0, 0, -1, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, -5, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, -13, 0, 0, 0, 0, 7, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, -13, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 3, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, -1, 0, 0, 0, 0, 7, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 + }; + ge25519_cached Ai[8]; + ge25519_p1p1 t; + ge25519_p3 u; + ge25519_p3 A2; + int i; + + ge25519_p3_to_cached(&Ai[0], A); + ge25519_p3_dbl(&t, A); + ge25519_p1p1_to_p3(&A2, &t); + ge25519_add(&t, &A2, &Ai[0]); + ge25519_p1p1_to_p3(&u, &t); + ge25519_p3_to_cached(&Ai[1], &u); + ge25519_add(&t, &A2, &Ai[1]); + ge25519_p1p1_to_p3(&u, &t); + ge25519_p3_to_cached(&Ai[2], &u); + ge25519_add(&t, &A2, &Ai[2]); + ge25519_p1p1_to_p3(&u, &t); + ge25519_p3_to_cached(&Ai[3], &u); + ge25519_add(&t, &A2, &Ai[3]); + ge25519_p1p1_to_p3(&u, &t); + ge25519_p3_to_cached(&Ai[4], &u); + ge25519_add(&t, &A2, &Ai[4]); + ge25519_p1p1_to_p3(&u, &t); + ge25519_p3_to_cached(&Ai[5], &u); + ge25519_add(&t, &A2, &Ai[5]); + ge25519_p1p1_to_p3(&u, &t); + ge25519_p3_to_cached(&Ai[6], &u); + ge25519_add(&t, &A2, &Ai[6]); + ge25519_p1p1_to_p3(&u, &t); + ge25519_p3_to_cached(&Ai[7], &u); + + ge25519_p3_0(r); + + for (i = 252; i >= 0; --i) { + ge25519_p3_dbl(&t, r); + + if (aslide[i] > 0) { + ge25519_p1p1_to_p3(&u, &t); + ge25519_add(&t, &u, &Ai[aslide[i] / 2]); + } else if (aslide[i] < 0) { + ge25519_p1p1_to_p3(&u, &t); + ge25519_sub(&t, &u, &Ai[(-aslide[i]) / 2]); + } + + ge25519_p1p1_to_p3(r, &t); + } +} + +int +ge25519_is_on_curve(const ge25519_p3 *p) +{ + fe25519 x2; + fe25519 y2; + fe25519 z2; + fe25519 z4; + fe25519 t0; + fe25519 t1; + + fe25519_sq(x2, p->X); + fe25519_sq(y2, p->Y); + fe25519_sq(z2, p->Z); + fe25519_sub(t0, y2, x2); + fe25519_mul(t0, t0, z2); + + fe25519_mul(t1, x2, y2); + fe25519_mul(t1, t1, d); + fe25519_sq(z4, z2); + fe25519_add(t1, t1, z4); + fe25519_sub(t0, t0, t1); + + return fe25519_iszero(t0); +} + +int +ge25519_is_on_main_subgroup(const ge25519_p3 *p) +{ + ge25519_p3 pl; + + ge25519_mul_l(&pl, p); + + return fe25519_iszero(pl.X); +} + +int +ge25519_is_canonical(const unsigned char *s) +{ + unsigned char c; + unsigned char d; + unsigned int i; + + c = (s[31] & 0x7f) ^ 0x7f; + for (i = 30; i > 0; i--) { + c |= s[i] ^ 0xff; + } + c = (((unsigned int) c) - 1U) >> 8; + d = (0xed - 1U - (unsigned int) s[0]) >> 8; + + return 1 - (c & d & 1); +} + +int +ge25519_has_small_order(const unsigned char s[32]) +{ + CRYPTO_ALIGN(16) + static const unsigned char blacklist[][32] = { + /* 0 (order 4) */ + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, + /* 1 (order 1) */ + { 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, + /* 2707385501144840649318225287225658788936804267575313519463743609750303402022 + (order 8) */ + { 0x26, 0xe8, 0x95, 0x8f, 0xc2, 0xb2, 0x27, 0xb0, 0x45, 0xc3, 0xf4, + 0x89, 0xf2, 0xef, 0x98, 0xf0, 0xd5, 0xdf, 0xac, 0x05, 0xd3, 0xc6, + 0x33, 0x39, 0xb1, 0x38, 0x02, 0x88, 0x6d, 0x53, 0xfc, 0x05 }, + /* 55188659117513257062467267217118295137698188065244968500265048394206261417927 + (order 8) */ + { 0xc7, 0x17, 0x6a, 0x70, 0x3d, 0x4d, 0xd8, 0x4f, 0xba, 0x3c, 0x0b, + 0x76, 0x0d, 0x10, 0x67, 0x0f, 0x2a, 0x20, 0x53, 0xfa, 0x2c, 0x39, + 0xcc, 0xc6, 0x4e, 0xc7, 0xfd, 0x77, 0x92, 0xac, 0x03, 0x7a }, + /* p-1 (order 2) */ + { 0xec, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f }, + /* p (=0, order 4) */ + { 0xed, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f }, + /* p+1 (=1, order 1) */ + { 0xee, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f } + }; + unsigned char c[7] = { 0 }; + unsigned int k; + size_t i, j; + + COMPILER_ASSERT(7 == sizeof blacklist / sizeof blacklist[0]); + for (j = 0; j < 31; j++) { + for (i = 0; i < sizeof blacklist / sizeof blacklist[0]; i++) { + c[i] |= s[j] ^ blacklist[i][j]; + } + } + for (i = 0; i < sizeof blacklist / sizeof blacklist[0]; i++) { + c[i] |= (s[j] & 0x7f) ^ blacklist[i][j]; + } + k = 0; + for (i = 0; i < sizeof blacklist / sizeof blacklist[0]; i++) { + k |= (c[i] - 1); + } + return (int) ((k >> 8) & 1); +} + +/* + Input: + a[0]+256*a[1]+...+256^31*a[31] = a + b[0]+256*b[1]+...+256^31*b[31] = b + * + Output: + s[0]+256*s[1]+...+256^31*s[31] = (ab) mod l + where l = 2^252 + 27742317777372353535851937790883648493. + */ + +void +sc25519_mul(unsigned char s[32], const unsigned char a[32], const unsigned char b[32]) +{ + int64_t a0 = 2097151 & load_3(a); + int64_t a1 = 2097151 & (load_4(a + 2) >> 5); + int64_t a2 = 2097151 & (load_3(a + 5) >> 2); + int64_t a3 = 2097151 & (load_4(a + 7) >> 7); + int64_t a4 = 2097151 & (load_4(a + 10) >> 4); + int64_t a5 = 2097151 & (load_3(a + 13) >> 1); + int64_t a6 = 2097151 & (load_4(a + 15) >> 6); + int64_t a7 = 2097151 & (load_3(a + 18) >> 3); + int64_t a8 = 2097151 & load_3(a + 21); + int64_t a9 = 2097151 & (load_4(a + 23) >> 5); + int64_t a10 = 2097151 & (load_3(a + 26) >> 2); + int64_t a11 = (load_4(a + 28) >> 7); + + int64_t b0 = 2097151 & load_3(b); + int64_t b1 = 2097151 & (load_4(b + 2) >> 5); + int64_t b2 = 2097151 & (load_3(b + 5) >> 2); + int64_t b3 = 2097151 & (load_4(b + 7) >> 7); + int64_t b4 = 2097151 & (load_4(b + 10) >> 4); + int64_t b5 = 2097151 & (load_3(b + 13) >> 1); + int64_t b6 = 2097151 & (load_4(b + 15) >> 6); + int64_t b7 = 2097151 & (load_3(b + 18) >> 3); + int64_t b8 = 2097151 & load_3(b + 21); + int64_t b9 = 2097151 & (load_4(b + 23) >> 5); + int64_t b10 = 2097151 & (load_3(b + 26) >> 2); + int64_t b11 = (load_4(b + 28) >> 7); + + int64_t s0; + int64_t s1; + int64_t s2; + int64_t s3; + int64_t s4; + int64_t s5; + int64_t s6; + int64_t s7; + int64_t s8; + int64_t s9; + int64_t s10; + int64_t s11; + int64_t s12; + int64_t s13; + int64_t s14; + int64_t s15; + int64_t s16; + int64_t s17; + int64_t s18; + int64_t s19; + int64_t s20; + int64_t s21; + int64_t s22; + int64_t s23; + + int64_t carry0; + int64_t carry1; + int64_t carry2; + int64_t carry3; + int64_t carry4; + int64_t carry5; + int64_t carry6; + int64_t carry7; + int64_t carry8; + int64_t carry9; + int64_t carry10; + int64_t carry11; + int64_t carry12; + int64_t carry13; + int64_t carry14; + int64_t carry15; + int64_t carry16; + int64_t carry17; + int64_t carry18; + int64_t carry19; + int64_t carry20; + int64_t carry21; + int64_t carry22; + + s0 = a0 * b0; + s1 = a0 * b1 + a1 * b0; + s2 = a0 * b2 + a1 * b1 + a2 * b0; + s3 = a0 * b3 + a1 * b2 + a2 * b1 + a3 * b0; + s4 = a0 * b4 + a1 * b3 + a2 * b2 + a3 * b1 + a4 * b0; + s5 = a0 * b5 + a1 * b4 + a2 * b3 + a3 * b2 + a4 * b1 + a5 * b0; + s6 = a0 * b6 + a1 * b5 + a2 * b4 + a3 * b3 + a4 * b2 + a5 * b1 + a6 * b0; + s7 = a0 * b7 + a1 * b6 + a2 * b5 + a3 * b4 + a4 * b3 + a5 * b2 + + a6 * b1 + a7 * b0; + s8 = a0 * b8 + a1 * b7 + a2 * b6 + a3 * b5 + a4 * b4 + a5 * b3 + + a6 * b2 + a7 * b1 + a8 * b0; + s9 = a0 * b9 + a1 * b8 + a2 * b7 + a3 * b6 + a4 * b5 + a5 * b4 + + a6 * b3 + a7 * b2 + a8 * b1 + a9 * b0; + s10 = a0 * b10 + a1 * b9 + a2 * b8 + a3 * b7 + a4 * b6 + a5 * b5 + + a6 * b4 + a7 * b3 + a8 * b2 + a9 * b1 + a10 * b0; + s11 = a0 * b11 + a1 * b10 + a2 * b9 + a3 * b8 + a4 * b7 + a5 * b6 + + a6 * b5 + a7 * b4 + a8 * b3 + a9 * b2 + a10 * b1 + a11 * b0; + s12 = a1 * b11 + a2 * b10 + a3 * b9 + a4 * b8 + a5 * b7 + a6 * b6 + + a7 * b5 + a8 * b4 + a9 * b3 + a10 * b2 + a11 * b1; + s13 = a2 * b11 + a3 * b10 + a4 * b9 + a5 * b8 + a6 * b7 + a7 * b6 + + a8 * b5 + a9 * b4 + a10 * b3 + a11 * b2; + s14 = a3 * b11 + a4 * b10 + a5 * b9 + a6 * b8 + a7 * b7 + a8 * b6 + + a9 * b5 + a10 * b4 + a11 * b3; + s15 = a4 * b11 + a5 * b10 + a6 * b9 + a7 * b8 + a8 * b7 + a9 * b6 + + a10 * b5 + a11 * b4; + s16 = + a5 * b11 + a6 * b10 + a7 * b9 + a8 * b8 + a9 * b7 + a10 * b6 + a11 * b5; + s17 = a6 * b11 + a7 * b10 + a8 * b9 + a9 * b8 + a10 * b7 + a11 * b6; + s18 = a7 * b11 + a8 * b10 + a9 * b9 + a10 * b8 + a11 * b7; + s19 = a8 * b11 + a9 * b10 + a10 * b9 + a11 * b8; + s20 = a9 * b11 + a10 * b10 + a11 * b9; + s21 = a10 * b11 + a11 * b10; + s22 = a11 * b11; + s23 = 0; + + carry0 = (s0 + (int64_t) (1L << 20)) >> 21; + s1 += carry0; + s0 -= carry0 * ((uint64_t) 1L << 21); + carry2 = (s2 + (int64_t) (1L << 20)) >> 21; + s3 += carry2; + s2 -= carry2 * ((uint64_t) 1L << 21); + carry4 = (s4 + (int64_t) (1L << 20)) >> 21; + s5 += carry4; + s4 -= carry4 * ((uint64_t) 1L << 21); + carry6 = (s6 + (int64_t) (1L << 20)) >> 21; + s7 += carry6; + s6 -= carry6 * ((uint64_t) 1L << 21); + carry8 = (s8 + (int64_t) (1L << 20)) >> 21; + s9 += carry8; + s8 -= carry8 * ((uint64_t) 1L << 21); + carry10 = (s10 + (int64_t) (1L << 20)) >> 21; + s11 += carry10; + s10 -= carry10 * ((uint64_t) 1L << 21); + carry12 = (s12 + (int64_t) (1L << 20)) >> 21; + s13 += carry12; + s12 -= carry12 * ((uint64_t) 1L << 21); + carry14 = (s14 + (int64_t) (1L << 20)) >> 21; + s15 += carry14; + s14 -= carry14 * ((uint64_t) 1L << 21); + carry16 = (s16 + (int64_t) (1L << 20)) >> 21; + s17 += carry16; + s16 -= carry16 * ((uint64_t) 1L << 21); + carry18 = (s18 + (int64_t) (1L << 20)) >> 21; + s19 += carry18; + s18 -= carry18 * ((uint64_t) 1L << 21); + carry20 = (s20 + (int64_t) (1L << 20)) >> 21; + s21 += carry20; + s20 -= carry20 * ((uint64_t) 1L << 21); + carry22 = (s22 + (int64_t) (1L << 20)) >> 21; + s23 += carry22; + s22 -= carry22 * ((uint64_t) 1L << 21); + + carry1 = (s1 + (int64_t) (1L << 20)) >> 21; + s2 += carry1; + s1 -= carry1 * ((uint64_t) 1L << 21); + carry3 = (s3 + (int64_t) (1L << 20)) >> 21; + s4 += carry3; + s3 -= carry3 * ((uint64_t) 1L << 21); + carry5 = (s5 + (int64_t) (1L << 20)) >> 21; + s6 += carry5; + s5 -= carry5 * ((uint64_t) 1L << 21); + carry7 = (s7 + (int64_t) (1L << 20)) >> 21; + s8 += carry7; + s7 -= carry7 * ((uint64_t) 1L << 21); + carry9 = (s9 + (int64_t) (1L << 20)) >> 21; + s10 += carry9; + s9 -= carry9 * ((uint64_t) 1L << 21); + carry11 = (s11 + (int64_t) (1L << 20)) >> 21; + s12 += carry11; + s11 -= carry11 * ((uint64_t) 1L << 21); + carry13 = (s13 + (int64_t) (1L << 20)) >> 21; + s14 += carry13; + s13 -= carry13 * ((uint64_t) 1L << 21); + carry15 = (s15 + (int64_t) (1L << 20)) >> 21; + s16 += carry15; + s15 -= carry15 * ((uint64_t) 1L << 21); + carry17 = (s17 + (int64_t) (1L << 20)) >> 21; + s18 += carry17; + s17 -= carry17 * ((uint64_t) 1L << 21); + carry19 = (s19 + (int64_t) (1L << 20)) >> 21; + s20 += carry19; + s19 -= carry19 * ((uint64_t) 1L << 21); + carry21 = (s21 + (int64_t) (1L << 20)) >> 21; + s22 += carry21; + s21 -= carry21 * ((uint64_t) 1L << 21); + + s11 += s23 * 666643; + s12 += s23 * 470296; + s13 += s23 * 654183; + s14 -= s23 * 997805; + s15 += s23 * 136657; + s16 -= s23 * 683901; + + s10 += s22 * 666643; + s11 += s22 * 470296; + s12 += s22 * 654183; + s13 -= s22 * 997805; + s14 += s22 * 136657; + s15 -= s22 * 683901; + + s9 += s21 * 666643; + s10 += s21 * 470296; + s11 += s21 * 654183; + s12 -= s21 * 997805; + s13 += s21 * 136657; + s14 -= s21 * 683901; + + s8 += s20 * 666643; + s9 += s20 * 470296; + s10 += s20 * 654183; + s11 -= s20 * 997805; + s12 += s20 * 136657; + s13 -= s20 * 683901; + + s7 += s19 * 666643; + s8 += s19 * 470296; + s9 += s19 * 654183; + s10 -= s19 * 997805; + s11 += s19 * 136657; + s12 -= s19 * 683901; + + s6 += s18 * 666643; + s7 += s18 * 470296; + s8 += s18 * 654183; + s9 -= s18 * 997805; + s10 += s18 * 136657; + s11 -= s18 * 683901; + + carry6 = (s6 + (int64_t) (1L << 20)) >> 21; + s7 += carry6; + s6 -= carry6 * ((uint64_t) 1L << 21); + carry8 = (s8 + (int64_t) (1L << 20)) >> 21; + s9 += carry8; + s8 -= carry8 * ((uint64_t) 1L << 21); + carry10 = (s10 + (int64_t) (1L << 20)) >> 21; + s11 += carry10; + s10 -= carry10 * ((uint64_t) 1L << 21); + carry12 = (s12 + (int64_t) (1L << 20)) >> 21; + s13 += carry12; + s12 -= carry12 * ((uint64_t) 1L << 21); + carry14 = (s14 + (int64_t) (1L << 20)) >> 21; + s15 += carry14; + s14 -= carry14 * ((uint64_t) 1L << 21); + carry16 = (s16 + (int64_t) (1L << 20)) >> 21; + s17 += carry16; + s16 -= carry16 * ((uint64_t) 1L << 21); + + carry7 = (s7 + (int64_t) (1L << 20)) >> 21; + s8 += carry7; + s7 -= carry7 * ((uint64_t) 1L << 21); + carry9 = (s9 + (int64_t) (1L << 20)) >> 21; + s10 += carry9; + s9 -= carry9 * ((uint64_t) 1L << 21); + carry11 = (s11 + (int64_t) (1L << 20)) >> 21; + s12 += carry11; + s11 -= carry11 * ((uint64_t) 1L << 21); + carry13 = (s13 + (int64_t) (1L << 20)) >> 21; + s14 += carry13; + s13 -= carry13 * ((uint64_t) 1L << 21); + carry15 = (s15 + (int64_t) (1L << 20)) >> 21; + s16 += carry15; + s15 -= carry15 * ((uint64_t) 1L << 21); + + s5 += s17 * 666643; + s6 += s17 * 470296; + s7 += s17 * 654183; + s8 -= s17 * 997805; + s9 += s17 * 136657; + s10 -= s17 * 683901; + + s4 += s16 * 666643; + s5 += s16 * 470296; + s6 += s16 * 654183; + s7 -= s16 * 997805; + s8 += s16 * 136657; + s9 -= s16 * 683901; + + s3 += s15 * 666643; + s4 += s15 * 470296; + s5 += s15 * 654183; + s6 -= s15 * 997805; + s7 += s15 * 136657; + s8 -= s15 * 683901; + + s2 += s14 * 666643; + s3 += s14 * 470296; + s4 += s14 * 654183; + s5 -= s14 * 997805; + s6 += s14 * 136657; + s7 -= s14 * 683901; + + s1 += s13 * 666643; + s2 += s13 * 470296; + s3 += s13 * 654183; + s4 -= s13 * 997805; + s5 += s13 * 136657; + s6 -= s13 * 683901; + + s0 += s12 * 666643; + s1 += s12 * 470296; + s2 += s12 * 654183; + s3 -= s12 * 997805; + s4 += s12 * 136657; + s5 -= s12 * 683901; + s12 = 0; + + carry0 = (s0 + (int64_t) (1L << 20)) >> 21; + s1 += carry0; + s0 -= carry0 * ((uint64_t) 1L << 21); + carry2 = (s2 + (int64_t) (1L << 20)) >> 21; + s3 += carry2; + s2 -= carry2 * ((uint64_t) 1L << 21); + carry4 = (s4 + (int64_t) (1L << 20)) >> 21; + s5 += carry4; + s4 -= carry4 * ((uint64_t) 1L << 21); + carry6 = (s6 + (int64_t) (1L << 20)) >> 21; + s7 += carry6; + s6 -= carry6 * ((uint64_t) 1L << 21); + carry8 = (s8 + (int64_t) (1L << 20)) >> 21; + s9 += carry8; + s8 -= carry8 * ((uint64_t) 1L << 21); + carry10 = (s10 + (int64_t) (1L << 20)) >> 21; + s11 += carry10; + s10 -= carry10 * ((uint64_t) 1L << 21); + + carry1 = (s1 + (int64_t) (1L << 20)) >> 21; + s2 += carry1; + s1 -= carry1 * ((uint64_t) 1L << 21); + carry3 = (s3 + (int64_t) (1L << 20)) >> 21; + s4 += carry3; + s3 -= carry3 * ((uint64_t) 1L << 21); + carry5 = (s5 + (int64_t) (1L << 20)) >> 21; + s6 += carry5; + s5 -= carry5 * ((uint64_t) 1L << 21); + carry7 = (s7 + (int64_t) (1L << 20)) >> 21; + s8 += carry7; + s7 -= carry7 * ((uint64_t) 1L << 21); + carry9 = (s9 + (int64_t) (1L << 20)) >> 21; + s10 += carry9; + s9 -= carry9 * ((uint64_t) 1L << 21); + carry11 = (s11 + (int64_t) (1L << 20)) >> 21; + s12 += carry11; + s11 -= carry11 * ((uint64_t) 1L << 21); + + s0 += s12 * 666643; + s1 += s12 * 470296; + s2 += s12 * 654183; + s3 -= s12 * 997805; + s4 += s12 * 136657; + s5 -= s12 * 683901; + s12 = 0; + + carry0 = s0 >> 21; + s1 += carry0; + s0 -= carry0 * ((uint64_t) 1L << 21); + carry1 = s1 >> 21; + s2 += carry1; + s1 -= carry1 * ((uint64_t) 1L << 21); + carry2 = s2 >> 21; + s3 += carry2; + s2 -= carry2 * ((uint64_t) 1L << 21); + carry3 = s3 >> 21; + s4 += carry3; + s3 -= carry3 * ((uint64_t) 1L << 21); + carry4 = s4 >> 21; + s5 += carry4; + s4 -= carry4 * ((uint64_t) 1L << 21); + carry5 = s5 >> 21; + s6 += carry5; + s5 -= carry5 * ((uint64_t) 1L << 21); + carry6 = s6 >> 21; + s7 += carry6; + s6 -= carry6 * ((uint64_t) 1L << 21); + carry7 = s7 >> 21; + s8 += carry7; + s7 -= carry7 * ((uint64_t) 1L << 21); + carry8 = s8 >> 21; + s9 += carry8; + s8 -= carry8 * ((uint64_t) 1L << 21); + carry9 = s9 >> 21; + s10 += carry9; + s9 -= carry9 * ((uint64_t) 1L << 21); + carry10 = s10 >> 21; + s11 += carry10; + s10 -= carry10 * ((uint64_t) 1L << 21); + carry11 = s11 >> 21; + s12 += carry11; + s11 -= carry11 * ((uint64_t) 1L << 21); + + s0 += s12 * 666643; + s1 += s12 * 470296; + s2 += s12 * 654183; + s3 -= s12 * 997805; + s4 += s12 * 136657; + s5 -= s12 * 683901; + + carry0 = s0 >> 21; + s1 += carry0; + s0 -= carry0 * ((uint64_t) 1L << 21); + carry1 = s1 >> 21; + s2 += carry1; + s1 -= carry1 * ((uint64_t) 1L << 21); + carry2 = s2 >> 21; + s3 += carry2; + s2 -= carry2 * ((uint64_t) 1L << 21); + carry3 = s3 >> 21; + s4 += carry3; + s3 -= carry3 * ((uint64_t) 1L << 21); + carry4 = s4 >> 21; + s5 += carry4; + s4 -= carry4 * ((uint64_t) 1L << 21); + carry5 = s5 >> 21; + s6 += carry5; + s5 -= carry5 * ((uint64_t) 1L << 21); + carry6 = s6 >> 21; + s7 += carry6; + s6 -= carry6 * ((uint64_t) 1L << 21); + carry7 = s7 >> 21; + s8 += carry7; + s7 -= carry7 * ((uint64_t) 1L << 21); + carry8 = s8 >> 21; + s9 += carry8; + s8 -= carry8 * ((uint64_t) 1L << 21); + carry9 = s9 >> 21; + s10 += carry9; + s9 -= carry9 * ((uint64_t) 1L << 21); + carry10 = s10 >> 21; + s11 += carry10; + s10 -= carry10 * ((uint64_t) 1L << 21); + + s[0] = s0 >> 0; + s[1] = s0 >> 8; + s[2] = (s0 >> 16) | (s1 * ((uint64_t) 1 << 5)); + s[3] = s1 >> 3; + s[4] = s1 >> 11; + s[5] = (s1 >> 19) | (s2 * ((uint64_t) 1 << 2)); + s[6] = s2 >> 6; + s[7] = (s2 >> 14) | (s3 * ((uint64_t) 1 << 7)); + s[8] = s3 >> 1; + s[9] = s3 >> 9; + s[10] = (s3 >> 17) | (s4 * ((uint64_t) 1 << 4)); + s[11] = s4 >> 4; + s[12] = s4 >> 12; + s[13] = (s4 >> 20) | (s5 * ((uint64_t) 1 << 1)); + s[14] = s5 >> 7; + s[15] = (s5 >> 15) | (s6 * ((uint64_t) 1 << 6)); + s[16] = s6 >> 2; + s[17] = s6 >> 10; + s[18] = (s6 >> 18) | (s7 * ((uint64_t) 1 << 3)); + s[19] = s7 >> 5; + s[20] = s7 >> 13; + s[21] = s8 >> 0; + s[22] = s8 >> 8; + s[23] = (s8 >> 16) | (s9 * ((uint64_t) 1 << 5)); + s[24] = s9 >> 3; + s[25] = s9 >> 11; + s[26] = (s9 >> 19) | (s10 * ((uint64_t) 1 << 2)); + s[27] = s10 >> 6; + s[28] = (s10 >> 14) | (s11 * ((uint64_t) 1 << 7)); + s[29] = s11 >> 1; + s[30] = s11 >> 9; + s[31] = s11 >> 17; +} + +/* + Input: + a[0]+256*a[1]+...+256^31*a[31] = a + b[0]+256*b[1]+...+256^31*b[31] = b + c[0]+256*c[1]+...+256^31*c[31] = c + * + Output: + s[0]+256*s[1]+...+256^31*s[31] = (ab+c) mod l + where l = 2^252 + 27742317777372353535851937790883648493. + */ + +void +sc25519_muladd(unsigned char s[32], const unsigned char a[32], + const unsigned char b[32], const unsigned char c[32]) +{ + int64_t a0 = 2097151 & load_3(a); + int64_t a1 = 2097151 & (load_4(a + 2) >> 5); + int64_t a2 = 2097151 & (load_3(a + 5) >> 2); + int64_t a3 = 2097151 & (load_4(a + 7) >> 7); + int64_t a4 = 2097151 & (load_4(a + 10) >> 4); + int64_t a5 = 2097151 & (load_3(a + 13) >> 1); + int64_t a6 = 2097151 & (load_4(a + 15) >> 6); + int64_t a7 = 2097151 & (load_3(a + 18) >> 3); + int64_t a8 = 2097151 & load_3(a + 21); + int64_t a9 = 2097151 & (load_4(a + 23) >> 5); + int64_t a10 = 2097151 & (load_3(a + 26) >> 2); + int64_t a11 = (load_4(a + 28) >> 7); + + int64_t b0 = 2097151 & load_3(b); + int64_t b1 = 2097151 & (load_4(b + 2) >> 5); + int64_t b2 = 2097151 & (load_3(b + 5) >> 2); + int64_t b3 = 2097151 & (load_4(b + 7) >> 7); + int64_t b4 = 2097151 & (load_4(b + 10) >> 4); + int64_t b5 = 2097151 & (load_3(b + 13) >> 1); + int64_t b6 = 2097151 & (load_4(b + 15) >> 6); + int64_t b7 = 2097151 & (load_3(b + 18) >> 3); + int64_t b8 = 2097151 & load_3(b + 21); + int64_t b9 = 2097151 & (load_4(b + 23) >> 5); + int64_t b10 = 2097151 & (load_3(b + 26) >> 2); + int64_t b11 = (load_4(b + 28) >> 7); + + int64_t c0 = 2097151 & load_3(c); + int64_t c1 = 2097151 & (load_4(c + 2) >> 5); + int64_t c2 = 2097151 & (load_3(c + 5) >> 2); + int64_t c3 = 2097151 & (load_4(c + 7) >> 7); + int64_t c4 = 2097151 & (load_4(c + 10) >> 4); + int64_t c5 = 2097151 & (load_3(c + 13) >> 1); + int64_t c6 = 2097151 & (load_4(c + 15) >> 6); + int64_t c7 = 2097151 & (load_3(c + 18) >> 3); + int64_t c8 = 2097151 & load_3(c + 21); + int64_t c9 = 2097151 & (load_4(c + 23) >> 5); + int64_t c10 = 2097151 & (load_3(c + 26) >> 2); + int64_t c11 = (load_4(c + 28) >> 7); + + int64_t s0; + int64_t s1; + int64_t s2; + int64_t s3; + int64_t s4; + int64_t s5; + int64_t s6; + int64_t s7; + int64_t s8; + int64_t s9; + int64_t s10; + int64_t s11; + int64_t s12; + int64_t s13; + int64_t s14; + int64_t s15; + int64_t s16; + int64_t s17; + int64_t s18; + int64_t s19; + int64_t s20; + int64_t s21; + int64_t s22; + int64_t s23; + + int64_t carry0; + int64_t carry1; + int64_t carry2; + int64_t carry3; + int64_t carry4; + int64_t carry5; + int64_t carry6; + int64_t carry7; + int64_t carry8; + int64_t carry9; + int64_t carry10; + int64_t carry11; + int64_t carry12; + int64_t carry13; + int64_t carry14; + int64_t carry15; + int64_t carry16; + int64_t carry17; + int64_t carry18; + int64_t carry19; + int64_t carry20; + int64_t carry21; + int64_t carry22; + + s0 = c0 + a0 * b0; + s1 = c1 + a0 * b1 + a1 * b0; + s2 = c2 + a0 * b2 + a1 * b1 + a2 * b0; + s3 = c3 + a0 * b3 + a1 * b2 + a2 * b1 + a3 * b0; + s4 = c4 + a0 * b4 + a1 * b3 + a2 * b2 + a3 * b1 + a4 * b0; + s5 = c5 + a0 * b5 + a1 * b4 + a2 * b3 + a3 * b2 + a4 * b1 + a5 * b0; + s6 = c6 + a0 * b6 + a1 * b5 + a2 * b4 + a3 * b3 + a4 * b2 + a5 * b1 + + a6 * b0; + s7 = c7 + a0 * b7 + a1 * b6 + a2 * b5 + a3 * b4 + a4 * b3 + a5 * b2 + + a6 * b1 + a7 * b0; + s8 = c8 + a0 * b8 + a1 * b7 + a2 * b6 + a3 * b5 + a4 * b4 + a5 * b3 + + a6 * b2 + a7 * b1 + a8 * b0; + s9 = c9 + a0 * b9 + a1 * b8 + a2 * b7 + a3 * b6 + a4 * b5 + a5 * b4 + + a6 * b3 + a7 * b2 + a8 * b1 + a9 * b0; + s10 = c10 + a0 * b10 + a1 * b9 + a2 * b8 + a3 * b7 + a4 * b6 + a5 * b5 + + a6 * b4 + a7 * b3 + a8 * b2 + a9 * b1 + a10 * b0; + s11 = c11 + a0 * b11 + a1 * b10 + a2 * b9 + a3 * b8 + a4 * b7 + a5 * b6 + + a6 * b5 + a7 * b4 + a8 * b3 + a9 * b2 + a10 * b1 + a11 * b0; + s12 = a1 * b11 + a2 * b10 + a3 * b9 + a4 * b8 + a5 * b7 + a6 * b6 + + a7 * b5 + a8 * b4 + a9 * b3 + a10 * b2 + a11 * b1; + s13 = a2 * b11 + a3 * b10 + a4 * b9 + a5 * b8 + a6 * b7 + a7 * b6 + + a8 * b5 + a9 * b4 + a10 * b3 + a11 * b2; + s14 = a3 * b11 + a4 * b10 + a5 * b9 + a6 * b8 + a7 * b7 + a8 * b6 + + a9 * b5 + a10 * b4 + a11 * b3; + s15 = a4 * b11 + a5 * b10 + a6 * b9 + a7 * b8 + a8 * b7 + a9 * b6 + + a10 * b5 + a11 * b4; + s16 = + a5 * b11 + a6 * b10 + a7 * b9 + a8 * b8 + a9 * b7 + a10 * b6 + a11 * b5; + s17 = a6 * b11 + a7 * b10 + a8 * b9 + a9 * b8 + a10 * b7 + a11 * b6; + s18 = a7 * b11 + a8 * b10 + a9 * b9 + a10 * b8 + a11 * b7; + s19 = a8 * b11 + a9 * b10 + a10 * b9 + a11 * b8; + s20 = a9 * b11 + a10 * b10 + a11 * b9; + s21 = a10 * b11 + a11 * b10; + s22 = a11 * b11; + s23 = 0; + + carry0 = (s0 + (int64_t) (1L << 20)) >> 21; + s1 += carry0; + s0 -= carry0 * ((uint64_t) 1L << 21); + carry2 = (s2 + (int64_t) (1L << 20)) >> 21; + s3 += carry2; + s2 -= carry2 * ((uint64_t) 1L << 21); + carry4 = (s4 + (int64_t) (1L << 20)) >> 21; + s5 += carry4; + s4 -= carry4 * ((uint64_t) 1L << 21); + carry6 = (s6 + (int64_t) (1L << 20)) >> 21; + s7 += carry6; + s6 -= carry6 * ((uint64_t) 1L << 21); + carry8 = (s8 + (int64_t) (1L << 20)) >> 21; + s9 += carry8; + s8 -= carry8 * ((uint64_t) 1L << 21); + carry10 = (s10 + (int64_t) (1L << 20)) >> 21; + s11 += carry10; + s10 -= carry10 * ((uint64_t) 1L << 21); + carry12 = (s12 + (int64_t) (1L << 20)) >> 21; + s13 += carry12; + s12 -= carry12 * ((uint64_t) 1L << 21); + carry14 = (s14 + (int64_t) (1L << 20)) >> 21; + s15 += carry14; + s14 -= carry14 * ((uint64_t) 1L << 21); + carry16 = (s16 + (int64_t) (1L << 20)) >> 21; + s17 += carry16; + s16 -= carry16 * ((uint64_t) 1L << 21); + carry18 = (s18 + (int64_t) (1L << 20)) >> 21; + s19 += carry18; + s18 -= carry18 * ((uint64_t) 1L << 21); + carry20 = (s20 + (int64_t) (1L << 20)) >> 21; + s21 += carry20; + s20 -= carry20 * ((uint64_t) 1L << 21); + carry22 = (s22 + (int64_t) (1L << 20)) >> 21; + s23 += carry22; + s22 -= carry22 * ((uint64_t) 1L << 21); + + carry1 = (s1 + (int64_t) (1L << 20)) >> 21; + s2 += carry1; + s1 -= carry1 * ((uint64_t) 1L << 21); + carry3 = (s3 + (int64_t) (1L << 20)) >> 21; + s4 += carry3; + s3 -= carry3 * ((uint64_t) 1L << 21); + carry5 = (s5 + (int64_t) (1L << 20)) >> 21; + s6 += carry5; + s5 -= carry5 * ((uint64_t) 1L << 21); + carry7 = (s7 + (int64_t) (1L << 20)) >> 21; + s8 += carry7; + s7 -= carry7 * ((uint64_t) 1L << 21); + carry9 = (s9 + (int64_t) (1L << 20)) >> 21; + s10 += carry9; + s9 -= carry9 * ((uint64_t) 1L << 21); + carry11 = (s11 + (int64_t) (1L << 20)) >> 21; + s12 += carry11; + s11 -= carry11 * ((uint64_t) 1L << 21); + carry13 = (s13 + (int64_t) (1L << 20)) >> 21; + s14 += carry13; + s13 -= carry13 * ((uint64_t) 1L << 21); + carry15 = (s15 + (int64_t) (1L << 20)) >> 21; + s16 += carry15; + s15 -= carry15 * ((uint64_t) 1L << 21); + carry17 = (s17 + (int64_t) (1L << 20)) >> 21; + s18 += carry17; + s17 -= carry17 * ((uint64_t) 1L << 21); + carry19 = (s19 + (int64_t) (1L << 20)) >> 21; + s20 += carry19; + s19 -= carry19 * ((uint64_t) 1L << 21); + carry21 = (s21 + (int64_t) (1L << 20)) >> 21; + s22 += carry21; + s21 -= carry21 * ((uint64_t) 1L << 21); + + s11 += s23 * 666643; + s12 += s23 * 470296; + s13 += s23 * 654183; + s14 -= s23 * 997805; + s15 += s23 * 136657; + s16 -= s23 * 683901; + + s10 += s22 * 666643; + s11 += s22 * 470296; + s12 += s22 * 654183; + s13 -= s22 * 997805; + s14 += s22 * 136657; + s15 -= s22 * 683901; + + s9 += s21 * 666643; + s10 += s21 * 470296; + s11 += s21 * 654183; + s12 -= s21 * 997805; + s13 += s21 * 136657; + s14 -= s21 * 683901; + + s8 += s20 * 666643; + s9 += s20 * 470296; + s10 += s20 * 654183; + s11 -= s20 * 997805; + s12 += s20 * 136657; + s13 -= s20 * 683901; + + s7 += s19 * 666643; + s8 += s19 * 470296; + s9 += s19 * 654183; + s10 -= s19 * 997805; + s11 += s19 * 136657; + s12 -= s19 * 683901; + + s6 += s18 * 666643; + s7 += s18 * 470296; + s8 += s18 * 654183; + s9 -= s18 * 997805; + s10 += s18 * 136657; + s11 -= s18 * 683901; + + carry6 = (s6 + (int64_t) (1L << 20)) >> 21; + s7 += carry6; + s6 -= carry6 * ((uint64_t) 1L << 21); + carry8 = (s8 + (int64_t) (1L << 20)) >> 21; + s9 += carry8; + s8 -= carry8 * ((uint64_t) 1L << 21); + carry10 = (s10 + (int64_t) (1L << 20)) >> 21; + s11 += carry10; + s10 -= carry10 * ((uint64_t) 1L << 21); + carry12 = (s12 + (int64_t) (1L << 20)) >> 21; + s13 += carry12; + s12 -= carry12 * ((uint64_t) 1L << 21); + carry14 = (s14 + (int64_t) (1L << 20)) >> 21; + s15 += carry14; + s14 -= carry14 * ((uint64_t) 1L << 21); + carry16 = (s16 + (int64_t) (1L << 20)) >> 21; + s17 += carry16; + s16 -= carry16 * ((uint64_t) 1L << 21); + + carry7 = (s7 + (int64_t) (1L << 20)) >> 21; + s8 += carry7; + s7 -= carry7 * ((uint64_t) 1L << 21); + carry9 = (s9 + (int64_t) (1L << 20)) >> 21; + s10 += carry9; + s9 -= carry9 * ((uint64_t) 1L << 21); + carry11 = (s11 + (int64_t) (1L << 20)) >> 21; + s12 += carry11; + s11 -= carry11 * ((uint64_t) 1L << 21); + carry13 = (s13 + (int64_t) (1L << 20)) >> 21; + s14 += carry13; + s13 -= carry13 * ((uint64_t) 1L << 21); + carry15 = (s15 + (int64_t) (1L << 20)) >> 21; + s16 += carry15; + s15 -= carry15 * ((uint64_t) 1L << 21); + + s5 += s17 * 666643; + s6 += s17 * 470296; + s7 += s17 * 654183; + s8 -= s17 * 997805; + s9 += s17 * 136657; + s10 -= s17 * 683901; + + s4 += s16 * 666643; + s5 += s16 * 470296; + s6 += s16 * 654183; + s7 -= s16 * 997805; + s8 += s16 * 136657; + s9 -= s16 * 683901; + + s3 += s15 * 666643; + s4 += s15 * 470296; + s5 += s15 * 654183; + s6 -= s15 * 997805; + s7 += s15 * 136657; + s8 -= s15 * 683901; + + s2 += s14 * 666643; + s3 += s14 * 470296; + s4 += s14 * 654183; + s5 -= s14 * 997805; + s6 += s14 * 136657; + s7 -= s14 * 683901; + + s1 += s13 * 666643; + s2 += s13 * 470296; + s3 += s13 * 654183; + s4 -= s13 * 997805; + s5 += s13 * 136657; + s6 -= s13 * 683901; + + s0 += s12 * 666643; + s1 += s12 * 470296; + s2 += s12 * 654183; + s3 -= s12 * 997805; + s4 += s12 * 136657; + s5 -= s12 * 683901; + s12 = 0; + + carry0 = (s0 + (int64_t) (1L << 20)) >> 21; + s1 += carry0; + s0 -= carry0 * ((uint64_t) 1L << 21); + carry2 = (s2 + (int64_t) (1L << 20)) >> 21; + s3 += carry2; + s2 -= carry2 * ((uint64_t) 1L << 21); + carry4 = (s4 + (int64_t) (1L << 20)) >> 21; + s5 += carry4; + s4 -= carry4 * ((uint64_t) 1L << 21); + carry6 = (s6 + (int64_t) (1L << 20)) >> 21; + s7 += carry6; + s6 -= carry6 * ((uint64_t) 1L << 21); + carry8 = (s8 + (int64_t) (1L << 20)) >> 21; + s9 += carry8; + s8 -= carry8 * ((uint64_t) 1L << 21); + carry10 = (s10 + (int64_t) (1L << 20)) >> 21; + s11 += carry10; + s10 -= carry10 * ((uint64_t) 1L << 21); + + carry1 = (s1 + (int64_t) (1L << 20)) >> 21; + s2 += carry1; + s1 -= carry1 * ((uint64_t) 1L << 21); + carry3 = (s3 + (int64_t) (1L << 20)) >> 21; + s4 += carry3; + s3 -= carry3 * ((uint64_t) 1L << 21); + carry5 = (s5 + (int64_t) (1L << 20)) >> 21; + s6 += carry5; + s5 -= carry5 * ((uint64_t) 1L << 21); + carry7 = (s7 + (int64_t) (1L << 20)) >> 21; + s8 += carry7; + s7 -= carry7 * ((uint64_t) 1L << 21); + carry9 = (s9 + (int64_t) (1L << 20)) >> 21; + s10 += carry9; + s9 -= carry9 * ((uint64_t) 1L << 21); + carry11 = (s11 + (int64_t) (1L << 20)) >> 21; + s12 += carry11; + s11 -= carry11 * ((uint64_t) 1L << 21); + + s0 += s12 * 666643; + s1 += s12 * 470296; + s2 += s12 * 654183; + s3 -= s12 * 997805; + s4 += s12 * 136657; + s5 -= s12 * 683901; + s12 = 0; + + carry0 = s0 >> 21; + s1 += carry0; + s0 -= carry0 * ((uint64_t) 1L << 21); + carry1 = s1 >> 21; + s2 += carry1; + s1 -= carry1 * ((uint64_t) 1L << 21); + carry2 = s2 >> 21; + s3 += carry2; + s2 -= carry2 * ((uint64_t) 1L << 21); + carry3 = s3 >> 21; + s4 += carry3; + s3 -= carry3 * ((uint64_t) 1L << 21); + carry4 = s4 >> 21; + s5 += carry4; + s4 -= carry4 * ((uint64_t) 1L << 21); + carry5 = s5 >> 21; + s6 += carry5; + s5 -= carry5 * ((uint64_t) 1L << 21); + carry6 = s6 >> 21; + s7 += carry6; + s6 -= carry6 * ((uint64_t) 1L << 21); + carry7 = s7 >> 21; + s8 += carry7; + s7 -= carry7 * ((uint64_t) 1L << 21); + carry8 = s8 >> 21; + s9 += carry8; + s8 -= carry8 * ((uint64_t) 1L << 21); + carry9 = s9 >> 21; + s10 += carry9; + s9 -= carry9 * ((uint64_t) 1L << 21); + carry10 = s10 >> 21; + s11 += carry10; + s10 -= carry10 * ((uint64_t) 1L << 21); + carry11 = s11 >> 21; + s12 += carry11; + s11 -= carry11 * ((uint64_t) 1L << 21); + + s0 += s12 * 666643; + s1 += s12 * 470296; + s2 += s12 * 654183; + s3 -= s12 * 997805; + s4 += s12 * 136657; + s5 -= s12 * 683901; + + carry0 = s0 >> 21; + s1 += carry0; + s0 -= carry0 * ((uint64_t) 1L << 21); + carry1 = s1 >> 21; + s2 += carry1; + s1 -= carry1 * ((uint64_t) 1L << 21); + carry2 = s2 >> 21; + s3 += carry2; + s2 -= carry2 * ((uint64_t) 1L << 21); + carry3 = s3 >> 21; + s4 += carry3; + s3 -= carry3 * ((uint64_t) 1L << 21); + carry4 = s4 >> 21; + s5 += carry4; + s4 -= carry4 * ((uint64_t) 1L << 21); + carry5 = s5 >> 21; + s6 += carry5; + s5 -= carry5 * ((uint64_t) 1L << 21); + carry6 = s6 >> 21; + s7 += carry6; + s6 -= carry6 * ((uint64_t) 1L << 21); + carry7 = s7 >> 21; + s8 += carry7; + s7 -= carry7 * ((uint64_t) 1L << 21); + carry8 = s8 >> 21; + s9 += carry8; + s8 -= carry8 * ((uint64_t) 1L << 21); + carry9 = s9 >> 21; + s10 += carry9; + s9 -= carry9 * ((uint64_t) 1L << 21); + carry10 = s10 >> 21; + s11 += carry10; + s10 -= carry10 * ((uint64_t) 1L << 21); + + s[0] = s0 >> 0; + s[1] = s0 >> 8; + s[2] = (s0 >> 16) | (s1 * ((uint64_t) 1 << 5)); + s[3] = s1 >> 3; + s[4] = s1 >> 11; + s[5] = (s1 >> 19) | (s2 * ((uint64_t) 1 << 2)); + s[6] = s2 >> 6; + s[7] = (s2 >> 14) | (s3 * ((uint64_t) 1 << 7)); + s[8] = s3 >> 1; + s[9] = s3 >> 9; + s[10] = (s3 >> 17) | (s4 * ((uint64_t) 1 << 4)); + s[11] = s4 >> 4; + s[12] = s4 >> 12; + s[13] = (s4 >> 20) | (s5 * ((uint64_t) 1 << 1)); + s[14] = s5 >> 7; + s[15] = (s5 >> 15) | (s6 * ((uint64_t) 1 << 6)); + s[16] = s6 >> 2; + s[17] = s6 >> 10; + s[18] = (s6 >> 18) | (s7 * ((uint64_t) 1 << 3)); + s[19] = s7 >> 5; + s[20] = s7 >> 13; + s[21] = s8 >> 0; + s[22] = s8 >> 8; + s[23] = (s8 >> 16) | (s9 * ((uint64_t) 1 << 5)); + s[24] = s9 >> 3; + s[25] = s9 >> 11; + s[26] = (s9 >> 19) | (s10 * ((uint64_t) 1 << 2)); + s[27] = s10 >> 6; + s[28] = (s10 >> 14) | (s11 * ((uint64_t) 1 << 7)); + s[29] = s11 >> 1; + s[30] = s11 >> 9; + s[31] = s11 >> 17; +} + +/* + Input: + a[0]+256*a[1]+...+256^31*a[31] = a + * + Output: + s[0]+256*s[1]+...+256^31*s[31] = a^2 mod l + where l = 2^252 + 27742317777372353535851937790883648493. + */ + +static inline void +sc25519_sq(unsigned char *s, const unsigned char *a) +{ + sc25519_mul(s, a, a); +} + +/* + Input: + s[0]+256*a[1]+...+256^31*a[31] = a + n + * + Output: + s[0]+256*s[1]+...+256^31*s[31] = x * s^(s^n) mod l + where l = 2^252 + 27742317777372353535851937790883648493. + Overwrites s in place. + */ + +static inline void +sc25519_sqmul(unsigned char s[32], const int n, const unsigned char a[32]) +{ + int i; + + for (i = 0; i < n; i++) { + sc25519_sq(s, s); + } + sc25519_mul(s, s, a); +} + +void +sc25519_invert(unsigned char recip[32], const unsigned char s[32]) +{ + unsigned char _10[32], _100[32], _1000[32], _10000[32], _100000[32], + _1000000[32], _10010011[32], _10010111[32], _100110[32], _1010[32], + _1010000[32], _1010011[32], _1011[32], _10110[32], _10111101[32], + _11[32], _1100011[32], _1100111[32], _11010011[32], _1101011[32], + _11100111[32], _11101011[32], _11110101[32]; + + sc25519_sq(_10, s); + sc25519_mul(_11, s, _10); + sc25519_mul(_100, s, _11); + sc25519_sq(_1000, _100); + sc25519_mul(_1010, _10, _1000); + sc25519_mul(_1011, s, _1010); + sc25519_sq(_10000, _1000); + sc25519_sq(_10110, _1011); + sc25519_mul(_100000, _1010, _10110); + sc25519_mul(_100110, _10000, _10110); + sc25519_sq(_1000000, _100000); + sc25519_mul(_1010000, _10000, _1000000); + sc25519_mul(_1010011, _11, _1010000); + sc25519_mul(_1100011, _10000, _1010011); + sc25519_mul(_1100111, _100, _1100011); + sc25519_mul(_1101011, _100, _1100111); + sc25519_mul(_10010011, _1000000, _1010011); + sc25519_mul(_10010111, _100, _10010011); + sc25519_mul(_10111101, _100110, _10010111); + sc25519_mul(_11010011, _10110, _10111101); + sc25519_mul(_11100111, _1010000, _10010111); + sc25519_mul(_11101011, _100, _11100111); + sc25519_mul(_11110101, _1010, _11101011); + + sc25519_mul(recip, _1011, _11110101); + sc25519_sqmul(recip, 126, _1010011); + sc25519_sqmul(recip, 9, _10); + sc25519_mul(recip, recip, _11110101); + sc25519_sqmul(recip, 7, _1100111); + sc25519_sqmul(recip, 9, _11110101); + sc25519_sqmul(recip, 11, _10111101); + sc25519_sqmul(recip, 8, _11100111); + sc25519_sqmul(recip, 9, _1101011); + sc25519_sqmul(recip, 6, _1011); + sc25519_sqmul(recip, 14, _10010011); + sc25519_sqmul(recip, 10, _1100011); + sc25519_sqmul(recip, 9, _10010111); + sc25519_sqmul(recip, 10, _11110101); + sc25519_sqmul(recip, 8, _11010011); + sc25519_sqmul(recip, 8, _11101011); +} + +/* + Input: + s[0]+256*s[1]+...+256^63*s[63] = s + * + Output: + s[0]+256*s[1]+...+256^31*s[31] = s mod l + where l = 2^252 + 27742317777372353535851937790883648493. + Overwrites s in place. + */ + +void +sc25519_reduce(unsigned char s[64]) +{ + int64_t s0 = 2097151 & load_3(s); + int64_t s1 = 2097151 & (load_4(s + 2) >> 5); + int64_t s2 = 2097151 & (load_3(s + 5) >> 2); + int64_t s3 = 2097151 & (load_4(s + 7) >> 7); + int64_t s4 = 2097151 & (load_4(s + 10) >> 4); + int64_t s5 = 2097151 & (load_3(s + 13) >> 1); + int64_t s6 = 2097151 & (load_4(s + 15) >> 6); + int64_t s7 = 2097151 & (load_3(s + 18) >> 3); + int64_t s8 = 2097151 & load_3(s + 21); + int64_t s9 = 2097151 & (load_4(s + 23) >> 5); + int64_t s10 = 2097151 & (load_3(s + 26) >> 2); + int64_t s11 = 2097151 & (load_4(s + 28) >> 7); + int64_t s12 = 2097151 & (load_4(s + 31) >> 4); + int64_t s13 = 2097151 & (load_3(s + 34) >> 1); + int64_t s14 = 2097151 & (load_4(s + 36) >> 6); + int64_t s15 = 2097151 & (load_3(s + 39) >> 3); + int64_t s16 = 2097151 & load_3(s + 42); + int64_t s17 = 2097151 & (load_4(s + 44) >> 5); + int64_t s18 = 2097151 & (load_3(s + 47) >> 2); + int64_t s19 = 2097151 & (load_4(s + 49) >> 7); + int64_t s20 = 2097151 & (load_4(s + 52) >> 4); + int64_t s21 = 2097151 & (load_3(s + 55) >> 1); + int64_t s22 = 2097151 & (load_4(s + 57) >> 6); + int64_t s23 = (load_4(s + 60) >> 3); + + int64_t carry0; + int64_t carry1; + int64_t carry2; + int64_t carry3; + int64_t carry4; + int64_t carry5; + int64_t carry6; + int64_t carry7; + int64_t carry8; + int64_t carry9; + int64_t carry10; + int64_t carry11; + int64_t carry12; + int64_t carry13; + int64_t carry14; + int64_t carry15; + int64_t carry16; + + s11 += s23 * 666643; + s12 += s23 * 470296; + s13 += s23 * 654183; + s14 -= s23 * 997805; + s15 += s23 * 136657; + s16 -= s23 * 683901; + + s10 += s22 * 666643; + s11 += s22 * 470296; + s12 += s22 * 654183; + s13 -= s22 * 997805; + s14 += s22 * 136657; + s15 -= s22 * 683901; + + s9 += s21 * 666643; + s10 += s21 * 470296; + s11 += s21 * 654183; + s12 -= s21 * 997805; + s13 += s21 * 136657; + s14 -= s21 * 683901; + + s8 += s20 * 666643; + s9 += s20 * 470296; + s10 += s20 * 654183; + s11 -= s20 * 997805; + s12 += s20 * 136657; + s13 -= s20 * 683901; + + s7 += s19 * 666643; + s8 += s19 * 470296; + s9 += s19 * 654183; + s10 -= s19 * 997805; + s11 += s19 * 136657; + s12 -= s19 * 683901; + + s6 += s18 * 666643; + s7 += s18 * 470296; + s8 += s18 * 654183; + s9 -= s18 * 997805; + s10 += s18 * 136657; + s11 -= s18 * 683901; + + carry6 = (s6 + (int64_t) (1L << 20)) >> 21; + s7 += carry6; + s6 -= carry6 * ((uint64_t) 1L << 21); + carry8 = (s8 + (int64_t) (1L << 20)) >> 21; + s9 += carry8; + s8 -= carry8 * ((uint64_t) 1L << 21); + carry10 = (s10 + (int64_t) (1L << 20)) >> 21; + s11 += carry10; + s10 -= carry10 * ((uint64_t) 1L << 21); + carry12 = (s12 + (int64_t) (1L << 20)) >> 21; + s13 += carry12; + s12 -= carry12 * ((uint64_t) 1L << 21); + carry14 = (s14 + (int64_t) (1L << 20)) >> 21; + s15 += carry14; + s14 -= carry14 * ((uint64_t) 1L << 21); + carry16 = (s16 + (int64_t) (1L << 20)) >> 21; + s17 += carry16; + s16 -= carry16 * ((uint64_t) 1L << 21); + + carry7 = (s7 + (int64_t) (1L << 20)) >> 21; + s8 += carry7; + s7 -= carry7 * ((uint64_t) 1L << 21); + carry9 = (s9 + (int64_t) (1L << 20)) >> 21; + s10 += carry9; + s9 -= carry9 * ((uint64_t) 1L << 21); + carry11 = (s11 + (int64_t) (1L << 20)) >> 21; + s12 += carry11; + s11 -= carry11 * ((uint64_t) 1L << 21); + carry13 = (s13 + (int64_t) (1L << 20)) >> 21; + s14 += carry13; + s13 -= carry13 * ((uint64_t) 1L << 21); + carry15 = (s15 + (int64_t) (1L << 20)) >> 21; + s16 += carry15; + s15 -= carry15 * ((uint64_t) 1L << 21); + + s5 += s17 * 666643; + s6 += s17 * 470296; + s7 += s17 * 654183; + s8 -= s17 * 997805; + s9 += s17 * 136657; + s10 -= s17 * 683901; + + s4 += s16 * 666643; + s5 += s16 * 470296; + s6 += s16 * 654183; + s7 -= s16 * 997805; + s8 += s16 * 136657; + s9 -= s16 * 683901; + + s3 += s15 * 666643; + s4 += s15 * 470296; + s5 += s15 * 654183; + s6 -= s15 * 997805; + s7 += s15 * 136657; + s8 -= s15 * 683901; + + s2 += s14 * 666643; + s3 += s14 * 470296; + s4 += s14 * 654183; + s5 -= s14 * 997805; + s6 += s14 * 136657; + s7 -= s14 * 683901; + + s1 += s13 * 666643; + s2 += s13 * 470296; + s3 += s13 * 654183; + s4 -= s13 * 997805; + s5 += s13 * 136657; + s6 -= s13 * 683901; + + s0 += s12 * 666643; + s1 += s12 * 470296; + s2 += s12 * 654183; + s3 -= s12 * 997805; + s4 += s12 * 136657; + s5 -= s12 * 683901; + s12 = 0; + + carry0 = (s0 + (int64_t) (1L << 20)) >> 21; + s1 += carry0; + s0 -= carry0 * ((uint64_t) 1L << 21); + carry2 = (s2 + (int64_t) (1L << 20)) >> 21; + s3 += carry2; + s2 -= carry2 * ((uint64_t) 1L << 21); + carry4 = (s4 + (int64_t) (1L << 20)) >> 21; + s5 += carry4; + s4 -= carry4 * ((uint64_t) 1L << 21); + carry6 = (s6 + (int64_t) (1L << 20)) >> 21; + s7 += carry6; + s6 -= carry6 * ((uint64_t) 1L << 21); + carry8 = (s8 + (int64_t) (1L << 20)) >> 21; + s9 += carry8; + s8 -= carry8 * ((uint64_t) 1L << 21); + carry10 = (s10 + (int64_t) (1L << 20)) >> 21; + s11 += carry10; + s10 -= carry10 * ((uint64_t) 1L << 21); + + carry1 = (s1 + (int64_t) (1L << 20)) >> 21; + s2 += carry1; + s1 -= carry1 * ((uint64_t) 1L << 21); + carry3 = (s3 + (int64_t) (1L << 20)) >> 21; + s4 += carry3; + s3 -= carry3 * ((uint64_t) 1L << 21); + carry5 = (s5 + (int64_t) (1L << 20)) >> 21; + s6 += carry5; + s5 -= carry5 * ((uint64_t) 1L << 21); + carry7 = (s7 + (int64_t) (1L << 20)) >> 21; + s8 += carry7; + s7 -= carry7 * ((uint64_t) 1L << 21); + carry9 = (s9 + (int64_t) (1L << 20)) >> 21; + s10 += carry9; + s9 -= carry9 * ((uint64_t) 1L << 21); + carry11 = (s11 + (int64_t) (1L << 20)) >> 21; + s12 += carry11; + s11 -= carry11 * ((uint64_t) 1L << 21); + + s0 += s12 * 666643; + s1 += s12 * 470296; + s2 += s12 * 654183; + s3 -= s12 * 997805; + s4 += s12 * 136657; + s5 -= s12 * 683901; + s12 = 0; + + carry0 = s0 >> 21; + s1 += carry0; + s0 -= carry0 * ((uint64_t) 1L << 21); + carry1 = s1 >> 21; + s2 += carry1; + s1 -= carry1 * ((uint64_t) 1L << 21); + carry2 = s2 >> 21; + s3 += carry2; + s2 -= carry2 * ((uint64_t) 1L << 21); + carry3 = s3 >> 21; + s4 += carry3; + s3 -= carry3 * ((uint64_t) 1L << 21); + carry4 = s4 >> 21; + s5 += carry4; + s4 -= carry4 * ((uint64_t) 1L << 21); + carry5 = s5 >> 21; + s6 += carry5; + s5 -= carry5 * ((uint64_t) 1L << 21); + carry6 = s6 >> 21; + s7 += carry6; + s6 -= carry6 * ((uint64_t) 1L << 21); + carry7 = s7 >> 21; + s8 += carry7; + s7 -= carry7 * ((uint64_t) 1L << 21); + carry8 = s8 >> 21; + s9 += carry8; + s8 -= carry8 * ((uint64_t) 1L << 21); + carry9 = s9 >> 21; + s10 += carry9; + s9 -= carry9 * ((uint64_t) 1L << 21); + carry10 = s10 >> 21; + s11 += carry10; + s10 -= carry10 * ((uint64_t) 1L << 21); + carry11 = s11 >> 21; + s12 += carry11; + s11 -= carry11 * ((uint64_t) 1L << 21); + + s0 += s12 * 666643; + s1 += s12 * 470296; + s2 += s12 * 654183; + s3 -= s12 * 997805; + s4 += s12 * 136657; + s5 -= s12 * 683901; + + carry0 = s0 >> 21; + s1 += carry0; + s0 -= carry0 * ((uint64_t) 1L << 21); + carry1 = s1 >> 21; + s2 += carry1; + s1 -= carry1 * ((uint64_t) 1L << 21); + carry2 = s2 >> 21; + s3 += carry2; + s2 -= carry2 * ((uint64_t) 1L << 21); + carry3 = s3 >> 21; + s4 += carry3; + s3 -= carry3 * ((uint64_t) 1L << 21); + carry4 = s4 >> 21; + s5 += carry4; + s4 -= carry4 * ((uint64_t) 1L << 21); + carry5 = s5 >> 21; + s6 += carry5; + s5 -= carry5 * ((uint64_t) 1L << 21); + carry6 = s6 >> 21; + s7 += carry6; + s6 -= carry6 * ((uint64_t) 1L << 21); + carry7 = s7 >> 21; + s8 += carry7; + s7 -= carry7 * ((uint64_t) 1L << 21); + carry8 = s8 >> 21; + s9 += carry8; + s8 -= carry8 * ((uint64_t) 1L << 21); + carry9 = s9 >> 21; + s10 += carry9; + s9 -= carry9 * ((uint64_t) 1L << 21); + carry10 = s10 >> 21; + s11 += carry10; + s10 -= carry10 * ((uint64_t) 1L << 21); + + s[0] = s0 >> 0; + s[1] = s0 >> 8; + s[2] = (s0 >> 16) | (s1 * ((uint64_t) 1 << 5)); + s[3] = s1 >> 3; + s[4] = s1 >> 11; + s[5] = (s1 >> 19) | (s2 * ((uint64_t) 1 << 2)); + s[6] = s2 >> 6; + s[7] = (s2 >> 14) | (s3 * ((uint64_t) 1 << 7)); + s[8] = s3 >> 1; + s[9] = s3 >> 9; + s[10] = (s3 >> 17) | (s4 * ((uint64_t) 1 << 4)); + s[11] = s4 >> 4; + s[12] = s4 >> 12; + s[13] = (s4 >> 20) | (s5 * ((uint64_t) 1 << 1)); + s[14] = s5 >> 7; + s[15] = (s5 >> 15) | (s6 * ((uint64_t) 1 << 6)); + s[16] = s6 >> 2; + s[17] = s6 >> 10; + s[18] = (s6 >> 18) | (s7 * ((uint64_t) 1 << 3)); + s[19] = s7 >> 5; + s[20] = s7 >> 13; + s[21] = s8 >> 0; + s[22] = s8 >> 8; + s[23] = (s8 >> 16) | (s9 * ((uint64_t) 1 << 5)); + s[24] = s9 >> 3; + s[25] = s9 >> 11; + s[26] = (s9 >> 19) | (s10 * ((uint64_t) 1 << 2)); + s[27] = s10 >> 6; + s[28] = (s10 >> 14) | (s11 * ((uint64_t) 1 << 7)); + s[29] = s11 >> 1; + s[30] = s11 >> 9; + s[31] = s11 >> 17; +} + +int +sc25519_is_canonical(const unsigned char s[32]) +{ + /* 2^252+27742317777372353535851937790883648493 */ + static const unsigned char L[32] = { + 0xed, 0xd3, 0xf5, 0x5c, 0x1a, 0x63, 0x12, 0x58, 0xd6, 0x9c, 0xf7, + 0xa2, 0xde, 0xf9, 0xde, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10 + }; + unsigned char c = 0; + unsigned char n = 1; + unsigned int i = 32; + + do { + i--; + c |= ((s[i] - L[i]) >> 8) & n; + n &= ((s[i] ^ L[i]) - 1) >> 8; + } while (i != 0); + + return (c != 0); +} + +/* multiply by the cofactor */ +static void +ge25519_clear_cofactor(ge25519_p3 *p3) +{ + ge25519_p1p1 p1; + ge25519_p2 p2; + + ge25519_p3_dbl(&p1, p3); + ge25519_p1p1_to_p2(&p2, &p1); + ge25519_p2_dbl(&p1, &p2); + ge25519_p1p1_to_p2(&p2, &p1); + ge25519_p2_dbl(&p1, &p2); + ge25519_p1p1_to_p3(p3, &p1); +} + +static void +ge25519_elligator2(unsigned char s[32], const fe25519 r, const unsigned char x_sign) +{ + fe25519 gx; + fe25519 negx; + fe25519 rr2; + fe25519 x, x2, x3; + ge25519_p3 p3; + unsigned int notsquare; + + fe25519_sq2(rr2, r); + rr2[0]++; + fe25519_invert(rr2, rr2); + fe25519_mul32(x, rr2, curve25519_A[0]); + fe25519_neg(x, x); + + fe25519_sq(x2, x); + fe25519_mul(x3, x, x2); + fe25519_add(gx, x3, x); + fe25519_mul32(x2, x2, curve25519_A[0]); + fe25519_add(gx, x2, gx); + + notsquare = fe25519_notsquare(gx); + fe25519_neg(negx, x); + fe25519_cmov(x, negx, notsquare); + fe25519_0(x2); + fe25519_cmov(x2, curve25519_A, notsquare); + fe25519_sub(x, x, x2); + + /* yed = (x-1)/(x+1) */ + { + fe25519 one; + fe25519 x_plus_one; + fe25519 x_plus_one_inv; + fe25519 x_minus_one; + fe25519 yed; + + fe25519_1(one); + fe25519_add(x_plus_one, x, one); + fe25519_sub(x_minus_one, x, one); + fe25519_invert(x_plus_one_inv, x_plus_one); + fe25519_mul(yed, x_minus_one, x_plus_one_inv); + fe25519_tobytes(s, yed); + } + + /* recover x */ + s[31] |= x_sign; + if (ge25519_frombytes(&p3, s) != 0) { + abort(); /* LCOV_EXCL_LINE */ + } + + ge25519_clear_cofactor(&p3); + ge25519_p3_tobytes(s, &p3); +} + +void +ge25519_from_uniform(unsigned char s[32], const unsigned char r[32]) +{ + fe25519 r_fe; + unsigned char x_sign; + + memcpy(s, r, 32); + x_sign = s[31] & 0x80; + s[31] &= 0x7f; + fe25519_frombytes(r_fe, s); + ge25519_elligator2(s, r_fe, x_sign); +} + +void +ge25519_from_hash(unsigned char s[32], const unsigned char h[64]) +{ + unsigned char fl[32]; + unsigned char gl[32]; + fe25519 fe_f; + fe25519 fe_g; + size_t i; + unsigned char x_sign; + + x_sign = h[0] & 0x80; + for (i = 0; i < 32; i++) { + fl[i] = h[63 - i]; + gl[i] = h[31 - i]; + } + fl[31] &= 0x7f; + gl[31] &= 0x7f; + fe25519_frombytes(fe_f, fl); + fe25519_frombytes(fe_g, gl); + fe_f[0] += (h[32] >> 7) * 19; + for (i = 0; i < sizeof (fe25519) / sizeof fe_f[0]; i++) { + fe_f[i] += 38 * fe_g[i]; + } + fe25519_reduce(fe_f, fe_f); + ge25519_elligator2(s, fe_f, x_sign); +} + +/* Ristretto group */ + +static int +ristretto255_sqrt_ratio_m1(fe25519 x, const fe25519 u, const fe25519 v) +{ + fe25519 v3; + fe25519 vxx; + fe25519 m_root_check, p_root_check, f_root_check; + fe25519 x_sqrtm1; + int has_m_root, has_p_root, has_f_root; + + fe25519_sq(v3, v); + fe25519_mul(v3, v3, v); /* v3 = v^3 */ + fe25519_sq(x, v3); + fe25519_mul(x, x, v); + fe25519_mul(x, x, u); /* x = uv^7 */ + + fe25519_pow22523(x, x); /* x = (uv^7)^((q-5)/8) */ + fe25519_mul(x, x, v3); + fe25519_mul(x, x, u); /* x = uv^3(uv^7)^((q-5)/8) */ + + fe25519_sq(vxx, x); + fe25519_mul(vxx, vxx, v); /* vx^2 */ + fe25519_sub(m_root_check, vxx, u); /* vx^2-u */ + fe25519_add(p_root_check, vxx, u); /* vx^2+u */ + fe25519_mul(f_root_check, u, sqrtm1); /* u*sqrt(-1) */ + fe25519_add(f_root_check, vxx, f_root_check); /* vx^2+u*sqrt(-1) */ + has_m_root = fe25519_iszero(m_root_check); + has_p_root = fe25519_iszero(p_root_check); + has_f_root = fe25519_iszero(f_root_check); + fe25519_mul(x_sqrtm1, x, sqrtm1); /* x*sqrt(-1) */ + + fe25519_cmov(x, x_sqrtm1, has_p_root | has_f_root); + fe25519_abs(x, x); + + return has_m_root | has_p_root; +} + +static int +ristretto255_is_canonical(const unsigned char *s) +{ + unsigned char c; + unsigned char d; + unsigned char e; + unsigned int i; + + c = (s[31] & 0x7f) ^ 0x7f; + for (i = 30; i > 0; i--) { + c |= s[i] ^ 0xff; + } + c = (((unsigned int) c) - 1U) >> 8; + d = (0xed - 1U - (unsigned int) s[0]) >> 8; + e = s[31] >> 7; + + return 1 - (((c & d) | e | s[0]) & 1); +} + +int +ristretto255_frombytes(ge25519_p3 *h, const unsigned char *s) +{ + fe25519 inv_sqrt; + fe25519 one; + fe25519 s_; + fe25519 ss; + fe25519 u1, u2; + fe25519 u1u1, u2u2; + fe25519 v; + fe25519 v_u2u2; + int was_square; + + if (ristretto255_is_canonical(s) == 0) { + return -1; + } + fe25519_frombytes(s_, s); + fe25519_sq(ss, s_); /* ss = s^2 */ + + fe25519_1(u1); + fe25519_sub(u1, u1, ss); /* u1 = 1-ss */ + fe25519_sq(u1u1, u1); /* u1u1 = u1^2 */ + + fe25519_1(u2); + fe25519_add(u2, u2, ss); /* u2 = 1+ss */ + fe25519_sq(u2u2, u2); /* u2u2 = u2^2 */ + + fe25519_mul(v, d, u1u1); /* v = d*u1^2 */ + fe25519_neg(v, v); /* v = -d*u1^2 */ + fe25519_sub(v, v, u2u2); /* v = -(d*u1^2)-u2^2 */ + + fe25519_mul(v_u2u2, v, u2u2); /* v_u2u2 = v*u2^2 */ + + fe25519_1(one); + was_square = ristretto255_sqrt_ratio_m1(inv_sqrt, one, v_u2u2); + fe25519_mul(h->X, inv_sqrt, u2); + fe25519_mul(h->Y, inv_sqrt, h->X); + fe25519_mul(h->Y, h->Y, v); + + fe25519_mul(h->X, h->X, s_); + fe25519_add(h->X, h->X, h->X); + fe25519_abs(h->X, h->X); + fe25519_mul(h->Y, u1, h->Y); + fe25519_1(h->Z); + fe25519_mul(h->T, h->X, h->Y); + + return - ((1 - was_square) | + fe25519_isnegative(h->T) | fe25519_iszero(h->Y)); +} + +void +ristretto255_p3_tobytes(unsigned char *s, const ge25519_p3 *h) +{ + fe25519 den1, den2; + fe25519 den_inv; + fe25519 eden; + fe25519 inv_sqrt; + fe25519 ix, iy; + fe25519 one; + fe25519 s_; + fe25519 t_z_inv; + fe25519 u1, u2; + fe25519 u1_u2u2; + fe25519 x_, y_; + fe25519 x_z_inv; + fe25519 z_inv; + fe25519 zmy; + int rotate; + + fe25519_add(u1, h->Z, h->Y); /* u1 = Z+Y */ + fe25519_sub(zmy, h->Z, h->Y); /* zmy = Z-Y */ + fe25519_mul(u1, u1, zmy); /* u1 = (Z+Y)*(Z-Y) */ + fe25519_mul(u2, h->X, h->Y); /* u2 = X*Y */ + + fe25519_sq(u1_u2u2, u2); /* u1_u2u2 = u2^2 */ + fe25519_mul(u1_u2u2, u1, u1_u2u2); /* u1_u2u2 = u1*u2^2 */ + + fe25519_1(one); + (void) ristretto255_sqrt_ratio_m1(inv_sqrt, one, u1_u2u2); + fe25519_mul(den1, inv_sqrt, u1); /* den1 = inv_sqrt*u1 */ + fe25519_mul(den2, inv_sqrt, u2); /* den2 = inv_sqrt*u2 */ + fe25519_mul(z_inv, den1, den2); /* z_inv = den1*den2 */ + fe25519_mul(z_inv, z_inv, h->T); /* z_inv = den1*den2*T */ + + fe25519_mul(ix, h->X, sqrtm1); /* ix = X*sqrt(-1) */ + fe25519_mul(iy, h->Y, sqrtm1); /* iy = Y*sqrt(-1) */ + fe25519_mul(eden, den1, invsqrtamd); /* eden = den1/sqrt(a-d) */ + + fe25519_mul(t_z_inv, h->T, z_inv); /* t_z_inv = T*z_inv */ + rotate = fe25519_isnegative(t_z_inv); + + fe25519_copy(x_, h->X); + fe25519_copy(y_, h->Y); + fe25519_copy(den_inv, den2); + + fe25519_cmov(x_, iy, rotate); + fe25519_cmov(y_, ix, rotate); + fe25519_cmov(den_inv, eden, rotate); + + fe25519_mul(x_z_inv, x_, z_inv); + fe25519_cneg(y_, y_, fe25519_isnegative(x_z_inv)); + + fe25519_sub(s_, h->Z, y_); + fe25519_mul(s_, den_inv, s_); + fe25519_abs(s_, s_); + fe25519_tobytes(s, s_); +} + +static void +ristretto255_elligator(ge25519_p3 *p, const fe25519 t) +{ + fe25519 c; + fe25519 n; + fe25519 one; + fe25519 r; + fe25519 rpd; + fe25519 s, s_prime; + fe25519 ss; + fe25519 u, v; + fe25519 w0, w1, w2, w3; + int wasnt_square; + + fe25519_1(one); + fe25519_sq(r, t); /* r = t^2 */ + fe25519_mul(r, sqrtm1, r); /* r = sqrt(-1)*t^2 */ + fe25519_add(u, r, one); /* u = r+1 */ + fe25519_mul(u, u, onemsqd); /* u = (r+1)*(1-d^2) */ + fe25519_1(c); + fe25519_neg(c, c); /* c = -1 */ + fe25519_add(rpd, r, d); /* rpd = r+d */ + fe25519_mul(v, r, d); /* v = r*d */ + fe25519_sub(v, c, v); /* v = c-r*d */ + fe25519_mul(v, v, rpd); /* v = (c-r*d)*(r+d) */ + + wasnt_square = 1 - ristretto255_sqrt_ratio_m1(s, u, v); + fe25519_mul(s_prime, s, t); + fe25519_abs(s_prime, s_prime); + fe25519_neg(s_prime, s_prime); /* s_prime = -|s*t| */ + fe25519_cmov(s, s_prime, wasnt_square); + fe25519_cmov(c, r, wasnt_square); + + fe25519_sub(n, r, one); /* n = r-1 */ + fe25519_mul(n, n, c); /* n = c*(r-1) */ + fe25519_mul(n, n, sqdmone); /* n = c*(r-1)*(d-1)^2 */ + fe25519_sub(n, n, v); /* n = c*(r-1)*(d-1)^2-v */ + + fe25519_add(w0, s, s); /* w0 = 2s */ + fe25519_mul(w0, w0, v); /* w0 = 2s*v */ + fe25519_mul(w1, n, sqrtadm1); /* w1 = n*sqrt(ad-1) */ + fe25519_sq(ss, s); /* ss = s^2 */ + fe25519_sub(w2, one, ss); /* w2 = 1-s^2 */ + fe25519_add(w3, one, ss); /* w3 = 1+s^2 */ + + fe25519_mul(p->X, w0, w3); + fe25519_mul(p->Y, w2, w1); + fe25519_mul(p->Z, w1, w3); + fe25519_mul(p->T, w0, w2); +} + +void +ristretto255_from_hash(unsigned char s[32], const unsigned char h[64]) +{ + fe25519 r0, r1; + ge25519_cached p1_cached; + ge25519_p1p1 p_p1p1; + ge25519_p3 p0, p1; + ge25519_p3 p; + + fe25519_frombytes(r0, h); + fe25519_frombytes(r1, h + 32); + ristretto255_elligator(&p0, r0); + ristretto255_elligator(&p1, r1); + ge25519_p3_to_cached(&p1_cached, &p1); + ge25519_add(&p_p1p1, &p0, &p1_cached); + ge25519_p1p1_to_p3(&p, &p_p1p1); + ristretto255_p3_tobytes(s, &p); +} diff --git a/packages/runtime/vendor/libsodium/ed25519_ref10.h b/packages/runtime/vendor/libsodium/ed25519_ref10.h new file mode 100644 index 000000000..3f4c45caf --- /dev/null +++ b/packages/runtime/vendor/libsodium/ed25519_ref10.h @@ -0,0 +1,142 @@ +#ifndef ed25519_ref10_H +#define ed25519_ref10_H + +#include +#include + +/* + fe means field element. + Here the field is \Z/(2^255-19). + */ + +#ifdef HAVE_TI_MODE +typedef uint64_t fe25519[5]; +#else +typedef int32_t fe25519[10]; +#endif + +void fe25519_invert(fe25519 out, const fe25519 z); +void fe25519_frombytes(fe25519 h, const unsigned char *s); +void fe25519_tobytes(unsigned char *s, const fe25519 h); + +#ifdef HAVE_TI_MODE +# include "ed25519_ref10_fe_51.h" +#else +# include "ed25519_ref10_fe_25_5.h" +#endif + + +/* + ge means group element. + + Here the group is the set of pairs (x,y) of field elements + satisfying -x^2 + y^2 = 1 + d x^2y^2 + where d = -121665/121666. + + Representations: + ge25519_p2 (projective): (X:Y:Z) satisfying x=X/Z, y=Y/Z + ge25519_p3 (extended): (X:Y:Z:T) satisfying x=X/Z, y=Y/Z, XY=ZT + ge25519_p1p1 (completed): ((X:Z),(Y:T)) satisfying x=X/Z, y=Y/T + ge25519_precomp (Duif): (y+x,y-x,2dxy) + */ + +typedef struct { + fe25519 X; + fe25519 Y; + fe25519 Z; +} ge25519_p2; + +typedef struct { + fe25519 X; + fe25519 Y; + fe25519 Z; + fe25519 T; +} ge25519_p3; + +typedef struct { + fe25519 X; + fe25519 Y; + fe25519 Z; + fe25519 T; +} ge25519_p1p1; + +typedef struct { + fe25519 yplusx; + fe25519 yminusx; + fe25519 xy2d; +} ge25519_precomp; + +typedef struct { + fe25519 YplusX; + fe25519 YminusX; + fe25519 Z; + fe25519 T2d; +} ge25519_cached; + +void ge25519_tobytes(unsigned char *s, const ge25519_p2 *h); + +void ge25519_p3_tobytes(unsigned char *s, const ge25519_p3 *h); + +int ge25519_frombytes(ge25519_p3 *h, const unsigned char *s); + +int ge25519_frombytes_negate_vartime(ge25519_p3 *h, const unsigned char *s); + +void ge25519_p3_to_cached(ge25519_cached *r, const ge25519_p3 *p); + +void ge25519_p1p1_to_p2(ge25519_p2 *r, const ge25519_p1p1 *p); + +void ge25519_p1p1_to_p3(ge25519_p3 *r, const ge25519_p1p1 *p); + +void ge25519_add(ge25519_p1p1 *r, const ge25519_p3 *p, const ge25519_cached *q); + +void ge25519_sub(ge25519_p1p1 *r, const ge25519_p3 *p, const ge25519_cached *q); + +void ge25519_scalarmult_base(ge25519_p3 *h, const unsigned char *a); + +void ge25519_double_scalarmult_vartime(ge25519_p2 *r, const unsigned char *a, + const ge25519_p3 *A, + const unsigned char *b); + +void ge25519_scalarmult(ge25519_p3 *h, const unsigned char *a, + const ge25519_p3 *p); + +int ge25519_is_canonical(const unsigned char *s); + +int ge25519_is_on_curve(const ge25519_p3 *p); + +int ge25519_is_on_main_subgroup(const ge25519_p3 *p); + +int ge25519_has_small_order(const unsigned char s[32]); + +void ge25519_from_uniform(unsigned char s[32], const unsigned char r[32]); + +void ge25519_from_hash(unsigned char s[32], const unsigned char h[64]); + +/* + Ristretto group + */ + +int ristretto255_frombytes(ge25519_p3 *h, const unsigned char *s); + +void ristretto255_p3_tobytes(unsigned char *s, const ge25519_p3 *h); + +void ristretto255_from_hash(unsigned char s[32], const unsigned char h[64]); + +/* + The set of scalars is \Z/l + where l = 2^252 + 27742317777372353535851937790883648493. + */ + +void sc25519_invert(unsigned char recip[32], const unsigned char s[32]); + +void sc25519_reduce(unsigned char s[64]); + +void sc25519_mul(unsigned char s[32], const unsigned char a[32], + const unsigned char b[32]); + +void sc25519_muladd(unsigned char s[32], const unsigned char a[32], + const unsigned char b[32], const unsigned char c[32]); + +int sc25519_is_canonical(const unsigned char s[32]); + +#endif diff --git a/packages/runtime/vendor/libsodium/ed25519_ref10_fe_25_5.h b/packages/runtime/vendor/libsodium/ed25519_ref10_fe_25_5.h new file mode 100644 index 000000000..a0816b219 --- /dev/null +++ b/packages/runtime/vendor/libsodium/ed25519_ref10_fe_25_5.h @@ -0,0 +1,1030 @@ +#include + +#include "common.h" +#include "utils.h" + +/* + h = 0 + */ + +static inline void +fe25519_0(fe25519 h) +{ + memset(&h[0], 0, 10 * sizeof h[0]); +} + +/* + h = 1 + */ + +static inline void +fe25519_1(fe25519 h) +{ + h[0] = 1; + h[1] = 0; + memset(&h[2], 0, 8 * sizeof h[0]); +} + +/* + h = f + g + Can overlap h with f or g. + * + Preconditions: + |f| bounded by 1.1*2^25,1.1*2^24,1.1*2^25,1.1*2^24,etc. + |g| bounded by 1.1*2^25,1.1*2^24,1.1*2^25,1.1*2^24,etc. + * + Postconditions: + |h| bounded by 1.1*2^26,1.1*2^25,1.1*2^26,1.1*2^25,etc. + */ + +static inline void +fe25519_add(fe25519 h, const fe25519 f, const fe25519 g) +{ + int32_t h0 = f[0] + g[0]; + int32_t h1 = f[1] + g[1]; + int32_t h2 = f[2] + g[2]; + int32_t h3 = f[3] + g[3]; + int32_t h4 = f[4] + g[4]; + int32_t h5 = f[5] + g[5]; + int32_t h6 = f[6] + g[6]; + int32_t h7 = f[7] + g[7]; + int32_t h8 = f[8] + g[8]; + int32_t h9 = f[9] + g[9]; + + h[0] = h0; + h[1] = h1; + h[2] = h2; + h[3] = h3; + h[4] = h4; + h[5] = h5; + h[6] = h6; + h[7] = h7; + h[8] = h8; + h[9] = h9; +} + +/* + h = f - g + Can overlap h with f or g. + * + Preconditions: + |f| bounded by 1.1*2^25,1.1*2^24,1.1*2^25,1.1*2^24,etc. + |g| bounded by 1.1*2^25,1.1*2^24,1.1*2^25,1.1*2^24,etc. + * + Postconditions: + |h| bounded by 1.1*2^26,1.1*2^25,1.1*2^26,1.1*2^25,etc. + */ + +static void +fe25519_sub(fe25519 h, const fe25519 f, const fe25519 g) +{ + int32_t h0 = f[0] - g[0]; + int32_t h1 = f[1] - g[1]; + int32_t h2 = f[2] - g[2]; + int32_t h3 = f[3] - g[3]; + int32_t h4 = f[4] - g[4]; + int32_t h5 = f[5] - g[5]; + int32_t h6 = f[6] - g[6]; + int32_t h7 = f[7] - g[7]; + int32_t h8 = f[8] - g[8]; + int32_t h9 = f[9] - g[9]; + + h[0] = h0; + h[1] = h1; + h[2] = h2; + h[3] = h3; + h[4] = h4; + h[5] = h5; + h[6] = h6; + h[7] = h7; + h[8] = h8; + h[9] = h9; +} + +/* + h = -f + * + Preconditions: + |f| bounded by 1.1*2^25,1.1*2^24,1.1*2^25,1.1*2^24,etc. + * + Postconditions: + |h| bounded by 1.1*2^25,1.1*2^24,1.1*2^25,1.1*2^24,etc. + */ + +static inline void +fe25519_neg(fe25519 h, const fe25519 f) +{ + int32_t h0 = -f[0]; + int32_t h1 = -f[1]; + int32_t h2 = -f[2]; + int32_t h3 = -f[3]; + int32_t h4 = -f[4]; + int32_t h5 = -f[5]; + int32_t h6 = -f[6]; + int32_t h7 = -f[7]; + int32_t h8 = -f[8]; + int32_t h9 = -f[9]; + + h[0] = h0; + h[1] = h1; + h[2] = h2; + h[3] = h3; + h[4] = h4; + h[5] = h5; + h[6] = h6; + h[7] = h7; + h[8] = h8; + h[9] = h9; +} + +/* + Replace (f,g) with (g,g) if b == 1; + replace (f,g) with (f,g) if b == 0. + * + Preconditions: b in {0,1}. + */ + +static void +fe25519_cmov(fe25519 f, const fe25519 g, unsigned int b) +{ + const uint32_t mask = (uint32_t) (-(int32_t) b); + + int32_t f0 = f[0]; + int32_t f1 = f[1]; + int32_t f2 = f[2]; + int32_t f3 = f[3]; + int32_t f4 = f[4]; + int32_t f5 = f[5]; + int32_t f6 = f[6]; + int32_t f7 = f[7]; + int32_t f8 = f[8]; + int32_t f9 = f[9]; + + int32_t x0 = f0 ^ g[0]; + int32_t x1 = f1 ^ g[1]; + int32_t x2 = f2 ^ g[2]; + int32_t x3 = f3 ^ g[3]; + int32_t x4 = f4 ^ g[4]; + int32_t x5 = f5 ^ g[5]; + int32_t x6 = f6 ^ g[6]; + int32_t x7 = f7 ^ g[7]; + int32_t x8 = f8 ^ g[8]; + int32_t x9 = f9 ^ g[9]; + + x0 &= mask; + x1 &= mask; + x2 &= mask; + x3 &= mask; + x4 &= mask; + x5 &= mask; + x6 &= mask; + x7 &= mask; + x8 &= mask; + x9 &= mask; + + f[0] = f0 ^ x0; + f[1] = f1 ^ x1; + f[2] = f2 ^ x2; + f[3] = f3 ^ x3; + f[4] = f4 ^ x4; + f[5] = f5 ^ x5; + f[6] = f6 ^ x6; + f[7] = f7 ^ x7; + f[8] = f8 ^ x8; + f[9] = f9 ^ x9; +} + +static void +fe25519_cswap(fe25519 f, fe25519 g, unsigned int b) +{ + const uint32_t mask = (uint32_t) (-(int64_t) b); + + int32_t f0 = f[0]; + int32_t f1 = f[1]; + int32_t f2 = f[2]; + int32_t f3 = f[3]; + int32_t f4 = f[4]; + int32_t f5 = f[5]; + int32_t f6 = f[6]; + int32_t f7 = f[7]; + int32_t f8 = f[8]; + int32_t f9 = f[9]; + + int32_t g0 = g[0]; + int32_t g1 = g[1]; + int32_t g2 = g[2]; + int32_t g3 = g[3]; + int32_t g4 = g[4]; + int32_t g5 = g[5]; + int32_t g6 = g[6]; + int32_t g7 = g[7]; + int32_t g8 = g[8]; + int32_t g9 = g[9]; + + int32_t x0 = f0 ^ g0; + int32_t x1 = f1 ^ g1; + int32_t x2 = f2 ^ g2; + int32_t x3 = f3 ^ g3; + int32_t x4 = f4 ^ g4; + int32_t x5 = f5 ^ g5; + int32_t x6 = f6 ^ g6; + int32_t x7 = f7 ^ g7; + int32_t x8 = f8 ^ g8; + int32_t x9 = f9 ^ g9; + + x0 &= mask; + x1 &= mask; + x2 &= mask; + x3 &= mask; + x4 &= mask; + x5 &= mask; + x6 &= mask; + x7 &= mask; + x8 &= mask; + x9 &= mask; + + f[0] = f0 ^ x0; + f[1] = f1 ^ x1; + f[2] = f2 ^ x2; + f[3] = f3 ^ x3; + f[4] = f4 ^ x4; + f[5] = f5 ^ x5; + f[6] = f6 ^ x6; + f[7] = f7 ^ x7; + f[8] = f8 ^ x8; + f[9] = f9 ^ x9; + + g[0] = g0 ^ x0; + g[1] = g1 ^ x1; + g[2] = g2 ^ x2; + g[3] = g3 ^ x3; + g[4] = g4 ^ x4; + g[5] = g5 ^ x5; + g[6] = g6 ^ x6; + g[7] = g7 ^ x7; + g[8] = g8 ^ x8; + g[9] = g9 ^ x9; +} + +/* + h = f + */ + +static inline void +fe25519_copy(fe25519 h, const fe25519 f) +{ + memcpy(h, f, 10 * sizeof h[0]); +} + +/* + return 1 if f is in {1,3,5,...,q-2} + return 0 if f is in {0,2,4,...,q-1} + + Preconditions: + |f| bounded by 1.1*2^26,1.1*2^25,1.1*2^26,1.1*2^25,etc. + */ + +static inline int +fe25519_isnegative(const fe25519 f) +{ + unsigned char s[32]; + + fe25519_tobytes(s, f); + + return s[0] & 1; +} + +/* + return 1 if f == 0 + return 0 if f != 0 + + Preconditions: + |f| bounded by 1.1*2^26,1.1*2^25,1.1*2^26,1.1*2^25,etc. + */ + +static inline int +fe25519_iszero(const fe25519 f) +{ + unsigned char s[32]; + + fe25519_tobytes(s, f); + + return sodium_is_zero(s, 32); +} + +/* + h = f * g + Can overlap h with f or g. + * + Preconditions: + |f| bounded by 1.65*2^26,1.65*2^25,1.65*2^26,1.65*2^25,etc. + |g| bounded by 1.65*2^26,1.65*2^25,1.65*2^26,1.65*2^25,etc. + * + Postconditions: + |h| bounded by 1.01*2^25,1.01*2^24,1.01*2^25,1.01*2^24,etc. + */ + +/* + Notes on implementation strategy: + * + Using schoolbook multiplication. + Karatsuba would save a little in some cost models. + * + Most multiplications by 2 and 19 are 32-bit precomputations; + cheaper than 64-bit postcomputations. + * + There is one remaining multiplication by 19 in the carry chain; + one *19 precomputation can be merged into this, + but the resulting data flow is considerably less clean. + * + There are 12 carries below. + 10 of them are 2-way parallelizable and vectorizable. + Can get away with 11 carries, but then data flow is much deeper. + * + With tighter constraints on inputs can squeeze carries into int32. + */ + +static void +fe25519_mul(fe25519 h, const fe25519 f, const fe25519 g) +{ + int32_t f0 = f[0]; + int32_t f1 = f[1]; + int32_t f2 = f[2]; + int32_t f3 = f[3]; + int32_t f4 = f[4]; + int32_t f5 = f[5]; + int32_t f6 = f[6]; + int32_t f7 = f[7]; + int32_t f8 = f[8]; + int32_t f9 = f[9]; + + int32_t g0 = g[0]; + int32_t g1 = g[1]; + int32_t g2 = g[2]; + int32_t g3 = g[3]; + int32_t g4 = g[4]; + int32_t g5 = g[5]; + int32_t g6 = g[6]; + int32_t g7 = g[7]; + int32_t g8 = g[8]; + int32_t g9 = g[9]; + + int32_t g1_19 = 19 * g1; /* 1.959375*2^29 */ + int32_t g2_19 = 19 * g2; /* 1.959375*2^30; still ok */ + int32_t g3_19 = 19 * g3; + int32_t g4_19 = 19 * g4; + int32_t g5_19 = 19 * g5; + int32_t g6_19 = 19 * g6; + int32_t g7_19 = 19 * g7; + int32_t g8_19 = 19 * g8; + int32_t g9_19 = 19 * g9; + int32_t f1_2 = 2 * f1; + int32_t f3_2 = 2 * f3; + int32_t f5_2 = 2 * f5; + int32_t f7_2 = 2 * f7; + int32_t f9_2 = 2 * f9; + + int64_t f0g0 = f0 * (int64_t) g0; + int64_t f0g1 = f0 * (int64_t) g1; + int64_t f0g2 = f0 * (int64_t) g2; + int64_t f0g3 = f0 * (int64_t) g3; + int64_t f0g4 = f0 * (int64_t) g4; + int64_t f0g5 = f0 * (int64_t) g5; + int64_t f0g6 = f0 * (int64_t) g6; + int64_t f0g7 = f0 * (int64_t) g7; + int64_t f0g8 = f0 * (int64_t) g8; + int64_t f0g9 = f0 * (int64_t) g9; + int64_t f1g0 = f1 * (int64_t) g0; + int64_t f1g1_2 = f1_2 * (int64_t) g1; + int64_t f1g2 = f1 * (int64_t) g2; + int64_t f1g3_2 = f1_2 * (int64_t) g3; + int64_t f1g4 = f1 * (int64_t) g4; + int64_t f1g5_2 = f1_2 * (int64_t) g5; + int64_t f1g6 = f1 * (int64_t) g6; + int64_t f1g7_2 = f1_2 * (int64_t) g7; + int64_t f1g8 = f1 * (int64_t) g8; + int64_t f1g9_38 = f1_2 * (int64_t) g9_19; + int64_t f2g0 = f2 * (int64_t) g0; + int64_t f2g1 = f2 * (int64_t) g1; + int64_t f2g2 = f2 * (int64_t) g2; + int64_t f2g3 = f2 * (int64_t) g3; + int64_t f2g4 = f2 * (int64_t) g4; + int64_t f2g5 = f2 * (int64_t) g5; + int64_t f2g6 = f2 * (int64_t) g6; + int64_t f2g7 = f2 * (int64_t) g7; + int64_t f2g8_19 = f2 * (int64_t) g8_19; + int64_t f2g9_19 = f2 * (int64_t) g9_19; + int64_t f3g0 = f3 * (int64_t) g0; + int64_t f3g1_2 = f3_2 * (int64_t) g1; + int64_t f3g2 = f3 * (int64_t) g2; + int64_t f3g3_2 = f3_2 * (int64_t) g3; + int64_t f3g4 = f3 * (int64_t) g4; + int64_t f3g5_2 = f3_2 * (int64_t) g5; + int64_t f3g6 = f3 * (int64_t) g6; + int64_t f3g7_38 = f3_2 * (int64_t) g7_19; + int64_t f3g8_19 = f3 * (int64_t) g8_19; + int64_t f3g9_38 = f3_2 * (int64_t) g9_19; + int64_t f4g0 = f4 * (int64_t) g0; + int64_t f4g1 = f4 * (int64_t) g1; + int64_t f4g2 = f4 * (int64_t) g2; + int64_t f4g3 = f4 * (int64_t) g3; + int64_t f4g4 = f4 * (int64_t) g4; + int64_t f4g5 = f4 * (int64_t) g5; + int64_t f4g6_19 = f4 * (int64_t) g6_19; + int64_t f4g7_19 = f4 * (int64_t) g7_19; + int64_t f4g8_19 = f4 * (int64_t) g8_19; + int64_t f4g9_19 = f4 * (int64_t) g9_19; + int64_t f5g0 = f5 * (int64_t) g0; + int64_t f5g1_2 = f5_2 * (int64_t) g1; + int64_t f5g2 = f5 * (int64_t) g2; + int64_t f5g3_2 = f5_2 * (int64_t) g3; + int64_t f5g4 = f5 * (int64_t) g4; + int64_t f5g5_38 = f5_2 * (int64_t) g5_19; + int64_t f5g6_19 = f5 * (int64_t) g6_19; + int64_t f5g7_38 = f5_2 * (int64_t) g7_19; + int64_t f5g8_19 = f5 * (int64_t) g8_19; + int64_t f5g9_38 = f5_2 * (int64_t) g9_19; + int64_t f6g0 = f6 * (int64_t) g0; + int64_t f6g1 = f6 * (int64_t) g1; + int64_t f6g2 = f6 * (int64_t) g2; + int64_t f6g3 = f6 * (int64_t) g3; + int64_t f6g4_19 = f6 * (int64_t) g4_19; + int64_t f6g5_19 = f6 * (int64_t) g5_19; + int64_t f6g6_19 = f6 * (int64_t) g6_19; + int64_t f6g7_19 = f6 * (int64_t) g7_19; + int64_t f6g8_19 = f6 * (int64_t) g8_19; + int64_t f6g9_19 = f6 * (int64_t) g9_19; + int64_t f7g0 = f7 * (int64_t) g0; + int64_t f7g1_2 = f7_2 * (int64_t) g1; + int64_t f7g2 = f7 * (int64_t) g2; + int64_t f7g3_38 = f7_2 * (int64_t) g3_19; + int64_t f7g4_19 = f7 * (int64_t) g4_19; + int64_t f7g5_38 = f7_2 * (int64_t) g5_19; + int64_t f7g6_19 = f7 * (int64_t) g6_19; + int64_t f7g7_38 = f7_2 * (int64_t) g7_19; + int64_t f7g8_19 = f7 * (int64_t) g8_19; + int64_t f7g9_38 = f7_2 * (int64_t) g9_19; + int64_t f8g0 = f8 * (int64_t) g0; + int64_t f8g1 = f8 * (int64_t) g1; + int64_t f8g2_19 = f8 * (int64_t) g2_19; + int64_t f8g3_19 = f8 * (int64_t) g3_19; + int64_t f8g4_19 = f8 * (int64_t) g4_19; + int64_t f8g5_19 = f8 * (int64_t) g5_19; + int64_t f8g6_19 = f8 * (int64_t) g6_19; + int64_t f8g7_19 = f8 * (int64_t) g7_19; + int64_t f8g8_19 = f8 * (int64_t) g8_19; + int64_t f8g9_19 = f8 * (int64_t) g9_19; + int64_t f9g0 = f9 * (int64_t) g0; + int64_t f9g1_38 = f9_2 * (int64_t) g1_19; + int64_t f9g2_19 = f9 * (int64_t) g2_19; + int64_t f9g3_38 = f9_2 * (int64_t) g3_19; + int64_t f9g4_19 = f9 * (int64_t) g4_19; + int64_t f9g5_38 = f9_2 * (int64_t) g5_19; + int64_t f9g6_19 = f9 * (int64_t) g6_19; + int64_t f9g7_38 = f9_2 * (int64_t) g7_19; + int64_t f9g8_19 = f9 * (int64_t) g8_19; + int64_t f9g9_38 = f9_2 * (int64_t) g9_19; + + int64_t h0 = f0g0 + f1g9_38 + f2g8_19 + f3g7_38 + f4g6_19 + f5g5_38 + + f6g4_19 + f7g3_38 + f8g2_19 + f9g1_38; + int64_t h1 = f0g1 + f1g0 + f2g9_19 + f3g8_19 + f4g7_19 + f5g6_19 + f6g5_19 + + f7g4_19 + f8g3_19 + f9g2_19; + int64_t h2 = f0g2 + f1g1_2 + f2g0 + f3g9_38 + f4g8_19 + f5g7_38 + f6g6_19 + + f7g5_38 + f8g4_19 + f9g3_38; + int64_t h3 = f0g3 + f1g2 + f2g1 + f3g0 + f4g9_19 + f5g8_19 + f6g7_19 + + f7g6_19 + f8g5_19 + f9g4_19; + int64_t h4 = f0g4 + f1g3_2 + f2g2 + f3g1_2 + f4g0 + f5g9_38 + f6g8_19 + + f7g7_38 + f8g6_19 + f9g5_38; + int64_t h5 = f0g5 + f1g4 + f2g3 + f3g2 + f4g1 + f5g0 + f6g9_19 + f7g8_19 + + f8g7_19 + f9g6_19; + int64_t h6 = f0g6 + f1g5_2 + f2g4 + f3g3_2 + f4g2 + f5g1_2 + f6g0 + + f7g9_38 + f8g8_19 + f9g7_38; + int64_t h7 = f0g7 + f1g6 + f2g5 + f3g4 + f4g3 + f5g2 + f6g1 + f7g0 + + f8g9_19 + f9g8_19; + int64_t h8 = f0g8 + f1g7_2 + f2g6 + f3g5_2 + f4g4 + f5g3_2 + f6g2 + f7g1_2 + + f8g0 + f9g9_38; + int64_t h9 = + f0g9 + f1g8 + f2g7 + f3g6 + f4g5 + f5g4 + f6g3 + f7g2 + f8g1 + f9g0; + + int64_t carry0; + int64_t carry1; + int64_t carry2; + int64_t carry3; + int64_t carry4; + int64_t carry5; + int64_t carry6; + int64_t carry7; + int64_t carry8; + int64_t carry9; + + /* + |h0| <= (1.65*1.65*2^52*(1+19+19+19+19)+1.65*1.65*2^50*(38+38+38+38+38)) + i.e. |h0| <= 1.4*2^60; narrower ranges for h2, h4, h6, h8 + |h1| <= (1.65*1.65*2^51*(1+1+19+19+19+19+19+19+19+19)) + i.e. |h1| <= 1.7*2^59; narrower ranges for h3, h5, h7, h9 + */ + + carry0 = (h0 + (int64_t)(1L << 25)) >> 26; + h1 += carry0; + h0 -= carry0 * ((uint64_t) 1L << 26); + carry4 = (h4 + (int64_t)(1L << 25)) >> 26; + h5 += carry4; + h4 -= carry4 * ((uint64_t) 1L << 26); + /* |h0| <= 2^25 */ + /* |h4| <= 2^25 */ + /* |h1| <= 1.71*2^59 */ + /* |h5| <= 1.71*2^59 */ + + carry1 = (h1 + (int64_t)(1L << 24)) >> 25; + h2 += carry1; + h1 -= carry1 * ((uint64_t) 1L << 25); + carry5 = (h5 + (int64_t)(1L << 24)) >> 25; + h6 += carry5; + h5 -= carry5 * ((uint64_t) 1L << 25); + /* |h1| <= 2^24; from now on fits into int32 */ + /* |h5| <= 2^24; from now on fits into int32 */ + /* |h2| <= 1.41*2^60 */ + /* |h6| <= 1.41*2^60 */ + + carry2 = (h2 + (int64_t)(1L << 25)) >> 26; + h3 += carry2; + h2 -= carry2 * ((uint64_t) 1L << 26); + carry6 = (h6 + (int64_t)(1L << 25)) >> 26; + h7 += carry6; + h6 -= carry6 * ((uint64_t) 1L << 26); + /* |h2| <= 2^25; from now on fits into int32 unchanged */ + /* |h6| <= 2^25; from now on fits into int32 unchanged */ + /* |h3| <= 1.71*2^59 */ + /* |h7| <= 1.71*2^59 */ + + carry3 = (h3 + (int64_t)(1L << 24)) >> 25; + h4 += carry3; + h3 -= carry3 * ((uint64_t) 1L << 25); + carry7 = (h7 + (int64_t)(1L << 24)) >> 25; + h8 += carry7; + h7 -= carry7 * ((uint64_t) 1L << 25); + /* |h3| <= 2^24; from now on fits into int32 unchanged */ + /* |h7| <= 2^24; from now on fits into int32 unchanged */ + /* |h4| <= 1.72*2^34 */ + /* |h8| <= 1.41*2^60 */ + + carry4 = (h4 + (int64_t)(1L << 25)) >> 26; + h5 += carry4; + h4 -= carry4 * ((uint64_t) 1L << 26); + carry8 = (h8 + (int64_t)(1L << 25)) >> 26; + h9 += carry8; + h8 -= carry8 * ((uint64_t) 1L << 26); + /* |h4| <= 2^25; from now on fits into int32 unchanged */ + /* |h8| <= 2^25; from now on fits into int32 unchanged */ + /* |h5| <= 1.01*2^24 */ + /* |h9| <= 1.71*2^59 */ + + carry9 = (h9 + (int64_t)(1L << 24)) >> 25; + h0 += carry9 * 19; + h9 -= carry9 * ((uint64_t) 1L << 25); + /* |h9| <= 2^24; from now on fits into int32 unchanged */ + /* |h0| <= 1.1*2^39 */ + + carry0 = (h0 + (int64_t)(1L << 25)) >> 26; + h1 += carry0; + h0 -= carry0 * ((uint64_t) 1L << 26); + /* |h0| <= 2^25; from now on fits into int32 unchanged */ + /* |h1| <= 1.01*2^24 */ + + h[0] = (int32_t) h0; + h[1] = (int32_t) h1; + h[2] = (int32_t) h2; + h[3] = (int32_t) h3; + h[4] = (int32_t) h4; + h[5] = (int32_t) h5; + h[6] = (int32_t) h6; + h[7] = (int32_t) h7; + h[8] = (int32_t) h8; + h[9] = (int32_t) h9; +} + +/* + h = f * f + Can overlap h with f. + * + Preconditions: + |f| bounded by 1.65*2^26,1.65*2^25,1.65*2^26,1.65*2^25,etc. + * + Postconditions: + |h| bounded by 1.01*2^25,1.01*2^24,1.01*2^25,1.01*2^24,etc. + */ + +static void +fe25519_sq(fe25519 h, const fe25519 f) +{ + int32_t f0 = f[0]; + int32_t f1 = f[1]; + int32_t f2 = f[2]; + int32_t f3 = f[3]; + int32_t f4 = f[4]; + int32_t f5 = f[5]; + int32_t f6 = f[6]; + int32_t f7 = f[7]; + int32_t f8 = f[8]; + int32_t f9 = f[9]; + + int32_t f0_2 = 2 * f0; + int32_t f1_2 = 2 * f1; + int32_t f2_2 = 2 * f2; + int32_t f3_2 = 2 * f3; + int32_t f4_2 = 2 * f4; + int32_t f5_2 = 2 * f5; + int32_t f6_2 = 2 * f6; + int32_t f7_2 = 2 * f7; + int32_t f5_38 = 38 * f5; /* 1.959375*2^30 */ + int32_t f6_19 = 19 * f6; /* 1.959375*2^30 */ + int32_t f7_38 = 38 * f7; /* 1.959375*2^30 */ + int32_t f8_19 = 19 * f8; /* 1.959375*2^30 */ + int32_t f9_38 = 38 * f9; /* 1.959375*2^30 */ + + int64_t f0f0 = f0 * (int64_t) f0; + int64_t f0f1_2 = f0_2 * (int64_t) f1; + int64_t f0f2_2 = f0_2 * (int64_t) f2; + int64_t f0f3_2 = f0_2 * (int64_t) f3; + int64_t f0f4_2 = f0_2 * (int64_t) f4; + int64_t f0f5_2 = f0_2 * (int64_t) f5; + int64_t f0f6_2 = f0_2 * (int64_t) f6; + int64_t f0f7_2 = f0_2 * (int64_t) f7; + int64_t f0f8_2 = f0_2 * (int64_t) f8; + int64_t f0f9_2 = f0_2 * (int64_t) f9; + int64_t f1f1_2 = f1_2 * (int64_t) f1; + int64_t f1f2_2 = f1_2 * (int64_t) f2; + int64_t f1f3_4 = f1_2 * (int64_t) f3_2; + int64_t f1f4_2 = f1_2 * (int64_t) f4; + int64_t f1f5_4 = f1_2 * (int64_t) f5_2; + int64_t f1f6_2 = f1_2 * (int64_t) f6; + int64_t f1f7_4 = f1_2 * (int64_t) f7_2; + int64_t f1f8_2 = f1_2 * (int64_t) f8; + int64_t f1f9_76 = f1_2 * (int64_t) f9_38; + int64_t f2f2 = f2 * (int64_t) f2; + int64_t f2f3_2 = f2_2 * (int64_t) f3; + int64_t f2f4_2 = f2_2 * (int64_t) f4; + int64_t f2f5_2 = f2_2 * (int64_t) f5; + int64_t f2f6_2 = f2_2 * (int64_t) f6; + int64_t f2f7_2 = f2_2 * (int64_t) f7; + int64_t f2f8_38 = f2_2 * (int64_t) f8_19; + int64_t f2f9_38 = f2 * (int64_t) f9_38; + int64_t f3f3_2 = f3_2 * (int64_t) f3; + int64_t f3f4_2 = f3_2 * (int64_t) f4; + int64_t f3f5_4 = f3_2 * (int64_t) f5_2; + int64_t f3f6_2 = f3_2 * (int64_t) f6; + int64_t f3f7_76 = f3_2 * (int64_t) f7_38; + int64_t f3f8_38 = f3_2 * (int64_t) f8_19; + int64_t f3f9_76 = f3_2 * (int64_t) f9_38; + int64_t f4f4 = f4 * (int64_t) f4; + int64_t f4f5_2 = f4_2 * (int64_t) f5; + int64_t f4f6_38 = f4_2 * (int64_t) f6_19; + int64_t f4f7_38 = f4 * (int64_t) f7_38; + int64_t f4f8_38 = f4_2 * (int64_t) f8_19; + int64_t f4f9_38 = f4 * (int64_t) f9_38; + int64_t f5f5_38 = f5 * (int64_t) f5_38; + int64_t f5f6_38 = f5_2 * (int64_t) f6_19; + int64_t f5f7_76 = f5_2 * (int64_t) f7_38; + int64_t f5f8_38 = f5_2 * (int64_t) f8_19; + int64_t f5f9_76 = f5_2 * (int64_t) f9_38; + int64_t f6f6_19 = f6 * (int64_t) f6_19; + int64_t f6f7_38 = f6 * (int64_t) f7_38; + int64_t f6f8_38 = f6_2 * (int64_t) f8_19; + int64_t f6f9_38 = f6 * (int64_t) f9_38; + int64_t f7f7_38 = f7 * (int64_t) f7_38; + int64_t f7f8_38 = f7_2 * (int64_t) f8_19; + int64_t f7f9_76 = f7_2 * (int64_t) f9_38; + int64_t f8f8_19 = f8 * (int64_t) f8_19; + int64_t f8f9_38 = f8 * (int64_t) f9_38; + int64_t f9f9_38 = f9 * (int64_t) f9_38; + + int64_t h0 = f0f0 + f1f9_76 + f2f8_38 + f3f7_76 + f4f6_38 + f5f5_38; + int64_t h1 = f0f1_2 + f2f9_38 + f3f8_38 + f4f7_38 + f5f6_38; + int64_t h2 = f0f2_2 + f1f1_2 + f3f9_76 + f4f8_38 + f5f7_76 + f6f6_19; + int64_t h3 = f0f3_2 + f1f2_2 + f4f9_38 + f5f8_38 + f6f7_38; + int64_t h4 = f0f4_2 + f1f3_4 + f2f2 + f5f9_76 + f6f8_38 + f7f7_38; + int64_t h5 = f0f5_2 + f1f4_2 + f2f3_2 + f6f9_38 + f7f8_38; + int64_t h6 = f0f6_2 + f1f5_4 + f2f4_2 + f3f3_2 + f7f9_76 + f8f8_19; + int64_t h7 = f0f7_2 + f1f6_2 + f2f5_2 + f3f4_2 + f8f9_38; + int64_t h8 = f0f8_2 + f1f7_4 + f2f6_2 + f3f5_4 + f4f4 + f9f9_38; + int64_t h9 = f0f9_2 + f1f8_2 + f2f7_2 + f3f6_2 + f4f5_2; + + int64_t carry0; + int64_t carry1; + int64_t carry2; + int64_t carry3; + int64_t carry4; + int64_t carry5; + int64_t carry6; + int64_t carry7; + int64_t carry8; + int64_t carry9; + + carry0 = (h0 + (int64_t)(1L << 25)) >> 26; + h1 += carry0; + h0 -= carry0 * ((uint64_t) 1L << 26); + carry4 = (h4 + (int64_t)(1L << 25)) >> 26; + h5 += carry4; + h4 -= carry4 * ((uint64_t) 1L << 26); + + carry1 = (h1 + (int64_t)(1L << 24)) >> 25; + h2 += carry1; + h1 -= carry1 * ((uint64_t) 1L << 25); + carry5 = (h5 + (int64_t)(1L << 24)) >> 25; + h6 += carry5; + h5 -= carry5 * ((uint64_t) 1L << 25); + + carry2 = (h2 + (int64_t)(1L << 25)) >> 26; + h3 += carry2; + h2 -= carry2 * ((uint64_t) 1L << 26); + carry6 = (h6 + (int64_t)(1L << 25)) >> 26; + h7 += carry6; + h6 -= carry6 * ((uint64_t) 1L << 26); + + carry3 = (h3 + (int64_t)(1L << 24)) >> 25; + h4 += carry3; + h3 -= carry3 * ((uint64_t) 1L << 25); + carry7 = (h7 + (int64_t)(1L << 24)) >> 25; + h8 += carry7; + h7 -= carry7 * ((uint64_t) 1L << 25); + + carry4 = (h4 + (int64_t)(1L << 25)) >> 26; + h5 += carry4; + h4 -= carry4 * ((uint64_t) 1L << 26); + carry8 = (h8 + (int64_t)(1L << 25)) >> 26; + h9 += carry8; + h8 -= carry8 * ((uint64_t) 1L << 26); + + carry9 = (h9 + (int64_t)(1L << 24)) >> 25; + h0 += carry9 * 19; + h9 -= carry9 * ((uint64_t) 1L << 25); + + carry0 = (h0 + (int64_t)(1L << 25)) >> 26; + h1 += carry0; + h0 -= carry0 * ((uint64_t) 1L << 26); + + h[0] = (int32_t) h0; + h[1] = (int32_t) h1; + h[2] = (int32_t) h2; + h[3] = (int32_t) h3; + h[4] = (int32_t) h4; + h[5] = (int32_t) h5; + h[6] = (int32_t) h6; + h[7] = (int32_t) h7; + h[8] = (int32_t) h8; + h[9] = (int32_t) h9; +} + +/* + h = 2 * f * f + Can overlap h with f. + * + Preconditions: + |f| bounded by 1.65*2^26,1.65*2^25,1.65*2^26,1.65*2^25,etc. + * + Postconditions: + |h| bounded by 1.01*2^25,1.01*2^24,1.01*2^25,1.01*2^24,etc. + */ + +static void +fe25519_sq2(fe25519 h, const fe25519 f) +{ + int32_t f0 = f[0]; + int32_t f1 = f[1]; + int32_t f2 = f[2]; + int32_t f3 = f[3]; + int32_t f4 = f[4]; + int32_t f5 = f[5]; + int32_t f6 = f[6]; + int32_t f7 = f[7]; + int32_t f8 = f[8]; + int32_t f9 = f[9]; + + int32_t f0_2 = 2 * f0; + int32_t f1_2 = 2 * f1; + int32_t f2_2 = 2 * f2; + int32_t f3_2 = 2 * f3; + int32_t f4_2 = 2 * f4; + int32_t f5_2 = 2 * f5; + int32_t f6_2 = 2 * f6; + int32_t f7_2 = 2 * f7; + int32_t f5_38 = 38 * f5; /* 1.959375*2^30 */ + int32_t f6_19 = 19 * f6; /* 1.959375*2^30 */ + int32_t f7_38 = 38 * f7; /* 1.959375*2^30 */ + int32_t f8_19 = 19 * f8; /* 1.959375*2^30 */ + int32_t f9_38 = 38 * f9; /* 1.959375*2^30 */ + + int64_t f0f0 = f0 * (int64_t) f0; + int64_t f0f1_2 = f0_2 * (int64_t) f1; + int64_t f0f2_2 = f0_2 * (int64_t) f2; + int64_t f0f3_2 = f0_2 * (int64_t) f3; + int64_t f0f4_2 = f0_2 * (int64_t) f4; + int64_t f0f5_2 = f0_2 * (int64_t) f5; + int64_t f0f6_2 = f0_2 * (int64_t) f6; + int64_t f0f7_2 = f0_2 * (int64_t) f7; + int64_t f0f8_2 = f0_2 * (int64_t) f8; + int64_t f0f9_2 = f0_2 * (int64_t) f9; + int64_t f1f1_2 = f1_2 * (int64_t) f1; + int64_t f1f2_2 = f1_2 * (int64_t) f2; + int64_t f1f3_4 = f1_2 * (int64_t) f3_2; + int64_t f1f4_2 = f1_2 * (int64_t) f4; + int64_t f1f5_4 = f1_2 * (int64_t) f5_2; + int64_t f1f6_2 = f1_2 * (int64_t) f6; + int64_t f1f7_4 = f1_2 * (int64_t) f7_2; + int64_t f1f8_2 = f1_2 * (int64_t) f8; + int64_t f1f9_76 = f1_2 * (int64_t) f9_38; + int64_t f2f2 = f2 * (int64_t) f2; + int64_t f2f3_2 = f2_2 * (int64_t) f3; + int64_t f2f4_2 = f2_2 * (int64_t) f4; + int64_t f2f5_2 = f2_2 * (int64_t) f5; + int64_t f2f6_2 = f2_2 * (int64_t) f6; + int64_t f2f7_2 = f2_2 * (int64_t) f7; + int64_t f2f8_38 = f2_2 * (int64_t) f8_19; + int64_t f2f9_38 = f2 * (int64_t) f9_38; + int64_t f3f3_2 = f3_2 * (int64_t) f3; + int64_t f3f4_2 = f3_2 * (int64_t) f4; + int64_t f3f5_4 = f3_2 * (int64_t) f5_2; + int64_t f3f6_2 = f3_2 * (int64_t) f6; + int64_t f3f7_76 = f3_2 * (int64_t) f7_38; + int64_t f3f8_38 = f3_2 * (int64_t) f8_19; + int64_t f3f9_76 = f3_2 * (int64_t) f9_38; + int64_t f4f4 = f4 * (int64_t) f4; + int64_t f4f5_2 = f4_2 * (int64_t) f5; + int64_t f4f6_38 = f4_2 * (int64_t) f6_19; + int64_t f4f7_38 = f4 * (int64_t) f7_38; + int64_t f4f8_38 = f4_2 * (int64_t) f8_19; + int64_t f4f9_38 = f4 * (int64_t) f9_38; + int64_t f5f5_38 = f5 * (int64_t) f5_38; + int64_t f5f6_38 = f5_2 * (int64_t) f6_19; + int64_t f5f7_76 = f5_2 * (int64_t) f7_38; + int64_t f5f8_38 = f5_2 * (int64_t) f8_19; + int64_t f5f9_76 = f5_2 * (int64_t) f9_38; + int64_t f6f6_19 = f6 * (int64_t) f6_19; + int64_t f6f7_38 = f6 * (int64_t) f7_38; + int64_t f6f8_38 = f6_2 * (int64_t) f8_19; + int64_t f6f9_38 = f6 * (int64_t) f9_38; + int64_t f7f7_38 = f7 * (int64_t) f7_38; + int64_t f7f8_38 = f7_2 * (int64_t) f8_19; + int64_t f7f9_76 = f7_2 * (int64_t) f9_38; + int64_t f8f8_19 = f8 * (int64_t) f8_19; + int64_t f8f9_38 = f8 * (int64_t) f9_38; + int64_t f9f9_38 = f9 * (int64_t) f9_38; + + int64_t h0 = f0f0 + f1f9_76 + f2f8_38 + f3f7_76 + f4f6_38 + f5f5_38; + int64_t h1 = f0f1_2 + f2f9_38 + f3f8_38 + f4f7_38 + f5f6_38; + int64_t h2 = f0f2_2 + f1f1_2 + f3f9_76 + f4f8_38 + f5f7_76 + f6f6_19; + int64_t h3 = f0f3_2 + f1f2_2 + f4f9_38 + f5f8_38 + f6f7_38; + int64_t h4 = f0f4_2 + f1f3_4 + f2f2 + f5f9_76 + f6f8_38 + f7f7_38; + int64_t h5 = f0f5_2 + f1f4_2 + f2f3_2 + f6f9_38 + f7f8_38; + int64_t h6 = f0f6_2 + f1f5_4 + f2f4_2 + f3f3_2 + f7f9_76 + f8f8_19; + int64_t h7 = f0f7_2 + f1f6_2 + f2f5_2 + f3f4_2 + f8f9_38; + int64_t h8 = f0f8_2 + f1f7_4 + f2f6_2 + f3f5_4 + f4f4 + f9f9_38; + int64_t h9 = f0f9_2 + f1f8_2 + f2f7_2 + f3f6_2 + f4f5_2; + + int64_t carry0; + int64_t carry1; + int64_t carry2; + int64_t carry3; + int64_t carry4; + int64_t carry5; + int64_t carry6; + int64_t carry7; + int64_t carry8; + int64_t carry9; + + h0 += h0; + h1 += h1; + h2 += h2; + h3 += h3; + h4 += h4; + h5 += h5; + h6 += h6; + h7 += h7; + h8 += h8; + h9 += h9; + + carry0 = (h0 + (int64_t)(1L << 25)) >> 26; + h1 += carry0; + h0 -= carry0 * ((uint64_t) 1L << 26); + carry4 = (h4 + (int64_t)(1L << 25)) >> 26; + h5 += carry4; + h4 -= carry4 * ((uint64_t) 1L << 26); + + carry1 = (h1 + (int64_t)(1L << 24)) >> 25; + h2 += carry1; + h1 -= carry1 * ((uint64_t) 1L << 25); + carry5 = (h5 + (int64_t)(1L << 24)) >> 25; + h6 += carry5; + h5 -= carry5 * ((uint64_t) 1L << 25); + + carry2 = (h2 + (int64_t)(1L << 25)) >> 26; + h3 += carry2; + h2 -= carry2 * ((uint64_t) 1L << 26); + carry6 = (h6 + (int64_t)(1L << 25)) >> 26; + h7 += carry6; + h6 -= carry6 * ((uint64_t) 1L << 26); + + carry3 = (h3 + (int64_t)(1L << 24)) >> 25; + h4 += carry3; + h3 -= carry3 * ((uint64_t) 1L << 25); + carry7 = (h7 + (int64_t)(1L << 24)) >> 25; + h8 += carry7; + h7 -= carry7 * ((uint64_t) 1L << 25); + + carry4 = (h4 + (int64_t)(1L << 25)) >> 26; + h5 += carry4; + h4 -= carry4 * ((uint64_t) 1L << 26); + carry8 = (h8 + (int64_t)(1L << 25)) >> 26; + h9 += carry8; + h8 -= carry8 * ((uint64_t) 1L << 26); + + carry9 = (h9 + (int64_t)(1L << 24)) >> 25; + h0 += carry9 * 19; + h9 -= carry9 * ((uint64_t) 1L << 25); + + carry0 = (h0 + (int64_t)(1L << 25)) >> 26; + h1 += carry0; + h0 -= carry0 * ((uint64_t) 1L << 26); + + h[0] = (int32_t) h0; + h[1] = (int32_t) h1; + h[2] = (int32_t) h2; + h[3] = (int32_t) h3; + h[4] = (int32_t) h4; + h[5] = (int32_t) h5; + h[6] = (int32_t) h6; + h[7] = (int32_t) h7; + h[8] = (int32_t) h8; + h[9] = (int32_t) h9; +} + +static inline void +fe25519_mul32(fe25519 h, const fe25519 f, uint32_t n) +{ + int64_t sn = (int64_t) n; + int32_t f0 = f[0]; + int32_t f1 = f[1]; + int32_t f2 = f[2]; + int32_t f3 = f[3]; + int32_t f4 = f[4]; + int32_t f5 = f[5]; + int32_t f6 = f[6]; + int32_t f7 = f[7]; + int32_t f8 = f[8]; + int32_t f9 = f[9]; + int64_t h0 = f0 * sn; + int64_t h1 = f1 * sn; + int64_t h2 = f2 * sn; + int64_t h3 = f3 * sn; + int64_t h4 = f4 * sn; + int64_t h5 = f5 * sn; + int64_t h6 = f6 * sn; + int64_t h7 = f7 * sn; + int64_t h8 = f8 * sn; + int64_t h9 = f9 * sn; + int64_t carry0, carry1, carry2, carry3, carry4, carry5, carry6, carry7, + carry8, carry9; + + carry9 = (h9 + ((int64_t) 1 << 24)) >> 25; + h0 += carry9 * 19; + h9 -= carry9 * ((int64_t) 1 << 25); + carry1 = (h1 + ((int64_t) 1 << 24)) >> 25; + h2 += carry1; + h1 -= carry1 * ((int64_t) 1 << 25); + carry3 = (h3 + ((int64_t) 1 << 24)) >> 25; + h4 += carry3; + h3 -= carry3 * ((int64_t) 1 << 25); + carry5 = (h5 + ((int64_t) 1 << 24)) >> 25; + h6 += carry5; + h5 -= carry5 * ((int64_t) 1 << 25); + carry7 = (h7 + ((int64_t) 1 << 24)) >> 25; + h8 += carry7; + h7 -= carry7 * ((int64_t) 1 << 25); + + carry0 = (h0 + ((int64_t) 1 << 25)) >> 26; + h1 += carry0; + h0 -= carry0 * ((int64_t) 1 << 26); + carry2 = (h2 + ((int64_t) 1 << 25)) >> 26; + h3 += carry2; + h2 -= carry2 * ((int64_t) 1 << 26); + carry4 = (h4 + ((int64_t) 1 << 25)) >> 26; + h5 += carry4; + h4 -= carry4 * ((int64_t) 1 << 26); + carry6 = (h6 + ((int64_t) 1 << 25)) >> 26; + h7 += carry6; + h6 -= carry6 * ((int64_t) 1 << 26); + carry8 = (h8 + ((int64_t) 1 << 25)) >> 26; + h9 += carry8; + h8 -= carry8 * ((int64_t) 1 << 26); + + h[0] = (int32_t) h0; + h[1] = (int32_t) h1; + h[2] = (int32_t) h2; + h[3] = (int32_t) h3; + h[4] = (int32_t) h4; + h[5] = (int32_t) h5; + h[6] = (int32_t) h6; + h[7] = (int32_t) h7; + h[8] = (int32_t) h8; + h[9] = (int32_t) h9; +} diff --git a/packages/runtime/vendor/libsodium/export.h b/packages/runtime/vendor/libsodium/export.h new file mode 100644 index 000000000..a0074fc9c --- /dev/null +++ b/packages/runtime/vendor/libsodium/export.h @@ -0,0 +1,57 @@ + +#ifndef sodium_export_H +#define sodium_export_H + +#include +#include +#include + +#if !defined(__clang__) && !defined(__GNUC__) +# ifdef __attribute__ +# undef __attribute__ +# endif +# define __attribute__(a) +#endif + +#ifdef SODIUM_STATIC +# define SODIUM_EXPORT +# define SODIUM_EXPORT_WEAK +#else +# if defined(_MSC_VER) +# ifdef SODIUM_DLL_EXPORT +# define SODIUM_EXPORT __declspec(dllexport) +# else +# define SODIUM_EXPORT __declspec(dllimport) +# endif +# else +# if defined(__SUNPRO_C) +# ifndef __GNU_C__ +# define SODIUM_EXPORT __attribute__ (visibility(__global)) +# else +# define SODIUM_EXPORT __attribute__ __global +# endif +# elif defined(_MSG_VER) +# define SODIUM_EXPORT extern __declspec(dllexport) +# else +# define SODIUM_EXPORT __attribute__ ((visibility ("default"))) +# endif +# endif +# if defined(__ELF__) && !defined(SODIUM_DISABLE_WEAK_FUNCTIONS) +# define SODIUM_EXPORT_WEAK SODIUM_EXPORT __attribute__((weak)) +# else +# define SODIUM_EXPORT_WEAK SODIUM_EXPORT +# endif +#endif + +#ifndef CRYPTO_ALIGN +# if defined(__INTEL_COMPILER) || defined(_MSC_VER) +# define CRYPTO_ALIGN(x) __declspec(align(x)) +# else +# define CRYPTO_ALIGN(x) __attribute__ ((aligned(x))) +# endif +#endif + +#define SODIUM_MIN(A, B) ((A) < (B) ? (A) : (B)) +#define SODIUM_SIZE_MAX SODIUM_MIN(UINT64_MAX, SIZE_MAX) + +#endif diff --git a/packages/runtime/vendor/libsodium/fe_25_5/base.h b/packages/runtime/vendor/libsodium/fe_25_5/base.h new file mode 100644 index 000000000..e18530bbb --- /dev/null +++ b/packages/runtime/vendor/libsodium/fe_25_5/base.h @@ -0,0 +1,1344 @@ +{ /* 0/31 */ + { + { 25967493, -14356035, 29566456, 3660896, -12694345, 4014787, 27544626, -11754271, -6079156, 2047605 }, + { -12545711, 934262, -2722910, 3049990, -727428, 9406986, 12720692, 5043384, 19500929, -15469378 }, + { -8738181, 4489570, 9688441, -14785194, 10184609, -12363380, 29287919, 11864899, -24514362, -4438546 } + }, + { + { -12815894, -12976347, -21581243, 11784320, -25355658, -2750717, -11717903, -3814571, -358445, -10211303 }, + { -21703237, 6903825, 27185491, 6451973, -29577724, -9554005, -15616551, 11189268, -26829678, -5319081 }, + { 26966642, 11152617, 32442495, 15396054, 14353839, -12752335, -3128826, -9541118, -15472047, -4166697 } + }, + { + { 15636291, -9688557, 24204773, -7912398, 616977, -16685262, 27787600, -14772189, 28944400, -1550024 }, + { 16568933, 4717097, -11556148, -1102322, 15682896, -11807043, 16354577, -11775962, 7689662, 11199574 }, + { 30464156, -5976125, -11779434, -15670865, 23220365, 15915852, 7512774, 10017326, -17749093, -9920357 } + }, + { + { -17036878, 13921892, 10945806, -6033431, 27105052, -16084379, -28926210, 15006023, 3284568, -6276540 }, + { 23599295, -8306047, -11193664, -7687416, 13236774, 10506355, 7464579, 9656445, 13059162, 10374397 }, + { 7798556, 16710257, 3033922, 2874086, 28997861, 2835604, 32406664, -3839045, -641708, -101325 } + }, + { + { 10861363, 11473154, 27284546, 1981175, -30064349, 12577861, 32867885, 14515107, -15438304, 10819380 }, + { 4708026, 6336745, 20377586, 9066809, -11272109, 6594696, -25653668, 12483688, -12668491, 5581306 }, + { 19563160, 16186464, -29386857, 4097519, 10237984, -4348115, 28542350, 13850243, -23678021, -15815942 } + }, + { + { -15371964, -12862754, 32573250, 4720197, -26436522, 5875511, -19188627, -15224819, -9818940, -12085777 }, + { -8549212, 109983, 15149363, 2178705, 22900618, 4543417, 3044240, -15689887, 1762328, 14866737 }, + { -18199695, -15951423, -10473290, 1707278, -17185920, 3916101, -28236412, 3959421, 27914454, 4383652 } + }, + { + { 5153746, 9909285, 1723747, -2777874, 30523605, 5516873, 19480852, 5230134, -23952439, -15175766 }, + { -30269007, -3463509, 7665486, 10083793, 28475525, 1649722, 20654025, 16520125, 30598449, 7715701 }, + { 28881845, 14381568, 9657904, 3680757, -20181635, 7843316, -31400660, 1370708, 29794553, -1409300 } + }, + { + { 14499471, -2729599, -33191113, -4254652, 28494862, 14271267, 30290735, 10876454, -33154098, 2381726 }, + { -7195431, -2655363, -14730155, 462251, -27724326, 3941372, -6236617, 3696005, -32300832, 15351955 }, + { 27431194, 8222322, 16448760, -3907995, -18707002, 11938355, -32961401, -2970515, 29551813, 10109425 } + } +}, +{ /* 1/31 */ + { + { -13657040, -13155431, -31283750, 11777098, 21447386, 6519384, -2378284, -1627556, 10092783, -4764171 }, + { 27939166, 14210322, 4677035, 16277044, -22964462, -12398139, -32508754, 12005538, -17810127, 12803510 }, + { 17228999, -15661624, -1233527, 300140, -1224870, -11714777, 30364213, -9038194, 18016357, 4397660 } + }, + { + { -10958843, -7690207, 4776341, -14954238, 27850028, -15602212, -26619106, 14544525, -17477504, 982639 }, + { 29253598, 15796703, -2863982, -9908884, 10057023, 3163536, 7332899, -4120128, -21047696, 9934963 }, + { 5793303, 16271923, -24131614, -10116404, 29188560, 1206517, -14747930, 4559895, -30123922, -10897950 } + }, + { + { -27643952, -11493006, 16282657, -11036493, 28414021, -15012264, 24191034, 4541697, -13338309, 5500568 }, + { 12650548, -1497113, 9052871, 11355358, -17680037, -8400164, -17430592, 12264343, 10874051, 13524335 }, + { 25556948, -3045990, 714651, 2510400, 23394682, -10415330, 33119038, 5080568, -22528059, 5376628 } + }, + { + { -26088264, -4011052, -17013699, -3537628, -6726793, 1920897, -22321305, -9447443, 4535768, 1569007 }, + { -2255422, 14606630, -21692440, -8039818, 28430649, 8775819, -30494562, 3044290, 31848280, 12543772 }, + { -22028579, 2943893, -31857513, 6777306, 13784462, -4292203, -27377195, -2062731, 7718482, 14474653 } + }, + { + { 2385315, 2454213, -22631320, 46603, -4437935, -15680415, 656965, -7236665, 24316168, -5253567 }, + { 13741529, 10911568, -33233417, -8603737, -20177830, -1033297, 33040651, -13424532, -20729456, 8321686 }, + { 21060490, -2212744, 15712757, -4336099, 1639040, 10656336, 23845965, -11874838, -9984458, 608372 } + }, + { + { -13672732, -15087586, -10889693, -7557059, -6036909, 11305547, 1123968, -6780577, 27229399, 23887 }, + { -23244140, -294205, -11744728, 14712571, -29465699, -2029617, 12797024, -6440308, -1633405, 16678954 }, + { -29500620, 4770662, -16054387, 14001338, 7830047, 9564805, -1508144, -4795045, -17169265, 4904953 } + }, + { + { 24059557, 14617003, 19037157, -15039908, 19766093, -14906429, 5169211, 16191880, 2128236, -4326833 }, + { -16981152, 4124966, -8540610, -10653797, 30336522, -14105247, -29806336, 916033, -6882542, -2986532 }, + { -22630907, 12419372, -7134229, -7473371, -16478904, 16739175, 285431, 2763829, 15736322, 4143876 } + }, + { + { 2379352, 11839345, -4110402, -5988665, 11274298, 794957, 212801, -14594663, 23527084, -16458268 }, + { 33431127, -11130478, -17838966, -15626900, 8909499, 8376530, -32625340, 4087881, -15188911, -14416214 }, + { 1767683, 7197987, -13205226, -2022635, -13091350, 448826, 5799055, 4357868, -4774191, -16323038 } + } +}, +{ /* 2/31 */ + { + { 6721966, 13833823, -23523388, -1551314, 26354293, -11863321, 23365147, -3949732, 7390890, 2759800 }, + { 4409041, 2052381, 23373853, 10530217, 7676779, -12885954, 21302353, -4264057, 1244380, -12919645 }, + { -4421239, 7169619, 4982368, -2957590, 30256825, -2777540, 14086413, 9208236, 15886429, 16489664 } + }, + { + { 1996075, 10375649, 14346367, 13311202, -6874135, -16438411, -13693198, 398369, -30606455, -712933 }, + { -25307465, 9795880, -2777414, 14878809, -33531835, 14780363, 13348553, 12076947, -30836462, 5113182 }, + { -17770784, 11797796, 31950843, 13929123, -25888302, 12288344, -30341101, -7336386, 13847711, 5387222 } + }, + { + { -18582163, -3416217, 17824843, -2340966, 22744343, -10442611, 8763061, 3617786, -19600662, 10370991 }, + { 20246567, -14369378, 22358229, -543712, 18507283, -10413996, 14554437, -8746092, 32232924, 16763880 }, + { 9648505, 10094563, 26416693, 14745928, -30374318, -6472621, 11094161, 15689506, 3140038, -16510092 } + }, + { + { -16160072, 5472695, 31895588, 4744994, 8823515, 10365685, -27224800, 9448613, -28774454, 366295 }, + { 19153450, 11523972, -11096490, -6503142, -24647631, 5420647, 28344573, 8041113, 719605, 11671788 }, + { 8678025, 2694440, -6808014, 2517372, 4964326, 11152271, -15432916, -15266516, 27000813, -10195553 } + }, + { + { -15157904, 7134312, 8639287, -2814877, -7235688, 10421742, 564065, 5336097, 6750977, -14521026 }, + { 11836410, -3979488, 26297894, 16080799, 23455045, 15735944, 1695823, -8819122, 8169720, 16220347 }, + { -18115838, 8653647, 17578566, -6092619, -8025777, -16012763, -11144307, -2627664, -5990708, -14166033 } + }, + { + { -23308498, -10968312, 15213228, -10081214, -30853605, -11050004, 27884329, 2847284, 2655861, 1738395 }, + { -27537433, -14253021, -25336301, -8002780, -9370762, 8129821, 21651608, -3239336, -19087449, -11005278 }, + { 1533110, 3437855, 23735889, 459276, 29970501, 11335377, 26030092, 5821408, 10478196, 8544890 } + }, + { + { 32173121, -16129311, 24896207, 3921497, 22579056, -3410854, 19270449, 12217473, 17789017, -3395995 }, + { -30552961, -2228401, -15578829, -10147201, 13243889, 517024, 15479401, -3853233, 30460520, 1052596 }, + { -11614875, 13323618, 32618793, 8175907, -15230173, 12596687, 27491595, -4612359, 3179268, -9478891 } + }, + { + { 31947069, -14366651, -4640583, -15339921, -15125977, -6039709, -14756777, -16411740, 19072640, -9511060 }, + { 11685058, 11822410, 3158003, -13952594, 33402194, -4165066, 5977896, -5215017, 473099, 5040608 }, + { -20290863, 8198642, -27410132, 11602123, 1290375, -2799760, 28326862, 1721092, -19558642, -3131606 } + } +}, +{ /* 3/31 */ + { + { 7881532, 10687937, 7578723, 7738378, -18951012, -2553952, 21820786, 8076149, -27868496, 11538389 }, + { -19935666, 3899861, 18283497, -6801568, -15728660, -11249211, 8754525, 7446702, -5676054, 5797016 }, + { -11295600, -3793569, -15782110, -7964573, 12708869, -8456199, 2014099, -9050574, -2369172, -5877341 } + }, + { + { -22472376, -11568741, -27682020, 1146375, 18956691, 16640559, 1192730, -3714199, 15123619, 10811505 }, + { 14352098, -3419715, -18942044, 10822655, 32750596, 4699007, -70363, 15776356, -28886779, -11974553 }, + { -28241164, -8072475, -4978962, -5315317, 29416931, 1847569, -20654173, -16484855, 4714547, -9600655 } + }, + { + { 15200332, 8368572, 19679101, 15970074, -31872674, 1959451, 24611599, -4543832, -11745876, 12340220 }, + { 12876937, -10480056, 33134381, 6590940, -6307776, 14872440, 9613953, 8241152, 15370987, 9608631 }, + { -4143277, -12014408, 8446281, -391603, 4407738, 13629032, -7724868, 15866074, -28210621, -8814099 } + }, + { + { 26660628, -15677655, 8393734, 358047, -7401291, 992988, -23904233, 858697, 20571223, 8420556 }, + { 14620715, 13067227, -15447274, 8264467, 14106269, 15080814, 33531827, 12516406, -21574435, -12476749 }, + { 236881, 10476226, 57258, -14677024, 6472998, 2466984, 17258519, 7256740, 8791136, 15069930 } + }, + { + { 1276410, -9371918, 22949635, -16322807, -23493039, -5702186, 14711875, 4874229, -30663140, -2331391 }, + { 5855666, 4990204, -13711848, 7294284, -7804282, 1924647, -1423175, -7912378, -33069337, 9234253 }, + { 20590503, -9018988, 31529744, -7352666, -2706834, 10650548, 31559055, -11609587, 18979186, 13396066 } + }, + { + { 24474287, 4968103, 22267082, 4407354, 24063882, -8325180, -18816887, 13594782, 33514650, 7021958 }, + { -11566906, -6565505, -21365085, 15928892, -26158305, 4315421, -25948728, -3916677, -21480480, 12868082 }, + { -28635013, 13504661, 19988037, -2132761, 21078225, 6443208, -21446107, 2244500, -12455797, -8089383 } + }, + { + { -30595528, 13793479, -5852820, 319136, -25723172, -6263899, 33086546, 8957937, -15233648, 5540521 }, + { -11630176, -11503902, -8119500, -7643073, 2620056, 1022908, -23710744, -1568984, -16128528, -14962807 }, + { 23152971, 775386, 27395463, 14006635, -9701118, 4649512, 1689819, 892185, -11513277, -15205948 } + }, + { + { 9770129, 9586738, 26496094, 4324120, 1556511, -3550024, 27453819, 4763127, -19179614, 5867134 }, + { -32765025, 1927590, 31726409, -4753295, 23962434, -16019500, 27846559, 5931263, -29749703, -16108455 }, + { 27461885, -2977536, 22380810, 1815854, -23033753, -3031938, 7283490, -15148073, -19526700, 7734629 } + } +}, +{ /* 4/31 */ + { + { -8010264, -9590817, -11120403, 6196038, 29344158, -13430885, 7585295, -3176626, 18549497, 15302069 }, + { -32658337, -6171222, -7672793, -11051681, 6258878, 13504381, 10458790, -6418461, -8872242, 8424746 }, + { 24687205, 8613276, -30667046, -3233545, 1863892, -1830544, 19206234, 7134917, -11284482, -828919 } + }, + { + { 11334899, -9218022, 8025293, 12707519, 17523892, -10476071, 10243738, -14685461, -5066034, 16498837 }, + { 8911542, 6887158, -9584260, -6958590, 11145641, -9543680, 17303925, -14124238, 6536641, 10543906 }, + { -28946384, 15479763, -17466835, 568876, -1497683, 11223454, -2669190, -16625574, -27235709, 8876771 } + }, + { + { -25742899, -12566864, -15649966, -846607, -33026686, -796288, -33481822, 15824474, -604426, -9039817 }, + { 10330056, 70051, 7957388, -9002667, 9764902, 15609756, 27698697, -4890037, 1657394, 3084098 }, + { 10477963, -7470260, 12119566, -13250805, 29016247, -5365589, 31280319, 14396151, -30233575, 15272409 } + }, + { + { -12288309, 3169463, 28813183, 16658753, 25116432, -5630466, -25173957, -12636138, -25014757, 1950504 }, + { -26180358, 9489187, 11053416, -14746161, -31053720, 5825630, -8384306, -8767532, 15341279, 8373727 }, + { 28685821, 7759505, -14378516, -12002860, -31971820, 4079242, 298136, -10232602, -2878207, 15190420 } + }, + { + { -32932876, 13806336, -14337485, -15794431, -24004620, 10940928, 8669718, 2742393, -26033313, -6875003 }, + { -1580388, -11729417, -25979658, -11445023, -17411874, -10912854, 9291594, -16247779, -12154742, 6048605 }, + { -30305315, 14843444, 1539301, 11864366, 20201677, 1900163, 13934231, 5128323, 11213262, 9168384 } + }, + { + { -26280513, 11007847, 19408960, -940758, -18592965, -4328580, -5088060, -11105150, 20470157, -16398701 }, + { -23136053, 9282192, 14855179, -15390078, -7362815, -14408560, -22783952, 14461608, 14042978, 5230683 }, + { 29969567, -2741594, -16711867, -8552442, 9175486, -2468974, 21556951, 3506042, -5933891, -12449708 } + }, + { + { -3144746, 8744661, 19704003, 4581278, -20430686, 6830683, -21284170, 8971513, -28539189, 15326563 }, + { -19464629, 10110288, -17262528, -3503892, -23500387, 1355669, -15523050, 15300988, -20514118, 9168260 }, + { -5353335, 4488613, -23803248, 16314347, 7780487, -15638939, -28948358, 9601605, 33087103, -9011387 } + }, + { + { -19443170, -15512900, -20797467, -12445323, -29824447, 10229461, -27444329, -15000531, -5996870, 15664672 }, + { 23294591, -16632613, -22650781, -8470978, 27844204, 11461195, 13099750, -2460356, 18151676, 13417686 }, + { -24722913, -4176517, -31150679, 5988919, -26858785, 6685065, 1661597, -12551441, 15271676, -15452665 } + } +}, +{ /* 5/31 */ + { + { 11433042, -13228665, 8239631, -5279517, -1985436, -725718, -18698764, 2167544, -6921301, -13440182 }, + { -31436171, 15575146, 30436815, 12192228, -22463353, 9395379, -9917708, -8638997, 12215110, 12028277 }, + { 14098400, 6555944, 23007258, 5757252, -15427832, -12950502, 30123440, 4617780, -16900089, -655628 } + }, + { + { -4026201, -15240835, 11893168, 13718664, -14809462, 1847385, -15819999, 10154009, 23973261, -12684474 }, + { -26531820, -3695990, -1908898, 2534301, -31870557, -16550355, 18341390, -11419951, 32013174, -10103539 }, + { -25479301, 10876443, -11771086, -14625140, -12369567, 1838104, 21911214, 6354752, 4425632, -837822 } + }, + { + { -10433389, -14612966, 22229858, -3091047, -13191166, 776729, -17415375, -12020462, 4725005, 14044970 }, + { 19268650, -7304421, 1555349, 8692754, -21474059, -9910664, 6347390, -1411784, -19522291, -16109756 }, + { -24864089, 12986008, -10898878, -5558584, -11312371, -148526, 19541418, 8180106, 9282262, 10282508 } + }, + { + { -26205082, 4428547, -8661196, -13194263, 4098402, -14165257, 15522535, 8372215, 5542595, -10702683 }, + { -10562541, 14895633, 26814552, -16673850, -17480754, -2489360, -2781891, 6993761, -18093885, 10114655 }, + { -20107055, -929418, 31422704, 10427861, -7110749, 6150669, -29091755, -11529146, 25953725, -106158 } + }, + { + { -4234397, -8039292, -9119125, 3046000, 2101609, -12607294, 19390020, 6094296, -3315279, 12831125 }, + { -15998678, 7578152, 5310217, 14408357, -33548620, -224739, 31575954, 6326196, 7381791, -2421839 }, + { -20902779, 3296811, 24736065, -16328389, 18374254, 7318640, 6295303, 8082724, -15362489, 12339664 } + }, + { + { 27724736, 2291157, 6088201, -14184798, 1792727, 5857634, 13848414, 15768922, 25091167, 14856294 }, + { -18866652, 8331043, 24373479, 8541013, -701998, -9269457, 12927300, -12695493, -22182473, -9012899 }, + { -11423429, -5421590, 11632845, 3405020, 30536730, -11674039, -27260765, 13866390, 30146206, 9142070 } + }, + { + { 3924129, -15307516, -13817122, -10054960, 12291820, -668366, -27702774, 9326384, -8237858, 4171294 }, + { -15921940, 16037937, 6713787, 16606682, -21612135, 2790944, 26396185, 3731949, 345228, -5462949 }, + { -21327538, 13448259, 25284571, 1143661, 20614966, -8849387, 2031539, -12391231, -16253183, -13582083 } + }, + { + { 31016211, -16722429, 26371392, -14451233, -5027349, 14854137, 17477601, 3842657, 28012650, -16405420 }, + { -5075835, 9368966, -8562079, -4600902, -15249953, 6970560, -9189873, 16292057, -8867157, 3507940 }, + { 29439664, 3537914, 23333589, 6997794, -17555561, -11018068, -15209202, -15051267, -9164929, 6580396 } + } +}, +{ /* 6/31 */ + { + { -12185861, -7679788, 16438269, 10826160, -8696817, -6235611, 17860444, -9273846, -2095802, 9304567 }, + { 20714564, -4336911, 29088195, 7406487, 11426967, -5095705, 14792667, -14608617, 5289421, -477127 }, + { -16665533, -10650790, -6160345, -13305760, 9192020, -1802462, 17271490, 12349094, 26939669, -3752294 } + }, + { + { -12889898, 9373458, 31595848, 16374215, 21471720, 13221525, -27283495, -12348559, -3698806, 117887 }, + { 22263325, -6560050, 3984570, -11174646, -15114008, -566785, 28311253, 5358056, -23319780, 541964 }, + { 16259219, 3261970, 2309254, -15534474, -16885711, -4581916, 24134070, -16705829, -13337066, -13552195 } + }, + { + { 9378160, -13140186, -22845982, -12745264, 28198281, -7244098, -2399684, -717351, 690426, 14876244 }, + { 24977353, -314384, -8223969, -13465086, 28432343, -1176353, -13068804, -12297348, -22380984, 6618999 }, + { -1538174, 11685646, 12944378, 13682314, -24389511, -14413193, 8044829, -13817328, 32239829, -5652762 } + }, + { + { -18603066, 4762990, -926250, 8885304, -28412480, -3187315, 9781647, -10350059, 32779359, 5095274 }, + { -33008130, -5214506, -32264887, -3685216, 9460461, -9327423, -24601656, 14506724, 21639561, -2630236 }, + { -16400943, -13112215, 25239338, 15531969, 3987758, -4499318, -1289502, -6863535, 17874574, 558605 } + }, + { + { -13600129, 10240081, 9171883, 16131053, -20869254, 9599700, 33499487, 5080151, 2085892, 5119761 }, + { -22205145, -2519528, -16381601, 414691, -25019550, 2170430, 30634760, -8363614, -31999993, -5759884 }, + { -6845704, 15791202, 8550074, -1312654, 29928809, -12092256, 27534430, -7192145, -22351378, 12961482 } + }, + { + { -24492060, -9570771, 10368194, 11582341, -23397293, -2245287, 16533930, 8206996, -30194652, -5159638 }, + { -11121496, -3382234, 2307366, 6362031, -135455, 8868177, -16835630, 7031275, 7589640, 8945490 }, + { -32152748, 8917967, 6661220, -11677616, -1192060, -15793393, 7251489, -11182180, 24099109, -14456170 } + }, + { + { 5019558, -7907470, 4244127, -14714356, -26933272, 6453165, -19118182, -13289025, -6231896, -10280736 }, + { 10853594, 10721687, 26480089, 5861829, -22995819, 1972175, -1866647, -10557898, -3363451, -6441124 }, + { -17002408, 5906790, 221599, -6563147, 7828208, -13248918, 24362661, -2008168, -13866408, 7421392 } + }, + { + { 8139927, -6546497, 32257646, -5890546, 30375719, 1886181, -21175108, 15441252, 28826358, -4123029 }, + { 6267086, 9695052, 7709135, -16603597, -32869068, -1886135, 14795160, -7840124, 13746021, -1742048 }, + { 28584902, 7787108, -6732942, -15050729, 22846041, -7571236, -3181936, -363524, 4771362, -8419958 } + } +}, +{ /* 7/31 */ + { + { 24949256, 6376279, -27466481, -8174608, -18646154, -9930606, 33543569, -12141695, 3569627, 11342593 }, + { 26514989, 4740088, 27912651, 3697550, 19331575, -11472339, 6809886, 4608608, 7325975, -14801071 }, + { -11618399, -14554430, -24321212, 7655128, -1369274, 5214312, -27400540, 10258390, -17646694, -8186692 } + }, + { + { 11431204, 15823007, 26570245, 14329124, 18029990, 4796082, -31446179, 15580664, 9280358, -3973687 }, + { -160783, -10326257, -22855316, -4304997, -20861367, -13621002, -32810901, -11181622, -15545091, 4387441 }, + { -20799378, 12194512, 3937617, -5805892, -27154820, 9340370, -24513992, 8548137, 20617071, -7482001 } + }, + { + { -938825, -3930586, -8714311, 16124718, 24603125, -6225393, -13775352, -11875822, 24345683, 10325460 }, + { -19855277, -1568885, -22202708, 8714034, 14007766, 6928528, 16318175, -1010689, 4766743, 3552007 }, + { -21751364, -16730916, 1351763, -803421, -4009670, 3950935, 3217514, 14481909, 10988822, -3994762 } + }, + { + { 15564307, -14311570, 3101243, 5684148, 30446780, -8051356, 12677127, -6505343, -8295852, 13296005 }, + { -9442290, 6624296, -30298964, -11913677, -4670981, -2057379, 31521204, 9614054, -30000824, 12074674 }, + { 4771191, -135239, 14290749, -13089852, 27992298, 14998318, -1413936, -1556716, 29832613, -16391035 } + }, + { + { 7064884, -7541174, -19161962, -5067537, -18891269, -2912736, 25825242, 5293297, -27122660, 13101590 }, + { -2298563, 2439670, -7466610, 1719965, -27267541, -16328445, 32512469, -5317593, -30356070, -4190957 }, + { -30006540, 10162316, -33180176, 3981723, -16482138, -13070044, 14413974, 9515896, 19568978, 9628812 } + }, + { + { 33053803, 199357, 15894591, 1583059, 27380243, -4580435, -17838894, -6106839, -6291786, 3437740 }, + { -18978877, 3884493, 19469877, 12726490, 15913552, 13614290, -22961733, 70104, 7463304, 4176122 }, + { -27124001, 10659917, 11482427, -16070381, 12771467, -6635117, -32719404, -5322751, 24216882, 5944158 } + }, + { + { 8894125, 7450974, -2664149, -9765752, -28080517, -12389115, 19345746, 14680796, 11632993, 5847885 }, + { 26942781, -2315317, 9129564, -4906607, 26024105, 11769399, -11518837, 6367194, -9727230, 4782140 }, + { 19916461, -4828410, -22910704, -11414391, 25606324, -5972441, 33253853, 8220911, 6358847, -1873857 } + }, + { + { 801428, -2081702, 16569428, 11065167, 29875704, 96627, 7908388, -4480480, -13538503, 1387155 }, + { 19646058, 5720633, -11416706, 12814209, 11607948, 12749789, 14147075, 15156355, -21866831, 11835260 }, + { 19299512, 1155910, 28703737, 14890794, 2925026, 7269399, 26121523, 15467869, -26560550, 5052483 } + } +}, +{ /* 8/31 */ + { + { -3017432, 10058206, 1980837, 3964243, 22160966, 12322533, -6431123, -12618185, 12228557, -7003677 }, + { 32944382, 14922211, -22844894, 5188528, 21913450, -8719943, 4001465, 13238564, -6114803, 8653815 }, + { 22865569, -4652735, 27603668, -12545395, 14348958, 8234005, 24808405, 5719875, 28483275, 2841751 } + }, + { + { -16420968, -1113305, -327719, -12107856, 21886282, -15552774, -1887966, -315658, 19932058, -12739203 }, + { -11656086, 10087521, -8864888, -5536143, -19278573, -3055912, 3999228, 13239134, -4777469, -13910208 }, + { 1382174, -11694719, 17266790, 9194690, -13324356, 9720081, 20403944, 11284705, -14013818, 3093230 } + }, + { + { 16650921, -11037932, -1064178, 1570629, -8329746, 7352753, -302424, 16271225, -24049421, -6691850 }, + { -21911077, -5927941, -4611316, -5560156, -31744103, -10785293, 24123614, 15193618, -21652117, -16739389 }, + { -9935934, -4289447, -25279823, 4372842, 2087473, 10399484, 31870908, 14690798, 17361620, 11864968 } + }, + { + { -11307610, 6210372, 13206574, 5806320, -29017692, -13967200, -12331205, -7486601, -25578460, -16240689 }, + { 14668462, -12270235, 26039039, 15305210, 25515617, 4542480, 10453892, 6577524, 9145645, -6443880 }, + { 5974874, 3053895, -9433049, -10385191, -31865124, 3225009, -7972642, 3936128, -5652273, -3050304 } + }, + { + { 30625386, -4729400, -25555961, -12792866, -20484575, 7695099, 17097188, -16303496, -27999779, 1803632 }, + { -3553091, 9865099, -5228566, 4272701, -5673832, -16689700, 14911344, 12196514, -21405489, 7047412 }, + { 20093277, 9920966, -11138194, -5343857, 13161587, 12044805, -32856851, 4124601, -32343828, -10257566 } + }, + { + { -20788824, 14084654, -13531713, 7842147, 19119038, -13822605, 4752377, -8714640, -21679658, 2288038 }, + { -26819236, -3283715, 29965059, 3039786, -14473765, 2540457, 29457502, 14625692, -24819617, 12570232 }, + { -1063558, -11551823, 16920318, 12494842, 1278292, -5869109, -21159943, -3498680, -11974704, 4724943 } + }, + { + { 17960970, -11775534, -4140968, -9702530, -8876562, -1410617, -12907383, -8659932, -29576300, 1903856 }, + { 23134274, -14279132, -10681997, -1611936, 20684485, 15770816, -12989750, 3190296, 26955097, 14109738 }, + { 15308788, 5320727, -30113809, -14318877, 22902008, 7767164, 29425325, -11277562, 31960942, 11934971 } + }, + { + { -27395711, 8435796, 4109644, 12222639, -24627868, 14818669, 20638173, 4875028, 10491392, 1379718 }, + { -13159415, 9197841, 3875503, -8936108, -1383712, -5879801, 33518459, 16176658, 21432314, 12180697 }, + { -11787308, 11500838, 13787581, -13832590, -22430679, 10140205, 1465425, 12689540, -10301319, -13872883 } + } +}, +{ /* 9/31 */ + { + { 5414091, -15386041, -21007664, 9643570, 12834970, 1186149, -2622916, -1342231, 26128231, 6032912 }, + { -26337395, -13766162, 32496025, -13653919, 17847801, -12669156, 3604025, 8316894, -25875034, -10437358 }, + { 3296484, 6223048, 24680646, -12246460, -23052020, 5903205, -8862297, -4639164, 12376617, 3188849 } + }, + { + { 29190488, -14659046, 27549113, -1183516, 3520066, -10697301, 32049515, -7309113, -16109234, -9852307 }, + { -14744486, -9309156, 735818, -598978, -20407687, -5057904, 25246078, -15795669, 18640741, -960977 }, + { -6928835, -16430795, 10361374, 5642961, 4910474, 12345252, -31638386, -494430, 10530747, 1053335 } + }, + { + { -29265967, -14186805, -13538216, -12117373, -19457059, -10655384, -31462369, -2948985, 24018831, 15026644 }, + { -22592535, -3145277, -2289276, 5953843, -13440189, 9425631, 25310643, 13003497, -2314791, -15145616 }, + { -27419985, -603321, -8043984, -1669117, -26092265, 13987819, -27297622, 187899, -23166419, -2531735 } + }, + { + { -21744398, -13810475, 1844840, 5021428, -10434399, -15911473, 9716667, 16266922, -5070217, 726099 }, + { 29370922, -6053998, 7334071, -15342259, 9385287, 2247707, -13661962, -4839461, 30007388, -15823341 }, + { -936379, 16086691, 23751945, -543318, -1167538, -5189036, 9137109, 730663, 9835848, 4555336 } + }, + { + { -23376435, 1410446, -22253753, -12899614, 30867635, 15826977, 17693930, 544696, -11985298, 12422646 }, + { 31117226, -12215734, -13502838, 6561947, -9876867, -12757670, -5118685, -4096706, 29120153, 13924425 }, + { -17400879, -14233209, 19675799, -2734756, -11006962, -5858820, -9383939, -11317700, 7240931, -237388 } + }, + { + { -31361739, -11346780, -15007447, -5856218, -22453340, -12152771, 1222336, 4389483, 3293637, -15551743 }, + { -16684801, -14444245, 11038544, 11054958, -13801175, -3338533, -24319580, 7733547, 12796905, -6335822 }, + { -8759414, -10817836, -25418864, 10783769, -30615557, -9746811, -28253339, 3647836, 3222231, -11160462 } + }, + { + { 18606113, 1693100, -25448386, -15170272, 4112353, 10045021, 23603893, -2048234, -7550776, 2484985 }, + { 9255317, -3131197, -12156162, -1004256, 13098013, -9214866, 16377220, -2102812, -19802075, -3034702 }, + { -22729289, 7496160, -5742199, 11329249, 19991973, -3347502, -31718148, 9936966, -30097688, -10618797 } + }, + { + { 21878590, -5001297, 4338336, 13643897, -3036865, 13160960, 19708896, 5415497, -7360503, -4109293 }, + { 27736861, 10103576, 12500508, 8502413, -3413016, -9633558, 10436918, -1550276, -23659143, -8132100 }, + { 19492550, -12104365, -29681976, -852630, -3208171, 12403437, 30066266, 8367329, 13243957, 8709688 } + } +}, +{ /* 10/31 */ + { + { 12015105, 2801261, 28198131, 10151021, 24818120, -4743133, -11194191, -5645734, 5150968, 7274186 }, + { 2831366, -12492146, 1478975, 6122054, 23825128, -12733586, 31097299, 6083058, 31021603, -9793610 }, + { -2529932, -2229646, 445613, 10720828, -13849527, -11505937, -23507731, 16354465, 15067285, -14147707 } + }, + { + { 7840942, 14037873, -33364863, 15934016, -728213, -3642706, 21403988, 1057586, -19379462, -12403220 }, + { 915865, -16469274, 15608285, -8789130, -24357026, 6060030, -17371319, 8410997, -7220461, 16527025 }, + { 32922597, -556987, 20336074, -16184568, 10903705, -5384487, 16957574, 52992, 23834301, 6588044 } + }, + { + { 32752030, 11232950, 3381995, -8714866, 22652988, -10744103, 17159699, 16689107, -20314580, -1305992 }, + { -4689649, 9166776, -25710296, -10847306, 11576752, 12733943, 7924251, -2752281, 1976123, -7249027 }, + { 21251222, 16309901, -2983015, -6783122, 30810597, 12967303, 156041, -3371252, 12331345, -8237197 } + }, + { + { 8651614, -4477032, -16085636, -4996994, 13002507, 2950805, 29054427, -5106970, 10008136, -4667901 }, + { 31486080, 15114593, -14261250, 12951354, 14369431, -7387845, 16347321, -13662089, 8684155, -10532952 }, + { 19443825, 11385320, 24468943, -9659068, -23919258, 2187569, -26263207, -6086921, 31316348, 14219878 } + }, + { + { -28594490, 1193785, 32245219, 11392485, 31092169, 15722801, 27146014, 6992409, 29126555, 9207390 }, + { 32382935, 1110093, 18477781, 11028262, -27411763, -7548111, -4980517, 10843782, -7957600, -14435730 }, + { 2814918, 7836403, 27519878, -7868156, -20894015, -11553689, -21494559, 8550130, 28346258, 1994730 } + }, + { + { -19578299, 8085545, -14000519, -3948622, 2785838, -16231307, -19516951, 7174894, 22628102, 8115180 }, + { -30405132, 955511, -11133838, -15078069, -32447087, -13278079, -25651578, 3317160, -9943017, 930272 }, + { -15303681, -6833769, 28856490, 1357446, 23421993, 1057177, 24091212, -1388970, -22765376, -10650715 } + }, + { + { -22751231, -5303997, -12907607, -12768866, -15811511, -7797053, -14839018, -16554220, -1867018, 8398970 }, + { -31969310, 2106403, -4736360, 1362501, 12813763, 16200670, 22981545, -6291273, 18009408, -15772772 }, + { -17220923, -9545221, -27784654, 14166835, 29815394, 7444469, 29551787, -3727419, 19288549, 1325865 } + }, + { + { 15100157, -15835752, -23923978, -1005098, -26450192, 15509408, 12376730, -3479146, 33166107, -8042750 }, + { 20909231, 13023121, -9209752, 16251778, -5778415, -8094914, 12412151, 10018715, 2213263, -13878373 }, + { 32529814, -11074689, 30361439, -16689753, -9135940, 1513226, 22922121, 6382134, -5766928, 8371348 } + } +}, +{ /* 11/31 */ + { + { 9923462, 11271500, 12616794, 3544722, -29998368, -1721626, 12891687, -8193132, -26442943, 10486144 }, + { -22597207, -7012665, 8587003, -8257861, 4084309, -12970062, 361726, 2610596, -23921530, -11455195 }, + { 5408411, -1136691, -4969122, 10561668, 24145918, 14240566, 31319731, -4235541, 19985175, -3436086 } + }, + { + { -13994457, 16616821, 14549246, 3341099, 32155958, 13648976, -17577068, 8849297, 65030, 8370684 }, + { -8320926, -12049626, 31204563, 5839400, -20627288, -1057277, -19442942, 6922164, 12743482, -9800518 }, + { -2361371, 12678785, 28815050, 4759974, -23893047, 4884717, 23783145, 11038569, 18800704, 255233 } + }, + { + { -5269658, -1773886, 13957886, 7990715, 23132995, 728773, 13393847, 9066957, 19258688, -14753793 }, + { -2936654, -10827535, -10432089, 14516793, -3640786, 4372541, -31934921, 2209390, -1524053, 2055794 }, + { 580882, 16705327, 5468415, -2683018, -30926419, -14696000, -7203346, -8994389, -30021019, 7394435 } + }, + { + { 23838809, 1822728, -15738443, 15242727, 8318092, -3733104, -21672180, -3492205, -4821741, 14799921 }, + { 13345610, 9759151, 3371034, -16137791, 16353039, 8577942, 31129804, 13496856, -9056018, 7402518 }, + { 2286874, -4435931, -20042458, -2008336, -13696227, 5038122, 11006906, -15760352, 8205061, 1607563 } + }, + { + { 14414086, -8002132, 3331830, -3208217, 22249151, -5594188, 18364661, -2906958, 30019587, -9029278 }, + { -27688051, 1585953, -10775053, 931069, -29120221, -11002319, -14410829, 12029093, 9944378, 8024 }, + { 4368715, -3709630, 29874200, -15022983, -20230386, -11410704, -16114594, -999085, -8142388, 5640030 } + }, + { + { 10299610, 13746483, 11661824, 16234854, 7630238, 5998374, 9809887, -16694564, 15219798, -14327783 }, + { 27425505, -5719081, 3055006, 10660664, 23458024, 595578, -15398605, -1173195, -18342183, 9742717 }, + { 6744077, 2427284, 26042789, 2720740, -847906, 1118974, 32324614, 7406442, 12420155, 1994844 } + }, + { + { 14012521, -5024720, -18384453, -9578469, -26485342, -3936439, -13033478, -10909803, 24319929, -6446333 }, + { 16412690, -4507367, 10772641, 15929391, -17068788, -4658621, 10555945, -10484049, -30102368, -4739048 }, + { 22397382, -7767684, -9293161, -12792868, 17166287, -9755136, -27333065, 6199366, 21880021, -12250760 } + }, + { + { -4283307, 5368523, -31117018, 8163389, -30323063, 3209128, 16557151, 8890729, 8840445, 4957760 }, + { -15447727, 709327, -6919446, -10870178, -29777922, 6522332, -21720181, 12130072, -14796503, 5005757 }, + { -2114751, -14308128, 23019042, 15765735, -25269683, 6002752, 10183197, -13239326, -16395286, -2176112 } + } +}, +{ /* 12/31 */ + { + { -19025756, 1632005, 13466291, -7995100, -23640451, 16573537, -32013908, -3057104, 22208662, 2000468 }, + { 3065073, -1412761, -25598674, -361432, -17683065, -5703415, -8164212, 11248527, -3691214, -7414184 }, + { 10379208, -6045554, 8877319, 1473647, -29291284, -12507580, 16690915, 2553332, -3132688, 16400289 } + }, + { + { 15716668, 1254266, -18472690, 7446274, -8448918, 6344164, -22097271, -7285580, 26894937, 9132066 }, + { 24158887, 12938817, 11085297, -8177598, -28063478, -4457083, -30576463, 64452, -6817084, -2692882 }, + { 13488534, 7794716, 22236231, 5989356, 25426474, -12578208, 2350710, -3418511, -4688006, 2364226 } + }, + { + { 16335052, 9132434, 25640582, 6678888, 1725628, 8517937, -11807024, -11697457, 15445875, -7798101 }, + { 29004207, -7867081, 28661402, -640412, -12794003, -7943086, 31863255, -4135540, -278050, -15759279 }, + { -6122061, -14866665, -28614905, 14569919, -10857999, -3591829, 10343412, -6976290, -29828287, -10815811 } + }, + { + { 27081650, 3463984, 14099042, -4517604, 1616303, -6205604, 29542636, 15372179, 17293797, 960709 }, + { 20263915, 11434237, -5765435, 11236810, 13505955, -10857102, -16111345, 6493122, -19384511, 7639714 }, + { -2830798, -14839232, 25403038, -8215196, -8317012, -16173699, 18006287, -16043750, 29994677, -15808121 } + }, + { + { 9769828, 5202651, -24157398, -13631392, -28051003, -11561624, -24613141, -13860782, -31184575, 709464 }, + { 12286395, 13076066, -21775189, -1176622, -25003198, 4057652, -32018128, -8890874, 16102007, 13205847 }, + { 13733362, 5599946, 10557076, 3195751, -5557991, 8536970, -25540170, 8525972, 10151379, 10394400 } + }, + { + { 4024660, -16137551, 22436262, 12276534, -9099015, -2686099, 19698229, 11743039, -33302334, 8934414 }, + { -15879800, -4525240, -8580747, -2934061, 14634845, -698278, -9449077, 3137094, -11536886, 11721158 }, + { 17555939, -5013938, 8268606, 2331751, -22738815, 9761013, 9319229, 8835153, -9205489, -1280045 } + }, + { + { -461409, -7830014, 20614118, 16688288, -7514766, -4807119, 22300304, 505429, 6108462, -6183415 }, + { -5070281, 12367917, -30663534, 3234473, 32617080, -8422642, 29880583, -13483331, -26898490, -7867459 }, + { -31975283, 5726539, 26934134, 10237677, -3173717, -605053, 24199304, 3795095, 7592688, -14992079 } + }, + { + { 21594432, -14964228, 17466408, -4077222, 32537084, 2739898, 6407723, 12018833, -28256052, 4298412 }, + { -20650503, -11961496, -27236275, 570498, 3767144, -1717540, 13891942, -1569194, 13717174, 10805743 }, + { -14676630, -15644296, 15287174, 11927123, 24177847, -8175568, -796431, 14860609, -26938930, -5863836 } + } +}, +{ /* 13/31 */ + { + { 12962541, 5311799, -10060768, 11658280, 18855286, -7954201, 13286263, -12808704, -4381056, 9882022 }, + { 18512079, 11319350, -20123124, 15090309, 18818594, 5271736, -22727904, 3666879, -23967430, -3299429 }, + { -6789020, -3146043, 16192429, 13241070, 15898607, -14206114, -10084880, -6661110, -2403099, 5276065 } + }, + { + { 30169808, -5317648, 26306206, -11750859, 27814964, 7069267, 7152851, 3684982, 1449224, 13082861 }, + { 10342826, 3098505, 2119311, 193222, 25702612, 12233820, 23697382, 15056736, -21016438, -8202000 }, + { -33150110, 3261608, 22745853, 7948688, 19370557, -15177665, -26171976, 6482814, -10300080, -11060101 } + }, + { + { 32869458, -5408545, 25609743, 15678670, -10687769, -15471071, 26112421, 2521008, -22664288, 6904815 }, + { 29506923, 4457497, 3377935, -9796444, -30510046, 12935080, 1561737, 3841096, -29003639, -6657642 }, + { 10340844, -6630377, -18656632, -2278430, 12621151, -13339055, 30878497, -11824370, -25584551, 5181966 } + }, + { + { 25940115, -12658025, 17324188, -10307374, -8671468, 15029094, 24396252, -16450922, -2322852, -12388574 }, + { -21765684, 9916823, -1300409, 4079498, -1028346, 11909559, 1782390, 12641087, 20603771, -6561742 }, + { -18882287, -11673380, 24849422, 11501709, 13161720, -4768874, 1925523, 11914390, 4662781, 7820689 } + }, + { + { 12241050, -425982, 8132691, 9393934, 32846760, -1599620, 29749456, 12172924, 16136752, 15264020 }, + { -10349955, -14680563, -8211979, 2330220, -17662549, -14545780, 10658213, 6671822, 19012087, 3772772 }, + { 3753511, -3421066, 10617074, 2028709, 14841030, -6721664, 28718732, -15762884, 20527771, 12988982 } + }, + { + { -14822485, -5797269, -3707987, 12689773, -898983, -10914866, -24183046, -10564943, 3299665, -12424953 }, + { -16777703, -15253301, -9642417, 4978983, 3308785, 8755439, 6943197, 6461331, -25583147, 8991218 }, + { -17226263, 1816362, -1673288, -6086439, 31783888, -8175991, -32948145, 7417950, -30242287, 1507265 } + }, + { + { 29692663, 6829891, -10498800, 4334896, 20945975, -11906496, -28887608, 8209391, 14606362, -10647073 }, + { -3481570, 8707081, 32188102, 5672294, 22096700, 1711240, -33020695, 9761487, 4170404, -2085325 }, + { -11587470, 14855945, -4127778, -1531857, -26649089, 15084046, 22186522, 16002000, -14276837, -8400798 } + }, + { + { -4811456, 13761029, -31703877, -2483919, -3312471, 7869047, -7113572, -9620092, 13240845, 10965870 }, + { -7742563, -8256762, -14768334, -13656260, -23232383, 12387166, 4498947, 14147411, 29514390, 4302863 }, + { -13413405, -12407859, 20757302, -13801832, 14785143, 8976368, -5061276, -2144373, 17846988, -13971927 } + } +}, +{ /* 14/31 */ + { + { -2244452, -754728, -4597030, -1066309, -6247172, 1455299, -21647728, -9214789, -5222701, 12650267 }, + { -9906797, -16070310, 21134160, 12198166, -27064575, 708126, 387813, 13770293, -19134326, 10958663 }, + { 22470984, 12369526, 23446014, -5441109, -21520802, -9698723, -11772496, -11574455, -25083830, 4271862 } + }, + { + { -25169565, -10053642, -19909332, 15361595, -5984358, 2159192, 75375, -4278529, -32526221, 8469673 }, + { 15854970, 4148314, -8893890, 7259002, 11666551, 13824734, -30531198, 2697372, 24154791, -9460943 }, + { 15446137, -15806644, 29759747, 14019369, 30811221, -9610191, -31582008, 12840104, 24913809, 9815020 } + }, + { + { -4709286, -5614269, -31841498, -12288893, -14443537, 10799414, -9103676, 13438769, 18735128, 9466238 }, + { 11933045, 9281483, 5081055, -5183824, -2628162, -4905629, -7727821, -10896103, -22728655, 16199064 }, + { 14576810, 379472, -26786533, -8317236, -29426508, -10812974, -102766, 1876699, 30801119, 2164795 } + }, + { + { 15995086, 3199873, 13672555, 13712240, -19378835, -4647646, -13081610, -15496269, -13492807, 1268052 }, + { -10290614, -3659039, -3286592, 10948818, 23037027, 3794475, -3470338, -12600221, -17055369, 3565904 }, + { 29210088, -9419337, -5919792, -4952785, 10834811, -13327726, -16512102, -10820713, -27162222, -14030531 } + }, + { + { -13161890, 15508588, 16663704, -8156150, -28349942, 9019123, -29183421, -3769423, 2244111, -14001979 }, + { -5152875, -3800936, -9306475, -6071583, 16243069, 14684434, -25673088, -16180800, 13491506, 4641841 }, + { 10813417, 643330, -19188515, -728916, 30292062, -16600078, 27548447, -7721242, 14476989, -12767431 } + }, + { + { 10292079, 9984945, 6481436, 8279905, -7251514, 7032743, 27282937, -1644259, -27912810, 12651324 }, + { -31185513, -813383, 22271204, 11835308, 10201545, 15351028, 17099662, 3988035, 21721536, -3148940 }, + { 10202177, -6545839, -31373232, -9574638, -32150642, -8119683, -12906320, 3852694, 13216206, 14842320 } + }, + { + { -15815640, -10601066, -6538952, -7258995, -6984659, -6581778, -31500847, 13765824, -27434397, 9900184 }, + { 14465505, -13833331, -32133984, -14738873, -27443187, 12990492, 33046193, 15796406, -7051866, -8040114 }, + { 30924417, -8279620, 6359016, -12816335, 16508377, 9071735, -25488601, 15413635, 9524356, -7018878 } + }, + { + { 12274201, -13175547, 32627641, -1785326, 6736625, 13267305, 5237659, -5109483, 15663516, 4035784 }, + { -2951309, 8903985, 17349946, 601635, -16432815, -4612556, -13732739, -15889334, -22258478, 4659091 }, + { -16916263, -4952973, -30393711, -15158821, 20774812, 15897498, 5736189, 15026997, -2178256, -13455585 } + } +}, +{ /* 15/31 */ + { + { -8858980, -2219056, 28571666, -10155518, -474467, -10105698, -3801496, 278095, 23440562, -290208 }, + { 10226241, -5928702, 15139956, 120818, -14867693, 5218603, 32937275, 11551483, -16571960, -7442864 }, + { 17932739, -12437276, -24039557, 10749060, 11316803, 7535897, 22503767, 5561594, -3646624, 3898661 } + }, + { + { 7749907, -969567, -16339731, -16464, -25018111, 15122143, -1573531, 7152530, 21831162, 1245233 }, + { 26958459, -14658026, 4314586, 8346991, -5677764, 11960072, -32589295, -620035, -30402091, -16716212 }, + { -12165896, 9166947, 33491384, 13673479, 29787085, 13096535, 6280834, 14587357, -22338025, 13987525 } + }, + { + { -24349909, 7778775, 21116000, 15572597, -4833266, -5357778, -4300898, -5124639, -7469781, -2858068 }, + { 9681908, -6737123, -31951644, 13591838, -6883821, 386950, 31622781, 6439245, -14581012, 4091397 }, + { -8426427, 1470727, -28109679, -1596990, 3978627, -5123623, -19622683, 12092163, 29077877, -14741988 } + }, + { + { 5269168, -6859726, -13230211, -8020715, 25932563, 1763552, -5606110, -5505881, -20017847, 2357889 }, + { 32264008, -15407652, -5387735, -1160093, -2091322, -3946900, 23104804, -12869908, 5727338, 189038 }, + { 14609123, -8954470, -6000566, -16622781, -14577387, -7743898, -26745169, 10942115, -25888931, -14884697 } + }, + { + { 20513500, 5557931, -15604613, 7829531, 26413943, -2019404, -21378968, 7471781, 13913677, -5137875 }, + { -25574376, 11967826, 29233242, 12948236, -6754465, 4713227, -8940970, 14059180, 12878652, 8511905 }, + { -25656801, 3393631, -2955415, -7075526, -2250709, 9366908, -30223418, 6812974, 5568676, -3127656 } + }, + { + { 11630004, 12144454, 2116339, 13606037, 27378885, 15676917, -17408753, -13504373, -14395196, 8070818 }, + { 27117696, -10007378, -31282771, -5570088, 1127282, 12772488, -29845906, 10483306, -11552749, -1028714 }, + { 10637467, -5688064, 5674781, 1072708, -26343588, -6982302, -1683975, 9177853, -27493162, 15431203 } + }, + { + { 20525145, 10892566, -12742472, 12779443, -29493034, 16150075, -28240519, 14943142, -15056790, -7935931 }, + { -30024462, 5626926, -551567, -9981087, 753598, 11981191, 25244767, -3239766, -3356550, 9594024 }, + { -23752644, 2636870, -5163910, -10103818, 585134, 7877383, 11345683, -6492290, 13352335, -10977084 } + }, + { + { -1931799, -5407458, 3304649, -12884869, 17015806, -4877091, -29783850, -7752482, -13215537, -319204 }, + { 20239939, 6607058, 6203985, 3483793, -18386976, -779229, -20723742, 15077870, -22750759, 14523817 }, + { 27406042, -6041657, 27423596, -4497394, 4996214, 10002360, -28842031, -4545494, -30172742, -4805667 } + } +}, +{ /* 16/31 */ + { + { 11374242, 12660715, 17861383, -12540833, 10935568, 1099227, -13886076, -9091740, -27727044, 11358504 }, + { -12730809, 10311867, 1510375, 10778093, -2119455, -9145702, 32676003, 11149336, -26123651, 4985768 }, + { -19096303, 341147, -6197485, -239033, 15756973, -8796662, -983043, 13794114, -19414307, -15621255 } + }, + { + { 6490081, 11940286, 25495923, -7726360, 8668373, -8751316, 3367603, 6970005, -1691065, -9004790 }, + { 1656497, 13457317, 15370807, 6364910, 13605745, 8362338, -19174622, -5475723, -16796596, -5031438 }, + { -22273315, -13524424, -64685, -4334223, -18605636, -10921968, -20571065, -7007978, -99853, -10237333 } + }, + { + { 17747465, 10039260, 19368299, -4050591, -20630635, -16041286, 31992683, -15857976, -29260363, -5511971 }, + { 31932027, -4986141, -19612382, 16366580, 22023614, 88450, 11371999, -3744247, 4882242, -10626905 }, + { 29796507, 37186, 19818052, 10115756, -11829032, 3352736, 18551198, 3272828, -5190932, -4162409 } + }, + { + { 12501286, 4044383, -8612957, -13392385, -32430052, 5136599, -19230378, -3529697, 330070, -3659409 }, + { 6384877, 2899513, 17807477, 7663917, -2358888, 12363165, 25366522, -8573892, -271295, 12071499 }, + { -8365515, -4042521, 25133448, -4517355, -6211027, 2265927, -32769618, 1936675, -5159697, 3829363 } + }, + { + { 28425966, -5835433, -577090, -4697198, -14217555, 6870930, 7921550, -6567787, 26333140, 14267664 }, + { -11067219, 11871231, 27385719, -10559544, -4585914, -11189312, 10004786, -8709488, -21761224, 8930324 }, + { -21197785, -16396035, 25654216, -1725397, 12282012, 11008919, 1541940, 4757911, -26491501, -16408940 } + }, + { + { 13537262, -7759490, -20604840, 10961927, -5922820, -13218065, -13156584, 6217254, -15943699, 13814990 }, + { -17422573, 15157790, 18705543, 29619, 24409717, -260476, 27361681, 9257833, -1956526, -1776914 }, + { -25045300, -10191966, 15366585, 15166509, -13105086, 8423556, -29171540, 12361135, -18685978, 4578290 } + }, + { + { 24579768, 3711570, 1342322, -11180126, -27005135, 14124956, -22544529, 14074919, 21964432, 8235257 }, + { -6528613, -2411497, 9442966, -5925588, 12025640, -1487420, -2981514, -1669206, 13006806, 2355433 }, + { -16304899, -13605259, -6632427, -5142349, 16974359, -10911083, 27202044, 1719366, 1141648, -12796236 } + }, + { + { -12863944, -13219986, -8318266, -11018091, -6810145, -4843894, 13475066, -3133972, 32674895, 13715045 }, + { 11423335, -5468059, 32344216, 8962751, 24989809, 9241752, -13265253, 16086212, -28740881, -15642093 }, + { -1409668, 12530728, -6368726, 10847387, 19531186, -14132160, -11709148, 7791794, -27245943, 4383347 } + } +}, +{ /* 17/31 */ + { + { -28970898, 5271447, -1266009, -9736989, -12455236, 16732599, -4862407, -4906449, 27193557, 6245191 }, + { -15193956, 5362278, -1783893, 2695834, 4960227, 12840725, 23061898, 3260492, 22510453, 8577507 }, + { -12632451, 11257346, -32692994, 13548177, -721004, 10879011, 31168030, 13952092, -29571492, -3635906 } + }, + { + { 3877321, -9572739, 32416692, 5405324, -11004407, -13656635, 3759769, 11935320, 5611860, 8164018 }, + { -16275802, 14667797, 15906460, 12155291, -22111149, -9039718, 32003002, -8832289, 5773085, -8422109 }, + { -23788118, -8254300, 1950875, 8937633, 18686727, 16459170, -905725, 12376320, 31632953, 190926 } + }, + { + { -24593607, -16138885, -8423991, 13378746, 14162407, 6901328, -8288749, 4508564, -25341555, -3627528 }, + { 8884438, -5884009, 6023974, 10104341, -6881569, -4941533, 18722941, -14786005, -1672488, 827625 }, + { -32720583, -16289296, -32503547, 7101210, 13354605, 2659080, -1800575, -14108036, -24878478, 1541286 } + }, + { + { 2901347, -1117687, 3880376, -10059388, -17620940, -3612781, -21802117, -3567481, 20456845, -1885033 }, + { 27019610, 12299467, -13658288, -1603234, -12861660, -4861471, -19540150, -5016058, 29439641, 15138866 }, + { 21536104, -6626420, -32447818, -10690208, -22408077, 5175814, -5420040, -16361163, 7779328, 109896 } + }, + { + { 30279744, 14648750, -8044871, 6425558, 13639621, -743509, 28698390, 12180118, 23177719, -554075 }, + { 26572847, 3405927, -31701700, 12890905, -19265668, 5335866, -6493768, 2378492, 4439158, -13279347 }, + { -22716706, 3489070, -9225266, -332753, 18875722, -1140095, 14819434, -12731527, -17717757, -5461437 } + }, + { + { -5056483, 16566551, 15953661, 3767752, -10436499, 15627060, -820954, 2177225, 8550082, -15114165 }, + { -18473302, 16596775, -381660, 15663611, 22860960, 15585581, -27844109, -3582739, -23260460, -8428588 }, + { -32480551, 15707275, -8205912, -5652081, 29464558, 2713815, -22725137, 15860482, -21902570, 1494193 } + }, + { + { -19562091, -14087393, -25583872, -9299552, 13127842, 759709, 21923482, 16529112, 8742704, 12967017 }, + { -28464899, 1553205, 32536856, -10473729, -24691605, -406174, -8914625, -2933896, -29903758, 15553883 }, + { 21877909, 3230008, 9881174, 10539357, -4797115, 2841332, 11543572, 14513274, 19375923, -12647961 } + }, + { + { 8832269, -14495485, 13253511, 5137575, 5037871, 4078777, 24880818, -6222716, 2862653, 9455043 }, + { 29306751, 5123106, 20245049, -14149889, 9592566, 8447059, -2077124, -2990080, 15511449, 4789663 }, + { -20679756, 7004547, 8824831, -9434977, -4045704, -3750736, -5754762, 108893, 23513200, 16652362 } + } +}, +{ /* 18/31 */ + { + { -33256173, 4144782, -4476029, -6579123, 10770039, -7155542, -6650416, -12936300, -18319198, 10212860 }, + { 2756081, 8598110, 7383731, -6859892, 22312759, -1105012, 21179801, 2600940, -9988298, -12506466 }, + { -24645692, 13317462, -30449259, -15653928, 21365574, -10869657, 11344424, 864440, -2499677, -16710063 } + }, + { + { -26432803, 6148329, -17184412, -14474154, 18782929, -275997, -22561534, 211300, 2719757, 4940997 }, + { -1323882, 3911313, -6948744, 14759765, -30027150, 7851207, 21690126, 8518463, 26699843, 5276295 }, + { -13149873, -6429067, 9396249, 365013, 24703301, -10488939, 1321586, 149635, -15452774, 7159369 } + }, + { + { 9987780, -3404759, 17507962, 9505530, 9731535, -2165514, 22356009, 8312176, 22477218, -8403385 }, + { 18155857, -16504990, 19744716, 9006923, 15154154, -10538976, 24256460, -4864995, -22548173, 9334109 }, + { 2986088, -4911893, 10776628, -3473844, 10620590, -7083203, -21413845, 14253545, -22587149, 536906 } + }, + { + { 4377756, 8115836, 24567078, 15495314, 11625074, 13064599, 7390551, 10589625, 10838060, -15420424 }, + { -19342404, 867880, 9277171, -3218459, -14431572, -1986443, 19295826, -15796950, 6378260, 699185 }, + { 7895026, 4057113, -7081772, -13077756, -17886831, -323126, -716039, 15693155, -5045064, -13373962 } + }, + { + { -7737563, -5869402, -14566319, -7406919, 11385654, 13201616, 31730678, -10962840, -3918636, -9669325 }, + { 10188286, -15770834, -7336361, 13427543, 22223443, 14896287, 30743455, 7116568, -21786507, 5427593 }, + { 696102, 13206899, 27047647, -10632082, 15285305, -9853179, 10798490, -4578720, 19236243, 12477404 } + }, + { + { -11229439, 11243796, -17054270, -8040865, -788228, -8167967, -3897669, 11180504, -23169516, 7733644 }, + { 17800790, -14036179, -27000429, -11766671, 23887827, 3149671, 23466177, -10538171, 10322027, 15313801 }, + { 26246234, 11968874, 32263343, -5468728, 6830755, -13323031, -15794704, -101982, -24449242, 10890804 } + }, + { + { -31365647, 10271363, -12660625, -6267268, 16690207, -13062544, -14982212, 16484931, 25180797, -5334884 }, + { -586574, 10376444, -32586414, -11286356, 19801893, 10997610, 2276632, 9482883, 316878, 13820577 }, + { -9882808, -4510367, -2115506, 16457136, -11100081, 11674996, 30756178, -7515054, 30696930, -3712849 } + }, + { + { 32988917, -9603412, 12499366, 7910787, -10617257, -11931514, -7342816, -9985397, -32349517, 7392473 }, + { -8855661, 15927861, 9866406, -3649411, -2396914, -16655781, -30409476, -9134995, 25112947, -2926644 }, + { -2504044, -436966, 25621774, -5678772, 15085042, -5479877, -24884878, -13526194, 5537438, -13914319 } + } +}, +{ /* 19/31 */ + { + { -11225584, 2320285, -9584280, 10149187, -33444663, 5808648, -14876251, -1729667, 31234590, 6090599 }, + { -9633316, 116426, 26083934, 2897444, -6364437, -2688086, 609721, 15878753, -6970405, -9034768 }, + { -27757857, 247744, -15194774, -9002551, 23288161, -10011936, -23869595, 6503646, 20650474, 1804084 } + }, + { + { -27589786, 15456424, 8972517, 8469608, 15640622, 4439847, 3121995, -10329713, 27842616, -202328 }, + { -15306973, 2839644, 22530074, 10026331, 4602058, 5048462, 28248656, 5031932, -11375082, 12714369 }, + { 20807691, -7270825, 29286141, 11421711, -27876523, -13868230, -21227475, 1035546, -19733229, 12796920 } + }, + { + { 12076899, -14301286, -8785001, -11848922, -25012791, 16400684, -17591495, -12899438, 3480665, -15182815 }, + { -32361549, 5457597, 28548107, 7833186, 7303070, -11953545, -24363064, -15921875, -33374054, 2771025 }, + { -21389266, 421932, 26597266, 6860826, 22486084, -6737172, -17137485, -4210226, -24552282, 15673397 } + }, + { + { -20184622, 2338216, 19788685, -9620956, -4001265, -8740893, -20271184, 4733254, 3727144, -12934448 }, + { 6120119, 814863, -11794402, -622716, 6812205, -15747771, 2019594, 7975683, 31123697, -10958981 }, + { 30069250, -11435332, 30434654, 2958439, 18399564, -976289, 12296869, 9204260, -16432438, 9648165 } + }, + { + { 32705432, -1550977, 30705658, 7451065, -11805606, 9631813, 3305266, 5248604, -26008332, -11377501 }, + { 17219865, 2375039, -31570947, -5575615, -19459679, 9219903, 294711, 15298639, 2662509, -16297073 }, + { -1172927, -7558695, -4366770, -4287744, -21346413, -8434326, 32087529, -1222777, 32247248, -14389861 } + }, + { + { 14312628, 1221556, 17395390, -8700143, -4945741, -8684635, -28197744, -9637817, -16027623, -13378845 }, + { -1428825, -9678990, -9235681, 6549687, -7383069, -468664, 23046502, 9803137, 17597934, 2346211 }, + { 18510800, 15337574, 26171504, 981392, -22241552, 7827556, -23491134, -11323352, 3059833, -11782870 } + }, + { + { 10141598, 6082907, 17829293, -1947643, 9830092, 13613136, -25556636, -5544586, -33502212, 3592096 }, + { 33114168, -15889352, -26525686, -13343397, 33076705, 8716171, 1151462, 1521897, -982665, -6837803 }, + { -32939165, -4255815, 23947181, -324178, -33072974, -12305637, -16637686, 3891704, 26353178, 693168 } + }, + { + { 30374239, 1595580, -16884039, 13186931, 4600344, 406904, 9585294, -400668, 31375464, 14369965 }, + { -14370654, -7772529, 1510301, 6434173, -18784789, -6262728, 32732230, -13108839, 17901441, 16011505 }, + { 18171223, -11934626, -12500402, 15197122, -11038147, -15230035, -19172240, -16046376, 8764035, 12309598 } + } +}, +{ /* 20/31 */ + { + { 5975908, -5243188, -19459362, -9681747, -11541277, 14015782, -23665757, 1228319, 17544096, -10593782 }, + { 5811932, -1715293, 3442887, -2269310, -18367348, -8359541, -18044043, -15410127, -5565381, 12348900 }, + { -31399660, 11407555, 25755363, 6891399, -3256938, 14872274, -24849353, 8141295, -10632534, -585479 } + }, + { + { -12675304, 694026, -5076145, 13300344, 14015258, -14451394, -9698672, -11329050, 30944593, 1130208 }, + { 8247766, -6710942, -26562381, -7709309, -14401939, -14648910, 4652152, 2488540, 23550156, -271232 }, + { 17294316, -3788438, 7026748, 15626851, 22990044, 113481, 2267737, -5908146, -408818, -137719 } + }, + { + { 16091085, -16253926, 18599252, 7340678, 2137637, -1221657, -3364161, 14550936, 3260525, -7166271 }, + { -4910104, -13332887, 18550887, 10864893, -16459325, -7291596, -23028869, -13204905, -12748722, 2701326 }, + { -8574695, 16099415, 4629974, -16340524, -20786213, -6005432, -10018363, 9276971, 11329923, 1862132 } + }, + { + { 14763076, -15903608, -30918270, 3689867, 3511892, 10313526, -21951088, 12219231, -9037963, -940300 }, + { 8894987, -3446094, 6150753, 3013931, 301220, 15693451, -31981216, -2909717, -15438168, 11595570 }, + { 15214962, 3537601, -26238722, -14058872, 4418657, -15230761, 13947276, 10730794, -13489462, -4363670 } + }, + { + { -2538306, 7682793, 32759013, 263109, -29984731, -7955452, -22332124, -10188635, 977108, 699994 }, + { -12466472, 4195084, -9211532, 550904, -15565337, 12917920, 19118110, -439841, -30534533, -14337913 }, + { 31788461, -14507657, 4799989, 7372237, 8808585, -14747943, 9408237, -10051775, 12493932, -5409317 } + }, + { + { -25680606, 5260744, -19235809, -6284470, -3695942, 16566087, 27218280, 2607121, 29375955, 6024730 }, + { 842132, -2794693, -4763381, -8722815, 26332018, -12405641, 11831880, 6985184, -9940361, 2854096 }, + { -4847262, -7969331, 2516242, -5847713, 9695691, -7221186, 16512645, 960770, 12121869, 16648078 } + }, + { + { -15218652, 14667096, -13336229, 2013717, 30598287, -464137, -31504922, -7882064, 20237806, 2838411 }, + { -19288047, 4453152, 15298546, -16178388, 22115043, -15972604, 12544294, -13470457, 1068881, -12499905 }, + { -9558883, -16518835, 33238498, 13506958, 30505848, -1114596, -8486907, -2630053, 12521378, 4845654 } + }, + { + { -28198521, 10744108, -2958380, 10199664, 7759311, -13088600, 3409348, -873400, -6482306, -12885870 }, + { -23561822, 6230156, -20382013, 10655314, -24040585, -11621172, 10477734, -1240216, -3113227, 13974498 }, + { 12966261, 15550616, -32038948, -1615346, 21025980, -629444, 5642325, 7188737, 18895762, 12629579 } + } +}, +{ /* 21/31 */ + { + { 14741879, -14946887, 22177208, -11721237, 1279741, 8058600, 11758140, 789443, 32195181, 3895677 }, + { 10758205, 15755439, -4509950, 9243698, -4879422, 6879879, -2204575, -3566119, -8982069, 4429647 }, + { -2453894, 15725973, -20436342, -10410672, -5803908, -11040220, -7135870, -11642895, 18047436, -15281743 } + }, + { + { -25173001, -11307165, 29759956, 11776784, -22262383, -15820455, 10993114, -12850837, -17620701, -9408468 }, + { 21987233, 700364, -24505048, 14972008, -7774265, -5718395, 32155026, 2581431, -29958985, 8773375 }, + { -25568350, 454463, -13211935, 16126715, 25240068, 8594567, 20656846, 12017935, -7874389, -13920155 } + }, + { + { 6028182, 6263078, -31011806, -11301710, -818919, 2461772, -31841174, -5468042, -1721788, -2776725 }, + { -12278994, 16624277, 987579, -5922598, 32908203, 1248608, 7719845, -4166698, 28408820, 6816612 }, + { -10358094, -8237829, 19549651, -12169222, 22082623, 16147817, 20613181, 13982702, -10339570, 5067943 } + }, + { + { -30505967, -3821767, 12074681, 13582412, -19877972, 2443951, -19719286, 12746132, 5331210, -10105944 }, + { 30528811, 3601899, -1957090, 4619785, -27361822, -15436388, 24180793, -12570394, 27679908, -1648928 }, + { 9402404, -13957065, 32834043, 10838634, -26580150, -13237195, 26653274, -8685565, 22611444, -12715406 } + }, + { + { 22190590, 1118029, 22736441, 15130463, -30460692, -5991321, 19189625, -4648942, 4854859, 6622139 }, + { -8310738, -2953450, -8262579, -3388049, -10401731, -271929, 13424426, -3567227, 26404409, 13001963 }, + { -31241838, -15415700, -2994250, 8939346, 11562230, -12840670, -26064365, -11621720, -15405155, 11020693 } + }, + { + { 1866042, -7949489, -7898649, -10301010, 12483315, 13477547, 3175636, -12424163, 28761762, 1406734 }, + { -448555, -1777666, 13018551, 3194501, -9580420, -11161737, 24760585, -4347088, 25577411, -13378680 }, + { -24290378, 4759345, -690653, -1852816, 2066747, 10693769, -29595790, 9884936, -9368926, 4745410 } + }, + { + { -9141284, 6049714, -19531061, -4341411, -31260798, 9944276, -15462008, -11311852, 10931924, -11931931 }, + { -16561513, 14112680, -8012645, 4817318, -8040464, -11414606, -22853429, 10856641, -20470770, 13434654 }, + { 22759489, -10073434, -16766264, -1871422, 13637442, -10168091, 1765144, -12654326, 28445307, -5364710 } + }, + { + { 29875063, 12493613, 2795536, -3786330, 1710620, 15181182, -10195717, -8788675, 9074234, 1167180 }, + { -26205683, 11014233, -9842651, -2635485, -26908120, 7532294, -18716888, -9535498, 3843903, 9367684 }, + { -10969595, -6403711, 9591134, 9582310, 11349256, 108879, 16235123, 8601684, -139197, 4242895 } + } +}, +{ /* 22/31 */ + { + { 22092954, -13191123, -2042793, -11968512, 32186753, -11517388, -6574341, 2470660, -27417366, 16625501 }, + { -11057722, 3042016, 13770083, -9257922, 584236, -544855, -7770857, 2602725, -27351616, 14247413 }, + { 6314175, -10264892, -32772502, 15957557, -10157730, 168750, -8618807, 14290061, 27108877, -1180880 } + }, + { + { -8586597, -7170966, 13241782, 10960156, -32991015, -13794596, 33547976, -11058889, -27148451, 981874 }, + { 22833440, 9293594, -32649448, -13618667, -9136966, 14756819, -22928859, -13970780, -10479804, -16197962 }, + { -7768587, 3326786, -28111797, 10783824, 19178761, 14905060, 22680049, 13906969, -15933690, 3797899 } + }, + { + { 21721356, -4212746, -12206123, 9310182, -3882239, -13653110, 23740224, -2709232, 20491983, -8042152 }, + { 9209270, -15135055, -13256557, -6167798, -731016, 15289673, 25947805, 15286587, 30997318, -6703063 }, + { 7392032, 16618386, 23946583, -8039892, -13265164, -1533858, -14197445, -2321576, 17649998, -250080 } + }, + { + { -9301088, -14193827, 30609526, -3049543, -25175069, -1283752, -15241566, -9525724, -2233253, 7662146 }, + { -17558673, 1763594, -33114336, 15908610, -30040870, -12174295, 7335080, -8472199, -3174674, 3440183 }, + { -19889700, -5977008, -24111293, -9688870, 10799743, -16571957, 40450, -4431835, 4862400, 1133 } + }, + { + { -32856209, -7873957, -5422389, 14860950, -16319031, 7956142, 7258061, 311861, -30594991, -7379421 }, + { -3773428, -1565936, 28985340, 7499440, 24445838, 9325937, 29727763, 16527196, 18278453, 15405622 }, + { -4381906, 8508652, -19898366, -3674424, -5984453, 15149970, -13313598, 843523, -21875062, 13626197 } + }, + { + { 2281448, -13487055, -10915418, -2609910, 1879358, 16164207, -10783882, 3953792, 13340839, 15928663 }, + { 31727126, -7179855, -18437503, -8283652, 2875793, -16390330, -25269894, -7014826, -23452306, 5964753 }, + { 4100420, -5959452, -17179337, 6017714, -18705837, 12227141, -26684835, 11344144, 2538215, -7570755 } + }, + { + { -9433605, 6123113, 11159803, -2156608, 30016280, 14966241, -20474983, 1485421, -629256, -15958862 }, + { -26804558, 4260919, 11851389, 9658551, -32017107, 16367492, -20205425, -13191288, 11659922, -11115118 }, + { 26180396, 10015009, -30844224, -8581293, 5418197, 9480663, 2231568, -10170080, 33100372, -1306171 } + }, + { + { 15121113, -5201871, -10389905, 15427821, -27509937, -15992507, 21670947, 4486675, -5931810, -14466380 }, + { 16166486, -9483733, -11104130, 6023908, -31926798, -1364923, 2340060, -16254968, -10735770, -10039824 }, + { 28042865, -3557089, -12126526, 12259706, -3717498, -6945899, 6766453, -8689599, 18036436, 5803270 } + } +}, +{ /* 23/31 */ + { + { -817581, 6763912, 11803561, 1585585, 10958447, -2671165, 23855391, 4598332, -6159431, -14117438 }, + { -31031306, -14256194, 17332029, -2383520, 31312682, -5967183, 696309, 50292, -20095739, 11763584 }, + { -594563, -2514283, -32234153, 12643980, 12650761, 14811489, 665117, -12613632, -19773211, -10713562 } + }, + { + { 30464590, -11262872, -4127476, -12734478, 19835327, -7105613, -24396175, 2075773, -17020157, 992471 }, + { 18357185, -6994433, 7766382, 16342475, -29324918, 411174, 14578841, 8080033, -11574335, -10601610 }, + { 19598397, 10334610, 12555054, 2555664, 18821899, -10339780, 21873263, 16014234, 26224780, 16452269 } + }, + { + { -30223925, 5145196, 5944548, 16385966, 3976735, 2009897, -11377804, -7618186, -20533829, 3698650 }, + { 14187449, 3448569, -10636236, -10810935, -22663880, -3433596, 7268410, -10890444, 27394301, 12015369 }, + { 19695761, 16087646, 28032085, 12999827, 6817792, 11427614, 20244189, -1312777, -13259127, -3402461 } + }, + { + { 30860103, 12735208, -1888245, -4699734, -16974906, 2256940, -8166013, 12298312, -8550524, -10393462 }, + { -5719826, -11245325, -1910649, 15569035, 26642876, -7587760, -5789354, -15118654, -4976164, 12651793 }, + { -2848395, 9953421, 11531313, -5282879, 26895123, -12697089, -13118820, -16517902, 9768698, -2533218 } + }, + { + { -24719459, 1894651, -287698, -4704085, 15348719, -8156530, 32767513, 12765450, 4940095, 10678226 }, + { 18860224, 15980149, -18987240, -1562570, -26233012, -11071856, -7843882, 13944024, -24372348, 16582019 }, + { -15504260, 4970268, -29893044, 4175593, -20993212, -2199756, -11704054, 15444560, -11003761, 7989037 } + }, + { + { 31490452, 5568061, -2412803, 2182383, -32336847, 4531686, -32078269, 6200206, -19686113, -14800171 }, + { -17308668, -15879940, -31522777, -2831, -32887382, 16375549, 8680158, -16371713, 28550068, -6857132 }, + { -28126887, -5688091, 16837845, -1820458, -6850681, 12700016, -30039981, 4364038, 1155602, 5988841 } + }, + { + { 21890435, -13272907, -12624011, 12154349, -7831873, 15300496, 23148983, -4470481, 24618407, 8283181 }, + { -33136107, -10512751, 9975416, 6841041, -31559793, 16356536, 3070187, -7025928, 1466169, 10740210 }, + { -1509399, -15488185, -13503385, -10655916, 32799044, 909394, -13938903, -5779719, -32164649, -15327040 } + }, + { + { 3960823, -14267803, -28026090, -15918051, -19404858, 13146868, 15567327, 951507, -3260321, -573935 }, + { 24740841, 5052253, -30094131, 8961361, 25877428, 6165135, -24368180, 14397372, -7380369, -6144105 }, + { -28888365, 3510803, -28103278, -1158478, -11238128, -10631454, -15441463, -14453128, -1625486, -6494814 } + } +}, +{ /* 24/31 */ + { + { 793299, -9230478, 8836302, -6235707, -27360908, -2369593, 33152843, -4885251, -9906200, -621852 }, + { 5666233, 525582, 20782575, -8038419, -24538499, 14657740, 16099374, 1468826, -6171428, -15186581 }, + { -4859255, -3779343, -2917758, -6748019, 7778750, 11688288, -30404353, -9871238, -1558923, -9863646 } + }, + { + { 10896332, -7719704, 824275, 472601, -19460308, 3009587, 25248958, 14783338, -30581476, -15757844 }, + { 10566929, 12612572, -31944212, 11118703, -12633376, 12362879, 21752402, 8822496, 24003793, 14264025 }, + { 27713862, -7355973, -11008240, 9227530, 27050101, 2504721, 23886875, -13117525, 13958495, -5732453 } + }, + { + { -23481610, 4867226, -27247128, 3900521, 29838369, -8212291, -31889399, -10041781, 7340521, -15410068 }, + { 4646514, -8011124, -22766023, -11532654, 23184553, 8566613, 31366726, -1381061, -15066784, -10375192 }, + { -17270517, 12723032, -16993061, 14878794, 21619651, -6197576, 27584817, 3093888, -8843694, 3849921 } + }, + { + { -9064912, 2103172, 25561640, -15125738, -5239824, 9582958, 32477045, -9017955, 5002294, -15550259 }, + { -12057553, -11177906, 21115585, -13365155, 8808712, -12030708, 16489530, 13378448, -25845716, 12741426 }, + { -5946367, 10645103, -30911586, 15390284, -3286982, -7118677, 24306472, 15852464, 28834118, -7646072 } + }, + { + { -17335748, -9107057, -24531279, 9434953, -8472084, -583362, -13090771, 455841, 20461858, 5491305 }, + { 13669248, -16095482, -12481974, -10203039, -14569770, -11893198, -24995986, 11293807, -28588204, -9421832 }, + { 28497928, 6272777, -33022994, 14470570, 8906179, -1225630, 18504674, -14165166, 29867745, -8795943 } + }, + { + { -16207023, 13517196, -27799630, -13697798, 24009064, -6373891, -6367600, -13175392, 22853429, -4012011 }, + { 24191378, 16712145, -13931797, 15217831, 14542237, 1646131, 18603514, -11037887, 12876623, -2112447 }, + { 17902668, 4518229, -411702, -2829247, 26878217, 5258055, -12860753, 608397, 16031844, 3723494 } + }, + { + { -28632773, 12763728, -20446446, 7577504, 33001348, -13017745, 17558842, -7872890, 23896954, -4314245 }, + { -20005381, -12011952, 31520464, 605201, 2543521, 5991821, -2945064, 7229064, -9919646, -8826859 }, + { 28816045, 298879, -28165016, -15920938, 19000928, -1665890, -12680833, -2949325, -18051778, -2082915 } + }, + { + { 16000882, -344896, 3493092, -11447198, -29504595, -13159789, 12577740, 16041268, -19715240, 7847707 }, + { 10151868, 10572098, 27312476, 7922682, 14825339, 4723128, -32855931, -6519018, -10020567, 3852848 }, + { -11430470, 15697596, -21121557, -4420647, 5386314, 15063598, 16514493, -15932110, 29330899, -15076224 } + } +}, +{ /* 25/31 */ + { + { -25499735, -4378794, -15222908, -6901211, 16615731, 2051784, 3303702, 15490, -27548796, 12314391 }, + { 15683520, -6003043, 18109120, -9980648, 15337968, -5997823, -16717435, 15921866, 16103996, -3731215 }, + { -23169824, -10781249, 13588192, -1628807, -3798557, -1074929, -19273607, 5402699, -29815713, -9841101 } + }, + { + { 23190676, 2384583, -32714340, 3462154, -29903655, -1529132, -11266856, 8911517, -25205859, 2739713 }, + { 21374101, -3554250, -33524649, 9874411, 15377179, 11831242, -33529904, 6134907, 4931255, 11987849 }, + { -7732, -2978858, -16223486, 7277597, 105524, -322051, -31480539, 13861388, -30076310, 10117930 } + }, + { + { -29501170, -10744872, -26163768, 13051539, -25625564, 5089643, -6325503, 6704079, 12890019, 15728940 }, + { -21972360, -11771379, -951059, -4418840, 14704840, 2695116, 903376, -10428139, 12885167, 8311031 }, + { -17516482, 5352194, 10384213, -13811658, 7506451, 13453191, 26423267, 4384730, 1888765, -5435404 } + }, + { + { -25817338, -3107312, -13494599, -3182506, 30896459, -13921729, -32251644, -12707869, -19464434, -3340243 }, + { -23607977, -2665774, -526091, 4651136, 5765089, 4618330, 6092245, 14845197, 17151279, -9854116 }, + { -24830458, -12733720, -15165978, 10367250, -29530908, -265356, 22825805, -7087279, -16866484, 16176525 } + }, + { + { -23583256, 6564961, 20063689, 3798228, -4740178, 7359225, 2006182, -10363426, -28746253, -10197509 }, + { -10626600, -4486402, -13320562, -5125317, 3432136, -6393229, 23632037, -1940610, 32808310, 1099883 }, + { 15030977, 5768825, -27451236, -2887299, -6427378, -15361371, -15277896, -6809350, 2051441, -15225865 } + }, + { + { -3362323, -7239372, 7517890, 9824992, 23555850, 295369, 5148398, -14154188, -22686354, 16633660 }, + { 4577086, -16752288, 13249841, -15304328, 19958763, -14537274, 18559670, -10759549, 8402478, -9864273 }, + { -28406330, -1051581, -26790155, -907698, -17212414, -11030789, 9453451, -14980072, 17983010, 9967138 } + }, + { + { -25762494, 6524722, 26585488, 9969270, 24709298, 1220360, -1677990, 7806337, 17507396, 3651560 }, + { -10420457, -4118111, 14584639, 15971087, -15768321, 8861010, 26556809, -5574557, -18553322, -11357135 }, + { 2839101, 14284142, 4029895, 3472686, 14402957, 12689363, -26642121, 8459447, -5605463, -7621941 } + }, + { + { -4839289, -3535444, 9744961, 2871048, 25113978, 3187018, -25110813, -849066, 17258084, -7977739 }, + { 18164541, -10595176, -17154882, -1542417, 19237078, -9745295, 23357533, -15217008, 26908270, 12150756 }, + { -30264870, -7647865, 5112249, -7036672, -1499807, -6974257, 43168, -5537701, -32302074, 16215819 } + } +}, +{ /* 26/31 */ + { + { -6898905, 9824394, -12304779, -4401089, -31397141, -6276835, 32574489, 12532905, -7503072, -8675347 }, + { -27343522, -16515468, -27151524, -10722951, 946346, 16291093, 254968, 7168080, 21676107, -1943028 }, + { 21260961, -8424752, -16831886, -11920822, -23677961, 3968121, -3651949, -6215466, -3556191, -7913075 } + }, + { + { 16544754, 13250366, -16804428, 15546242, -4583003, 12757258, -2462308, -8680336, -18907032, -9662799 }, + { -2415239, -15577728, 18312303, 4964443, -15272530, -12653564, 26820651, 16690659, 25459437, -4564609 }, + { -25144690, 11425020, 28423002, -11020557, -6144921, -15826224, 9142795, -2391602, -6432418, -1644817 } + }, + { + { -23104652, 6253476, 16964147, -3768872, -25113972, -12296437, -27457225, -16344658, 6335692, 7249989 }, + { -30333227, 13979675, 7503222, -12368314, -11956721, -4621693, -30272269, 2682242, 25993170, -12478523 }, + { 4364628, 5930691, 32304656, -10044554, -8054781, 15091131, 22857016, -10598955, 31820368, 15075278 } + }, + { + { 31879134, -8918693, 17258761, 90626, -8041836, -4917709, 24162788, -9650886, -17970238, 12833045 }, + { 19073683, 14851414, -24403169, -11860168, 7625278, 11091125, -19619190, 2074449, -9413939, 14905377 }, + { 24483667, -11935567, -2518866, -11547418, -1553130, 15355506, -25282080, 9253129, 27628530, -7555480 } + }, + { + { 17597607, 8340603, 19355617, 552187, 26198470, -3176583, 4593324, -9157582, -14110875, 15297016 }, + { 510886, 14337390, -31785257, 16638632, 6328095, 2713355, -20217417, -11864220, 8683221, 2921426 }, + { 18606791, 11874196, 27155355, -5281482, -24031742, 6265446, -25178240, -1278924, 4674690, 13890525 } + }, + { + { 13609624, 13069022, -27372361, -13055908, 24360586, 9592974, 14977157, 9835105, 4389687, 288396 }, + { 9922506, -519394, 13613107, 5883594, -18758345, -434263, -12304062, 8317628, 23388070, 16052080 }, + { 12720016, 11937594, -31970060, -5028689, 26900120, 8561328, -20155687, -11632979, -14754271, -10812892 } + }, + { + { 15961858, 14150409, 26716931, -665832, -22794328, 13603569, 11829573, 7467844, -28822128, 929275 }, + { 11038231, -11582396, -27310482, -7316562, -10498527, -16307831, -23479533, -9371869, -21393143, 2465074 }, + { 20017163, -4323226, 27915242, 1529148, 12396362, 15675764, 13817261, -9658066, 2463391, -4622140 } + }, + { + { -16358878, -12663911, -12065183, 4996454, -1256422, 1073572, 9583558, 12851107, 4003896, 12673717 }, + { -1731589, -15155870, -3262930, 16143082, 19294135, 13385325, 14741514, -9103726, 7903886, 2348101 }, + { 24536016, -16515207, 12715592, -3862155, 1511293, 10047386, -3842346, -7129159, -28377538, 10048127 } + } +}, +{ /* 27/31 */ + { + { -12622226, -6204820, 30718825, 2591312, -10617028, 12192840, 18873298, -7297090, -32297756, 15221632 }, + { -26478122, -11103864, 11546244, -1852483, 9180880, 7656409, -21343950, 2095755, 29769758, 6593415 }, + { -31994208, -2907461, 4176912, 3264766, 12538965, -868111, 26312345, -6118678, 30958054, 8292160 } + }, + { + { 31429822, -13959116, 29173532, 15632448, 12174511, -2760094, 32808831, 3977186, 26143136, -3148876 }, + { 22648901, 1402143, -22799984, 13746059, 7936347, 365344, -8668633, -1674433, -3758243, -2304625 }, + { -15491917, 8012313, -2514730, -12702462, -23965846, -10254029, -1612713, -1535569, -16664475, 8194478 } + }, + { + { 27338066, -7507420, -7414224, 10140405, -19026427, -6589889, 27277191, 8855376, 28572286, 3005164 }, + { 26287124, 4821776, 25476601, -4145903, -3764513, -15788984, -18008582, 1182479, -26094821, -13079595 }, + { -7171154, 3178080, 23970071, 6201893, -17195577, -4489192, -21876275, -13982627, 32208683, -1198248 } + }, + { + { -16657702, 2817643, -10286362, 14811298, 6024667, 13349505, -27315504, -10497842, -27672585, -11539858 }, + { 15941029, -9405932, -21367050, 8062055, 31876073, -238629, -15278393, -1444429, 15397331, -4130193 }, + { 8934485, -13485467, -23286397, -13423241, -32446090, 14047986, 31170398, -1441021, -27505566, 15087184 } + }, + { + { -18357243, -2156491, 24524913, -16677868, 15520427, -6360776, -15502406, 11461896, 16788528, -5868942 }, + { -1947386, 16013773, 21750665, 3714552, -17401782, -16055433, -3770287, -10323320, 31322514, -11615635 }, + { 21426655, -5650218, -13648287, -5347537, -28812189, -4920970, -18275391, -14621414, 13040862, -12112948 } + }, + { + { 11293895, 12478086, -27136401, 15083750, -29307421, 14748872, 14555558, -13417103, 1613711, 4896935 }, + { -25894883, 15323294, -8489791, -8057900, 25967126, -13425460, 2825960, -4897045, -23971776, -11267415 }, + { -15924766, -5229880, -17443532, 6410664, 3622847, 10243618, 20615400, 12405433, -23753030, -8436416 } + }, + { + { -7091295, 12556208, -20191352, 9025187, -17072479, 4333801, 4378436, 2432030, 23097949, -566018 }, + { 4565804, -16025654, 20084412, -7842817, 1724999, 189254, 24767264, 10103221, -18512313, 2424778 }, + { 366633, -11976806, 8173090, -6890119, 30788634, 5745705, -7168678, 1344109, -3642553, 12412659 } + }, + { + { -24001791, 7690286, 14929416, -168257, -32210835, -13412986, 24162697, -15326504, -3141501, 11179385 }, + { 18289522, -14724954, 8056945, 16430056, -21729724, 7842514, -6001441, -1486897, -18684645, -11443503 }, + { 476239, 6601091, -6152790, -9723375, 17503545, -4863900, 27672959, 13403813, 11052904, 5219329 } + } +}, +{ /* 28/31 */ + { + { 20678546, -8375738, -32671898, 8849123, -5009758, 14574752, 31186971, -3973730, 9014762, -8579056 }, + { -13644050, -10350239, -15962508, 5075808, -1514661, -11534600, -33102500, 9160280, 8473550, -3256838 }, + { 24900749, 14435722, 17209120, -15292541, -22592275, 9878983, -7689309, -16335821, -24568481, 11788948 } + }, + { + { -3118155, -11395194, -13802089, 14797441, 9652448, -6845904, -20037437, 10410733, -24568470, -1458691 }, + { -15659161, 16736706, -22467150, 10215878, -9097177, 7563911, 11871841, -12505194, -18513325, 8464118 }, + { -23400612, 8348507, -14585951, -861714, -3950205, -6373419, 14325289, 8628612, 33313881, -8370517 } + }, + { + { -20186973, -4967935, 22367356, 5271547, -1097117, -4788838, -24805667, -10236854, -8940735, -5818269 }, + { -6948785, -1795212, -32625683, -16021179, 32635414, -7374245, 15989197, -12838188, 28358192, -4253904 }, + { -23561781, -2799059, -32351682, -1661963, -9147719, 10429267, -16637684, 4072016, -5351664, 5596589 } + }, + { + { -28236598, -3390048, 12312896, 6213178, 3117142, 16078565, 29266239, 2557221, 1768301, 15373193 }, + { -7243358, -3246960, -4593467, -7553353, -127927, -912245, -1090902, -4504991, -24660491, 3442910 }, + { -30210571, 5124043, 14181784, 8197961, 18964734, -11939093, 22597931, 7176455, -18585478, 13365930 } + }, + { + { -7877390, -1499958, 8324673, 4690079, 6261860, 890446, 24538107, -8570186, -9689599, -3031667 }, + { 25008904, -10771599, -4305031, -9638010, 16265036, 15721635, 683793, -11823784, 15723479, -15163481 }, + { -9660625, 12374379, -27006999, -7026148, -7724114, -12314514, 11879682, 5400171, 519526, -1235876 } + }, + { + { 22258397, -16332233, -7869817, 14613016, -22520255, -2950923, -20353881, 7315967, 16648397, 7605640 }, + { -8081308, -8464597, -8223311, 9719710, 19259459, -15348212, 23994942, -5281555, -9468848, 4763278 }, + { -21699244, 9220969, -15730624, 1084137, -25476107, -2852390, 31088447, -7764523, -11356529, 728112 } + }, + { + { 26047220, -11751471, -6900323, -16521798, 24092068, 9158119, -4273545, -12555558, -29365436, -5498272 }, + { 17510331, -322857, 5854289, 8403524, 17133918, -3112612, -28111007, 12327945, 10750447, 10014012 }, + { -10312768, 3936952, 9156313, -8897683, 16498692, -994647, -27481051, -666732, 3424691, 7540221 } + }, + { + { 30322361, -6964110, 11361005, -4143317, 7433304, 4989748, -7071422, -16317219, -9244265, 15258046 }, + { 13054562, -2779497, 19155474, 469045, -12482797, 4566042, 5631406, 2711395, 1062915, -5136345 }, + { -19240248, -11254599, -29509029, -7499965, -5835763, 13005411, -6066489, 12194497, 32960380, 1459310 } + } +}, +{ /* 29/31 */ + { + { 19852034, 7027924, 23669353, 10020366, 8586503, -6657907, 394197, -6101885, 18638003, -11174937 }, + { 31395534, 15098109, 26581030, 8030562, -16527914, -5007134, 9012486, -7584354, -6643087, -5442636 }, + { -9192165, -2347377, -1997099, 4529534, 25766844, 607986, -13222, 9677543, -32294889, -6456008 } + }, + { + { -2444496, -149937, 29348902, 8186665, 1873760, 12489863, -30934579, -7839692, -7852844, -8138429 }, + { -15236356, -15433509, 7766470, 746860, 26346930, -10221762, -27333451, 10754588, -9431476, 5203576 }, + { 31834314, 14135496, -770007, 5159118, 20917671, -16768096, -7467973, -7337524, 31809243, 7347066 } + }, + { + { -9606723, -11874240, 20414459, 13033986, 13716524, -11691881, 19797970, -12211255, 15192876, -2087490 }, + { -12663563, -2181719, 1168162, -3804809, 26747877, -14138091, 10609330, 12694420, 33473243, -13382104 }, + { 33184999, 11180355, 15832085, -11385430, -1633671, 225884, 15089336, -11023903, -6135662, 14480053 } + }, + { + { 31308717, -5619998, 31030840, -1897099, 15674547, -6582883, 5496208, 13685227, 27595050, 8737275 }, + { -20318852, -15150239, 10933843, -16178022, 8335352, -7546022, -31008351, -12610604, 26498114, 66511 }, + { 22644454, -8761729, -16671776, 4884562, -3105614, -13559366, 30540766, -4286747, -13327787, -7515095 } + }, + { + { -28017847, 9834845, 18617207, -2681312, -3401956, -13307506, 8205540, 13585437, -17127465, 15115439 }, + { 23711543, -672915, 31206561, -8362711, 6164647, -9709987, -33535882, -1426096, 8236921, 16492939 }, + { -23910559, -13515526, -26299483, -4503841, 25005590, -7687270, 19574902, 10071562, 6708380, -6222424 } + }, + { + { 2101391, -4930054, 19702731, 2367575, -15427167, 1047675, 5301017, 9328700, 29955601, -11678310 }, + { 3096359, 9271816, -21620864, -15521844, -14847996, -7592937, -25892142, -12635595, -9917575, 6216608 }, + { -32615849, 338663, -25195611, 2510422, -29213566, -13820213, 24822830, -6146567, -26767480, 7525079 } + }, + { + { -23066649, -13985623, 16133487, -7896178, -3389565, 778788, -910336, -2782495, -19386633, 11994101 }, + { 21691500, -13624626, -641331, -14367021, 3285881, -3483596, -25064666, 9718258, -7477437, 13381418 }, + { 18445390, -4202236, 14979846, 11622458, -1727110, -3582980, 23111648, -6375247, 28535282, 15779576 } + }, + { + { 30098053, 3089662, -9234387, 16662135, -21306940, 11308411, -14068454, 12021730, 9955285, -16303356 }, + { 9734894, -14576830, -7473633, -9138735, 2060392, 11313496, -18426029, 9924399, 20194861, 13380996 }, + { -26378102, -7965207, -22167821, 15789297, -18055342, -6168792, -1984914, 15707771, 26342023, 10146099 } + } +}, +{ /* 30/31 */ + { + { -26016874, -219943, 21339191, -41388, 19745256, -2878700, -29637280, 2227040, 21612326, -545728 }, + { -13077387, 1184228, 23562814, -5970442, -20351244, -6348714, 25764461, 12243797, -20856566, 11649658 }, + { -10031494, 11262626, 27384172, 2271902, 26947504, -15997771, 39944, 6114064, 33514190, 2333242 } + }, + { + { -21433588, -12421821, 8119782, 7219913, -21830522, -9016134, -6679750, -12670638, 24350578, -13450001 }, + { -4116307, -11271533, -23886186, 4843615, -30088339, 690623, -31536088, -10406836, 8317860, 12352766 }, + { 18200138, -14475911, -33087759, -2696619, -23702521, -9102511, -23552096, -2287550, 20712163, 6719373 } + }, + { + { 26656208, 6075253, -7858556, 1886072, -28344043, 4262326, 11117530, -3763210, 26224235, -3297458 }, + { -17168938, -14854097, -3395676, -16369877, -19954045, 14050420, 21728352, 9493610, 18620611, -16428628 }, + { -13323321, 13325349, 11432106, 5964811, 18609221, 6062965, -5269471, -9725556, -30701573, -16479657 } + }, + { + { -23860538, -11233159, 26961357, 1640861, -32413112, -16737940, 12248509, -5240639, 13735342, 1934062 }, + { 25089769, 6742589, 17081145, -13406266, 21909293, -16067981, -15136294, -3765346, -21277997, 5473616 }, + { 31883677, -7961101, 1083432, -11572403, 22828471, 13290673, -7125085, 12469656, 29111212, -5451014 } + }, + { + { 24244947, -15050407, -26262976, 2791540, -14997599, 16666678, 24367466, 6388839, -10295587, 452383 }, + { -25640782, -3417841, 5217916, 16224624, 19987036, -4082269, -24236251, -5915248, 15766062, 8407814 }, + { -20406999, 13990231, 15495425, 16395525, 5377168, 15166495, -8917023, -4388953, -8067909, 2276718 } + }, + { + { 30157918, 12924066, -17712050, 9245753, 19895028, 3368142, -23827587, 5096219, 22740376, -7303417 }, + { 2041139, -14256350, 7783687, 13876377, -25946985, -13352459, 24051124, 13742383, -15637599, 13295222 }, + { 33338237, -8505733, 12532113, 7977527, 9106186, -1715251, -17720195, -4612972, -4451357, -14669444 } + }, + { + { -20045281, 5454097, -14346548, 6447146, 28862071, 1883651, -2469266, -4141880, 7770569, 9620597 }, + { 23208068, 7979712, 33071466, 8149229, 1758231, -10834995, 30945528, -1694323, -33502340, -14767970 }, + { 1439958, -16270480, -1079989, -793782, 4625402, 10647766, -5043801, 1220118, 30494170, -11440799 } + }, + { + { -5037580, -13028295, -2970559, -3061767, 15640974, -6701666, -26739026, 926050, -1684339, -13333647 }, + { 13908495, -3549272, 30919928, -6273825, -21521863, 7989039, 9021034, 9078865, 3353509, 4033511 }, + { -29663431, -15113610, 32259991, -344482, 24295849, -12912123, 23161163, 8839127, 27485041, 7356032 } + } +}, +{ /* 31/31 */ + { + { 9661027, 705443, 11980065, -5370154, -1628543, 14661173, -6346142, 2625015, 28431036, -16771834 }, + { -23839233, -8311415, -25945511, 7480958, -17681669, -8354183, -22545972, 14150565, 15970762, 4099461 }, + { 29262576, 16756590, 26350592, -8793563, 8529671, -11208050, 13617293, -9937143, 11465739, 8317062 } + }, + { + { -25493081, -6962928, 32500200, -9419051, -23038724, -2302222, 14898637, 3848455, 20969334, -5157516 }, + { -20384450, -14347713, -18336405, 13884722, -33039454, 2842114, -21610826, -3649888, 11177095, 14989547 }, + { -24496721, -11716016, 16959896, 2278463, 12066309, 10137771, 13515641, 2581286, -28487508, 9930240 } + }, + { + { -17751622, -2097826, 16544300, -13009300, -15914807, -14949081, 18345767, -13403753, 16291481, -5314038 }, + { -33229194, 2553288, 32678213, 9875984, 8534129, 6889387, -9676774, 6957617, 4368891, 9788741 }, + { 16660756, 7281060, -10830758, 12911820, 20108584, -8101676, -21722536, -8613148, 16250552, -11111103 } + }, + { + { -19765507, 2390526, -16551031, 14161980, 1905286, 6414907, 4689584, 10604807, -30190403, 4782747 }, + { -1354539, 14736941, -7367442, -13292886, 7710542, -14155590, -9981571, 4383045, 22546403, 437323 }, + { 31665577, -12180464, -16186830, 1491339, -18368625, 3294682, 27343084, 2786261, -30633590, -14097016 } + }, + { + { -14467279, -683715, -33374107, 7448552, 19294360, 14334329, -19690631, 2355319, -19284671, -6114373 }, + { 15121312, -15796162, 6377020, -6031361, -10798111, -12957845, 18952177, 15496498, -29380133, 11754228 }, + { -2637277, -13483075, 8488727, -14303896, 12728761, -1622493, 7141596, 11724556, 22761615, -10134141 } + }, + { + { 16918416, 11729663, -18083579, 3022987, -31015732, -13339659, -28741185, -12227393, 32851222, 11717399 }, + { 11166634, 7338049, -6722523, 4531520, -29468672, -7302055, 31474879, 3483633, -1193175, -4030831 }, + { -185635, 9921305, 31456609, -13536438, -12013818, 13348923, 33142652, 6546660, -19985279, -3948376 } + }, + { + { -32460596, 11266712, -11197107, -7899103, 31703694, 3855903, -8537131, -12833048, -30772034, -15486313 }, + { -18006477, 12709068, 3991746, -6479188, -21491523, -10550425, -31135347, -16049879, 10928917, 3011958 }, + { -6957757, -15594337, 31696059, 334240, 29576716, 14796075, -30831056, -12805180, 18008031, 10258577 } + }, + { + { -22448644, 15655569, 7018479, -4410003, -30314266, -1201591, -1853465, 1367120, 25127874, 6671743 }, + { 29701166, -14373934, -10878120, 9279288, -17568, 13127210, 21382910, 11042292, 25838796, 4642684 }, + { -20430234, 14955537, -24126347, 8124619, -5369288, -5990470, 30468147, -13900640, 18423289, 4177476 } + } +} diff --git a/packages/runtime/vendor/libsodium/fe_25_5/base2.h b/packages/runtime/vendor/libsodium/fe_25_5/base2.h new file mode 100644 index 000000000..90a1457ea --- /dev/null +++ b/packages/runtime/vendor/libsodium/fe_25_5/base2.h @@ -0,0 +1,40 @@ +{ + { 25967493, -14356035, 29566456, 3660896, -12694345, 4014787, 27544626, -11754271, -6079156, 2047605 }, + { -12545711, 934262, -2722910, 3049990, -727428, 9406986, 12720692, 5043384, 19500929, -15469378 }, + { -8738181, 4489570, 9688441, -14785194, 10184609, -12363380, 29287919, 11864899, -24514362, -4438546 } +}, +{ + { 15636291, -9688557, 24204773, -7912398, 616977, -16685262, 27787600, -14772189, 28944400, -1550024 }, + { 16568933, 4717097, -11556148, -1102322, 15682896, -11807043, 16354577, -11775962, 7689662, 11199574 }, + { 30464156, -5976125, -11779434, -15670865, 23220365, 15915852, 7512774, 10017326, -17749093, -9920357 } +}, +{ + { 10861363, 11473154, 27284546, 1981175, -30064349, 12577861, 32867885, 14515107, -15438304, 10819380 }, + { 4708026, 6336745, 20377586, 9066809, -11272109, 6594696, -25653668, 12483688, -12668491, 5581306 }, + { 19563160, 16186464, -29386857, 4097519, 10237984, -4348115, 28542350, 13850243, -23678021, -15815942 } +}, +{ + { 5153746, 9909285, 1723747, -2777874, 30523605, 5516873, 19480852, 5230134, -23952439, -15175766 }, + { -30269007, -3463509, 7665486, 10083793, 28475525, 1649722, 20654025, 16520125, 30598449, 7715701 }, + { 28881845, 14381568, 9657904, 3680757, -20181635, 7843316, -31400660, 1370708, 29794553, -1409300 } +}, +{ + { -22518993, -6692182, 14201702, -8745502, -23510406, 8844726, 18474211, -1361450, -13062696, 13821877 }, + { -6455177, -7839871, 3374702, -4740862, -27098617, -10571707, 31655028, -7212327, 18853322, -14220951 }, + { 4566830, -12963868, -28974889, -12240689, -7602672, -2830569, -8514358, -10431137, 2207753, -3209784 } +}, +{ + { -25154831, -4185821, 29681144, 7868801, -6854661, -9423865, -12437364, -663000, -31111463, -16132436 }, + { 25576264, -2703214, 7349804, -11814844, 16472782, 9300885, 3844789, 15725684, 171356, 6466918 }, + { 23103977, 13316479, 9739013, -16149481, 817875, -15038942, 8965339, -14088058, -30714912, 16193877 } +}, +{ + { -33521811, 3180713, -2394130, 14003687, -16903474, -16270840, 17238398, 4729455, -18074513, 9256800 }, + { -25182317, -4174131, 32336398, 5036987, -21236817, 11360617, 22616405, 9761698, -19827198, 630305 }, + { -13720693, 2639453, -24237460, -7406481, 9494427, -5774029, -6554551, -15960994, -2449256, -14291300 } +}, +{ + { -3151181, -5046075, 9282714, 6866145, -31907062, -863023, -18940575, 15033784, 25105118, -7894876 }, + { -24326370, 15950226, -31801215, -14592823, -11662737, -5090925, 1573892, -2625887, 2198790, -15804619 }, + { -3099351, 10324967, -2241613, 7453183, -5446979, -2735503, -13812022, -16236442, -32461234, -12290683 } +} diff --git a/packages/runtime/vendor/libsodium/fe_25_5/constants.h b/packages/runtime/vendor/libsodium/fe_25_5/constants.h new file mode 100644 index 000000000..dd6da1b87 --- /dev/null +++ b/packages/runtime/vendor/libsodium/fe_25_5/constants.h @@ -0,0 +1,40 @@ +/* 37095705934669439343138083508754565189542113879843219016388785533085940283555 */ +static const fe25519 d = { + -10913610, 13857413, -15372611, 6949391, 114729, -8787816, -6275908, -3247719, -18696448, -12055116 +}; + +/* 2 * d = + * 16295367250680780974490674513165176452449235426866156013048779062215315747161 + */ +static const fe25519 d2 = { + -21827239, -5839606, -30745221, 13898782, 229458, 15978800, -12551817, -6495438, 29715968, 9444199 }; + +/* sqrt(-1) */ +static const fe25519 sqrtm1 = { + -32595792, -7943725, 9377950, 3500415, 12389472, -272473, -25146209, -2005654, 326686, 11406482 +}; + +/* A = 486662 */ +static const fe25519 curve25519_A = { + 486662, 0, 0, 0, 0, 0, 0, 0, 0, 0 +}; + +/* sqrt(ad - 1) with a = -1 (mod p) */ +static const fe25519 sqrtadm1 = { + 24849947, -153582, -23613485, 6347715, -21072328, -667138, -25271143, -15367704, -870347, 14525639 +}; + +/* 1 / sqrt(a - d) */ +static const fe25519 invsqrtamd = { + 6111485, 4156064, -27798727, 12243468, -25904040, 120897, 20826367, -7060776, 6093568, -1986012 +}; + +/* 1 - d ^ 2 */ +static const fe25519 onemsqd = { + 6275446, -16617371, -22938544, -3773710, 11667077, 7397348, -27922721, 1766195, -24433858, 672203 +}; + +/* (d - 1) ^ 2 */ +static const fe25519 sqdmone = { + 15551795, -11097455, -13425098, -10125071, -11896535, 10178284, -26634327, 4729244, -5282110, -10116402 +}; diff --git a/packages/runtime/vendor/libsodium/fe_25_5/fe.h b/packages/runtime/vendor/libsodium/fe_25_5/fe.h new file mode 100644 index 000000000..9e11afd11 --- /dev/null +++ b/packages/runtime/vendor/libsodium/fe_25_5/fe.h @@ -0,0 +1,220 @@ +/* + Ignores top bit of s. + */ + +void +fe25519_frombytes(fe25519 h, const unsigned char *s) +{ + int64_t h0 = load_4(s); + int64_t h1 = load_3(s + 4) << 6; + int64_t h2 = load_3(s + 7) << 5; + int64_t h3 = load_3(s + 10) << 3; + int64_t h4 = load_3(s + 13) << 2; + int64_t h5 = load_4(s + 16); + int64_t h6 = load_3(s + 20) << 7; + int64_t h7 = load_3(s + 23) << 5; + int64_t h8 = load_3(s + 26) << 4; + int64_t h9 = (load_3(s + 29) & 8388607) << 2; + + int64_t carry0; + int64_t carry1; + int64_t carry2; + int64_t carry3; + int64_t carry4; + int64_t carry5; + int64_t carry6; + int64_t carry7; + int64_t carry8; + int64_t carry9; + + carry9 = (h9 + (int64_t)(1L << 24)) >> 25; + h0 += carry9 * 19; + h9 -= carry9 * ((uint64_t) 1L << 25); + carry1 = (h1 + (int64_t)(1L << 24)) >> 25; + h2 += carry1; + h1 -= carry1 * ((uint64_t) 1L << 25); + carry3 = (h3 + (int64_t)(1L << 24)) >> 25; + h4 += carry3; + h3 -= carry3 * ((uint64_t) 1L << 25); + carry5 = (h5 + (int64_t)(1L << 24)) >> 25; + h6 += carry5; + h5 -= carry5 * ((uint64_t) 1L << 25); + carry7 = (h7 + (int64_t)(1L << 24)) >> 25; + h8 += carry7; + h7 -= carry7 * ((uint64_t) 1L << 25); + + carry0 = (h0 + (int64_t)(1L << 25)) >> 26; + h1 += carry0; + h0 -= carry0 * ((uint64_t) 1L << 26); + carry2 = (h2 + (int64_t)(1L << 25)) >> 26; + h3 += carry2; + h2 -= carry2 * ((uint64_t) 1L << 26); + carry4 = (h4 + (int64_t)(1L << 25)) >> 26; + h5 += carry4; + h4 -= carry4 * ((uint64_t) 1L << 26); + carry6 = (h6 + (int64_t)(1L << 25)) >> 26; + h7 += carry6; + h6 -= carry6 * ((uint64_t) 1L << 26); + carry8 = (h8 + (int64_t)(1L << 25)) >> 26; + h9 += carry8; + h8 -= carry8 * ((uint64_t) 1L << 26); + + h[0] = (int32_t) h0; + h[1] = (int32_t) h1; + h[2] = (int32_t) h2; + h[3] = (int32_t) h3; + h[4] = (int32_t) h4; + h[5] = (int32_t) h5; + h[6] = (int32_t) h6; + h[7] = (int32_t) h7; + h[8] = (int32_t) h8; + h[9] = (int32_t) h9; +} + +/* + Preconditions: + |h| bounded by 1.1*2^26,1.1*2^25,1.1*2^26,1.1*2^25,etc. + + Write p=2^255-19; q=floor(h/p). + Basic claim: q = floor(2^(-255)(h + 19 2^(-25)h9 + 2^(-1))). + + Proof: + Have |h|<=p so |q|<=1 so |19^2 2^(-255) q|<1/4. + Also have |h-2^230 h9|<2^231 so |19 2^(-255)(h-2^230 h9)|<1/4. + + Write y=2^(-1)-19^2 2^(-255)q-19 2^(-255)(h-2^230 h9). + Then 0> 25; + q = (h0 + q) >> 26; + q = (h1 + q) >> 25; + q = (h2 + q) >> 26; + q = (h3 + q) >> 25; + q = (h4 + q) >> 26; + q = (h5 + q) >> 25; + q = (h6 + q) >> 26; + q = (h7 + q) >> 25; + q = (h8 + q) >> 26; + q = (h9 + q) >> 25; + + /* Goal: Output h-(2^255-19)q, which is between 0 and 2^255-20. */ + h0 += 19 * q; + /* Goal: Output h-2^255 q, which is between 0 and 2^255-20. */ + + carry0 = h0 >> 26; + h1 += carry0; + h0 -= carry0 * ((uint32_t) 1L << 26); + carry1 = h1 >> 25; + h2 += carry1; + h1 -= carry1 * ((uint32_t) 1L << 25); + carry2 = h2 >> 26; + h3 += carry2; + h2 -= carry2 * ((uint32_t) 1L << 26); + carry3 = h3 >> 25; + h4 += carry3; + h3 -= carry3 * ((uint32_t) 1L << 25); + carry4 = h4 >> 26; + h5 += carry4; + h4 -= carry4 * ((uint32_t) 1L << 26); + carry5 = h5 >> 25; + h6 += carry5; + h5 -= carry5 * ((uint32_t) 1L << 25); + carry6 = h6 >> 26; + h7 += carry6; + h6 -= carry6 * ((uint32_t) 1L << 26); + carry7 = h7 >> 25; + h8 += carry7; + h7 -= carry7 * ((uint32_t) 1L << 25); + carry8 = h8 >> 26; + h9 += carry8; + h8 -= carry8 * ((uint32_t) 1L << 26); + carry9 = h9 >> 25; + h9 -= carry9 * ((uint32_t) 1L << 25); + + h[0] = h0; + h[1] = h1; + h[2] = h2; + h[3] = h3; + h[4] = h4; + h[5] = h5; + h[6] = h6; + h[7] = h7; + h[8] = h8; + h[9] = h9; +} + +/* + Goal: Output h0+...+2^255 h10-2^255 q, which is between 0 and 2^255-20. + Have h0+...+2^230 h9 between 0 and 2^255-1; + evidently 2^255 h10-2^255 q = 0. + + Goal: Output h0+...+2^230 h9. + */ + +void +fe25519_tobytes(unsigned char *s, const fe25519 h) +{ + fe25519 t; + + fe25519_reduce(t, h); + s[0] = t[0] >> 0; + s[1] = t[0] >> 8; + s[2] = t[0] >> 16; + s[3] = (t[0] >> 24) | (t[1] * ((uint32_t) 1 << 2)); + s[4] = t[1] >> 6; + s[5] = t[1] >> 14; + s[6] = (t[1] >> 22) | (t[2] * ((uint32_t) 1 << 3)); + s[7] = t[2] >> 5; + s[8] = t[2] >> 13; + s[9] = (t[2] >> 21) | (t[3] * ((uint32_t) 1 << 5)); + s[10] = t[3] >> 3; + s[11] = t[3] >> 11; + s[12] = (t[3] >> 19) | (t[4] * ((uint32_t) 1 << 6)); + s[13] = t[4] >> 2; + s[14] = t[4] >> 10; + s[15] = t[4] >> 18; + s[16] = t[5] >> 0; + s[17] = t[5] >> 8; + s[18] = t[5] >> 16; + s[19] = (t[5] >> 24) | (t[6] * ((uint32_t) 1 << 1)); + s[20] = t[6] >> 7; + s[21] = t[6] >> 15; + s[22] = (t[6] >> 23) | (t[7] * ((uint32_t) 1 << 3)); + s[23] = t[7] >> 5; + s[24] = t[7] >> 13; + s[25] = (t[7] >> 21) | (t[8] * ((uint32_t) 1 << 4)); + s[26] = t[8] >> 4; + s[27] = t[8] >> 12; + s[28] = (t[8] >> 20) | (t[9] * ((uint32_t) 1 << 6)); + s[29] = t[9] >> 2; + s[30] = t[9] >> 10; + s[31] = t[9] >> 18; +} diff --git a/packages/runtime/vendor/libsodium/hash_sha512_cp.c b/packages/runtime/vendor/libsodium/hash_sha512_cp.c new file mode 100644 index 000000000..59a8b9368 --- /dev/null +++ b/packages/runtime/vendor/libsodium/hash_sha512_cp.c @@ -0,0 +1,284 @@ + +/*- + * Copyright 2005,2007,2009 Colin Percival + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + * + */ + +#include +#include +#include +#include + +#include + +#include "crypto_hash_sha512.h" +#include "common.h" +#include "utils.h" + +static void +be64enc_vect(unsigned char *dst, const uint64_t *src, size_t len) +{ + size_t i; + + for (i = 0; i < len / 8; i++) { + STORE64_BE(dst + i * 8, src[i]); + } +} + +static void +be64dec_vect(uint64_t *dst, const unsigned char *src, size_t len) +{ + size_t i; + + for (i = 0; i < len / 8; i++) { + dst[i] = LOAD64_BE(src + i * 8); + } +} + +static const uint64_t Krnd[80] = { + 0x428a2f98d728ae22ULL, 0x7137449123ef65cdULL, 0xb5c0fbcfec4d3b2fULL, + 0xe9b5dba58189dbbcULL, 0x3956c25bf348b538ULL, 0x59f111f1b605d019ULL, + 0x923f82a4af194f9bULL, 0xab1c5ed5da6d8118ULL, 0xd807aa98a3030242ULL, + 0x12835b0145706fbeULL, 0x243185be4ee4b28cULL, 0x550c7dc3d5ffb4e2ULL, + 0x72be5d74f27b896fULL, 0x80deb1fe3b1696b1ULL, 0x9bdc06a725c71235ULL, + 0xc19bf174cf692694ULL, 0xe49b69c19ef14ad2ULL, 0xefbe4786384f25e3ULL, + 0x0fc19dc68b8cd5b5ULL, 0x240ca1cc77ac9c65ULL, 0x2de92c6f592b0275ULL, + 0x4a7484aa6ea6e483ULL, 0x5cb0a9dcbd41fbd4ULL, 0x76f988da831153b5ULL, + 0x983e5152ee66dfabULL, 0xa831c66d2db43210ULL, 0xb00327c898fb213fULL, + 0xbf597fc7beef0ee4ULL, 0xc6e00bf33da88fc2ULL, 0xd5a79147930aa725ULL, + 0x06ca6351e003826fULL, 0x142929670a0e6e70ULL, 0x27b70a8546d22ffcULL, + 0x2e1b21385c26c926ULL, 0x4d2c6dfc5ac42aedULL, 0x53380d139d95b3dfULL, + 0x650a73548baf63deULL, 0x766a0abb3c77b2a8ULL, 0x81c2c92e47edaee6ULL, + 0x92722c851482353bULL, 0xa2bfe8a14cf10364ULL, 0xa81a664bbc423001ULL, + 0xc24b8b70d0f89791ULL, 0xc76c51a30654be30ULL, 0xd192e819d6ef5218ULL, + 0xd69906245565a910ULL, 0xf40e35855771202aULL, 0x106aa07032bbd1b8ULL, + 0x19a4c116b8d2d0c8ULL, 0x1e376c085141ab53ULL, 0x2748774cdf8eeb99ULL, + 0x34b0bcb5e19b48a8ULL, 0x391c0cb3c5c95a63ULL, 0x4ed8aa4ae3418acbULL, + 0x5b9cca4f7763e373ULL, 0x682e6ff3d6b2b8a3ULL, 0x748f82ee5defb2fcULL, + 0x78a5636f43172f60ULL, 0x84c87814a1f0ab72ULL, 0x8cc702081a6439ecULL, + 0x90befffa23631e28ULL, 0xa4506cebde82bde9ULL, 0xbef9a3f7b2c67915ULL, + 0xc67178f2e372532bULL, 0xca273eceea26619cULL, 0xd186b8c721c0c207ULL, + 0xeada7dd6cde0eb1eULL, 0xf57d4f7fee6ed178ULL, 0x06f067aa72176fbaULL, + 0x0a637dc5a2c898a6ULL, 0x113f9804bef90daeULL, 0x1b710b35131c471bULL, + 0x28db77f523047d84ULL, 0x32caab7b40c72493ULL, 0x3c9ebe0a15c9bebcULL, + 0x431d67c49c100d4cULL, 0x4cc5d4becb3e42b6ULL, 0x597f299cfc657e2aULL, + 0x5fcb6fab3ad6faecULL, 0x6c44198c4a475817ULL +}; + +#define Ch(x, y, z) ((x & (y ^ z)) ^ z) +#define Maj(x, y, z) ((x & (y | z)) | (y & z)) +#define SHR(x, n) (x >> n) +#define ROTR(x, n) ROTR64(x, n) +#define S0(x) (ROTR(x, 28) ^ ROTR(x, 34) ^ ROTR(x, 39)) +#define S1(x) (ROTR(x, 14) ^ ROTR(x, 18) ^ ROTR(x, 41)) +#define s0(x) (ROTR(x, 1) ^ ROTR(x, 8) ^ SHR(x, 7)) +#define s1(x) (ROTR(x, 19) ^ ROTR(x, 61) ^ SHR(x, 6)) + +#define RND(a, b, c, d, e, f, g, h, k) \ + h += S1(e) + Ch(e, f, g) + k; \ + d += h; \ + h += S0(a) + Maj(a, b, c); + +#define RNDr(S, W, i, ii) \ + RND(S[(80 - i) % 8], S[(81 - i) % 8], S[(82 - i) % 8], S[(83 - i) % 8], \ + S[(84 - i) % 8], S[(85 - i) % 8], S[(86 - i) % 8], S[(87 - i) % 8], \ + W[i + ii] + Krnd[i + ii]) + +#define MSCH(W, ii, i) \ + W[i + ii + 16] = \ + s1(W[i + ii + 14]) + W[i + ii + 9] + s0(W[i + ii + 1]) + W[i + ii] + +static void +SHA512_Transform(uint64_t *state, const uint8_t block[128], uint64_t W[80], + uint64_t S[8]) +{ + int i; + + be64dec_vect(W, block, 128); + memcpy(S, state, 64); + for (i = 0; i < 80; i += 16) { + RNDr(S, W, 0, i); + RNDr(S, W, 1, i); + RNDr(S, W, 2, i); + RNDr(S, W, 3, i); + RNDr(S, W, 4, i); + RNDr(S, W, 5, i); + RNDr(S, W, 6, i); + RNDr(S, W, 7, i); + RNDr(S, W, 8, i); + RNDr(S, W, 9, i); + RNDr(S, W, 10, i); + RNDr(S, W, 11, i); + RNDr(S, W, 12, i); + RNDr(S, W, 13, i); + RNDr(S, W, 14, i); + RNDr(S, W, 15, i); + if (i == 64) { + break; + } + MSCH(W, 0, i); + MSCH(W, 1, i); + MSCH(W, 2, i); + MSCH(W, 3, i); + MSCH(W, 4, i); + MSCH(W, 5, i); + MSCH(W, 6, i); + MSCH(W, 7, i); + MSCH(W, 8, i); + MSCH(W, 9, i); + MSCH(W, 10, i); + MSCH(W, 11, i); + MSCH(W, 12, i); + MSCH(W, 13, i); + MSCH(W, 14, i); + MSCH(W, 15, i); + } + for (i = 0; i < 8; i++) { + state[i] += S[i]; + } +} + +static const uint8_t PAD[128] = { + 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 +}; + +static void +SHA512_Pad(crypto_hash_sha512_state *state, uint64_t tmp64[80 + 8]) +{ + unsigned int r; + unsigned int i; + + ACQUIRE_FENCE; + r = (unsigned int) ((state->count[1] >> 3) & 0x7f); + if (r < 112) { + for (i = 0; i < 112 - r; i++) { + state->buf[r + i] = PAD[i]; + } + } else { + for (i = 0; i < 128 - r; i++) { + state->buf[r + i] = PAD[i]; + } + SHA512_Transform(state->state, state->buf, &tmp64[0], &tmp64[80]); + memset(&state->buf[0], 0, 112); + } + be64enc_vect(&state->buf[112], state->count, 16); + SHA512_Transform(state->state, state->buf, &tmp64[0], &tmp64[80]); +} + +int +crypto_hash_sha512_init(crypto_hash_sha512_state *state) +{ + static const uint64_t sha512_initial_state[8] = { + 0x6a09e667f3bcc908ULL, 0xbb67ae8584caa73bULL, 0x3c6ef372fe94f82bULL, + 0xa54ff53a5f1d36f1ULL, 0x510e527fade682d1ULL, 0x9b05688c2b3e6c1fULL, + 0x1f83d9abfb41bd6bULL, 0x5be0cd19137e2179ULL + }; + + state->count[0] = state->count[1] = (uint64_t) 0U; + memcpy(state->state, sha512_initial_state, sizeof sha512_initial_state); + + return 0; +} + +int +crypto_hash_sha512_update(crypto_hash_sha512_state *state, + const unsigned char *in, unsigned long long inlen) +{ + uint64_t tmp64[80 + 8]; + uint64_t bitlen[2]; + unsigned long long i; + unsigned long long r; + + if (inlen <= 0U) { + return 0; + } + ACQUIRE_FENCE; + r = (unsigned long long) ((state->count[1] >> 3) & 0x7f); + + bitlen[1] = ((uint64_t) inlen) << 3; + bitlen[0] = ((uint64_t) inlen) >> 61; + /* LCOV_EXCL_START */ + if ((state->count[1] += bitlen[1]) < bitlen[1]) { + state->count[0]++; + } + /* LCOV_EXCL_STOP */ + state->count[0] += bitlen[0]; + if (inlen < 128 - r) { + for (i = 0; i < inlen; i++) { + state->buf[r + i] = in[i]; + } + return 0; + } + for (i = 0; i < 128 - r; i++) { + state->buf[r + i] = in[i]; + } + SHA512_Transform(state->state, state->buf, &tmp64[0], &tmp64[80]); + in += 128 - r; + inlen -= 128 - r; + + while (inlen >= 128) { + SHA512_Transform(state->state, in, &tmp64[0], &tmp64[80]); + in += 128; + inlen -= 128; + } + inlen &= 127; + for (i = 0; i < inlen; i++) { + state->buf[i] = in[i]; + } + sodium_memzero((void *) tmp64, sizeof tmp64); + + return 0; +} + +int +crypto_hash_sha512_final(crypto_hash_sha512_state *state, unsigned char *out) +{ + uint64_t tmp64[80 + 8]; + + SHA512_Pad(state, tmp64); + be64enc_vect(out, state->state, 64); + sodium_memzero((void *) tmp64, sizeof tmp64); + sodium_memzero((void *) state, sizeof *state); + + return 0; +} + +int +crypto_hash_sha512(unsigned char *out, const unsigned char *in, + unsigned long long inlen) +{ + crypto_hash_sha512_state state; + + crypto_hash_sha512_init(&state); + crypto_hash_sha512_update(&state, in, inlen); + crypto_hash_sha512_final(&state, out); + + return 0; +} diff --git a/packages/runtime/vendor/libsodium/keypair.c b/packages/runtime/vendor/libsodium/keypair.c new file mode 100644 index 000000000..18059adfe --- /dev/null +++ b/packages/runtime/vendor/libsodium/keypair.c @@ -0,0 +1,84 @@ + +#include + +#include "crypto_hash_sha512.h" +#include "crypto_scalarmult_curve25519.h" +#include "crypto_sign_ed25519.h" +#include "sign_ed25519_ref10.h" +#include "ed25519_ref10.h" +#include "randombytes.h" +#include "utils.h" + +int +crypto_sign_ed25519_seed_keypair(unsigned char *pk, unsigned char *sk, + const unsigned char *seed) +{ + ge25519_p3 A; + + crypto_hash_sha512(sk, seed, 32); + sk[0] &= 248; + sk[31] &= 127; + sk[31] |= 64; + + ge25519_scalarmult_base(&A, sk); + ge25519_p3_tobytes(pk, &A); + + memmove(sk, seed, 32); + memmove(sk + 32, pk, 32); + + return 0; +} + +int +crypto_sign_ed25519_keypair(unsigned char *pk, unsigned char *sk) +{ + unsigned char seed[32]; + int ret; + + randombytes_buf(seed, sizeof seed); + ret = crypto_sign_ed25519_seed_keypair(pk, sk, seed); + sodium_memzero(seed, sizeof seed); + + return ret; +} + +int +crypto_sign_ed25519_pk_to_curve25519(unsigned char *curve25519_pk, + const unsigned char *ed25519_pk) +{ + ge25519_p3 A; + fe25519 x; + fe25519 one_minus_y; + + if (ge25519_has_small_order(ed25519_pk) != 0 || + ge25519_frombytes_negate_vartime(&A, ed25519_pk) != 0 || + ge25519_is_on_main_subgroup(&A) == 0) { + return -1; + } + fe25519_1(one_minus_y); + /* assumes A.Z=1 */ + fe25519_sub(one_minus_y, one_minus_y, A.Y); + fe25519_1(x); + fe25519_add(x, x, A.Y); + fe25519_invert(one_minus_y, one_minus_y); + fe25519_mul(x, x, one_minus_y); + fe25519_tobytes(curve25519_pk, x); + + return 0; +} + +int +crypto_sign_ed25519_sk_to_curve25519(unsigned char *curve25519_sk, + const unsigned char *ed25519_sk) +{ + unsigned char h[crypto_hash_sha512_BYTES]; + + crypto_hash_sha512(h, ed25519_sk, 32); + h[0] &= 248; + h[31] &= 127; + h[31] |= 64; + memcpy(curve25519_sk, h, crypto_scalarmult_curve25519_BYTES); + sodium_memzero(h, sizeof h); + + return 0; +} diff --git a/packages/runtime/vendor/libsodium/open.c b/packages/runtime/vendor/libsodium/open.c new file mode 100644 index 000000000..0670866d6 --- /dev/null +++ b/packages/runtime/vendor/libsodium/open.c @@ -0,0 +1,98 @@ + +#include +#include +#include + +#include "crypto_hash_sha512.h" +#include "crypto_sign_ed25519.h" +#include "crypto_verify_32.h" +#include "sign_ed25519_ref10.h" +#include "common.h" +#include "ed25519_ref10.h" +#include "utils.h" + +int +_crypto_sign_ed25519_verify_detached(const unsigned char *sig, + const unsigned char *m, + unsigned long long mlen, + const unsigned char *pk, + int prehashed) +{ + crypto_hash_sha512_state hs; + unsigned char h[64]; + unsigned char rcheck[32]; + ge25519_p3 A; + ge25519_p2 R; + + ACQUIRE_FENCE; +#ifdef ED25519_COMPAT + if (sig[63] & 224) { + return -1; + } +#else + if (sc25519_is_canonical(sig + 32) == 0 || + ge25519_has_small_order(sig) != 0) { + return -1; + } + if (ge25519_is_canonical(pk) == 0 || + ge25519_has_small_order(pk) != 0) { + return -1; + } +#endif + if (ge25519_frombytes_negate_vartime(&A, pk) != 0) { + return -1; + } + _crypto_sign_ed25519_ref10_hinit(&hs, prehashed); + crypto_hash_sha512_update(&hs, sig, 32); + crypto_hash_sha512_update(&hs, pk, 32); + crypto_hash_sha512_update(&hs, m, mlen); + crypto_hash_sha512_final(&hs, h); + sc25519_reduce(h); + + ge25519_double_scalarmult_vartime(&R, h, &A, sig + 32); + ge25519_tobytes(rcheck, &R); + + return crypto_verify_32(rcheck, sig) | (-(rcheck == sig)) | + sodium_memcmp(sig, rcheck, 32); +} + +int +crypto_sign_ed25519_verify_detached(const unsigned char *sig, + const unsigned char *m, + unsigned long long mlen, + const unsigned char *pk) +{ + return _crypto_sign_ed25519_verify_detached(sig, m, mlen, pk, 0); +} + +int +crypto_sign_ed25519_open(unsigned char *m, unsigned long long *mlen_p, + const unsigned char *sm, unsigned long long smlen, + const unsigned char *pk) +{ + unsigned long long mlen; + + if (smlen < 64 || smlen - 64 > crypto_sign_ed25519_MESSAGEBYTES_MAX) { + goto badsig; + } + mlen = smlen - 64; + if (crypto_sign_ed25519_verify_detached(sm, sm + 64, mlen, pk) != 0) { + if (m != NULL) { + memset(m, 0, mlen); + } + goto badsig; + } + if (mlen_p != NULL) { + *mlen_p = mlen; + } + if (m != NULL) { + memmove(m, sm + 64, mlen); + } + return 0; + +badsig: + if (mlen_p != NULL) { + *mlen_p = 0; + } + return -1; +} diff --git a/packages/runtime/vendor/libsodium/randombytes.h b/packages/runtime/vendor/libsodium/randombytes.h new file mode 100644 index 000000000..c83a4df50 --- /dev/null +++ b/packages/runtime/vendor/libsodium/randombytes.h @@ -0,0 +1,72 @@ + +#ifndef randombytes_H +#define randombytes_H + +#include +#include + +#include + +#include "export.h" + +#ifdef __cplusplus +# ifdef __GNUC__ +# pragma GCC diagnostic ignored "-Wlong-long" +# endif +extern "C" { +#endif + +typedef struct randombytes_implementation { + const char *(*implementation_name)(void); /* required */ + uint32_t (*random)(void); /* required */ + void (*stir)(void); /* optional */ + uint32_t (*uniform)(const uint32_t upper_bound); /* optional, a default implementation will be used if NULL */ + void (*buf)(void * const buf, const size_t size); /* required */ + int (*close)(void); /* optional */ +} randombytes_implementation; + +#define randombytes_BYTES_MAX SODIUM_MIN(SODIUM_SIZE_MAX, 0xffffffffUL) + +#define randombytes_SEEDBYTES 32U +SODIUM_EXPORT +size_t randombytes_seedbytes(void); + +SODIUM_EXPORT +void randombytes_buf(void * const buf, const size_t size) + __attribute__ ((nonnull)); + +SODIUM_EXPORT +void randombytes_buf_deterministic(void * const buf, const size_t size, + const unsigned char seed[randombytes_SEEDBYTES]) + __attribute__ ((nonnull)); + +SODIUM_EXPORT +uint32_t randombytes_random(void); + +SODIUM_EXPORT +uint32_t randombytes_uniform(const uint32_t upper_bound); + +SODIUM_EXPORT +void randombytes_stir(void); + +SODIUM_EXPORT +int randombytes_close(void); + +SODIUM_EXPORT +int randombytes_set_implementation(const randombytes_implementation *impl) + __attribute__ ((nonnull)); + +SODIUM_EXPORT +const char *randombytes_implementation_name(void); + +/* -- NaCl compatibility interface -- */ + +SODIUM_EXPORT +void randombytes(unsigned char * const buf, const unsigned long long buf_len) + __attribute__ ((nonnull)); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/packages/runtime/vendor/libsodium/sign.c b/packages/runtime/vendor/libsodium/sign.c new file mode 100644 index 000000000..1b4a2923c --- /dev/null +++ b/packages/runtime/vendor/libsodium/sign.c @@ -0,0 +1,128 @@ + +#include + +#include "crypto_hash_sha512.h" +#include "crypto_sign_ed25519.h" +#include "sign_ed25519_ref10.h" +#include "ed25519_ref10.h" +#include "randombytes.h" +#include "utils.h" + +void +_crypto_sign_ed25519_ref10_hinit(crypto_hash_sha512_state *hs, int prehashed) +{ + static const unsigned char DOM2PREFIX[32 + 2] = { + 'S', 'i', 'g', 'E', 'd', '2', '5', '5', '1', '9', ' ', + 'n', 'o', ' ', + 'E', 'd', '2', '5', '5', '1', '9', ' ', + 'c', 'o', 'l', 'l', 'i', 's', 'i', 'o', 'n', 's', 1, 0 + }; + + crypto_hash_sha512_init(hs); + if (prehashed) { + crypto_hash_sha512_update(hs, DOM2PREFIX, sizeof DOM2PREFIX); + } +} + +static inline void +_crypto_sign_ed25519_clamp(unsigned char k[32]) +{ + k[0] &= 248; + k[31] &= 127; + k[31] |= 64; +} + +#ifdef ED25519_NONDETERMINISTIC +/* r = hash(k || K || noise || pad || M) (mod q) */ +static void +_crypto_sign_ed25519_synthetic_r_hv(crypto_hash_sha512_state *hs, + unsigned char tmp[64], + const unsigned char az[64]) +{ + crypto_hash_sha512_update(hs, az, 64); + randombytes_buf(tmp, 32); + memset(tmp + 32, 0, 32); + crypto_hash_sha512_update(hs, tmp, 64); +} +#endif + +int +_crypto_sign_ed25519_detached(unsigned char *sig, unsigned long long *siglen_p, + const unsigned char *m, unsigned long long mlen, + const unsigned char *sk, int prehashed) +{ + crypto_hash_sha512_state hs; + unsigned char az[64]; + unsigned char nonce[64]; + unsigned char hram[64]; + ge25519_p3 R; + + _crypto_sign_ed25519_ref10_hinit(&hs, prehashed); + + crypto_hash_sha512(az, sk, 32); +#ifdef ED25519_NONDETERMINISTIC + _crypto_sign_ed25519_synthetic_r_hv(&hs, nonce /* tmp */, az); +#else + crypto_hash_sha512_update(&hs, az + 32, 32); +#endif + + crypto_hash_sha512_update(&hs, m, mlen); + crypto_hash_sha512_final(&hs, nonce); + + memmove(sig + 32, sk + 32, 32); + + sc25519_reduce(nonce); + ge25519_scalarmult_base(&R, nonce); + ge25519_p3_tobytes(sig, &R); + + _crypto_sign_ed25519_ref10_hinit(&hs, prehashed); + crypto_hash_sha512_update(&hs, sig, 64); + crypto_hash_sha512_update(&hs, m, mlen); + crypto_hash_sha512_final(&hs, hram); + + sc25519_reduce(hram); + _crypto_sign_ed25519_clamp(az); + sc25519_muladd(sig + 32, hram, az, nonce); + + sodium_memzero(az, sizeof az); + sodium_memzero(nonce, sizeof nonce); + + if (siglen_p != NULL) { + *siglen_p = 64U; + } + return 0; +} + +int +crypto_sign_ed25519_detached(unsigned char *sig, unsigned long long *siglen_p, + const unsigned char *m, unsigned long long mlen, + const unsigned char *sk) +{ + return _crypto_sign_ed25519_detached(sig, siglen_p, m, mlen, sk, 0); +} + +int +crypto_sign_ed25519(unsigned char *sm, unsigned long long *smlen_p, + const unsigned char *m, unsigned long long mlen, + const unsigned char *sk) +{ + unsigned long long siglen; + + memmove(sm + crypto_sign_ed25519_BYTES, m, mlen); + /* LCOV_EXCL_START */ + if (crypto_sign_ed25519_detached( + sm, &siglen, sm + crypto_sign_ed25519_BYTES, mlen, sk) != 0 || + siglen != crypto_sign_ed25519_BYTES) { + if (smlen_p != NULL) { + *smlen_p = 0; + } + memset(sm, 0, mlen + crypto_sign_ed25519_BYTES); + return -1; + } + /* LCOV_EXCL_STOP */ + + if (smlen_p != NULL) { + *smlen_p = mlen + siglen; + } + return 0; +} diff --git a/packages/runtime/vendor/libsodium/sign_ed25519_ref10.h b/packages/runtime/vendor/libsodium/sign_ed25519_ref10.h new file mode 100644 index 000000000..29f45a854 --- /dev/null +++ b/packages/runtime/vendor/libsodium/sign_ed25519_ref10.h @@ -0,0 +1,18 @@ +#ifndef sign_ed25519_ref10_H +#define sign_ed25519_ref10_H + +void _crypto_sign_ed25519_ref10_hinit(crypto_hash_sha512_state *hs, + int prehashed); + +int _crypto_sign_ed25519_detached(unsigned char *sig, + unsigned long long *siglen_p, + const unsigned char *m, + unsigned long long mlen, + const unsigned char *sk, int prehashed); + +int _crypto_sign_ed25519_verify_detached(const unsigned char *sig, + const unsigned char *m, + unsigned long long mlen, + const unsigned char *pk, + int prehashed); +#endif diff --git a/packages/runtime/vendor/libsodium/utils.h b/packages/runtime/vendor/libsodium/utils.h new file mode 100644 index 000000000..f9b369796 --- /dev/null +++ b/packages/runtime/vendor/libsodium/utils.h @@ -0,0 +1,179 @@ + +#ifndef sodium_utils_H +#define sodium_utils_H + +#include + +#include "export.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#ifndef SODIUM_C99 +# if defined(__cplusplus) || !defined(__STDC_VERSION__) || __STDC_VERSION__ < 199901L +# define SODIUM_C99(X) +# else +# define SODIUM_C99(X) X +# endif +#endif + +SODIUM_EXPORT +void sodium_memzero(void * const pnt, const size_t len); + +SODIUM_EXPORT +void sodium_stackzero(const size_t len); + +/* + * WARNING: sodium_memcmp() must be used to verify if two secret keys + * are equal, in constant time. + * It returns 0 if the keys are equal, and -1 if they differ. + * This function is not designed for lexicographical comparisons. + */ +SODIUM_EXPORT +int sodium_memcmp(const void * const b1_, const void * const b2_, size_t len) + __attribute__ ((warn_unused_result)); + +/* + * sodium_compare() returns -1 if b1_ < b2_, 1 if b1_ > b2_ and 0 if b1_ == b2_ + * It is suitable for lexicographical comparisons, or to compare nonces + * and counters stored in little-endian format. + * However, it is slower than sodium_memcmp(). + */ +SODIUM_EXPORT +int sodium_compare(const unsigned char *b1_, const unsigned char *b2_, + size_t len) __attribute__ ((warn_unused_result)); + +SODIUM_EXPORT +int sodium_is_zero(const unsigned char *n, const size_t nlen); + +SODIUM_EXPORT +void sodium_increment(unsigned char *n, const size_t nlen); + +SODIUM_EXPORT +void sodium_add(unsigned char *a, const unsigned char *b, const size_t len); + +SODIUM_EXPORT +void sodium_sub(unsigned char *a, const unsigned char *b, const size_t len); + +SODIUM_EXPORT +char *sodium_bin2hex(char * const hex, const size_t hex_maxlen, + const unsigned char * const bin, const size_t bin_len) + __attribute__ ((nonnull(1))); + +SODIUM_EXPORT +int sodium_hex2bin(unsigned char * const bin, const size_t bin_maxlen, + const char * const hex, const size_t hex_len, + const char * const ignore, size_t * const bin_len, + const char ** const hex_end) + __attribute__ ((nonnull(1))); + +#define sodium_base64_VARIANT_ORIGINAL 1 +#define sodium_base64_VARIANT_ORIGINAL_NO_PADDING 3 +#define sodium_base64_VARIANT_URLSAFE 5 +#define sodium_base64_VARIANT_URLSAFE_NO_PADDING 7 + +/* + * Computes the required length to encode BIN_LEN bytes as a base64 string + * using the given variant. The computed length includes a trailing \0. + */ +#define sodium_base64_ENCODED_LEN(BIN_LEN, VARIANT) \ + (((BIN_LEN) / 3U) * 4U + \ + ((((BIN_LEN) - ((BIN_LEN) / 3U) * 3U) | (((BIN_LEN) - ((BIN_LEN) / 3U) * 3U) >> 1)) & 1U) * \ + (4U - (~((((VARIANT) & 2U) >> 1) - 1U) & (3U - ((BIN_LEN) - ((BIN_LEN) / 3U) * 3U)))) + 1U) + +SODIUM_EXPORT +size_t sodium_base64_encoded_len(const size_t bin_len, const int variant); + +SODIUM_EXPORT +char *sodium_bin2base64(char * const b64, const size_t b64_maxlen, + const unsigned char * const bin, const size_t bin_len, + const int variant) __attribute__ ((nonnull(1))); + +SODIUM_EXPORT +int sodium_base642bin(unsigned char * const bin, const size_t bin_maxlen, + const char * const b64, const size_t b64_len, + const char * const ignore, size_t * const bin_len, + const char ** const b64_end, const int variant) + __attribute__ ((nonnull(1))); + +SODIUM_EXPORT +int sodium_mlock(void * const addr, const size_t len) + __attribute__ ((nonnull)); + +SODIUM_EXPORT +int sodium_munlock(void * const addr, const size_t len) + __attribute__ ((nonnull)); + +/* WARNING: sodium_malloc() and sodium_allocarray() are not general-purpose + * allocation functions. + * + * They return a pointer to a region filled with 0xd0 bytes, immediately + * followed by a guard page. + * As a result, accessing a single byte after the requested allocation size + * will intentionally trigger a segmentation fault. + * + * A canary and an additional guard page placed before the beginning of the + * region may also kill the process if a buffer underflow is detected. + * + * The memory layout is: + * [unprotected region size (read only)][guard page (no access)][unprotected pages (read/write)][guard page (no access)] + * With the layout of the unprotected pages being: + * [optional padding][16-bytes canary][user region] + * + * However: + * - These functions are significantly slower than standard functions + * - Each allocation requires 3 or 4 additional pages + * - The returned address will not be aligned if the allocation size is not + * a multiple of the required alignment. For this reason, these functions + * are designed to store data, such as secret keys and messages. + * + * sodium_malloc() can be used to allocate any libsodium data structure. + * + * The crypto_generichash_state structure is packed and its length is + * either 357 or 361 bytes. For this reason, when using sodium_malloc() to + * allocate a crypto_generichash_state structure, padding must be added in + * order to ensure proper alignment. crypto_generichash_statebytes() + * returns the rounded up structure size, and should be preferred to sizeof(): + * state = sodium_malloc(crypto_generichash_statebytes()); + */ + +SODIUM_EXPORT +void *sodium_malloc(const size_t size) + __attribute__ ((malloc)); + +SODIUM_EXPORT +void *sodium_allocarray(size_t count, size_t size) + __attribute__ ((malloc)); + +SODIUM_EXPORT +void sodium_free(void *ptr); + +SODIUM_EXPORT +int sodium_mprotect_noaccess(void *ptr) __attribute__ ((nonnull)); + +SODIUM_EXPORT +int sodium_mprotect_readonly(void *ptr) __attribute__ ((nonnull)); + +SODIUM_EXPORT +int sodium_mprotect_readwrite(void *ptr) __attribute__ ((nonnull)); + +SODIUM_EXPORT +int sodium_pad(size_t *padded_buflen_p, unsigned char *buf, + size_t unpadded_buflen, size_t blocksize, size_t max_buflen) + __attribute__ ((nonnull(2))); + +SODIUM_EXPORT +int sodium_unpad(size_t *unpadded_buflen_p, const unsigned char *buf, + size_t padded_buflen, size_t blocksize) + __attribute__ ((nonnull(2))); + +/* -------- */ + +int _sodium_alloc_init(void); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/packages/runtime/vendor/libsodium/verify.c b/packages/runtime/vendor/libsodium/verify.c new file mode 100644 index 000000000..0c3323a50 --- /dev/null +++ b/packages/runtime/vendor/libsodium/verify.c @@ -0,0 +1,95 @@ + +#include +#include + +#include "crypto_verify_16.h" +#include "crypto_verify_32.h" +#include "crypto_verify_64.h" + +size_t +crypto_verify_16_bytes(void) +{ + return crypto_verify_16_BYTES; +} + +size_t +crypto_verify_32_bytes(void) +{ + return crypto_verify_32_BYTES; +} + +size_t +crypto_verify_64_bytes(void) +{ + return crypto_verify_64_BYTES; +} + +#if defined(HAVE_EMMINTRIN_H) && defined(__SSE2__) + +# include + +static inline int +crypto_verify_n(const unsigned char *x_, const unsigned char *y_, + const int n) +{ + const __m128i zero = _mm_setzero_si128(); + volatile __m128i v1, v2, z; + volatile int m; + int i; + + const volatile __m128i *volatile x = + (const volatile __m128i *volatile) (const void *) x_; + const volatile __m128i *volatile y = + (const volatile __m128i *volatile) (const void *) y_; + v1 = _mm_loadu_si128((const __m128i *) &x[0]); + v2 = _mm_loadu_si128((const __m128i *) &y[0]); + z = _mm_xor_si128(v1, v2); + for (i = 1; i < n / 16; i++) { + v1 = _mm_loadu_si128((const __m128i *) &x[i]); + v2 = _mm_loadu_si128((const __m128i *) &y[i]); + z = _mm_or_si128(z, _mm_xor_si128(v1, v2)); + } + m = _mm_movemask_epi8(_mm_cmpeq_epi32(z, zero)); + v1 = zero; v2 = zero; z = zero; + + return (int) (((uint32_t) m + 1U) >> 16) - 1; +} + +#else + +static inline int +crypto_verify_n(const unsigned char *x_, const unsigned char *y_, + const int n) +{ + const volatile unsigned char *volatile x = + (const volatile unsigned char *volatile) x_; + const volatile unsigned char *volatile y = + (const volatile unsigned char *volatile) y_; + volatile uint_fast16_t d = 0U; + int i; + + for (i = 0; i < n; i++) { + d |= x[i] ^ y[i]; + } + return (1 & ((d - 1) >> 8)) - 1; +} + +#endif + +int +crypto_verify_16(const unsigned char *x, const unsigned char *y) +{ + return crypto_verify_n(x, y, crypto_verify_16_BYTES); +} + +int +crypto_verify_32(const unsigned char *x, const unsigned char *y) +{ + return crypto_verify_n(x, y, crypto_verify_32_BYTES); +} + +int +crypto_verify_64(const unsigned char *x, const unsigned char *y) +{ + return crypto_verify_n(x, y, crypto_verify_64_BYTES); +} diff --git a/packages/threads/package.json b/packages/threads/package.json index 2aeeb7cbb..11b8ef624 100644 --- a/packages/threads/package.json +++ b/packages/threads/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/threads", - "version": "0.4.1", + "version": "0.4.2", "description": "Zero-copy publication of immutable object graphs to worker threads (scriptc), with a deep-freezing Node.js implementation", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/types/package.json b/packages/types/package.json index ffbbdf4a4..689c7b72f 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/types", - "version": "0.4.1", + "version": "0.4.2", "description": "Contract types for scriptc: type-only annotations (Int32, NoThrow, Immutable) that scriptc checks and optimizes, with no runtime semantics", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2404c3885..589de2a51 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -55,7 +55,7 @@ importers: version: 5.9.3 vercel: specifier: 58.1.0 - version: 58.1.0(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3)(rollup@4.62.2) + version: 58.1.0(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(rollup@4.62.2) vitest: specifier: ^3.0.0 version: 3.2.7(patch_hash=03a418fae669c2cd9846dc3c49e6c1b8ee7047bbdcda9d4f5d5ee09eb525a1be)(@edge-runtime/vm@3.2.0)(@types/node@24.13.3)(tsx@4.23.0) @@ -110,8 +110,8 @@ importers: specifier: workspace:* version: link:../runtime tsc-ts: - specifier: 0.0.5 - version: 0.0.5 + specifier: 0.0.7 + version: 0.0.7 typescript: specifier: 7.0.2 version: 7.0.2 @@ -271,14 +271,14 @@ packages: effect: ^4.0.0 redis: '>=5.0.0 <7.0.0' - '@emnapi/core@2.0.0-alpha.3': - resolution: {integrity: sha512-AZypUeJ/yByuxyS7BlSNRDOMLMlROYtjYdIAuBmJssVz1UJDSeYxLrdizhXCFYhedC5bqd/ASy8EuNXbVVXp9g==} + '@emnapi/core@1.11.3': + resolution: {integrity: sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg==} - '@emnapi/runtime@2.0.0-alpha.3': - resolution: {integrity: sha512-hFPAhMUjJD9BSyCANEISPOogeXC9Zo9ZQl7L6vKnaVsMkCtzznaW/naYypeyl0Gv5rYfWYsZbpixTMpjDJzQeA==} + '@emnapi/runtime@1.11.3': + resolution: {integrity: sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==} - '@emnapi/wasi-threads@2.0.1': - resolution: {integrity: sha512-9DsSk+o5NBX0CCJT8s0EROGSGxjR/tKu6aBTaVyq+SjAEQH4XcdcRxPBRzsBLizTTJ49MJjF+jgu3qnO9GLQcQ==} + '@emnapi/wasi-threads@1.2.3': + resolution: {integrity: sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g==} '@esbuild/aix-ppc64@0.27.0': resolution: {integrity: sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A==} @@ -1360,8 +1360,8 @@ packages: cpu: [x64] os: [win32] - '@scriptc/threads@0.3.6': - resolution: {integrity: sha512-XZb4JhXM/ivhx4MMLxqLLF2mCI28nUK+VVKlCnELO83tacUuGlq7YCe/8LwvzO7fZwy/rCH4+genAYYEc6BF6Q==} + '@scriptc/threads@0.4.1': + resolution: {integrity: sha512-wKgEBiVc07ZIGNFGwJpV5JbTXJN1N42qG6/g7ipnwt3klsU0nuWH2ToKV9vYH+RQstF7+twq6zMCf2qhUloTHQ==} engines: {node: '>=24.0.0'} '@sinclair/typebox@0.25.24': @@ -1377,42 +1377,42 @@ packages: '@ts-morph/common@0.11.1': resolution: {integrity: sha512-7hWZS0NRpEsNV8vWJzg7FEz6V8MaLNeJOmwmghqUXTpzk16V1LLZhdo+4QvE/+zv4cVci0OviuJFnqhEfoV3+g==} - '@tsc-ts/darwin-arm64@0.0.5': - resolution: {integrity: sha512-yP64b2VpAKWG92KepBqxs1P89wpwiheYwNMk4EmmhSl6aw/ImIBrdHYNjvao3sg6FHZCXTc+PB6Vukh6hyQiJQ==} + '@tsc-ts/darwin-arm64@0.0.7': + resolution: {integrity: sha512-nIaTJKBpG6n60xxRYfBlh02G6aSO2punIvj4qPy8/CXZu0Kw4ra/J3PYJtrtYMIh/eIGo4+f1Q01v5dFPPwZbw==} cpu: [arm64] os: [darwin] - '@tsc-ts/darwin-x64@0.0.5': - resolution: {integrity: sha512-CEeHOFfpx5H9V3WSq+V7XGU8tfc06nRN818auiRVwuwPHvpti9gHq0HYkvf/43PH1pIGcgf68sQYdIxJhrQDKQ==} + '@tsc-ts/darwin-x64@0.0.7': + resolution: {integrity: sha512-8ITDyF3FG4SYeS1cV4+oRpbbk0jVsfd23Ww138wbQymbOLYjWPafzj4bymxnIE5kEWjZPVqTFDbaYqgWwNStag==} cpu: [x64] os: [darwin] - '@tsc-ts/linux-arm64-gnu@0.0.5': - resolution: {integrity: sha512-qwybOqGDEa35eV9x/3WO81ExAUbUMY8288ATC83kUfYGmOAVStaO+JM2IdrsiI8hLWVC7/f0RFgZ+7dyWh9/nA==} + '@tsc-ts/linux-arm64-gnu@0.0.7': + resolution: {integrity: sha512-nXGjtbM3YrARWOb+nkDaFdhKGSzPf3ofl/gV4x8JnVQ8SgK4IWJ5J0u93sk3mTYgdLj/Gcobp/D3lZ6WIzjKvA==} cpu: [arm64] os: [linux] libc: [glibc] - '@tsc-ts/linux-arm64-musl@0.0.5': - resolution: {integrity: sha512-6oSw3NbQIrnxxCRuEe190MbMFoOrM+P+u1usascQMgSjsMcVRnJ/ztvTnRrCDtfPEHJ5EC8drgWIpcdrhGsB0w==} + '@tsc-ts/linux-arm64-musl@0.0.7': + resolution: {integrity: sha512-hSjpQsOhrLaDsbvEO5EH9nC3Vy0WAt3aB+GejytEh9fl4F3kT0BasbBP4wJqu6S5SG9upLW7ZClXiNVnGYDGlQ==} cpu: [arm64] os: [linux] libc: [musl] - '@tsc-ts/linux-x64-gnu@0.0.5': - resolution: {integrity: sha512-KrlBLUcG7M6p7lN+ke+kHVLqy7IiY/H0FxFZzq5slXanhrps/BfjdkUahsyREg7KVprr08xFuXqJTBtLEI4eow==} + '@tsc-ts/linux-x64-gnu@0.0.7': + resolution: {integrity: sha512-MCU2MFefQGG/++nepg09BtYnsN2mVzeYfIS6GW30ChxUt2nJUMkEvzbDim9czDevW4LRqHgfnwEt+hs2ud1SDQ==} cpu: [x64] os: [linux] libc: [glibc] - '@tsc-ts/linux-x64-musl@0.0.5': - resolution: {integrity: sha512-fr8yGzkocCcTYoE+NwC+/WGPNteQ2GmOPJTy/z9NcsLchnpSIZ1fzFll15UYPKQlwQdNRJXxKK3npVBtLRdHTg==} + '@tsc-ts/linux-x64-musl@0.0.7': + resolution: {integrity: sha512-abTZZkn74772MPABStuXWJ1GHI07nbEf4a3Nwl7NYI5mLFYxeLSbq6+cGh0BUJeBeMhFNsjtQ5/j5sGmsrJ2tg==} cpu: [x64] os: [linux] libc: [musl] - '@tsc-ts/win32-x64-msvc@0.0.5': - resolution: {integrity: sha512-SQwufJafzsiOBYncHOxYkyELWjLlBijcKB+xOs6hr6Lxxp5MMLoPtvDFNG88c7G5xv2TDb1XbvyxnjQltr6SHw==} + '@tsc-ts/win32-x64-msvc@0.0.7': + resolution: {integrity: sha512-EOhLTkuWYytJYklR5BI1jRtTpEIPh1FQJKD8R37qqlcnPrELlRO6Nc2ncDpdCoYbpN+KrewIVPB3xD4o3VGUaw==} cpu: [x64] os: [win32] @@ -2577,8 +2577,8 @@ packages: ts-toolbelt@6.15.5: resolution: {integrity: sha512-FZIXf1ksVyLcfr7M317jbB67XFJhOO1YqdTcuGaq9q5jLUoTikukZ+98TPjKiP2jC5CgmYdWWYs0s2nLSU0/1A==} - tsc-ts@0.0.5: - resolution: {integrity: sha512-Eea+zNv7y696pqNZtXBd0AX7/6PXIhR7aaEb0EhgpHadbk7VEiURD94/NngeLpYWts2tPjetkpwFLJY0+6yOnQ==} + tsc-ts@0.0.7: + resolution: {integrity: sha512-KRWTXRUvwMXi4At6xOWPDa/0BreY3MR2tPHooEMNWLZUA2eLphNe7PJiHclPLLn9RCnHY4ffDTTeF5hqgyBvtA==} engines: {node: '>=20'} hasBin: true @@ -2840,18 +2840,18 @@ snapshots: - bufferutil - utf-8-validate - '@emnapi/core@2.0.0-alpha.3': + '@emnapi/core@1.11.3': dependencies: - '@emnapi/wasi-threads': 2.0.1 + '@emnapi/wasi-threads': 1.2.3 tslib: 2.8.1 optional: true - '@emnapi/runtime@2.0.0-alpha.3': + '@emnapi/runtime@1.11.3': dependencies: tslib: 2.8.1 optional: true - '@emnapi/wasi-threads@2.0.1': + '@emnapi/wasi-threads@1.2.3': dependencies: tslib: 2.8.1 optional: true @@ -3090,10 +3090,10 @@ snapshots: '@napi-rs/keyring-win32-ia32-msvc': 1.2.0 '@napi-rs/keyring-win32-x64-msvc': 1.2.0 - '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3)': + '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)': dependencies: - '@emnapi/core': 2.0.0-alpha.3 - '@emnapi/runtime': 2.0.0-alpha.3 + '@emnapi/core': 1.11.3 + '@emnapi/runtime': 1.11.3 '@tybys/wasm-util': 0.10.3 optional: true @@ -3159,9 +3159,9 @@ snapshots: '@oxc-transform/binding-openharmony-arm64@0.111.0': optional: true - '@oxc-transform/binding-wasm32-wasi@0.111.0(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3)': + '@oxc-transform/binding-wasm32-wasi@0.111.0(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)': dependencies: - '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3) + '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3) transitivePeerDependencies: - '@emnapi/core' - '@emnapi/runtime' @@ -3340,9 +3340,9 @@ snapshots: '@rolldown/binding-openharmony-arm64@1.0.0-rc.1': optional: true - '@rolldown/binding-wasm32-wasi@1.0.0-rc.1(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3)': + '@rolldown/binding-wasm32-wasi@1.0.0-rc.1(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)': dependencies: - '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3) + '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3) transitivePeerDependencies: - '@emnapi/core' - '@emnapi/runtime' @@ -3439,7 +3439,7 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.62.2': optional: true - '@scriptc/threads@0.3.6': {} + '@scriptc/threads@0.4.1': {} '@sinclair/typebox@0.25.24': {} @@ -3454,25 +3454,25 @@ snapshots: mkdirp: 1.0.4 path-browserify: 1.0.1 - '@tsc-ts/darwin-arm64@0.0.5': + '@tsc-ts/darwin-arm64@0.0.7': optional: true - '@tsc-ts/darwin-x64@0.0.5': + '@tsc-ts/darwin-x64@0.0.7': optional: true - '@tsc-ts/linux-arm64-gnu@0.0.5': + '@tsc-ts/linux-arm64-gnu@0.0.7': optional: true - '@tsc-ts/linux-arm64-musl@0.0.5': + '@tsc-ts/linux-arm64-musl@0.0.7': optional: true - '@tsc-ts/linux-x64-gnu@0.0.5': + '@tsc-ts/linux-x64-gnu@0.0.7': optional: true - '@tsc-ts/linux-x64-musl@0.0.5': + '@tsc-ts/linux-x64-musl@0.0.7': optional: true - '@tsc-ts/win32-x64-msvc@0.0.5': + '@tsc-ts/win32-x64-msvc@0.0.7': optional: true '@tybys/wasm-util@0.10.3': @@ -3563,7 +3563,7 @@ snapshots: '@typescript/typescript-win32-x64@7.0.2': optional: true - '@vercel/backends@0.8.27(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3)(rollup@4.62.2)': + '@vercel/backends@0.8.27(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(rollup@4.62.2)': dependencies: '@vercel/build-utils': 13.36.0 '@vercel/nft': 1.10.0(rollup@4.62.2) @@ -3571,10 +3571,10 @@ snapshots: execa: 3.2.0 fs-extra: 11.1.0 get-port: 5.1.1 - oxc-transform: 0.111.0(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3) + oxc-transform: 0.111.0(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3) path-to-regexp: 8.3.0 resolve.exports: 2.0.3 - rolldown: 1.0.0-rc.1(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3) + rolldown: 1.0.0-rc.1(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3) srvx: 0.11.16 ts-morph: 12.0.0 tsx: 4.21.0 @@ -3600,9 +3600,9 @@ snapshots: cjs-module-lexer: 1.2.3 es-module-lexer: 1.5.0 - '@vercel/cervel@0.1.35(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3)(rollup@4.62.2)': + '@vercel/cervel@0.1.35(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(rollup@4.62.2)': dependencies: - '@vercel/backends': 0.8.27(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3)(rollup@4.62.2) + '@vercel/backends': 0.8.27(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(rollup@4.62.2) transitivePeerDependencies: - '@emnapi/core' - '@emnapi/runtime' @@ -3638,9 +3638,9 @@ snapshots: '@vercel/error-utils@2.2.0': {} - '@vercel/express@0.1.118(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3)(rollup@4.62.2)': + '@vercel/express@0.1.118(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(rollup@4.62.2)': dependencies: - '@vercel/cervel': 0.1.35(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3)(rollup@4.62.2) + '@vercel/cervel': 0.1.35(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(rollup@4.62.2) '@vercel/nft': 1.10.0(rollup@4.62.2) '@vercel/node': 5.9.0(rollup@4.62.2) '@vercel/static-config': 3.4.0 @@ -4442,7 +4442,7 @@ snapshots: os-paths@4.4.0: {} - oxc-transform@0.111.0(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3): + oxc-transform@0.111.0(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3): optionalDependencies: '@oxc-transform/binding-android-arm-eabi': 0.111.0 '@oxc-transform/binding-android-arm64': 0.111.0 @@ -4460,7 +4460,7 @@ snapshots: '@oxc-transform/binding-linux-x64-gnu': 0.111.0 '@oxc-transform/binding-linux-x64-musl': 0.111.0 '@oxc-transform/binding-openharmony-arm64': 0.111.0 - '@oxc-transform/binding-wasm32-wasi': 0.111.0(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3) + '@oxc-transform/binding-wasm32-wasi': 0.111.0(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3) '@oxc-transform/binding-win32-arm64-msvc': 0.111.0 '@oxc-transform/binding-win32-ia32-msvc': 0.111.0 '@oxc-transform/binding-win32-x64-msvc': 0.111.0 @@ -4604,7 +4604,7 @@ snapshots: reusify@1.1.0: {} - rolldown@1.0.0-rc.1(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3): + rolldown@1.0.0-rc.1(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3): dependencies: '@oxc-project/types': 0.110.0 '@rolldown/pluginutils': 1.0.0-rc.1 @@ -4619,7 +4619,7 @@ snapshots: '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.1 '@rolldown/binding-linux-x64-musl': 1.0.0-rc.1 '@rolldown/binding-openharmony-arm64': 1.0.0-rc.1 - '@rolldown/binding-wasm32-wasi': 1.0.0-rc.1(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3) + '@rolldown/binding-wasm32-wasi': 1.0.0-rc.1(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3) '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.1 '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.1 transitivePeerDependencies: @@ -4813,17 +4813,17 @@ snapshots: ts-toolbelt@6.15.5: {} - tsc-ts@0.0.5: + tsc-ts@0.0.7: dependencies: - '@scriptc/threads': 0.3.6 + '@scriptc/threads': 0.4.1 optionalDependencies: - '@tsc-ts/darwin-arm64': 0.0.5 - '@tsc-ts/darwin-x64': 0.0.5 - '@tsc-ts/linux-arm64-gnu': 0.0.5 - '@tsc-ts/linux-arm64-musl': 0.0.5 - '@tsc-ts/linux-x64-gnu': 0.0.5 - '@tsc-ts/linux-x64-musl': 0.0.5 - '@tsc-ts/win32-x64-msvc': 0.0.5 + '@tsc-ts/darwin-arm64': 0.0.7 + '@tsc-ts/darwin-x64': 0.0.7 + '@tsc-ts/linux-arm64-gnu': 0.0.7 + '@tsc-ts/linux-arm64-musl': 0.0.7 + '@tsc-ts/linux-x64-gnu': 0.0.7 + '@tsc-ts/linux-x64-musl': 0.0.7 + '@tsc-ts/win32-x64-msvc': 0.0.7 tslib@2.8.1: optional: true @@ -4894,9 +4894,9 @@ snapshots: dependencies: punycode: 2.3.1 - vercel@58.1.0(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3)(rollup@4.62.2): + vercel@58.1.0(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(rollup@4.62.2): dependencies: - '@vercel/backends': 0.8.27(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3)(rollup@4.62.2) + '@vercel/backends': 0.8.27(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(rollup@4.62.2) '@vercel/blob': 2.4.0 '@vercel/build-utils': 13.36.0 '@vercel/cli-auth': 0.3.1 @@ -4904,7 +4904,7 @@ snapshots: '@vercel/container': 0.1.0 '@vercel/detect-agent': 1.2.3 '@vercel/elysia': 0.1.104(rollup@4.62.2) - '@vercel/express': 0.1.118(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3)(rollup@4.62.2) + '@vercel/express': 0.1.118(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(rollup@4.62.2) '@vercel/fastify': 0.1.107(rollup@4.62.2) '@vercel/fun': 1.3.0 '@vercel/go': 3.10.2 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index a3e992b0a..ceb1506e9 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -7,14 +7,15 @@ minimumReleaseAgeExclude: - "effect@4.0.0" - "@effect/platform-node@4.0.0" - "@effect/platform-node-shared@4.0.0" - - "@tsc-ts/darwin-arm64@0.0.5" - - "@tsc-ts/darwin-x64@0.0.5" - - "@tsc-ts/linux-arm64-gnu@0.0.5" - - "@tsc-ts/linux-arm64-musl@0.0.5" - - "@tsc-ts/linux-x64-gnu@0.0.5" - - "@tsc-ts/linux-x64-musl@0.0.5" - - "@tsc-ts/win32-x64-msvc@0.0.5" - - "tsc-ts@0.0.5" + - "@tsc-ts/darwin-arm64@0.0.7" + - "@tsc-ts/darwin-x64@0.0.7" + - "@tsc-ts/linux-arm64-gnu@0.0.7" + - "@tsc-ts/linux-arm64-musl@0.0.7" + - "@tsc-ts/linux-x64-gnu@0.0.7" + - "@tsc-ts/linux-x64-musl@0.0.7" + - "@tsc-ts/win32-x64-msvc@0.0.7" + - "tsc-ts@0.0.7" + - "@scriptc/threads@0.4.1" allowBuilds: esbuild: true overrides: diff --git a/scripts/generate-ts7-ast-schema.mjs b/scripts/generate-ts7-ast-schema.mjs index a1bc80117..07af474de 100644 --- a/scripts/generate-ts7-ast-schema.mjs +++ b/scripts/generate-ts7-ast-schema.mjs @@ -44,6 +44,7 @@ const enumNames = [ "ModuleDetectionKind", "OuterExpressionKinds", "LanguageVariant", + "JsxEmit", ]; const enumLines = [ "/* eslint-disable @typescript-eslint/no-duplicate-enum-values -- TypeScript aliases are part of the protocol. */", @@ -67,6 +68,7 @@ for (const name of enumNames) { "ModuleKind", "ModuleResolutionKind", "ModuleDetectionKind", + "JsxEmit", ].includes(name) ) { const values = require(join(packageRoot, "dist/enums", `${basename}.js`))[name]; diff --git a/scripts/test-ts7.mjs b/scripts/test-ts7.mjs index e022c1ff5..b776bea3e 100644 --- a/scripts/test-ts7.mjs +++ b/scripts/test-ts7.mjs @@ -25,6 +25,7 @@ if (process.argv.includes("--baselines-only")) { const ALLOWED_TYPESCRIPT5_IMPORTS = new Set([ "packages/compiler/src/frontend/comptime-node.ts", + "packages/compiler/src/frontend/jsx-source.test.ts", "packages/compiler/src/frontend/ts7/world-check.ts", "packages/compiler/src/frontend/ts7/source-parser.test.ts", "packages/compiler/src/library/semantic-source.ts", diff --git a/tests/corpus/4638-contract-program-lifetime-arrays.ts b/tests/corpus/4638-contract-program-lifetime-arrays.ts new file mode 100644 index 000000000..a50094f8b --- /dev/null +++ b/tests/corpus/4638-contract-program-lifetime-arrays.ts @@ -0,0 +1,117 @@ +import type { ProgramLifetime } from "@scriptc/types"; + +// Arrays of ProgramLifetime class instances: literals, growth, removal +// from both ends, splicing, slicing, concatenation, searching, sorting, +// filling, copying methods, holes, length changes, Sets and Maps built +// from them, arrays held in class fields, optional elements, inspection +// and JSON. Each operation sees the same instances, in the same order. + +class Sym implements ProgramLifetime { + readonly name: string; + readonly rank: number; + readonly decls: Sym[] = []; + parent: Sym | undefined = undefined; + constructor(name: string, rank: number) { + this.name = name; + this.rank = rank; + } +} + +class Scope implements ProgramLifetime { + readonly symbols: Sym[] = []; + readonly optional: (Sym | undefined)[] = []; + readonly nullable: (Sym | null)[] = []; +} + +const names = (xs: readonly (Sym | undefined | null)[]): string => + xs.map((s) => (s === undefined ? "u" : s === null ? "n" : s.name)).join(","); + +const a = new Sym("a", 3); +const b = new Sym("b", 1); +const c = new Sym("c", 2); +const d = new Sym("d", 5); +const e = new Sym("e", 4); + +// Literals, push, pop, shift and unshift. +const xs: Sym[] = [a, b, c]; +xs.push(d); +xs.push(e, a); +console.log(names(xs), xs.length); +console.log(xs.pop()?.name, xs.shift()?.name, names(xs)); +xs.unshift(e, d); +console.log(names(xs), xs[0] === e, xs.at(-1) === e); + +// Growth well past the inline slots, then reads by index. +const many: Sym[] = []; +for (let i = 0; i < 1000; i++) many.push(i % 2 === 0 ? a : new Sym(`s${i}`, i)); +let ranks = 0; +for (let i = 0; i < many.length; i++) ranks += many[i]!.rank; +console.log(many.length, ranks, many[999]!.name, many[998] === a); + +// Splice, slice, concat, spread and copying methods. +const ys = xs.slice(1, 4); +const removed = xs.splice(1, 2, a, b, c); +console.log(names(ys), names(removed), names(xs)); +const zs = [...ys, ...removed].concat(xs); +console.log(names(zs), zs.length); +console.log(names(zs.toReversed()), names(zs.with(0, c)), names(zs.toSpliced(1, 3))); + +// Searching by identity. +console.log(zs.indexOf(c), zs.lastIndexOf(c), zs.includes(d), zs.indexOf(new Sym("c", 2))); +console.log(zs.find((s) => s.rank > 3)?.name, zs.findIndex((s) => s === b), zs.some((s) => s === e)); +console.log(zs.every((s) => s.rank > 0), zs.filter((s) => s.rank % 2 === 1).length); + +// Sorting with comparators, reversing and filling. +const sorted = [...zs].sort((p, q) => p.rank - q.rank || p.name.localeCompare(q.name)); +console.log(names(sorted)); +console.log(names(zs.toSorted((p, q) => q.name.localeCompare(p.name)))); +sorted.reverse(); +console.log(names(sorted)); +const filled: Sym[] = new Array(5).fill(b); +filled.fill(c, 2, 4); +console.log(names(filled), filled.copyWithin(0, 3).map((s) => s.name).join("")); + +// Holes, length changes and iteration. +const holey: Sym[] = [a, b]; +holey[5] = c; +console.log(holey.length, 2 in holey, holey[5]?.name, holey.findIndex((s) => s === c)); +holey.length = 3; +console.log(holey.length, holey[5]); +let visited = ""; +for (const s of xs) visited += s.name; +xs.forEach((s, i) => { + visited += i; +}); +console.log(visited, names(xs.map((s) => s.parent ?? s))); + +// Arrays inside class instances, optional and nullable elements. +const scope = new Scope(); +for (const s of [a, b, c, d, e]) { + scope.symbols.push(s); + s.decls.push(s, a); + s.parent = scope.symbols[0]; +} +scope.optional.push(a, undefined, c); +scope.nullable.push(null, b, null); +scope.optional[3] = e; +console.log(names(scope.symbols), names(scope.optional), names(scope.nullable)); +console.log(scope.optional.indexOf(undefined), scope.nullable.indexOf(null), scope.optional.length); +console.log(c.decls.length, c.decls[1] === a, e.parent === a, names(e.decls)); + +// Sets and Maps built from arrays, and arrays built from them. +const set = new Set(zs); +console.log(set.size, set.has(d), names([...set])); +const byName = new Map(zs.map((s) => [s.name, s] as const)); +console.log(names([...byName.values()]), byName.get("b") === b); +const grouped = new Map(); +for (const s of many.slice(0, 10)) { + const list = grouped.get(s.rank % 3) ?? []; + list.push(s); + grouped.set(s.rank % 3, list); +} +console.log([...grouped.entries()].map(([k, v]) => `${k}:${v.length}`).join(" ")); + +// Inspection and JSON. +console.log(xs.slice(0, 2).map((s) => s.name)); +console.log(JSON.stringify(scope.nullable.map((s) => s?.rank ?? null))); +console.log(JSON.stringify([a, b].map((s) => ({ name: s.name, rank: s.rank })))); diff --git a/tests/corpus/4639-contract-program-lifetime-maps.ts b/tests/corpus/4639-contract-program-lifetime-maps.ts new file mode 100644 index 000000000..21f7da33c --- /dev/null +++ b/tests/corpus/4639-contract-program-lifetime-maps.ts @@ -0,0 +1,120 @@ +import type { ProgramLifetime } from "@scriptc/types"; + +// Maps from strings to ProgramLifetime class instances: get, set, has and +// delete, growth past the linear scan into bucket tables, deletion with +// compaction, clear, every iteration form, copies, Sets of keys and +// values, maps held in class fields, optional and nullable values, +// inspection, and a cast through unknown. Each read sees the same +// instance that was stored. + +class Sym implements ProgramLifetime { + readonly name: string; + readonly rank: number; + readonly members = new Map(); + constructor(name: string, rank: number) { + this.name = name; + this.rank = rank; + } +} + +class Scope implements ProgramLifetime { + readonly locals = new Map(); + readonly maybe = new Map(); + readonly nullable = new Map(); +} + +function show(m: ReadonlyMap): string { + return [...m].map(([k, v]) => `${k}=${v === undefined ? "u" : v === null ? "n" : v.name}`).join(","); +} + +const a = new Sym("a", 3); +const b = new Sym("b", 1); +const c = new Sym("c", 2); + +// get, set, has, delete, overwrite and size. +const m = new Map(); +m.set("x", a).set("y", b); +m.set("z", c); +console.log(show(m), m.size, m.get("y") === b, m.has("z"), m.has("w"), m.get("w")); +m.set("x", c); +console.log(show(m), m.get("x") === c, m.delete("y"), m.delete("y"), show(m)); +const seeded = new Map([ + ["p", a], + ["q", b], +]); +console.log(show(seeded), seeded.get("q")?.rank); + +// Growth into bucket tables, then deletes that compact the entries. +const big = new Map(); +const all: Sym[] = []; +for (let i = 0; i < 3000; i++) { + const s = new Sym(`s${i}`, i); + all.push(s); + big.set(s.name, s); +} +let same = 0; +for (let i = 0; i < 3000; i++) if (big.get(`s${i}`) === all[i]) same++; +console.log(big.size, same, big.get("s1234")?.rank, big.has("s3000")); +for (let i = 0; i < 3000; i++) if (i % 3 !== 0) big.delete(`s${i}`); +same = 0; +for (const [k, v] of big) if (v === all[Number(k.slice(1))] && v.rank % 3 === 0) same++; +console.log(big.size, same, big.get("s2997")?.name, big.get("s2998")); +for (let i = 0; i < 3000; i += 3) big.set(`s${i}`, all[(i + 1) % 3000]!); +console.log(big.get("s0")?.name, big.get("s2997")?.name, big.size); + +// Iteration forms. +const order: string[] = []; +m.forEach((v, k) => { + order.push(`${k}:${v.name}`); +}); +console.log(order.join(" ")); +console.log([...m.keys()].join(), [...m.values()].map((s) => s.name).join(), [...m.entries()].length); +console.log(Array.from(m.values(), (s) => s.rank).join(), Array.from(m).length); +for (const v of m.values()) v.members.set("owner", a); +console.log(c.members.get("owner") === a, a.members.size); + +// Mutation during iteration: entries added by the callback are visited. +const grow = new Map([["g0", a]]); +let visits = 0; +for (const [k] of grow) { + visits++; + if (visits < 4) grow.set(`${k}+`, b); +} +console.log(visits, show(grow)); + +// Copies, Sets of keys and values, clear. +const copy = new Map(m); +copy.set("extra", b); +console.log(show(m), show(copy), copy.get("x") === m.get("x")); +const keySet = new Set(m.keys()); +const valueSet = new Set(m.values()); +console.log([...keySet].join(), valueSet.has(c), valueSet.has(b), valueSet.size); +copy.clear(); +console.log(copy.size, copy.get("x"), show(m)); +copy.set("again", a); +console.log(show(copy)); + +// Maps in class fields, optional and nullable values. +const scope = new Scope(); +scope.locals.set("a", a); +scope.maybe.set("a", a).set("gone", undefined); +scope.nullable.set("b", b).set("none", null); +console.log(show(scope.locals), show(scope.maybe), show(scope.nullable)); +console.log(scope.maybe.has("gone"), scope.maybe.get("gone"), scope.nullable.get("none"), scope.nullable.get("b")?.name); +const v = scope.maybe.get("a"); +console.log(v === a, v?.rank); + +// Objects from entries, inspection and JSON. +const obj = Object.fromEntries(m); +console.log(Object.keys(obj).join(), obj["z"] === c); +console.log(new Map([["only", b]])); +console.log(JSON.stringify([...m.keys()]), JSON.stringify(Object.fromEntries([...m].map(([k, s]) => [k, s.rank])))); + +// Through unknown and back. +const u: unknown = m; +if (u instanceof Map) { + const back = u as Map; + const seen: string[] = []; + for (const [k, s] of back) seen.push(`${String(k)}:${(s as Sym).name}`); + console.log(back.size, seen.join(), back.get("x") === c); +} diff --git a/tests/corpus/6706-dh-groups.ts b/tests/corpus/6706-dh-groups.ts new file mode 100644 index 000000000..24c8b5773 --- /dev/null +++ b/tests/corpus/6706-dh-groups.ts @@ -0,0 +1,52 @@ +// crypto.getDiffieHellman and createDiffieHellmanGroup on every modp +// group: the RFC primes and generator, verifyError, case-insensitive names, +// private keys of the group's width, and key pairs that agree on one +// secret of the prime's length. +import { createDiffieHellmanGroup, getDiffieHellman, type DiffieHellmanGroup } from "node:crypto"; + +const groups = ["modp1", "modp2", "modp5", "modp14", "modp15", "modp16", "modp17", "modp18"]; + +function bitLength(bytes: Buffer): number { + if (bytes.length === 0) return 0; + return (bytes.length - 1) * 8 + bytes[0]!.toString(2).length; +} + +for (const name of groups) { + const group: DiffieHellmanGroup = getDiffieHellman(name); + const prime: Buffer = group.getPrime(); + console.log( + name, + group.verifyError, + bitLength(prime), + prime.toString("hex").slice(0, 24), + prime.toString("hex").slice(-16), + group.getGenerator("hex"), + ); +} + +// Key exchange on the groups whose exponentiations stay quick. +for (const name of ["modp1", "modp2", "modp5", "modp14"]) { + const alice = getDiffieHellman(name); + const bob = createDiffieHellmanGroup(name.toUpperCase()); + const alicePublic: Buffer = alice.generateKeys(); + const bobPublic: string = bob.generateKeys("base64"); + const fromAlice: Buffer = alice.computeSecret(bobPublic, "base64"); + const fromBob: string = bob.computeSecret(alicePublic, null, "hex"); + const width = bitLength(alice.getPrivateKey()); + console.log( + name, + fromAlice.length, + fromAlice.toString("hex") === fromBob, + alicePublic.length <= fromAlice.length, + name === "modp1" || name === "modp2" ? width <= bitLength(alice.getPrime()) : width, + ); + // generateKeys keeps the key pair it has. + console.log(alice.generateKeys("hex") === alicePublic.toString("hex"), alice.getPublicKey().equals(alicePublic)); +} + +// The prime and generator in every encoding. +const modp5 = getDiffieHellman("Modp5"); +for (const encoding of ["hex", "base64", "base64url", "latin1", "binary"] as const) { + const prime: string = modp5.getPrime(encoding as "hex"); + console.log(encoding, prime.length, JSON.stringify(modp5.getGenerator(encoding as "hex"))); +} diff --git a/tests/corpus/6707-dh-explicit-params.ts b/tests/corpus/6707-dh-explicit-params.ts new file mode 100644 index 000000000..57e239d73 --- /dev/null +++ b/tests/corpus/6707-dh-explicit-params.ts @@ -0,0 +1,169 @@ +// crypto.createDiffieHellman over explicit parameters: fixed private keys +// give Node's public keys and secrets, verifyError reports DH_check's +// findings, and parameters that equal an OpenSSL named group draw private +// keys of the group's length. +import { createDiffieHellman, getDiffieHellman, type DiffieHellman } from "node:crypto"; + +// A 512-bit safe prime with p ≡ 23 (mod 24). +const P512 = + "f29864297728009aab9d4220c05ca2d80a5e4ac2f016cd098543a2486bde8bcb" + + "deed9eab7029c8d2b7a681f9495bfc7b1c39c6db64457bb79beeb74a4b78f5ff"; + +function bitLength(bytes: Buffer): number { + if (bytes.length === 0) return 0; + return (bytes.length - 1) * 8 + bytes[0]!.toString(2).length; +} + +function fixed(prime: Buffer, generator: number | Buffer, key: string): DiffieHellman { + const dh = createDiffieHellman(prime, generator); + dh.setPrivateKey(key, "hex"); + return dh; +} + +const prime512 = Buffer.from(P512, "hex"); +for (const generator of [2, 3, 5, Buffer.from([0x0b])]) { + const alice = fixed(prime512, generator, "0123456789abcdef0123456789abcdef"); + const bob = fixed(prime512, generator, "fedcba9876543210fedcba9876543210ff"); + const alicePublic: string = alice.generateKeys("hex"); + const bobPublic: Buffer = bob.generateKeys(); + const secret: string = alice.computeSecret(bobPublic, null, "hex"); + console.log(alice.getGenerator("hex"), alice.verifyError, alicePublic.slice(0, 32), bobPublic.length); + console.log(secret, secret === bob.computeSecret(alicePublic, "hex", "hex")); +} + +// Secrets pad to the prime's length; public keys are minimal. +const modp1 = getDiffieHellman("modp1").getPrime(); +const small = fixed(modp1, 2, "01"); +console.log(small.generateKeys("hex"), small.getPrivateKey("hex")); +const big = fixed(modp1, 2, "ff".repeat(100)); +console.log(big.generateKeys().length, big.getPrivateKey().length); +const padded: Buffer = small.computeSecret(Buffer.from([4])); +console.log(padded.length, padded.subarray(0, 4).toString("hex"), padded.subarray(-2).toString("hex")); +console.log(big.computeSecret(Buffer.from([2]), null, "base64url")); + +// A private key of 0 still yields a public key; reading it back and +// deriving a secret from it fail. A private key of 1 answers the peer's +// key. +function report(run: () => unknown): void { + try { + const result = run(); + console.log(Buffer.isBuffer(result) ? result.toString("hex") : result); + } catch (error) { + const e = error as Error & { code?: string }; + console.log(e.name, e.code, e.message); + } +} +const zero = fixed(prime512, 2, "00"); +console.log(zero.generateKeys("hex")); +report(() => zero.getPrivateKey()); +report(() => zero.computeSecret(Buffer.from([5]))); +report(() => fixed(prime512, 2, "01").computeSecret(Buffer.from([5]))); + +// A private key longer than the prime: 8800 bits. +const long = fixed(modp1, 2, "ff".repeat(1100)); +console.log(long.generateKeys("hex").slice(0, 32), long.computeSecret(Buffer.from([7]), null, "hex").slice(-32)); + +// setPublicKey and setPrivateKey answer the object and replace the keys. +const keys = createDiffieHellman(prime512); +keys.setPublicKey(Buffer.from([0, 0, 7])); +keys.setPrivateKey("0005", "hex"); +console.log(keys.getPublicKey("hex"), keys.getPrivateKey("hex")); +console.log(keys.generateKeys("hex").length, keys.getPublicKey("hex") !== "07"); + +// verifyError for small, even, composite and non-safe primes and for +// unsuitable generators. +const cases: [string, number | string][] = [ + ["01", 2], + ["02", 2], + ["03", 2], + ["0d", 2], + ["15", 2], + ["17", 2], + ["17", 5], + ["17", "16"], + ["17", "1e"], + ["18", 2], + ["000017", 2], + ["05", 2], + ["05", 5], + ["07", 2], + ["09", 2], + ["0b", 5], + ["0f", 2], + ["11", 2], + ["2f", 2], + ["3b", 5], + ["0231", 2], + ["0451", 2], + ["07ff", 2], +]; +for (const [prime, generator] of cases) { + const g = typeof generator === "number" ? generator : Buffer.from(generator, "hex"); + console.log(prime, generator, createDiffieHellman(Buffer.from(prime, "hex"), g).verifyError); +} +const almost = Buffer.from(P512, "hex"); +almost[almost.length - 1] = 0xfe; +console.log(createDiffieHellman(almost).verifyError); +const composite = Buffer.from(P512, "hex"); +composite[composite.length - 1] = 0xfd; +console.log(createDiffieHellman(composite).verifyError); +const generatorPMinus1 = Buffer.from(P512, "hex"); +generatorPMinus1[generatorPMinus1.length - 1] = 0xfe; +console.log(createDiffieHellman(prime512, generatorPMinus1).verifyError); + +// Generated keys: bits(p) - 2 bits for other primes, at most the named +// group's key length for RFC 3526 and RFC 5114 parameters. +const generated = createDiffieHellman(prime512); +generated.generateKeys(); +console.log(bitLength(generated.getPrivateKey())); +const modp14 = getDiffieHellman("modp14").getPrime(); +const named = createDiffieHellman(modp14, Buffer.from([0, 2])); +named.generateKeys(); +console.log(named.verifyError, bitLength(named.getPrivateKey()) <= 225); +const notNamed = createDiffieHellman(modp1); +notNamed.generateKeys(); +console.log(bitLength(notNamed.getPrivateKey())); +const rfc5114 = createDiffieHellman( + Buffer.from( + "b10b8f96a080e01dde92de5eae5d54ec52c99fbcfb06a3c69a6a9dca52d23b616073e28675a23d189838ef1e2ee652c0" + + "13ecb4aea906112324975c3cd49b83bfaccbdd7d90c4bd7098488e9c219a73724effd6fae5644738faa31a4ff55bccc0" + + "a151af5f0dc8b4bd45bf37df365c1a65e68cfda76d4da708df1fb2bc2e4a4371", + "hex", + ), + Buffer.from( + "a4d1cbd5c3fd34126765a442efb99905f8104dd258ac507fd6406cff14266d31266fea1e5c41564b777e690f5504f213" + + "160217b4b01b886a5e91547f9e2749f4d7fbd7d3b9a92ee1909d0d2263f80a76a6a24c087a091f531dbf0a0169b6a28a" + + "d662a4d18e73afa32d779d5918d08bc8858f4dcef97c2a24855e6eeb22b3b2e5", + "hex", + ), +); +rfc5114.generateKeys(); +console.log(rfc5114.verifyError, bitLength(rfc5114.getPrivateKey()) <= 160); +// RFC 5114's 2048-bit group with a 256-bit subgroup draws 224-bit keys. +const rfc5114q256 = createDiffieHellman( + Buffer.from( + "87a8e61db4b6663cffbbd19c651959998ceef608660dd0f25d2ceed4435e3b00e00df8f1d61957d4faf7df4561b2aa30" + + "16c3d91134096faa3bf4296d830e9a7c209e0c6497517abd5a8a9d306bcf67ed91f9e6725b4758c022e0b1ef4275bf7b" + + "6c5bfc11d45f9088b941f54eb1e59bb8bc39a0bf12307f5c4fdb70c581b23f76b63acae1caa6b7902d52526735488a0e" + + "f13c6d9a51bfa4ab3ad8347796524d8ef6a167b5a41825d967e144e5140564251ccacb83e6b486f6b3ca3f7971506026" + + "c0b857f689962856ded4010abd0be621c3a3960a54e710c375f26375d7014103a4b54330c198af126116d2276e11715f" + + "693877fad7ef09cadb094ae91e1a1597", + "hex", + ), + Buffer.from( + "3fb32c9b73134d0b2e77506660edbd484ca7b18f21ef205407f4793a1a0ba12510dbc15077be463fff4fed4aac0bb555" + + "be3a6c1b0c6b47b1bc3773bf7e8c6f62901228f8c28cbb18a55ae31341000a650196f931c77a57f2ddf463e5e9ec144b" + + "777de62aaab8a8628ac376d282d6ed3864e67982428ebc831d14348f6f2f9193b5045af2767164e1dfc967c1fb3f2e55" + + "a4bd1bffe83b9c80d052b985d182ea0adb2a3b7313d3fe14c8484b1e052588b9b7d2bbd2df016199ecd06e1557cd0915" + + "b3353bbb64e0ec377fd028370df92b52c7891428cdc67eb6184b523d1db246c32f63078490f00ef8d647d148d4795451" + + "5e2327cfef98c582664b4c0f6cc41659", + "hex", + ), +); +let widest = 0; +for (let i = 0; i < 8; i++) { + const keys = createDiffieHellman(rfc5114q256.getPrime(), rfc5114q256.getGenerator()); + keys.generateKeys(); + widest = Math.max(widest, bitLength(keys.getPrivateKey())); +} +console.log(rfc5114q256.verifyError, widest <= 224, widest > 200); diff --git a/tests/corpus/6708-dh-errors.js b/tests/corpus/6708-dh-errors.js new file mode 100644 index 000000000..0c711c96d --- /dev/null +++ b/tests/corpus/6708-dh-errors.js @@ -0,0 +1,175 @@ +// createDiffieHellman, getDiffieHellman and DiffieHellman errors from +// JavaScript: argument types and ranges, the keyEncoding and generator +// forms, OpenSSL's generator and modulus errors with their library and +// reason properties, parameter sizes, group names, encodings, keys read +// before they exist, public keys outside (1, p - 1), key generation and +// secrets on primes outside 512 to 10000 bits, and members a +// DiffieHellmanGroup does not have. +import crypto from "node:crypto"; + +// Values the declared overloads reject reach the calls through an +// untyped function, as they would from other untyped code. +const loose = (value) => value; + +function attempt(label, run) { + try { + const result = run(); + if (Buffer.isBuffer(result)) console.log(label, "ok", result.toString("hex")); + else if (typeof result === "object" && result !== null) + console.log(label, "ok", result.getGenerator("hex"), result.getPrime().length); + else console.log(label, "ok", JSON.stringify(result)); + } catch (e) { + const extra = e.library === undefined ? "" : ` [${Object.keys(e).join(",")}] ${e.library} / ${e.reason}`; + console.log(label, e.name, e.code, e.message + extra); + } +} + +const prime = crypto.getDiffieHellman("modp1").getPrime(); + +attempt("size missing", () => crypto.createDiffieHellman(loose(undefined))); +attempt("size null", () => crypto.createDiffieHellman(loose(null))); +attempt("size boolean", () => crypto.createDiffieHellman(loose(true))); +attempt("size object", () => crypto.createDiffieHellman(loose({}))); +attempt("size bigint", () => crypto.createDiffieHellman(loose(5n))); +attempt("size fraction", () => crypto.createDiffieHellman(1.5)); +attempt("size NaN", () => crypto.createDiffieHellman(NaN)); +attempt("size 2^31", () => crypto.createDiffieHellman(2 ** 31)); +attempt("size 2^40", () => crypto.createDiffieHellman(2 ** 40)); +attempt("size -1", () => crypto.createDiffieHellman(-1)); +attempt("size -0", () => crypto.createDiffieHellman(-0)); +attempt("size 1", () => crypto.createDiffieHellman(1)); +attempt("size 1 bad generator", () => crypto.createDiffieHellman(1, 1)); +attempt("size 2 generator 1", () => crypto.createDiffieHellman(2, 1)); +attempt("size 2", () => crypto.createDiffieHellman(2)); +attempt("size 511", () => crypto.createDiffieHellman(511)); +attempt("size 10001", () => crypto.createDiffieHellman(10001)); +attempt("size generator string", () => crypto.createDiffieHellman(64, loose("02"))); +attempt("size generator view", () => crypto.createDiffieHellman(64, loose(Buffer.from([2])))); + +attempt("generator 0", () => crypto.createDiffieHellman(prime, 0)); +attempt("generator 1", () => crypto.createDiffieHellman(prime, 1)); +attempt("generator -1", () => crypto.createDiffieHellman(prime, -1)); +attempt("generator fraction", () => crypto.createDiffieHellman(prime, 2.5)); +attempt("generator 2^31", () => crypto.createDiffieHellman(prime, 2 ** 31)); +attempt("generator NaN", () => crypto.createDiffieHellman(prime, NaN)); +attempt("generator null", () => crypto.createDiffieHellman(prime, loose(null))); +attempt("generator empty string", () => crypto.createDiffieHellman(prime, loose(""))); +attempt("generator true", () => crypto.createDiffieHellman(prime, loose(true))); +attempt("generator object", () => crypto.createDiffieHellman(prime, loose({}))); +attempt("generator view 1", () => crypto.createDiffieHellman(prime, Buffer.from([1]))); +attempt("generator view 0", () => crypto.createDiffieHellman(prime, Buffer.from([0]))); +attempt("generator view empty", () => crypto.createDiffieHellman(prime, Buffer.alloc(0))); +attempt("generator view 0002", () => crypto.createDiffieHellman(prime, Buffer.from([0, 2]))); +attempt("generator = p", () => crypto.createDiffieHellman(prime, prime)); +attempt("generator > p", () => crypto.createDiffieHellman(prime, Buffer.concat([Buffer.from([1]), prime]))); +attempt("generator string", () => crypto.createDiffieHellman(prime, loose("abc"))); +attempt("generator string hex", () => crypto.createDiffieHellman(prime, loose("05"), loose("hex"))); +attempt("generator bad encoding", () => crypto.createDiffieHellman(prime, loose("05"), loose("nope"))); +attempt("generator uint16", () => crypto.createDiffieHellman(prime, new Uint16Array([0x0300]))); + +attempt("prime hex", () => crypto.createDiffieHellman(prime.toString("hex"), "hex")); +attempt("prime base64 generator", () => crypto.createDiffieHellman(prime.toString("base64"), "base64", 5)); +attempt("prime utf8", () => crypto.createDiffieHellman("abc").verifyError); +attempt("prime buffer encoding", () => crypto.createDiffieHellman("abc", "buffer")); +attempt("keyEncoding as generator", () => crypto.createDiffieHellman("abc", loose("nope")).verifyError); +attempt("keyEncoding as generator with encoding", () => crypto.createDiffieHellman("abc", loose("nope"), loose("hex"))); +attempt("keyEncoding number", () => crypto.createDiffieHellman(prime, loose(5))); +attempt("keyEncoding on a view", () => crypto.createDiffieHellman(prime, loose("hex"), 5)); +attempt("prime ArrayBuffer", () => crypto.createDiffieHellman(new Uint8Array(prime).buffer)); +attempt("prime DataView", () => crypto.createDiffieHellman(new DataView(new ArrayBuffer(3))).verifyError); +attempt("prime zero", () => crypto.createDiffieHellman(Buffer.alloc(4)).getPrime()); + +attempt("group unknown", () => crypto.getDiffieHellman("modp3")); +attempt("group empty", () => crypto.getDiffieHellman("")); +attempt("group ffdhe", () => crypto.createDiffieHellmanGroup("ffdhe2048")); +attempt("group case", () => crypto.getDiffieHellman("MoDp2")); +attempt("group missing", () => crypto.getDiffieHellman(loose(undefined))); +attempt("group number", () => crypto.getDiffieHellman(loose(14))); + +const group = crypto.getDiffieHellman("modp2"); +attempt("group public before keys", () => group.getPublicKey()); +attempt("group private before keys", () => group.getPrivateKey()); +attempt("group secret before keys", () => group.computeSecret(Buffer.from([5]))); +const untypedGroup = loose(group); +attempt("group setPublicKey", () => untypedGroup.setPublicKey(Buffer.from([5]))); +attempt("group setPrivateKey", () => untypedGroup.setPrivateKey(Buffer.from([5]))); +group.generateKeys(); +attempt("secret 0", () => group.computeSecret(Buffer.from([0]))); +attempt("secret empty", () => group.computeSecret(Buffer.alloc(0))); +attempt("secret 1", () => group.computeSecret(Buffer.from([0, 1]))); +attempt("secret p - 1", () => { + const key = Buffer.from(group.getPrime()); + key[key.length - 1]--; + return group.computeSecret(key); +}); +attempt("secret p", () => group.computeSecret(group.getPrime())); +attempt("secret 2", () => group.computeSecret(Buffer.from([2])).length); +attempt("secret number", () => group.computeSecret(loose(5))); +attempt("secret bad input encoding", () => group.computeSecret("ab", loose("nope"))); +attempt("secret bad output encoding", () => untypedGroup.computeSecret(Buffer.from([2]), null, "nope")); +attempt("getPrime bad encoding", () => untypedGroup.getPrime("nope")); +attempt("getPrime HEX", () => untypedGroup.getPrime("HEX").slice(0, 8)); +attempt("getGenerator utf16le", () => untypedGroup.getGenerator("utf16le")); +attempt("verifyError call", () => untypedGroup.verifyError()); +attempt("unknown member", () => untypedGroup.bogus()); + +const fresh = crypto.createDiffieHellman(prime); +attempt("public before keys", () => fresh.getPublicKey()); +attempt("private before keys", () => fresh.getPrivateKey()); +attempt("secret before keys", () => fresh.computeSecret(Buffer.from([5]))); +attempt("setPublicKey number", () => fresh.setPublicKey(loose(5))); +attempt("setPrivateKey bad encoding", () => fresh.setPrivateKey("ab", loose("nope"))); +attempt("setPublicKey 0", () => loose(fresh.setPublicKey(Buffer.from([0]))).getPublicKey()); +attempt("setPrivateKey utf8", () => loose(fresh.setPrivateKey("ab")).getPrivateKey("hex")); + +// Primes outside 512 to 10000 bits: key generation and secrets fail, but a +// private key the program sets still yields a public key. +for (const value of [23, 1, 0, 24]) { + const label = `p ${value}`; + const dh = crypto.createDiffieHellman(Buffer.from([value])); + attempt(`${label} generateKeys`, () => dh.generateKeys()); + attempt(`${label} secret 2`, () => dh.computeSecret(Buffer.from([2]))); + attempt(`${label} secret 5`, () => dh.computeSecret(Buffer.from([5]))); + dh.setPrivateKey(Buffer.from([3])); + attempt(`${label} generateKeys with key`, () => dh.generateKeys()); + attempt(`${label} public with key`, () => dh.getPublicKey()); + attempt(`${label} secret with key`, () => dh.computeSecret(Buffer.from([5]))); +} +const even = Buffer.from(prime); +even[even.length - 1] = 0xfe; +const evenDh = crypto.createDiffieHellman(even); +attempt("even generateKeys", () => evenDh.generateKeys()); +evenDh.setPrivateKey(Buffer.from([3])); +attempt("even generateKeys with key", () => evenDh.generateKeys()); +attempt("p 23 generator 30", () => + loose(crypto.createDiffieHellman(Buffer.from([23]), 30).setPrivateKey(Buffer.from([2]))).generateKeys(), +); +attempt("p 9", () => loose(crypto.createDiffieHellman(Buffer.from([9])).setPrivateKey(Buffer.from([3]))).generateKeys()); + +// Secrets of 0, 1 and p - 1 fail like any other derivation failure: a peer +// key of order 3 or 6 modulo a prime with p ≡ 1 (mod 6), and a peer key +// sharing the factor of a square modulus. +const smooth = Buffer.from( + "ce306415e770bb1cde0380e3c5cf4e8110b5c8e82ca36b1d9074fa1e9d1ed41b0ae295f19288302647d2da0003fcac6fa21d8953afce2c30171844815ec7d6df", + "hex", +); +const orderThree = Buffer.from( + "6be4b47bb9f8143334dab6349ac191ef7208ae3d5e684a2c318d43acc45ebd2a27d79b075c6e14a4e6a75899d98ce05af71ef05e1869b1add76d1622ab5e7b1d", + "hex", +); +const orderSix = Buffer.from( + "624baf9a2d78a6e9a928caaf2b0dbc919ead1aaace3b20f15ee7b671d8c016f0e30afaea361a1b81612b81662a6fcc14aafe98f597647a823fab2e5eb3695bc2", + "hex", +); +function withPrivateKey(modulus, key) { + const dh = crypto.createDiffieHellman(modulus); + dh.setPrivateKey(Buffer.from([key])); + return dh; +} +attempt("secret 1", () => withPrivateKey(smooth, 3).computeSecret(orderThree).toString("hex")); +attempt("secret p - 1", () => withPrivateKey(smooth, 3).computeSecret(orderSix).toString("hex")); +attempt("secret of order 3, key 4", () => withPrivateKey(smooth, 4).computeSecret(orderThree).toString("hex").slice(0, 16)); +const factor = BigInt("0xdf22630cec2362c510f444af6784da7a3a714ed787eb3d6bf9158d18c087cc5d"); +const square = Buffer.from((factor * factor).toString(16).padStart(128, "0"), "hex"); +const factorKey = Buffer.from(factor.toString(16).padStart(128, "0"), "hex"); +attempt("secret 0", () => withPrivateKey(square, 2).computeSecret(factorKey).toString("hex")); diff --git a/tests/corpus/6709-dh-generate.ts b/tests/corpus/6709-dh-generate.ts new file mode 100644 index 000000000..b041605c2 --- /dev/null +++ b/tests/corpus/6709-dh-generate.ts @@ -0,0 +1,47 @@ +// crypto.createDiffieHellman(primeLength, generator) generates a safe +// prime of the requested length: p ≡ 23 (mod 24) for the generator 2, +// p ≡ 59 (mod 60) for 5 and p ≡ 11 (mod 12) otherwise. A second party built +// from the first one's prime and generator agrees on the secret. +import { createDiffieHellman } from "node:crypto"; + +function bitLength(bytes: Buffer): number { + if (bytes.length === 0) return 0; + return (bytes.length - 1) * 8 + bytes[0]!.toString(2).length; +} + +function mod(bytes: Buffer, m: number): number { + let r = 0; + for (const byte of bytes) r = (r * 256 + byte) % m; + return r; +} + +for (const generator of [undefined, 2, 5, 3, 7]) { + const alice = generator === undefined ? createDiffieHellman(512) : createDiffieHellman(512, generator); + const prime = alice.getPrime(); + console.log( + generator, + bitLength(prime), + alice.getGenerator("hex"), + alice.verifyError, + generator === undefined || generator === 2 + ? mod(prime, 24) + : generator === 5 + ? mod(prime, 60) + : mod(prime, 12), + ); + const bob = createDiffieHellman(alice.getPrime(), alice.getGenerator()); + const alicePublic = alice.generateKeys(); + const bobPublic = bob.generateKeys("hex"); + const secret = alice.computeSecret(bobPublic, "hex"); + console.log( + bob.verifyError, + bitLength(alice.getPrivateKey()), + secret.length, + secret.equals(bob.computeSecret(alicePublic)), + ); +} + +// A string prime with its encoding, and the generator as a string. +const shared = createDiffieHellman(512); +const copy = createDiffieHellman(shared.getPrime("base64"), "base64", shared.getGenerator("hex"), "hex"); +console.log(copy.getPrime().equals(shared.getPrime()), copy.getGenerator("hex"), copy.verifyError); diff --git a/tests/corpus/6710-dh-encodings.cjs b/tests/corpus/6710-dh-encodings.cjs new file mode 100644 index 000000000..100ee95e2 --- /dev/null +++ b/tests/corpus/6710-dh-encodings.cjs @@ -0,0 +1,73 @@ +// DiffieHellman string inputs and outputs from CommonJS: primes and +// generators as strings in every binary encoding, the keyEncoding slot +// read as the generator, keys set in every Buffer encoding and read in the +// declared ones, and computeSecret's input and output encodings. +const crypto = require("node:crypto"); + +// Values the declared overloads do not cover reach the calls through an +// untyped function, as they would from other untyped code. +const loose = (value) => value; + +const prime = crypto.getDiffieHellman("modp2").getPrime(); +const encodings = ["hex", "base64", "base64url", "latin1", "binary", "HEX", "Base64"]; + +for (const encoding of encodings) { + const text = prime.toString(encoding.toLowerCase()); + const dh = crypto.createDiffieHellman(text, encoding, "05", "hex"); + console.log(encoding, dh.getPrime().equals(prime), dh.getGenerator("hex")); +} +console.log(crypto.createDiffieHellman(prime.toString("hex"), "hex", "05", "hex").verifyError); + +// A truthy keyEncoding that names no encoding is the generator, and the +// generator slot its encoding. +console.log(crypto.createDiffieHellman(prime, 7).getGenerator("hex")); +console.log(crypto.createDiffieHellman(prime, "\x05").getGenerator("hex")); +console.log(crypto.createDiffieHellman(prime, Buffer.from([3])).getGenerator("hex")); +console.log(crypto.createDiffieHellman(prime, "buffer", 11).getGenerator("hex")); +console.log(crypto.createDiffieHellman(prime, "0d", "hex").getGenerator("hex")); +console.log(crypto.createDiffieHellman(prime, "base64", "Ew==", "base64").getGenerator("hex")); +console.log(crypto.createDiffieHellman(prime, "", 0, "").getGenerator("hex")); + +// Keys in and out of every encoding. +const alice = crypto.createDiffieHellman(prime); +const bob = crypto.createDiffieHellman(prime); +alice.setPrivateKey("q83vEjRWeJA=", "base64"); +bob.setPrivateKey(Buffer.from("10325476", "hex")); +alice.generateKeys(); +bob.generateKeys(); +function report(encoding, key, secret) { + const bytes = Buffer.isBuffer(secret) ? secret : Buffer.from(secret, encoding); + console.log(encoding, typeof key, key.length, bytes.toString("hex").slice(0, 24)); +} +const hex = alice.getPublicKey("hex"); +report("hex", hex, bob.computeSecret(hex, "hex", "hex")); +const base64 = alice.getPublicKey("base64"); +report("base64", base64, bob.computeSecret(base64, "base64", "base64")); +const base64url = alice.getPublicKey("base64url"); +report("base64url", base64url, bob.computeSecret(base64url, "base64url", "base64url")); +const binary = alice.getPublicKey("binary"); +report("binary", binary, bob.computeSecret(binary, "binary", "binary")); +const view = alice.getPublicKey(); +report("undefined", view, bob.computeSecret(view, undefined, undefined)); +report("null", view, bob.computeSecret(view, null, null)); +// Encodings @types/node does not declare for these methods reach them +// through an untyped receiver. +const untypedBob = loose(bob); +console.log(alice.getPrivateKey("hex"), alice.getPrivateKey("base64url"), untypedBob.getPrivateKey("latin1").length); +console.log(untypedBob.getPublicKey("utf16le").length, untypedBob.getPrime("ucs2").length, untypedBob.getGenerator("buffer")); +console.log(untypedBob.computeSecret(loose(alice).getPublicKey("latin1"), "latin1", "base64").slice(0, 16)); + +// setPublicKey and setPrivateKey read strings through their encoding; +// 'buffer' and a missing encoding read utf8. +const keys = crypto.createDiffieHellman(prime); +keys.setPublicKey("ffee", "hex"); +loose(keys).setPrivateKey("zz", "buffer"); +console.log(keys.getPublicKey("hex"), keys.getPrivateKey("hex")); +keys.setPublicKey("é"); +console.log(keys.getPublicKey("hex")); +keys.setPublicKey("é", "latin1"); +console.log(keys.getPublicKey("hex")); +keys.setPrivateKey(new Uint16Array([0x0102, 0x0304])); +console.log(keys.getPrivateKey("hex")); +keys.setPrivateKey(new DataView(new Uint8Array([9, 8, 7]).buffer)); +console.log(keys.getPrivateKey("hex")); diff --git a/tests/corpus/6711-dh-handles.ts b/tests/corpus/6711-dh-handles.ts new file mode 100644 index 000000000..c0c5522be --- /dev/null +++ b/tests/corpus/6711-dh-handles.ts @@ -0,0 +1,56 @@ +// DiffieHellman and DiffieHellmanGroup values as ordinary values: class +// fields, optional fields, arrays, Map values, parameters and return +// values, with verifyError read through each. +import * as crypto from "node:crypto"; +import { type DiffieHellman, type DiffieHellmanGroup } from "node:crypto"; + +const prime = crypto.getDiffieHellman("modp1").getPrime(); + +class Party { + readonly dh: DiffieHellman; + peer: DiffieHellman | null = null; + group?: DiffieHellmanGroup; + + constructor(seed: number) { + this.dh = crypto.createDiffieHellman(prime, 2); + this.dh.setPrivateKey(Buffer.from([seed, seed + 1, seed + 2])); + this.dh.generateKeys(); + } + + secretWith(other: Party): string { + return this.dh.computeSecret(other.dh.getPublicKey(), null, "hex"); + } +} + +const alice = new Party(1); +const bob = new Party(7); +alice.peer = bob.dh; +console.log(alice.secretWith(bob) === bob.secretWith(alice), alice.secretWith(bob).slice(0, 32)); +console.log(alice.peer !== null && alice.peer.getPublicKey().equals(bob.dh.getPublicKey())); +console.log(alice.group === undefined); +alice.group = crypto.getDiffieHellman("modp2"); +console.log(alice.group.verifyError, alice.group.getPrime().length); + +function verify(dh: DiffieHellman | DiffieHellmanGroup): number { + return dh.verifyError; +} + +function make(bytes: number[]): DiffieHellman { + return crypto.createDiffieHellman(Buffer.from(bytes)); +} + +const all: (DiffieHellman | DiffieHellmanGroup)[] = [alice.dh, make([23]), make([24]), crypto.getDiffieHellman("modp5")]; +console.log(all.map(verify).join(",")); +for (const dh of all) console.log(dh.getGenerator("hex"), dh.getPrime().length); + +const byName = new Map(); +for (const name of ["modp1", "modp2"]) byName.set(name, crypto.createDiffieHellmanGroup(name)); +for (const [name, group] of byName) { + group.generateKeys(); + console.log(name, group.getGenerator("hex"), group.computeSecret(alice.dh.getPublicKey()).length); +} + +let maybe: DiffieHellman | undefined; +console.log(maybe?.verifyError); +maybe = make([0x0d]); +console.log(maybe?.verifyError, maybe.getPrime("hex")); diff --git a/tests/corpus/6712-dh-peer-keys.js b/tests/corpus/6712-dh-peer-keys.js new file mode 100644 index 000000000..819fcf960 --- /dev/null +++ b/tests/corpus/6712-dh-peer-keys.js @@ -0,0 +1,117 @@ +// DiffieHellman computeSecret's peer key checks, and keys, primes and +// generators with leading zero bytes: under the RFC 5114 groups a peer key +// outside the order-q subgroup is invalid, while other parameters only +// check the range; a key longer than the prime is too large, and leading +// zeros of any length are ignored. +import crypto from "node:crypto"; + +function attempt(label, run) { + try { + console.log(label, "ok", JSON.stringify(run())); + } catch (e) { + console.log(label, e.name, e.code, e.message); + } +} + +const zeros = Buffer.alloc(100000); +const padded = (bytes) => Buffer.concat([zeros, bytes]); + +// Each group's parameters, and a peer key of small order: h^((p - 1) / r) +// for a small prime r dividing (p - 1) / q. +const groups = [ + { + label: "RFC 5114 1024-bit", + order: 7, + p: "B10B8F96A080E01DDE92DE5EAE5D54EC52C99FBCFB06A3C69A6A9DCA52D23B616073E28675A23D189838EF1E2EE652C013ECB4AEA906112324975C3CD49B83BFACCBDD7D90C4BD7098488E9C219A73724EFFD6FAE5644738FAA31A4FF55BCCC0A151AF5F0DC8B4BD45BF37DF365C1A65E68CFDA76D4DA708DF1FB2BC2E4A4371", + g: "A4D1CBD5C3FD34126765A442EFB99905F8104DD258AC507FD6406CFF14266D31266FEA1E5C41564B777E690F5504F213160217B4B01B886A5E91547F9E2749F4D7FBD7D3B9A92EE1909D0D2263F80A76A6A24C087A091F531DBF0A0169B6A28AD662A4D18E73AFA32D779D5918D08BC8858F4DCEF97C2A24855E6EEB22B3B2E5", + small: "24986e0c35fbae0d16a21b2aab7eac0dc32e9ca8c24c1eb9387366ee8a6df2bee49b6f46ea2525d208c1bc3d87e3749586616e0161a305f670830ba1fa9e4c5a1bd5ae9f4dc2032f1fd99bbcf3f4444a09d4da3ccbde680580ee8d78feced92d1efb235fc9f25c8525a4e5a453ccf7375119cee48b05421c093d10c8b6e90ab8", + }, + { + label: "RFC 5114 2048-bit, 224-bit q", + order: 3, + p: "AD107E1E9123A9D0D660FAA79559C51FA20D64E5683B9FD1B54B1597B61D0A75E6FA141DF95A56DBAF9A3C407BA1DF15EB3D688A309C180E1DE6B85A1274A0A66D3F8152AD6AC2129037C9EDEFDA4DF8D91E8FEF55B7394B7AD5B7D0B6C12207C9F98D11ED34DBF6C6BA0B2C8BBC27BE6A00E0A0B9C49708B3BF8A317091883681286130BC8985DB1602E714415D9330278273C7DE31EFDC7310F7121FD5A07415987D9ADC0A486DCDF93ACC44328387315D75E198C641A480CD86A1B9E587E8BE60E69CC928B2B9C52172E413042E9B23F10B0E16E79763C9B53DCF4BA80A29E3FB73C16B8E75B97EF363E2FFA31F71CF9DE5384E71B81C0AC4DFFE0C10E64F", + g: "AC4032EF4F2D9AE39DF30B5C8FFDAC506CDEBE7B89998CAF74866A08CFE4FFE3A6824A4E10B9A6F0DD921F01A70C4AFAAB739D7700C29F52C57DB17C620A8652BE5E9001A8D66AD7C17669101999024AF4D027275AC1348BB8A762D0521BC98AE247150422EA1ED409939D54DA7460CDB5F6C6B250717CBEF180EB34118E98D119529A45D6F834566E3025E316A330EFBB77A86F0C1AB15B051AE3D428C8F8ACB70A8137150B8EEB10E183EDD19963DDD9E263E4770589EF6AA21E7F5F2FF381B539CCE3409D13CD566AFBB48D6C019181E1BCFE94B30269EDFE72FE9B6AA4BD7B5A0F1C71CFFF4C19C418E1F6EC017981BC087F2A7065B384B890D3191F2BFA", + small: "73ef184cd50bd427992f4bbeaf1d7712f8cb8264929f533ded004618b29064e3583963fc634df866ef9aa5f158077765219401ad4bf0a2c42204241c8a728af1012cfd399a76f13a0e2cb153c0888a9eb0e05312610923777d185f46bf865bbfe86a0e79a6761d7e153cb46e2221dc3415a7e78c22d33c3d1e2363c041bb57badd344969ee2a1c4917d26ab51a20bc343b4db9964871b0918b2b8e97ff7d297e8b8ca2329426755fdf302a6401030c0f0206a0da004cac53d4408d2ff47cff332435d1f01cbeee301b8dda896be231fd38264f1a5c2b51365a8e74e608a2510c26c92c8d091e18577e854c43d5dc3b2779552f6723878deb5ec58bce2da61abe", + }, + { + label: "RFC 5114 2048-bit, 256-bit q", + order: 7, + p: "87A8E61DB4B6663CFFBBD19C651959998CEEF608660DD0F25D2CEED4435E3B00E00DF8F1D61957D4FAF7DF4561B2AA3016C3D91134096FAA3BF4296D830E9A7C209E0C6497517ABD5A8A9D306BCF67ED91F9E6725B4758C022E0B1EF4275BF7B6C5BFC11D45F9088B941F54EB1E59BB8BC39A0BF12307F5C4FDB70C581B23F76B63ACAE1CAA6B7902D52526735488A0EF13C6D9A51BFA4AB3AD8347796524D8EF6A167B5A41825D967E144E5140564251CCACB83E6B486F6B3CA3F7971506026C0B857F689962856DED4010ABD0BE621C3A3960A54E710C375F26375D7014103A4B54330C198AF126116D2276E11715F693877FAD7EF09CADB094AE91E1A1597", + g: "3FB32C9B73134D0B2E77506660EDBD484CA7B18F21EF205407F4793A1A0BA12510DBC15077BE463FFF4FED4AAC0BB555BE3A6C1B0C6B47B1BC3773BF7E8C6F62901228F8C28CBB18A55AE31341000A650196F931C77A57F2DDF463E5E9EC144B777DE62AAAB8A8628AC376D282D6ED3864E67982428EBC831D14348F6F2F9193B5045AF2767164E1DFC967C1FB3F2E55A4BD1BFFE83B9C80D052B985D182EA0ADB2A3B7313D3FE14C8484B1E052588B9B7D2BBD2DF016199ECD06E1557CD0915B3353BBB64E0EC377FD028370DF92B52C7891428CDC67EB6184B523D1DB246C32F63078490F00EF8D647D148D47954515E2327CFEF98C582664B4C0F6CC41659", + small: "7e22fad9cc23b5949616a26b060dcec3557a81d98dc45f51943f4e29b06dd73e6fbf201c01c4c2620b81698048fcb21655d1276bfb402a41c3af50528f2df02b3440a3b7a1855dfe31a549ddce9563ed18fe1530a3a649f87fa4d427d6d2e1b73cf3848177651080f2ca96628fed411c331d9e28d28da5f0c65f2516f9bb4c72e4c9050f5d654bcc0139e66fbc582ae32d345ad84a249d9cea131c6a9af59ecaefbc190ca265394eb8190ff91a6af58327060ca4900829eaa3a1c26f86737d7510bfd55c430bcc1f2db6a62c3bfe1717e5236945c475bb7b36dc2fa5ab06b089325dfd864a6b044622e62a5638ce23f319cf564826ce1e5c1bc1166896c5f205", + }, +]; + +for (const group of groups) { + const p = Buffer.from(group.p, "hex"); + const g = Buffer.from(group.g, "hex"); + const alice = crypto.createDiffieHellman(p, g); + const bob = crypto.createDiffieHellman(p, g); + alice.generateKeys(); + bob.generateKeys(); + const name = group.label; + attempt(`${name}: verifyError`, () => alice.verifyError); + attempt(`${name}: agreed`, () => alice.computeSecret(bob.getPublicKey()).equals(bob.computeSecret(alice.getPublicKey()))); + attempt(`${name}: order ${group.order}`, () => alice.computeSecret(Buffer.from(group.small, "hex")).length); + attempt(`${name}: 2`, () => alice.computeSecret(Buffer.from([2])).length); + attempt(`${name}: p - 2`, () => { + const key = Buffer.from(p); + key[key.length - 1] -= 2; + return alice.computeSecret(key).length; + }); + attempt(`${name}: p - 1`, () => { + const key = Buffer.from(p); + key[key.length - 1] -= 1; + return alice.computeSecret(key).length; + }); + attempt(`${name}: without keys`, () => crypto.createDiffieHellman(p, g).computeSecret(Buffer.from([2])).length); + // The same prime with another generator is not the named group. The + // private key is a multiple of neither 3 nor 7, so the small-order key + // gives a secret other than 1. + const other = crypto.createDiffieHellman(p, 2); + other.setPrivateKey(Buffer.from("0123456789abcdee", "hex")); + attempt(`${name}, generator 2: order ${group.order}`, () => other.computeSecret(Buffer.from(group.small, "hex")).length); + attempt(`${name}, generator 2: 2`, () => other.computeSecret(Buffer.from([2])).length); +} + +// The safe prime groups check only the range: 11 is a quadratic +// non-residue modulo the 2048-bit MODP prime. +const modp14 = crypto.getDiffieHellman("modp14"); +modp14.generateKeys(); +attempt("modp14: non-residue", () => modp14.computeSecret(Buffer.from([11])).length); +const explicit = crypto.createDiffieHellman(modp14.getPrime(), modp14.getGenerator()); +explicit.generateKeys(); +attempt("modp14 explicit: non-residue", () => explicit.computeSecret(Buffer.from([11])).length); + +// Keys longer than the prime, and leading zeros. +const peer = crypto.createDiffieHellman(modp14.getPrime(), 2); +peer.generateKeys(); +const secret = modp14.computeSecret(peer.getPublicKey()); +attempt("100000 bytes of 0xff", () => modp14.computeSecret(Buffer.alloc(100000, 0xff)).length); +attempt("prime and one byte", () => modp14.computeSecret(Buffer.concat([Buffer.from([1]), modp14.getPrime()])).length); +attempt("hex of 100000 bytes", () => modp14.computeSecret(Buffer.alloc(100000, 0x7f).toString("hex"), "hex").length); +attempt("zero-padded key", () => modp14.computeSecret(padded(peer.getPublicKey())).equals(secret)); +attempt("zero-padded hex key", () => modp14.computeSecret(padded(peer.getPublicKey()).toString("hex"), "hex").equals(secret)); +attempt("zeros", () => modp14.computeSecret(zeros).length); +attempt("zero-padded 1", () => modp14.computeSecret(padded(Buffer.from([1]))).length); +attempt("zero-padded p - 1", () => { + const key = Buffer.from(modp14.getPrime()); + key[key.length - 1] -= 1; + return modp14.computeSecret(padded(key)).length; +}); +attempt("zero-padded public key", () => { + const dh = crypto.createDiffieHellman(modp14.getPrime(), 2); + dh.setPublicKey(padded(peer.getPublicKey())); + return dh.getPublicKey().equals(peer.getPublicKey()); +}); +attempt("zero-padded private key", () => { + const dh = crypto.createDiffieHellman(modp14.getPrime(), 2); + dh.setPrivateKey(padded(peer.getPrivateKey())); + return [dh.getPrivateKey().equals(peer.getPrivateKey()), dh.generateKeys().equals(peer.getPublicKey())]; +}); +attempt("zero-padded prime and generator", () => { + const dh = crypto.createDiffieHellman(padded(modp14.getPrime()), padded(Buffer.from([2]))); + return [dh.verifyError, dh.getPrime().equals(modp14.getPrime()), dh.getGenerator("hex")]; +}); +attempt("generator of 100000 bytes", () => crypto.createDiffieHellman(modp14.getPrime(), Buffer.alloc(100000, 0xff)).verifyError); +attempt("zero-padded generator 1", () => crypto.createDiffieHellman(modp14.getPrime(), padded(Buffer.from([1]))).verifyError); diff --git a/tests/corpus/6822-jsx-classic-runtime/factory.ts b/tests/corpus/6822-jsx-classic-runtime/factory.ts new file mode 100644 index 000000000..df09c6dde --- /dev/null +++ b/tests/corpus/6822-jsx-classic-runtime/factory.ts @@ -0,0 +1,7 @@ +// A React-shaped factory for the pragma file: createElement and Fragment. +export const Fragment = "frag"; + +export function createElement(tag: string, props: object | null, ...children: unknown[]): string { + const inner = children.map((child) => String(child)).join(","); + return `${tag}${props === null ? "" : JSON.stringify(props)}(${inner})`; +} diff --git a/tests/corpus/6822-jsx-classic-runtime/html.ts b/tests/corpus/6822-jsx-classic-runtime/html.ts new file mode 100644 index 000000000..2e4beb5cc --- /dev/null +++ b/tests/corpus/6822-jsx-classic-runtime/html.ts @@ -0,0 +1,45 @@ +// A small server-side JSX runtime: h builds elements, render writes HTML. +export type Leaf = string | number | boolean | null | undefined | Element; +export type Child = Leaf | Leaf[]; +export type Props = { [name: string]: unknown }; +export type Component = (props: Props, children: Child[]) => Element; +export interface Element { + tag: string; + props: Props | null; + children: Child[]; +} + +export const Fragment = "#fragment"; + +export function h(tag: string | Component, props: Props | null, ...children: Child[]): Element { + if (typeof tag === "function") return tag(props ?? {}, children); + return { tag, props, children }; +} + +function escape(text: string): string { + return text.replace(/&/g, "&").replace(/${inner}`; +} + +declare global { + namespace JSX { + type Element = import("./html.js").Element; + interface IntrinsicElements { + [name: string]: object; + } + } +} diff --git a/tests/corpus/6822-jsx-classic-runtime/list.jsx b/tests/corpus/6822-jsx-classic-runtime/list.jsx new file mode 100644 index 000000000..d5aa3aee1 --- /dev/null +++ b/tests/corpus/6822-jsx-classic-runtime/list.jsx @@ -0,0 +1,22 @@ +/** @jsx tag */ +// JSX in a JavaScript module, with a JavaScript factory that builds strings. +function tag(name, props, ...children) { + const attributes = Object.entries(props ?? {}) + .map(([key, value]) => ` ${key}=${JSON.stringify(value)}`) + .join(""); + return `<${name}${attributes}>${children.flat().join("")}`; +} + +const names = ["a", "b"]; + +export function List() { + return ( +
      + {names.map((name, index) => ( +
    1. + {name.toUpperCase()} +
    2. + ))} +
    + ); +} diff --git a/tests/corpus/6822-jsx-classic-runtime/main.ts b/tests/corpus/6822-jsx-classic-runtime/main.ts new file mode 100644 index 000000000..4378557a0 --- /dev/null +++ b/tests/corpus/6822-jsx-classic-runtime/main.ts @@ -0,0 +1,15 @@ +// @tsc-jsx +// JSX in .tsx modules with the classic runtime: jsxFactory, jsxFragmentFactory and per-file pragmas. +import { render } from "./html.js"; +import { List } from "./list.js"; +import { Page } from "./page.js"; +import { viaPragma } from "./pragma.js"; + +const items = [ + { id: 1, name: "write ", done: true }, + { id: 2, name: "ship", done: false }, +]; +console.log(render(Page("Today", items))); +console.log(render(Page("Empty", []))); +console.log(viaPragma); +console.log(List()); diff --git a/tests/corpus/6822-jsx-classic-runtime/page.tsx b/tests/corpus/6822-jsx-classic-runtime/page.tsx new file mode 100644 index 000000000..c3fa6a03c --- /dev/null +++ b/tests/corpus/6822-jsx-classic-runtime/page.tsx @@ -0,0 +1,40 @@ +import { Fragment, h, type Child, type Props } from "./html.js"; + +const Ui = { + Badge(props: Props, children: Child[]) { + return {...children}; + }, +}; + +function Row(props: Props) { + const name = typeof props.name === "string" ? props.name : "?"; + const done = props.done === true; + return ( +
  • + ); +} + +export function Page(title: string, items: { id: number; name: string; done: boolean }[]) { + const extra = { lang: "en", dir: "ltr" }; + return ( + <> +

    {title}

    +
      + {items.map((item) => ( + + ))} +
    +

    + {items.filter((item) => item.done).length} of {items.length} done + multi word + badge +

    + + + <>nested {"fragment"} + + ); +} diff --git a/tests/corpus/6822-jsx-classic-runtime/pragma.tsx b/tests/corpus/6822-jsx-classic-runtime/pragma.tsx new file mode 100644 index 000000000..3aea8c330 --- /dev/null +++ b/tests/corpus/6822-jsx-classic-runtime/pragma.tsx @@ -0,0 +1,11 @@ +/** @jsx React.createElement */ +/** @jsxFrag React.Fragment */ +// The per-file pragmas select another factory than the tsconfig's. +import * as React from "./factory.js"; + +export const viaPragma = ( +
    + <>{1} + text +
    +); diff --git a/tests/corpus/6822-jsx-classic-runtime/tsconfig.json b/tests/corpus/6822-jsx-classic-runtime/tsconfig.json new file mode 100644 index 000000000..6ca523cf2 --- /dev/null +++ b/tests/corpus/6822-jsx-classic-runtime/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "strict": true, + "allowJs": true, + "jsx": "react", + "jsxFactory": "h", + "jsxFragmentFactory": "Fragment" + } +} diff --git a/tests/corpus/6824-jsx-automatic-runtime/alt.tsx b/tests/corpus/6824-jsx-automatic-runtime/alt.tsx new file mode 100644 index 000000000..a76d0ca72 --- /dev/null +++ b/tests/corpus/6824-jsx-automatic-runtime/alt.tsx @@ -0,0 +1,3 @@ +/** @jsxImportSource #alt */ +// This file's JSX uses the runtime the pragma names. +export const viaPragma = [one, {1}{2}, <>frag]; diff --git a/tests/corpus/6824-jsx-automatic-runtime/alt/jsx-runtime.ts b/tests/corpus/6824-jsx-automatic-runtime/alt/jsx-runtime.ts new file mode 100644 index 000000000..89b942ed2 --- /dev/null +++ b/tests/corpus/6824-jsx-automatic-runtime/alt/jsx-runtime.ts @@ -0,0 +1,17 @@ +// A second runtime, selected per file with @jsxImportSource. +export const Fragment = "alt-fragment"; + +export function jsx(type: string, props: { children?: unknown }): string { + return `${type}(${JSON.stringify(props.children ?? null)})`; +} + +export function jsxs(type: string, props: { children: unknown[] }): string { + return `${type}[${props.children.map((child) => JSON.stringify(child)).join(",")}]`; +} + +export namespace JSX { + export type Element = string; + export interface IntrinsicElements { + [name: string]: object; + } +} diff --git a/tests/corpus/6824-jsx-automatic-runtime/main.ts b/tests/corpus/6824-jsx-automatic-runtime/main.ts new file mode 100644 index 000000000..b25a4bc09 --- /dev/null +++ b/tests/corpus/6824-jsx-automatic-runtime/main.ts @@ -0,0 +1,10 @@ +// @tsc-jsx +// JSX with the automatic runtime: jsx/jsxs imports from jsxImportSource, keys, and per-file @jsxImportSource. +import { calls } from "#jsx/jsx-runtime"; +import { viaPragma } from "./alt.js"; +import { page } from "./page.js"; +import { render } from "./render.js"; + +console.log(render(page(["ada", "lin"]))); +console.log(calls.join(" ")); +console.log(viaPragma.join(" | ")); diff --git a/tests/corpus/6824-jsx-automatic-runtime/package.json b/tests/corpus/6824-jsx-automatic-runtime/package.json new file mode 100644 index 000000000..53f44dc61 --- /dev/null +++ b/tests/corpus/6824-jsx-automatic-runtime/package.json @@ -0,0 +1,8 @@ +{ + "type": "module", + "imports": { + "#jsx": "./runtime/index.ts", + "#jsx/jsx-runtime": "./runtime/jsx-runtime.ts", + "#alt/jsx-runtime": "./alt/jsx-runtime.ts" + } +} diff --git a/tests/corpus/6824-jsx-automatic-runtime/page.tsx b/tests/corpus/6824-jsx-automatic-runtime/page.tsx new file mode 100644 index 000000000..fe7fd8c0c --- /dev/null +++ b/tests/corpus/6824-jsx-automatic-runtime/page.tsx @@ -0,0 +1,33 @@ +import type { Props } from "#jsx/jsx-runtime"; + +function Card(props: Props) { + return ( +
    +

    {typeof props.title === "string" ? props.title : "?"}

    + {props.children} +
    + ); +} + +export function page(names: string[]) { + const extra = { id: "list", role: "list" }; + return ( + <> + +
      + {names.map((name, index) => ( +
    • + {name} +
    • + ))} +
    +
    +

    fallback

    +

    + two {"children"} +

    +
    {...names}
    +
    + + ); +} diff --git a/tests/corpus/6824-jsx-automatic-runtime/render.ts b/tests/corpus/6824-jsx-automatic-runtime/render.ts new file mode 100644 index 000000000..6f5f1e3c9 --- /dev/null +++ b/tests/corpus/6824-jsx-automatic-runtime/render.ts @@ -0,0 +1,15 @@ +// Renders elements to HTML-like text. +import type { Element } from "#jsx/jsx-runtime"; + +export function render(value: unknown): string { + if (value === null || value === undefined || typeof value === "boolean") return ""; + if (typeof value === "string" || typeof value === "number") return String(value); + if (Array.isArray(value)) return value.map(render).join(""); + const element = value as Element; + const { children, ...rest } = element.props; + const inner = render(children); + if (element.type === "#fragment") return inner; + let attributes = element.key === undefined ? "" : ` key="${element.key}"`; + for (const [name, item] of Object.entries(rest)) attributes += ` ${name}=${JSON.stringify(item)}`; + return `<${element.type}${attributes}>${inner}`; +} diff --git a/tests/corpus/6824-jsx-automatic-runtime/runtime/index.ts b/tests/corpus/6824-jsx-automatic-runtime/runtime/index.ts new file mode 100644 index 000000000..c7809cdc0 --- /dev/null +++ b/tests/corpus/6824-jsx-automatic-runtime/runtime/index.ts @@ -0,0 +1,12 @@ +// The import source's createElement: TypeScript calls it for an element +// whose key follows a spread of props. +import { calls, type Element, type Props } from "./jsx-runtime.js"; + +export function createElement(type: string, props: Props | null, ...children: unknown[]): Element { + calls.push(`createElement:${type}`); + const all: Props = { ...props }; + if (children.length > 0) all["children"] = children.length === 1 ? children[0] : children; + const key = all["key"]; + delete all["key"]; + return { type, props: all, key: key === undefined ? undefined : String(key) }; +} diff --git a/tests/corpus/6824-jsx-automatic-runtime/runtime/jsx-runtime.ts b/tests/corpus/6824-jsx-automatic-runtime/runtime/jsx-runtime.ts new file mode 100644 index 000000000..185aafbd7 --- /dev/null +++ b/tests/corpus/6824-jsx-automatic-runtime/runtime/jsx-runtime.ts @@ -0,0 +1,33 @@ +// The automatic runtime: jsx, jsxs and Fragment, with children in props. +export type Props = { [name: string]: unknown }; +export interface Element { + type: string; + props: Props; + key: string | undefined; +} +export type Component = (props: Props) => Element; + +export const Fragment = "#fragment"; +export const calls: string[] = []; + +function element(kind: string, type: string | Component, props: Props, key?: unknown): Element { + const name = typeof type === "function" ? "component" : type; + calls.push(`${kind}:${name}${key === undefined ? "" : `#${String(key)}`}`); + if (typeof type === "function") return type(props); + return { type, props, key: key === undefined ? undefined : String(key) }; +} + +export function jsx(type: string | Component, props: Props, key?: unknown): Element { + return element("jsx", type, props, key); +} + +export function jsxs(type: string | Component, props: Props, key?: unknown): Element { + return element("jsxs", type, props, key); +} + +export namespace JSX { + export type Element = import("./jsx-runtime.js").Element; + export interface IntrinsicElements { + [name: string]: object; + } +} diff --git a/tests/corpus/6824-jsx-automatic-runtime/tsconfig.json b/tests/corpus/6824-jsx-automatic-runtime/tsconfig.json new file mode 100644 index 000000000..2e5b6aee7 --- /dev/null +++ b/tests/corpus/6824-jsx-automatic-runtime/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "strict": true, + "module": "nodenext", + "jsx": "react-jsx", + "jsxImportSource": "#jsx" + } +} diff --git a/tests/corpus/6825-jsx-automatic-development/main.ts b/tests/corpus/6825-jsx-automatic-development/main.ts new file mode 100644 index 000000000..9ff046128 --- /dev/null +++ b/tests/corpus/6825-jsx-automatic-development/main.ts @@ -0,0 +1,5 @@ +// @tsc-jsx +// JSX with react-jsxdev: jsxDEV gets the key, static children, the source position and `this`. +import { views } from "./view.js"; + +for (const view of views()) console.log(view); diff --git a/tests/corpus/6825-jsx-automatic-development/package.json b/tests/corpus/6825-jsx-automatic-development/package.json new file mode 100644 index 000000000..cbd76bd4f --- /dev/null +++ b/tests/corpus/6825-jsx-automatic-development/package.json @@ -0,0 +1,6 @@ +{ + "type": "module", + "imports": { + "#dev/jsx-dev-runtime": "./runtime/jsx-dev-runtime.ts" + } +} diff --git a/tests/corpus/6825-jsx-automatic-development/runtime/jsx-dev-runtime.ts b/tests/corpus/6825-jsx-automatic-development/runtime/jsx-dev-runtime.ts new file mode 100644 index 000000000..f4e3aea4b --- /dev/null +++ b/tests/corpus/6825-jsx-automatic-development/runtime/jsx-dev-runtime.ts @@ -0,0 +1,31 @@ +// The development runtime: jsxDEV receives the key, whether the children +// are static, the element's source position and the calling `this`. +export const Fragment = "#fragment"; + +interface Source { + fileName: string; + lineNumber: number; + columnNumber: number; +} + +export function jsxDEV( + type: string | ((props: { [name: string]: unknown }) => string), + props: { [name: string]: unknown }, + key: unknown, + isStatic: boolean, + source: Source, + self: unknown, +): string { + const file = source.fileName.slice(source.fileName.lastIndexOf("/") + 1); + const where = `${file}:${source.lineNumber}:${source.columnNumber}`; + const owner = self === undefined ? "none" : typeof self; + const name = typeof type === "function" ? type(props) : type; + return `${name}{key=${String(key)} static=${isStatic} at=${where} self=${owner}}`; +} + +export namespace JSX { + export type Element = string; + export interface IntrinsicElements { + [name: string]: object; + } +} diff --git a/tests/corpus/6825-jsx-automatic-development/tsconfig.json b/tests/corpus/6825-jsx-automatic-development/tsconfig.json new file mode 100644 index 000000000..e1e88d270 --- /dev/null +++ b/tests/corpus/6825-jsx-automatic-development/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "strict": true, + "module": "nodenext", + "jsx": "react-jsxdev", + "jsxImportSource": "#dev" + } +} diff --git a/tests/corpus/6825-jsx-automatic-development/view.tsx b/tests/corpus/6825-jsx-automatic-development/view.tsx new file mode 100644 index 000000000..05fdc7b32 --- /dev/null +++ b/tests/corpus/6825-jsx-automatic-development/view.tsx @@ -0,0 +1,21 @@ +function Leaf() { + return "leaf"; +} + +export class Panel { + title = "panel"; + render() { + return
    {1}{2}
    ; + } +} + +export function views(): string[] { + return [ +

    one

    , + , + <> + + , + new Panel().render(), + ]; +} diff --git a/tests/corpus/6870-error-constructor-values.js b/tests/corpus/6870-error-constructor-values.js new file mode 100644 index 000000000..79c970914 --- /dev/null +++ b/tests/corpus/6870-error-constructor-values.js @@ -0,0 +1,65 @@ +// The error constructors as values in JavaScript: an error's `constructor` +// is the same function the global names, whose name, length, typeof and +// String() are Node's, for errors the program throws, errors the runtime +// throws, and errors read back from catch bindings and containers. +function describe(label, error) { + const ctor = error.constructor; + console.log(label, error.name, error.constructor.name, typeof ctor, ctor.length, String(ctor)); +} + +describe('Error', new Error('e')); +describe('TypeError', new TypeError('t')); +describe('RangeError', new RangeError('r')); +describe('SyntaxError', new SyntaxError('s')); +describe('ReferenceError', new ReferenceError('f')); +describe('EvalError', new EvalError('v')); +describe('URIError', new URIError('u')); +describe('AggregateError', new AggregateError([], 'a')); + +const thrown = [ + () => JSON.parse('{'), + () => { + const holder = JSON.parse('{"value":null}'); + return holder.value.missing; + }, + () => new Array(-1), + () => decodeURIComponent('%'), + () => { + throw new TypeError('own'); + }, +]; +const constructors = { SyntaxError, TypeError, RangeError, URIError }; +const caught = []; +for (const run of thrown) { + try { + run(); + } catch (error) { + caught.push(error); + console.log('caught', error.constructor.name, error.constructor === constructors[error.name]); + } +} + +const byName = new Map(); +for (const error of caught) byName.set(error.constructor.name, (byName.get(error.constructor.name) ?? 0) + 1); +console.log('kinds', JSON.stringify([...byName])); + +console.log('identity', new TypeError('x').constructor === TypeError, new RangeError('x').constructor === TypeError); +console.log('globals', Error.name, TypeError.length, AggregateError.length, typeof RangeError, String(SyntaxError)); + +const renamed = new TypeError('x'); +renamed.name = 'Custom'; +console.log('renamed', renamed.name, renamed.constructor.name); + +class ParseFailure extends SyntaxError {} +const failure = new ParseFailure('p'); +console.log('subclass', failure.name, failure.constructor.name, failure instanceof SyntaxError); + +// Effect's isStackTraceLimitWritable reflects on Error as a value. +const limit = Object.getOwnPropertyDescriptor(Error, 'stackTraceLimit'); +console.log('stackTraceLimit', JSON.stringify(limit), Object.hasOwn(limit, 'writable') ? limit.writable === true : limit.set !== undefined); +Error.stackTraceLimit = 4; +console.log('stackTraceLimit now', Object.getOwnPropertyDescriptor(Error, 'stackTraceLimit').value); +Error.stackTraceLimit = 10; +for (const ctor of [Error, TypeError, AggregateError, Object, Map]) { + console.log('extensible', ctor.name, Object.isExtensible(ctor), Object.isFrozen(ctor), Object.isSealed(ctor)); +} diff --git a/tests/corpus/6871-worker-data-destructuring.cjs b/tests/corpus/6871-worker-data-destructuring.cjs new file mode 100644 index 000000000..63ae789bd --- /dev/null +++ b/tests/corpus/6871-worker-data-destructuring.cjs @@ -0,0 +1,30 @@ +// Destructuring the members of a worker's workerData in JavaScript: plain, +// renamed, defaulted, nested and rest bindings read the data the worker was +// started with, as reading `workerData.` does. +'use strict'; +const { Worker, isMainThread, parentPort, workerData } = require('node:worker_threads'); + +if (isMainThread) { + const sab = new SharedArrayBuffer(8); + const data = { name: 'job-7', size: 3, options: { verbose: true, tags: ['a', 'b'] }, counters: new Int32Array(sab), extra: 1, more: 'x' }; + const worker = new Worker(__filename, { workerData: data }); + worker.on('message', (lines) => { + for (const line of lines) console.log(line); + console.log('main sees', new Int32Array(sab)[0]); + }); +} else { + const lines = []; + const { name, size } = workerData; + lines.push(`plain ${name} ${size}`); + const { name: jobName, missing = 'fallback', size: count = 10 } = workerData; + lines.push(`renamed ${jobName} ${missing} ${count}`); + const { + options: { verbose, tags: [firstTag, ...otherTags] }, + } = workerData; + lines.push(`nested ${verbose} ${firstTag} ${JSON.stringify(otherTags)}`); + const { name: ignored, size: alsoIgnored, options, counters, ...rest } = workerData; + lines.push(`rest ${JSON.stringify(rest)} ${typeof options} ${ignored === name && alsoIgnored === size}`); + counters[0] = 41; + counters[0] += 1; + parentPort.postMessage(lines); +} diff --git a/tests/corpus/6872-regex-start-filter.ts b/tests/corpus/6872-regex-start-filter.ts new file mode 100644 index 000000000..872342e93 --- /dev/null +++ b/tests/corpus/6872-regex-start-filter.ts @@ -0,0 +1,79 @@ +// Searches whose patterns begin with fixed sequences of characters and +// classes (literal alternations, classes, fixed repetitions), over subjects +// of every length from 0 to 80 and with matches at every offset: match, +// matchAll, search, exec with captures, global and sticky lastIndex, +// replace, case-insensitive letters, multiline anchors and word boundaries. +// Results must not depend on where a match falls relative to 16-character +// blocks or the end of the subject. +const patterns: RegExp[] = [ + /agggtaaa|tttaccct/gi, + /[cgt]gggtaaa|tttaccc[acg]/gi, + /ab|abc|b/g, + /(ab)(c|d)x/g, + /x[0-9]{3}y/g, + /(?:no|yes)(!+)/gi, + /^ab/gm, + /\bcat\b/g, + /a{2}(b+)c/g, + /[A-Z][a-z]{2}-[0-9]/g, +]; + +function describe(re: RegExp, subject: string): string { + const parts: string[] = []; + const all = subject.match(re); + parts.push(`match=${all === null ? "null" : JSON.stringify(all)}`); + parts.push(`search=${subject.search(re)}`); + const rows: string[] = []; + for (const m of subject.matchAll(re)) rows.push(`${m.index}:${JSON.stringify(m.slice(0))}`); + parts.push(`all=[${rows.join(" ")}]`); + parts.push(`replace=${JSON.stringify(subject.replace(re, "<$&>"))}`); + return parts.join(" "); +} + +const filler = "qwertyuiopasdfghjklzxcvbnm"; +const needles = ["agggtaaa", "TTTACCCT", "tttacccg", "abcx", "abdx", "x123y", "NO!!", "yes!", "cat", "aabbbc", "Abc-1", "ab"]; +let checksum = 0; +let lines = 0; +for (let length = 0; length <= 80; length += 7) { + for (const needle of needles) { + for (const offset of [0, 1, 14, 15, 16, 17, 31, 32, 33]) { + if (offset + needle.length > length) continue; + let subject = ""; + while (subject.length < length) subject += filler; + subject = subject.slice(0, offset) + needle + subject.slice(offset + needle.length, length); + for (const re of patterns) { + const text = describe(re, subject); + for (let i = 0; i < text.length; i++) checksum = (checksum * 31 + text.charCodeAt(i)) >>> 0; + lines++; + if (length === 35 && (offset === 15 || offset === 16)) console.log(re.source, offset, needle, text); + } + } + } +} +console.log("lines", lines, "checksum", checksum); + +// Overlapping candidates: every start position looks like a match for the +// first characters, and only some complete. +const dense = "agggtaa".repeat(9) + "agggtaaa" + "tttaccc".repeat(5) + "tttaccct"; +console.log(describe(/agggtaaa|tttaccct/g, dense)); + +// exec with lastIndex, global and sticky. +const re = /x[0-9]{3}y/g; +const text = "zzx111yzzzzzzzzzzzzzzzzzzx222yx333y" + "z".repeat(20) + "x444y"; +let m: RegExpExecArray | null; +const found: string[] = []; +while ((m = re.exec(text)) !== null) found.push(`${m[0]}@${m.index}->${re.lastIndex}`); +console.log("exec", found.join(" ")); +const sticky = /x[0-9]{3}y/y; +sticky.lastIndex = 2; +console.log("sticky", sticky.test(text), sticky.lastIndex, sticky.test(text), sticky.lastIndex); + +// Short subjects and the tail. +for (const subject of ["", "a", "agggtaa", "agggtaaa", "zagggtaaa", "z".repeat(16) + "agggtaaa", "z".repeat(23) + "tttaccct"]) { + console.log(JSON.stringify(subject.slice(-12)), subject.length, describe(/agggtaaa|tttaccct/gi, subject)); +} + +// A long subject with matches spread through it. +let long = ""; +for (let i = 0; i < 2000; i++) long += i % 97 === 0 ? "aGgGtAaA" : i % 89 === 0 ? "tttACCCt" : filler[i % filler.length]; +console.log("long", (long.match(/agggtaaa|tttaccct/gi) ?? []).length, long.search(/tttaccct/i)); diff --git a/tests/corpus/6997-typed-array-invariant-index-terms.ts b/tests/corpus/6997-typed-array-invariant-index-terms.ts new file mode 100644 index 000000000..53d9f10c0 --- /dev/null +++ b/tests/corpus/6997-typed-array-invariant-index-terms.ts @@ -0,0 +1,78 @@ +// Typed-array loops whose index adds a loop-invariant number with no known +// integer range, such as the row base of a matrix-vector product. Rows +// with integer, fractional, negative, -0 and NaN bases, windows at 2^32 and +// beyond, and matrices too short for their last rows, read and write as +// Node does. + +function matvec(w: Float32Array, v: Float32Array, out: Float32Array, cols: number): void { + for (let r = 0; r < out.length; r++) { + const base = r * cols; + let s0 = 0, s1 = 0, s2 = 0, s3 = 0; + for (let j = 0; j < cols; j += 4) { + s0 += w[base + j]! * v[j]!; + s1 += w[base + j + 1]! * v[j + 1]!; + s2 += w[base + j + 2]! * v[j + 2]!; + s3 += w[base + j + 3]! * v[j + 3]!; + } + out[r] = s0 + s1 + s2 + s3; + } +} + +function rowSums(m: Float64Array, rows: number, cols: number, first: number): number[] { + const sums: number[] = []; + for (let r = first; r < first + rows; r++) { + let s = 0; + for (let c = 0; c < cols; c++) s += m[r * cols + c]!; + sums.push(s); + } + return sums; +} + +function shiftedCopy(src: Int16Array, out: Int16Array, offset: number): void { + for (let i = 0; i < out.length; i++) out[i] = src[i - offset]! + 1; +} + +function window(src: Uint8Array, start: number, k: number): number { + let s = 0; + for (let i = 0; i < k; i++) s += src[start + i]! * (i + 1); + return s; +} + +const rows = 6, cols = 8; +const w = new Float32Array(rows * cols); +for (let i = 0; i < w.length; i++) w[i] = ((i * 37) % 23) / 4 - 2; +const v = new Float32Array(cols); +for (let i = 0; i < cols; i++) v[i] = (i % 5) - 1.5; +const out = new Float32Array(rows); +matvec(w, v, out, cols); +console.log("matvec", out.join(",")); +const tall = new Float32Array(rows + 2); +matvec(w, v, tall, cols); +console.log("matvec past rows", tall.join(",")); +const odd = new Float32Array(3); +matvec(w, v, odd, 6.5); +console.log("matvec fractional cols", odd.join(",")); +matvec(w, v, odd, NaN); +console.log("matvec NaN cols", odd.join(",")); + +const m = new Float64Array(30); +for (let i = 0; i < m.length; i++) m[i] = i * 1.5 - 7; +console.log("rows", rowSums(m, 5, 6, 0).join(",")); +console.log("rows from -1", rowSums(m, 3, 6, -1).join(",")); +console.log("rows fractional", rowSums(m, 2, 2.5, 1).join(",")); +console.log("rows past end", rowSums(m, 3, 6, 4).join(",")); +console.log("rows -0 base", rowSums(m, 1, 6, -0).join(",")); + +const src16 = new Int16Array([10, 20, 30, 40, 50, 60]); +const shifted = new Int16Array(6); +shiftedCopy(src16, shifted, 2); +console.log("shifted", shifted.join(",")); +shiftedCopy(src16, shifted, -3); +console.log("shifted back", shifted.join(",")); +shiftedCopy(src16, shifted, 0.5); +console.log("shifted half", shifted.join(",")); + +const bytes = new Uint8Array(40); +for (let i = 0; i < bytes.length; i++) bytes[i] = (i * 29) & 255; +console.log("window", window(bytes, 3, 8), window(bytes, 36, 8), window(bytes, -2, 4)); +console.log("window far", window(bytes, 2 ** 32, 3), window(bytes, 2 ** 32 + 1, 3), window(bytes, 2 ** 40, 2), window(bytes, -(2 ** 32), 2)); diff --git a/tests/corpus/6998-typed-array-while-loop-versions.ts b/tests/corpus/6998-typed-array-while-loop-versions.ts new file mode 100644 index 000000000..383a2437f --- /dev/null +++ b/tests/corpus/6998-typed-array-while-loop-versions.ts @@ -0,0 +1,149 @@ +// While loops that scan typed arrays with a counter below a bound: scanners +// with nested loops and `&&` conditions, `| 0` increments, look-behind and +// look-ahead reads, writes, DataView reads, string-length bounds, and +// bounds or starts past the array, fractional, negative, -0 or NaN. + +function words(bytes: Uint8Array, end: number): number { + let pos = 0; + let count = 0; + while (pos < end) { + while (pos < end && (bytes[pos] === 32 || bytes[pos] === 10)) pos++; + if (pos < end) count++; + while (pos < end && bytes[pos] !== 32 && bytes[pos] !== 10) pos++; + } + return count; +} + +function identifiers(chars: Uint16Array, start: number, end: number): string { + let pos = start; + let count = 0; + let mix = 0; + while (pos < end) { + let hash = 0x811c9dc5 | 0; + while (pos < end) { + const c = chars[pos]!; + if (!((c >= 97 && c <= 122) || c === 95)) break; + hash = Math.imul(hash ^ c, 0x01000193); + pos = (pos + 1) | 0; + } + mix = (mix + hash) | 0; + count++; + pos = (pos + 1) | 0; + } + return `${count} ${mix >>> 0} ${pos}`; +} + +function around(values: Int16Array, start: number, end: number): number[] { + const out: number[] = []; + let i = start; + while (i <= end) { + out.push(values[i - 1]! + values[i + 2]! * 2); + i += 1; + } + return out; +} + +function fill(out: Float32Array, end: number, value: number): number { + let i = 0; + while (i < end) { + out[i] = value + i; + if (i < end - 1 && out[i] > 2) { + i += 2; + continue; + } + i++; + } + return i; +} + +function afterInner(bytes: Uint8Array): number { + let pos = 0; + let sum = 0; + while (pos < bytes.length) { + while (pos < bytes.length && bytes[pos] !== 0) pos++; + // The inner loop may stop at the length: this read stays checked. + sum += bytes[pos] ?? -1; + pos++; + } + return sum; +} + +function firstAbove(samples: Float64Array, limit: number): number { + let i = 0; + while (i < samples.length) { + if (samples[i]! > limit) return i; + i = i + 1; + } + return -1; +} + +function halves(view: DataView, end: number): number { + let at = 0; + let total = 0; + while (at < end) { + total += view.getUint16(at, true); + at += 2; + } + return total; +} + +function letters(text: string, codes: Uint8Array): number { + let i = 0; + let sum = 0; + while (i < text.length) { + sum += codes[i]! * (text.charCodeAt(i) & 7); + i++; + } + return sum; +} + +function notCounters(bytes: Uint8Array): string { + let i = 1; + let doubled = 0; + while (i < bytes.length) { + doubled += bytes[i]!; + i = i * 2; + } + let j = bytes.length - 1; + let down = 0; + while (j < bytes.length && j >= 0) { + down += bytes[j]!; + j--; + } + return `${doubled} ${down}`; +} + +const text = "let alpha = beta_gamma\nreturn delta "; +const bytes = new Uint8Array(text.length); +const chars = new Uint16Array(text.length); +for (let i = 0; i < text.length; i++) { + bytes[i] = text.charCodeAt(i); + chars[i] = text.charCodeAt(i); +} +console.log("words", words(bytes, bytes.length), words(bytes, 10), words(bytes, bytes.length + 5), words(bytes, 7.5), words(bytes, NaN)); +console.log("identifiers", identifiers(chars, 0, chars.length)); +console.log("identifiers past end", identifiers(chars, 4, chars.length + 3)); +console.log("identifiers starts", identifiers(chars, -2, 9), identifiers(chars, 2.5, 12), identifiers(chars, -0, 6)); + +const values = new Int16Array([5, -6, 7, -8, 9, -10, 11]); +console.log("around", around(values, 1, 4).join(","), around(values, 0, 6).join(","), around(values, -1, 1).join(",")); + +const floats = new Float32Array(6); +console.log("fill", fill(floats, 6, 0.5), floats.join(",")); +const short = new Float32Array(3); +console.log("fill past end", fill(short, 5, 1), short.join(",")); + +console.log("after inner", afterInner(new Uint8Array([1, 2, 0, 3, 0, 4, 5]))); +console.log("first above", firstAbove(new Float64Array([0.5, 1.5, 2.5]), 1), firstAbove(new Float64Array([0.5]), 1)); + +const buffer = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]); +const view = new DataView(buffer.buffer); +console.log("halves", halves(view, 8)); +try { + console.log("halves past end", halves(view, 10)); +} catch (error) { + console.log((error as Error).name, (error as Error).message); +} + +console.log("letters", letters("abcdef", new Uint8Array([1, 2, 3, 4, 5, 6])), letters("abcdefgh", new Uint8Array([1, 2, 3]))); +console.log("not counters", notCounters(new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]))); diff --git a/tests/corpus/7002-empty-array-contextual-slots/main.ts b/tests/corpus/7002-empty-array-contextual-slots/main.ts new file mode 100644 index 000000000..ec08a297a --- /dev/null +++ b/tests/corpus/7002-empty-array-contextual-slots/main.ts @@ -0,0 +1,58 @@ +// Empty array literals whose own type is `never[]`: `return []` in an async +// function (its contextual type is `string[] | PromiseLike`) and +// an object literal's `files: []` field returned or passed where typed +// arrays are declared. Each value holds the declared element type. +interface Listing { + files: string[]; + sizes: number[]; +} + +async function readNames(text: string): Promise { + try { + return JSON.parse(text) as string[]; + } catch { + return []; + } +} + +const loadIds = async (fail: boolean): Promise => { + if (fail) return []; + return [1, 2]; +}; + +function emptyListing(): Listing { + const listing = { files: [], sizes: [] }; + return listing; +} + +function index(): Record<"files" | "dirs", string[]> { + const out = { files: [], dirs: [] }; + return out; +} + +function total(listing: Listing): number { + return listing.sizes.reduce((sum, size) => sum + size, 0); +} + +async function main(): Promise { + const names = await readNames("not json"); + names.push("fallback"); + console.log(names, (await readNames('["a","b"]')).join("+")); + const ids = await loadIds(true); + ids.push(7); + console.log(ids, await loadIds(false)); + + const listing = emptyListing(); + listing.files.push("a.txt"); + listing.sizes.push(3, 4); + console.log(listing, total(listing)); + + const dirs = index(); + dirs.dirs.push("src"); + console.log(JSON.stringify(dirs), dirs.files.length); + + const blank = { files: [], sizes: [] }; + console.log(total(blank), JSON.stringify(blank)); +} + +main(); diff --git a/tests/corpus/7005-dyn-constructor-instanceof/main.ts b/tests/corpus/7005-dyn-constructor-instanceof/main.ts new file mode 100644 index 000000000..21fcbbd26 --- /dev/null +++ b/tests/corpus/7005-dyn-constructor-instanceof/main.ts @@ -0,0 +1,12 @@ +// A package's own functions, read off an object the checker cannot type, +// on the right of `instanceof`: Node walks the value's prototype chain for +// an ES5 constructor's prototype, at module load and in calls, and throws a +// TypeError for an arrow function, which has no prototype, when the value +// is an object. +import { arrowCheck, arrowObjects, describe, rejects, tags } from "tag-types"; + +console.log(tags()); +console.log(describe("{}"), describe("[1]"), describe("null"), describe('"s"')); +console.log(rejects('{"tag":"!x"}')); +console.log(arrowCheck("{}"), arrowCheck("1")); +console.log(arrowObjects()); diff --git a/tests/corpus/7005-dyn-constructor-instanceof/node_modules/tag-types/index.d.ts b/tests/corpus/7005-dyn-constructor-instanceof/node_modules/tag-types/index.d.ts new file mode 100644 index 000000000..5b451adfa --- /dev/null +++ b/tests/corpus/7005-dyn-constructor-instanceof/node_modules/tag-types/index.d.ts @@ -0,0 +1,5 @@ +export declare function describe(json: string): string; +export declare function tags(): string; +export declare function rejects(json: string): string; +export declare function arrowCheck(json: string): string; +export declare function arrowObjects(): string; diff --git a/tests/corpus/7005-dyn-constructor-instanceof/node_modules/tag-types/index.js b/tests/corpus/7005-dyn-constructor-instanceof/node_modules/tag-types/index.js new file mode 100644 index 000000000..81165bd58 --- /dev/null +++ b/tests/corpus/7005-dyn-constructor-instanceof/node_modules/tag-types/index.js @@ -0,0 +1,71 @@ +"use strict"; +// A bundler's lazy module object holding an ES5 constructor: the checker +// cannot type the object, so the constructor is an untyped value where +// `instanceof` tests it. +var types = {}; +var hasTypes; +function requireTypes() { + if (hasTypes) return types; + hasTypes = 1; + function Type(tag, options) { + this.tag = tag; + this.kind = (options && options.kind) || "scalar"; + } + function Schema(list) { + this.list = list; + } + types.Type = Type; + types.Schema = Schema; + types.make = (tag) => new Type(tag); + return types; +} +var loaded = requireTypes(); +var Type = loaded.Type; +var Schema = loaded.Schema; +var make = loaded.make; + +function compileList(list) { + var tags = []; + list.forEach(function (item) { + if (!(item instanceof Type)) throw new Error("not a Type: " + JSON.stringify(item)); + tags.push(item.tag + ":" + item.kind); + }); + return tags; +} +// Runs when the module loads, as js-yaml's default schema does. +var defaults = new Schema(compileList([new Type("!str"), new Type("!seq", { kind: "sequence" })])); + +module.exports.describe = function (json) { + var value = JSON.parse(json); + return [value instanceof Type, value instanceof Schema, defaults instanceof Schema, defaults instanceof Type].join(","); +}; +module.exports.tags = function () { + return defaults.list.join(" "); +}; +function rejects(json) { + try { + compileList([JSON.parse(json)]); + return "accepted"; + } catch (error) { + return error.message; + } +} +module.exports.rejects = rejects; +// An arrow function has no prototype: Node throws a TypeError for an object. +module.exports.arrowCheck = function (json) { + try { + return String(JSON.parse(json) instanceof make); + } catch (error) { + return error.name + ": " + error.message; + } +}; +// Every object reaches the TypeError, typed arrays, promises and Maps too. +module.exports.arrowObjects = function () { + return [new Uint8Array(2), Promise.resolve(1), new Map(), [1]].map(function (value) { + try { + return String(value instanceof make); + } catch (error) { + return error.name; + } + }).join(","); +}; diff --git a/tests/corpus/7005-dyn-constructor-instanceof/node_modules/tag-types/package.json b/tests/corpus/7005-dyn-constructor-instanceof/node_modules/tag-types/package.json new file mode 100644 index 000000000..a469a45f3 --- /dev/null +++ b/tests/corpus/7005-dyn-constructor-instanceof/node_modules/tag-types/package.json @@ -0,0 +1 @@ +{ "name": "tag-types", "version": "1.0.0", "main": "./index.js", "types": "./index.d.ts" } diff --git a/tests/corpus/7008-js-unit-initialized-properties/main.ts b/tests/corpus/7008-js-unit-initialized-properties/main.ts new file mode 100644 index 000000000..3ac113d6a --- /dev/null +++ b/tests/corpus/7008-js-unit-initialized-properties/main.ts @@ -0,0 +1,8 @@ +// A JavaScript package whose object literals start a property as +// `undefined` or `null` and later store records, numbers and arrays in it, +// as the diff package's path list does (`{ oldPos: -1, lastComponent: +// undefined }`). Node runs the later writes; each property holds them. +import { describe, session } from "edit-paths"; + +console.log(describe()); +console.log(session()); diff --git a/tests/corpus/7008-js-unit-initialized-properties/node_modules/edit-paths/index.d.ts b/tests/corpus/7008-js-unit-initialized-properties/node_modules/edit-paths/index.d.ts new file mode 100644 index 000000000..68e2b409b --- /dev/null +++ b/tests/corpus/7008-js-unit-initialized-properties/node_modules/edit-paths/index.d.ts @@ -0,0 +1,2 @@ +export declare function describe(): string; +export declare function session(): string; diff --git a/tests/corpus/7008-js-unit-initialized-properties/node_modules/edit-paths/index.js b/tests/corpus/7008-js-unit-initialized-properties/node_modules/edit-paths/index.js new file mode 100644 index 000000000..ec3e7c4d0 --- /dev/null +++ b/tests/corpus/7008-js-unit-initialized-properties/node_modules/edit-paths/index.js @@ -0,0 +1,43 @@ +"use strict"; +// A Myers-style path list, as the diff package keeps it: each path starts +// with `lastComponent: undefined` and later holds a linked list of records, +// and a state object starts with `null` fields that later hold other values. +function addToPath(path, added, removed, oldPosInc) { + var last = path.lastComponent; + if (last && last.added === added && last.removed === removed) { + return { oldPos: path.oldPos + oldPosInc, lastComponent: { count: last.count + 1, added: added, removed: removed, previousComponent: last.previousComponent } }; + } + return { oldPos: path.oldPos + oldPosInc, lastComponent: { count: 1, added: added, removed: removed, previousComponent: last } }; +} +function extractCommon(basePath, common) { + if (common) basePath.lastComponent = { count: common, previousComponent: basePath.lastComponent, added: false, removed: false }; + basePath.oldPos += common; +} +function components(lastComponent) { + var list = []; + while (lastComponent) { + list.push(lastComponent); + var next = lastComponent.previousComponent; + delete lastComponent.previousComponent; + lastComponent = next; + } + return list.reverse(); +} +function describe() { + var bestPath = [{ oldPos: -1, lastComponent: undefined }]; + extractCommon(bestPath[0], 1); + var path = addToPath(bestPath[0], false, true, 1); + path = addToPath(path, true, false, 0); + path = addToPath(path, true, false, 0); + return components(path.lastComponent).map(function (c) { + return (c.added ? "+" : c.removed ? "-" : "=") + c.count + ("previousComponent" in c ? "!" : ""); + }).join(" "); +} +function session() { + var state = { user: null, retries: null, tags: undefined }; + state.user = { name: "ada" }; + state.retries = 3; + state.tags = ["a", "b"]; + return state.user.name + " " + state.retries + " " + state.tags.join("+") + " " + JSON.stringify(state); +} +module.exports = { describe: describe, session: session }; diff --git a/tests/corpus/7008-js-unit-initialized-properties/node_modules/edit-paths/package.json b/tests/corpus/7008-js-unit-initialized-properties/node_modules/edit-paths/package.json new file mode 100644 index 000000000..98648c289 --- /dev/null +++ b/tests/corpus/7008-js-unit-initialized-properties/node_modules/edit-paths/package.json @@ -0,0 +1 @@ +{ "name": "edit-paths", "version": "1.0.0", "main": "./index.js", "types": "./index.d.ts" } diff --git a/tests/corpus/7011-for-in-dictionary-deletes.ts b/tests/corpus/7011-for-in-dictionary-deletes.ts new file mode 100644 index 000000000..91dac573b --- /dev/null +++ b/tests/corpus/7011-for-in-dictionary-deletes.ts @@ -0,0 +1,42 @@ +// for-in over a string-keyed dictionary visits the keys present when the +// walk starts, in insertion order, and skips a key deleted before its +// turn; keys added during the walk are not visited. A key deleted and +// added again before its turn is visited. +const d: Record = {}; +for (let i = 0; i < 8; i++) d["k" + i] = i; +const seen: string[] = []; +for (const key in d) { + seen.push(key); + if (key === "k1") { + delete d["k3"]; + delete d["k0"]; + d["k8"] = 8; + } + if (key === "k2") { + delete d["k5"]; + d["k5"] = 50; + } + if (key === "k4") delete d["k4"]; +} +console.log(seen.join(",")); +console.log(Object.keys(d).join(","), JSON.stringify(d)); + +// A numeric-looking key is walked in integer order before string keys. +const n: Record = {}; +n["b"] = "x"; +n["2"] = "y"; +n["10"] = "z"; +n["a"] = "w"; +const order: string[] = []; +for (const key in n) { + order.push(key); + if (key === "2") delete n["a"]; +} +console.log(order.join(",")); + +// A larger dictionary walked many times. +const big: Record = {}; +for (let i = 0; i < 3000; i++) big["e" + i] = i; +let total = 0; +for (let pass = 0; pass < 20; pass++) for (const key in big) total += big[key]!; +console.log(total); diff --git a/tests/corpus/7080-crypto-ed25519.mjs b/tests/corpus/7080-crypto-ed25519.mjs new file mode 100644 index 000000000..355d98225 --- /dev/null +++ b/tests/corpus/7080-crypto-ed25519.mjs @@ -0,0 +1,34 @@ +// Ed25519 KeyObjects in node:crypto: the RFC 8032 test 1 key through +// PKCS#8, SubjectPublicKeyInfo and JWK export and import, its deterministic +// signatures from crypto.sign and crypto.verify, generateKeyPairSync, the +// two-argument generateKeyPair callback, and Node's errors for digests, +// Sign objects and the RSA and EC key encodings. +import * as c from "node:crypto"; +const t = (l, f) => { try { const v = f(); console.log(l, "=>", typeof v === "object" && v !== null && !Buffer.isBuffer(v) ? JSON.stringify(v) : Buffer.isBuffer(v) ? v.toString("hex") : String(v).slice(0, 200)); } catch (e) { console.log(l, "!!", e.name, e.code, e.message); } }; +const seed = Buffer.from("9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60", "hex"); +const der = Buffer.concat([Buffer.from("302e020100300506032b657004220420", "hex"), seed]); +const k = c.createPrivateKey({ key: der, format: "der", type: "pkcs8" }); +const p = c.createPublicKey(k); +t("type", () => [k.type, k.asymmetricKeyType, JSON.stringify(k.asymmetricKeyDetails), p.type].join()); +t("pkcs8 pem", () => k.export({ type: "pkcs8", format: "pem" })); +t("spki pem", () => p.export({ type: "spki", format: "pem" })); +t("spki der", () => p.export({ type: "spki", format: "der" })); +t("jwk priv", () => k.export({ format: "jwk" })); +t("jwk pub", () => p.export({ format: "jwk" })); +t("sig empty", () => c.sign(null, Buffer.alloc(0), k)); +t("sig abc", () => c.sign(undefined, Buffer.from("abc"), k)); +t("verify", () => c.verify(null, Buffer.from("abc"), p, c.sign(null, Buffer.from("abc"), k))); +t("verify bad", () => c.verify(null, Buffer.from("abd"), p, c.sign(null, Buffer.from("abc"), k))); +t("sign sha256", () => c.sign("sha256", Buffer.from("abc"), k)); +t("createSign", () => c.createSign("sha256").update("x").sign(k)); +t("pkcs1 export", () => k.export({ type: "pkcs1", format: "pem" })); +t("sec1 export", () => k.export({ type: "sec1", format: "pem" })); +t("gen cb form", () => { c.generateKeyPair("ed25519", (e, pub, priv) => console.log("cb", e, pub.asymmetricKeyType, priv.type)); return "scheduled"; }); +t("gen", () => { const g = c.generateKeyPairSync("ed25519"); return [g.publicKey.asymmetricKeyType, Object.keys(g).join()].join(); }); +t("gen opts", () => { const g = c.generateKeyPairSync("ed25519", { publicKeyEncoding: { type: "spki", format: "der" } }); return g.publicKey.length; }); +t("jwk import", () => c.createPrivateKey({ key: k.export({ format: "jwk" }), format: "jwk" }).equals(k)); +t("jwk pub import", () => c.createPublicKey({ key: p.export({ format: "jwk" }), format: "jwk" }).equals(p)); +t("dsaEncoding", () => c.sign(null, Buffer.from("x"), { key: k, dsaEncoding: "ieee-p1363" }).length); +t("verify short sig", () => c.verify(null, Buffer.from("x"), p, Buffer.alloc(10))); +t("pem parse", () => c.createPrivateKey(k.export({ type: "pkcs8", format: "pem" })).equals(k)); +t("equals ec", () => k.equals(c.generateKeyPairSync("ec", { namedCurve: "P-256" }).privateKey)); diff --git a/tests/corpus/7081-webcrypto-ed25519.mjs b/tests/corpus/7081-webcrypto-ed25519.mjs new file mode 100644 index 000000000..5cf7728f1 --- /dev/null +++ b/tests/corpus/7081-webcrypto-ed25519.mjs @@ -0,0 +1,39 @@ +// WebCrypto Ed25519: the RFC 8032 test 1 key imported from pkcs8, jwk and +// raw, its deterministic signatures, verify, generateKey, every export +// format, and Node's DOMExceptions for usages, raw private export, short +// raw keys and mismatched JWK members. + +const t = async (label, fn) => { try { const v = await fn(); console.log(label, "=>", v instanceof ArrayBuffer ? Buffer.from(v).toString("hex") : typeof v === "object" && v !== null ? JSON.stringify(v) : v); } catch (e) { console.log(label, "!!", e.name, e.code, e.message); } }; +const seed = Buffer.from("9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60", "hex"); +const pkcs8 = Buffer.concat([Buffer.from("302e020100300506032b657004220420", "hex"), seed]); +const priv = await crypto.subtle.importKey("pkcs8", pkcs8, { name: "Ed25519" }, true, ["sign"]); +await t("algorithm", () => [JSON.stringify(priv.algorithm), priv.type, priv.usages.join(), priv.extractable].join(" ")); +await t("sign empty", () => crypto.subtle.sign("Ed25519", priv, new Uint8Array(0))); +await t("sign abc", () => crypto.subtle.sign({ name: "Ed25519" }, priv, new TextEncoder().encode("abc"))); +await t("jwk priv", () => crypto.subtle.exportKey("jwk", priv)); +await t("pkcs8 back", async () => Buffer.from(await crypto.subtle.exportKey("pkcs8", priv)).equals(pkcs8)); +const jwk = await crypto.subtle.exportKey("jwk", priv); +const pubJwk = { kty: jwk.kty, crv: jwk.crv, x: jwk.x }; +const pub = await crypto.subtle.importKey("jwk", pubJwk, "Ed25519", true, ["verify"]); +await t("raw pub", () => crypto.subtle.exportKey("raw", pub)); +await t("spki pub", () => crypto.subtle.exportKey("spki", pub)); +await t("jwk pub", () => crypto.subtle.exportKey("jwk", pub)); +const sig = await crypto.subtle.sign("Ed25519", priv, new TextEncoder().encode("abc")); +await t("verify", () => crypto.subtle.verify("Ed25519", pub, sig, new TextEncoder().encode("abc"))); +await t("verify bad", () => crypto.subtle.verify("Ed25519", pub, sig, new TextEncoder().encode("abd"))); +await t("verify short", () => crypto.subtle.verify("Ed25519", pub, new Uint8Array(10), new TextEncoder().encode("abc"))); +const rawImported = await crypto.subtle.importKey("raw", await crypto.subtle.exportKey("raw", pub), { name: "Ed25519" }, false, ["verify"]); +await t("raw import verify", () => crypto.subtle.verify("Ed25519", rawImported, sig, new TextEncoder().encode("abc"))); +const pair = await crypto.subtle.generateKey({ name: "Ed25519" }, true, ["sign", "verify"]); +await t("generated", async () => [pair.privateKey.usages.join(), pair.publicKey.usages.join(), pair.publicKey.extractable, (await crypto.subtle.exportKey("raw", pair.publicKey)).byteLength, await crypto.subtle.verify("Ed25519", pair.publicKey, await crypto.subtle.sign("Ed25519", pair.privateKey, new Uint8Array([1])), new Uint8Array([1]))].join(" ")); +await t("raw private", () => crypto.subtle.exportKey("raw", pair.privateKey)); +await t("bad usage", () => crypto.subtle.generateKey({ name: "Ed25519" }, true, ["encrypt"])); +await t("verify only", () => crypto.subtle.generateKey("Ed25519", true, ["verify"])); +await t("raw short", () => crypto.subtle.importKey("raw", new Uint8Array(31), { name: "Ed25519" }, true, ["verify"])); +await t("raw sign usage", () => crypto.subtle.importKey("raw", new Uint8Array(32), "Ed25519", true, ["sign"])); +await t("jwk alg EdDSA", async () => (await crypto.subtle.importKey("jwk", { ...pubJwk, alg: "EdDSA" }, "Ed25519", true, ["verify"])).type); +await t("jwk alg bad", () => crypto.subtle.importKey("jwk", { ...pubJwk, alg: "RS256" }, "Ed25519", true, ["verify"])); +await t("jwk crv bad", () => crypto.subtle.importKey("jwk", { ...pubJwk, crv: "Ed448" }, "Ed25519", true, ["verify"])); +await t("jwk kty bad", () => crypto.subtle.importKey("jwk", { ...pubJwk, kty: "EC" }, "Ed25519", true, ["verify"])); +await t("ecdsa key as ed", async () => { const e = await crypto.subtle.generateKey({ name: "ECDSA", namedCurve: "P-256" }, true, ["sign", "verify"]); return crypto.subtle.importKey("spki", await crypto.subtle.exportKey("spki", e.publicKey), "Ed25519", true, ["verify"]); }); +await t("sign wrong alg", () => crypto.subtle.sign({ name: "ECDSA", hash: "SHA-256" }, priv, new Uint8Array(1))); diff --git a/tests/corpus/7082-crypto-key-objects.ts b/tests/corpus/7082-crypto-key-objects.ts new file mode 100644 index 000000000..816dd6be0 --- /dev/null +++ b/tests/corpus/7082-crypto-key-objects.ts @@ -0,0 +1,132 @@ +// Private and public KeyObjects: createPrivateKey and createPublicKey from +// PEM, DER, JWK, a private KeyObject and a certificate; the type, +// asymmetricKeyType and asymmetricKeyDetails getters; export() as PKCS#1, +// SEC1, PKCS#8, SubjectPublicKeyInfo, DER and JWK; equals(); instanceof; +// and Node's errors for invalid options and inputs, including a private or +// public key passed to createHmac. +import * as crypto from "node:crypto"; + +const RSA = `-----BEGIN RSA PRIVATE KEY----- +MIICWwIBAAKBgQDWei4BREJm+4C6u/+F4NI7kTOQ9gf/a+Ecd2SZxfpXRtXT0QVN +MAI1uQNmB4abYzvd1G2V2XKsTJFJcJWibjAz56UxtxxggTpSAalFZYoPm6HfabwJ +b/YWh0K0OrNFvdkeXLLMiHQsxmIQ2lrFoycQEpZ4/yxjLe6LQF+L79k2ywIDAQAB +AoGAMWS/9ZC3zim7ssWbCkUFhaxh+M+AdgVtOXYRmAxtI+5vTjHEC8uzppWuP7Aa +Au0um17RwgHrE90qVIR2IYtI1C9K9RLUNlKKVPqnSSmr0qkXR246R/Zd8eho1XSS +MX6KmnbmRijUx9Rw1AbD2ZmYCrXsPf2HsCdH+SGQ1ZTunnECQQD090dTA21Jyu/b +ADglqgixJCPIoc6qpxst+W1LreIo4UvkSmgnRYvsKDlM7FNI+6f7igI625KXIAnl +xVlmGLXDAkEA4CNVeP07fmQUBs1aPQ9LCOvN42SV6S8Fjbf4XHIYG5Kp48/98oJj +1pFsXNa8nn8fmN9zFw8yM/yd22iZdueCWQJAWMja//po/OrhniVpNDV5awKnOYtz +xSRF1JbnhxthseFeSUKylzE8sn4trnSlBvm2+5MclcqJSd7h13Z8nWVtUwJAZ0SA +Ve0+ov4TKAsnH2jTZnnWc4VdeIFcqq3vdRBwQQvy0G59D2W6ABsidSfO4auqcDHL +gf1TYRZr2LI5usnxiQJAGt3e0QC0q0WN4p49YhyXXVuHccx79RFgfN7V1lkAyYuL +2bPV41mEtD/AbSrObtqqvOaWZjwSr8JCyPdPZZAIMw== +-----END RSA PRIVATE KEY----- +`; +const EC = `-----BEGIN EC PRIVATE KEY----- +MHcCAQEEILpVcXOEZQXpH/q3ec3AsLg4x4Bld6uZLqDC56OYKQK4oAoGCCqGSM49 +AwEHoUQDQgAE0gwi0a/4kuVxjsO3vw79rT3QSxnPY7sooVlI+ENq35D7meWZv/dp +FTGFte7Bd8lFxaZ10dMo0tl++AOtt7umPg== +-----END EC PRIVATE KEY----- +`; +const EC384 = `-----BEGIN EC PRIVATE KEY----- +MIGkAgEBBDDer7W8pQD96zdS8UWxg34nMTxqREszwP6niYYOdw75LHPmHFKWeMKW +dmG6d3n4bRugBwYFK4EEACKhZANiAATIQKDRVJ501CTQtbK2IslxdO8Oma2ZU4ee +GRuQFg6ajhKXa0iCU5o9rpdObfCecyiYApdtVpahD2SwPua5CZyq/0jUn9frwKtB +uEeB/3T/yYRCD4zGUc7TtUDh8c7Gt5c= +-----END EC PRIVATE KEY----- +`; +const CERT = `-----BEGIN CERTIFICATE----- +MIIDuzCCAqOgAwIBAgIPAPAN+s4AESIzRFVmd4iZMA0GCSqGSIb3DQEBDAUAMIGc +MQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2Fu +IEZyYW5jaXNjbzEUMBIGA1UECgwLRXhhbXBsZSBPcmcxETAPBgNVBAsMCERldiBU +ZWFtMRgwFgYDVQQDDA9FeGFtcGxlIFJvb3QgQ0ExHTAbBgkqhkiG9w0BCQEWDmNh +QGV4YW1wbGUuY29tMB4XDTI2MTAyMTE2MzM1MloXDTM2MTAxODE2MzM1MlowJzER +MA8GA1UECgwITGVhZiBPcmcxEjAQBgNVBAMMCWxvY2FsaG9zdDBZMBMGByqGSM49 +AgEGCCqGSM49AwEHA0IABJVfVy9EH/VHQuRqpFFnXk0F00ncDht6APgVPDoybK8H +PlcM4T5H1yAOMHnlH29LK1jtMqU1aAOiiQRzPdCvqoujggE3MIIBMzAJBgNVHRME +AjAAMGgGA1UdEQRhMF+CCWxvY2FsaG9zdIINKi5leGFtcGxlLmNvbYcEfwAAAYcQ +AAAAAAAAAAAAAAAAAAAAAYERYWRtaW5AZXhhbXBsZS5jb22GGGh0dHBzOi8vZXhh +bXBsZS5jb20vcGF0aDBdBggrBgEFBQcBAQRRME8wIwYIKwYBBQUHMAGGF2h0dHA6 +Ly9vY3NwLmV4YW1wbGUuY29tMCgGCCsGAQUFBzAChhxodHRwOi8vY2EuZXhhbXBs +ZS5jb20vY2EuY3J0MB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAdBgNV +HQ4EFgQUdKIx6ULFLPNP/kzwUFjSa5D19RswHwYDVR0jBBgwFoAUP+yQyItpNYEl +s26Jfv+NnciGaK8wDQYJKoZIhvcNAQEMBQADggEBABmQ8f2VsXNaMM9X1MEhi27D +LjwcOaaGEINNZq7xu/HL6UljMb1Odsuz66DkiH3Exd3MrWy6OlOpJJF0VzsqMsoD +0CRBxXF28o6Jil3vOGVwlYNmRidpkAkj4VyWDvv8FCS9VbsBtXaSHwu9wcCwePE0 +cGJRfTKoZ6KJ8vQ57DBEZ/ZszuhehOZYqrTmoUdjZ123kXdxQvK+ALn4SlIXHBhN +9unhd+8i00EmkXO/REw8paMcMVVGR+ioOEjyNHqZo6vkRqgpKggn1Yq8cTTDOMnS +P1o42EQZLlbuvx0kqInBqwqjDx4nZiPaifoDc9BF6iZPBvm8dhrEfghfB1tkNk0= +-----END CERTIFICATE----- +`; + +function attempt(label: string, run: () => unknown): void { + try { + const value = run(); + console.log(label, typeof value === "string" ? JSON.stringify(value) : value); + } catch (e) { + const err = e as NodeJS.ErrnoException; + console.log(label, "throws", err.name, err.code, err.message); + } +} + +const rsa = crypto.createPrivateKey(RSA); +const ec = crypto.createPrivateKey(Buffer.from(EC)); +const ec384 = crypto.createPrivateKey({ key: EC384, format: "pem" }); +const rsaPublic = crypto.createPublicKey(rsa); +const ecPublic = crypto.createPublicKey(EC); +const certPublic = crypto.createPublicKey(CERT); +const keys: [string, crypto.KeyObject][] = [ + ["rsa", rsa], + ["ec", ec], + ["ec384", ec384], + ["rsaPublic", rsaPublic], + ["ecPublic", ecPublic], + ["certPublic", certPublic], +]; +for (const [label, key] of keys) { + console.log(label, key.type, key.asymmetricKeyType, key.asymmetricKeyDetails, key instanceof crypto.KeyObject); +} + +attempt("pkcs1", () => rsa.export({ type: "pkcs1", format: "pem" }) === RSA + "\n"); +attempt("sec1", () => ec.export({ type: "sec1", format: "pem" }) === EC + "\n"); +attempt("rsa pkcs8", () => rsa.export({ type: "pkcs8", format: "pem" })); +attempt("ec pkcs8", () => ec.export({ type: "pkcs8", format: "pem" })); +attempt("rsa spki", () => rsaPublic.export({ type: "spki", format: "pem" })); +attempt("rsa public pkcs1", () => rsaPublic.export({ type: "pkcs1", format: "pem" })); +attempt("ec spki", () => ecPublic.export({ type: "spki", format: "pem" })); +attempt("cert spki", () => certPublic.export({ type: "spki", format: "pem" })); +attempt("der", () => (rsa.export({ type: "pkcs8", format: "der" }) as Buffer).toString("base64")); +attempt("ec jwk", () => JSON.stringify(ec.export({ format: "jwk" }))); +attempt("ec384 public jwk", () => JSON.stringify(crypto.createPublicKey(ec384).export({ format: "jwk" }))); +attempt("rsa jwk", () => JSON.stringify(rsa.export({ format: "jwk" }))); +attempt("rsa public jwk", () => JSON.stringify(rsaPublic.export({ format: "jwk" }))); + +// Round trips through DER and JWK. +const der = rsa.export({ type: "pkcs1", format: "der" }) as Buffer; +attempt("from der", () => crypto.createPrivateKey({ key: der, format: "der", type: "pkcs1" }).equals(rsa)); +const spki = ecPublic.export({ type: "spki", format: "der" }) as Buffer; +attempt("public from der", () => crypto.createPublicKey({ key: spki, format: "der", type: "spki" }).equals(ecPublic)); +attempt("from jwk", () => crypto.createPrivateKey({ key: ec.export({ format: "jwk" }), format: "jwk" }).equals(ec)); +attempt("rsa from jwk", () => crypto.createPrivateKey({ key: rsa.export({ format: "jwk" }), format: "jwk" }).equals(rsa)); +attempt("public from jwk", () => crypto.createPublicKey({ key: rsaPublic.export({ format: "jwk" }), format: "jwk" }).equals(rsaPublic)); +attempt("pkcs8 parse", () => crypto.createPrivateKey(ec.export({ type: "pkcs8", format: "pem" })).equals(ec)); +attempt("equals", () => [rsaPublic.equals(crypto.createPublicKey(RSA)), ec.equals(ecPublic), ec.equals(ec384)].join()); +attempt("tag", () => Object.prototype.toString.call(rsa)); + +// Errors. +attempt("export no options", () => rsa.export()); +attempt("export no format", () => rsa.export({} as crypto.AsymmetricKeyExportOptions<"pem">)); +attempt("export no type", () => rsa.export({ format: "pem" } as crypto.AsymmetricKeyExportOptions<"pem">)); +attempt("export spki private", () => rsa.export({ type: "spki", format: "pem" })); +attempt("export sec1 rsa", () => rsa.export({ type: "sec1", format: "pem" })); +attempt("export pkcs1 ec", () => ec.export({ type: "pkcs1", format: "der" })); +attempt("export pkcs8 public", () => ecPublic.export({ type: "pkcs8", format: "pem" })); +attempt("private from public pem", () => crypto.createPrivateKey(ecPublic.export({ type: "spki", format: "pem" }) as string)); +attempt("private from garbage", () => crypto.createPrivateKey("not a key")); +attempt("public from public", () => crypto.createPublicKey(ecPublic)); +attempt("der without type", () => crypto.createPrivateKey({ key: der, format: "der" } as crypto.PrivateKeyInput)); +attempt("hmac private key", () => crypto.createHmac("sha256", rsa)); +attempt("hmac public key", () => crypto.createHmac("sha256", ecPublic)); +// Last: OpenSSL keeps an error queued after comparing keys of different +// types, which a later call in Node can report. +attempt("equals other type", () => rsa.equals(ec)); diff --git a/tests/corpus/7083-crypto-sign-verify.ts b/tests/corpus/7083-crypto-sign-verify.ts new file mode 100644 index 000000000..4f6b86e72 --- /dev/null +++ b/tests/corpus/7083-crypto-sign-verify.ts @@ -0,0 +1,88 @@ +// crypto.sign and crypto.verify, and the Sign and Verify objects: RSA +// PKCS#1 v1.5 signatures (deterministic, so they compare with Node), RSA-PSS, +// ECDSA in DER and IEEE P1363 encodings, keys given as KeyObjects, PEM and +// option objects, output and signature encodings, and Node's errors. +import * as crypto from "node:crypto"; + +const RSA = `-----BEGIN RSA PRIVATE KEY----- +MIICWwIBAAKBgQDWei4BREJm+4C6u/+F4NI7kTOQ9gf/a+Ecd2SZxfpXRtXT0QVN +MAI1uQNmB4abYzvd1G2V2XKsTJFJcJWibjAz56UxtxxggTpSAalFZYoPm6HfabwJ +b/YWh0K0OrNFvdkeXLLMiHQsxmIQ2lrFoycQEpZ4/yxjLe6LQF+L79k2ywIDAQAB +AoGAMWS/9ZC3zim7ssWbCkUFhaxh+M+AdgVtOXYRmAxtI+5vTjHEC8uzppWuP7Aa +Au0um17RwgHrE90qVIR2IYtI1C9K9RLUNlKKVPqnSSmr0qkXR246R/Zd8eho1XSS +MX6KmnbmRijUx9Rw1AbD2ZmYCrXsPf2HsCdH+SGQ1ZTunnECQQD090dTA21Jyu/b +ADglqgixJCPIoc6qpxst+W1LreIo4UvkSmgnRYvsKDlM7FNI+6f7igI625KXIAnl +xVlmGLXDAkEA4CNVeP07fmQUBs1aPQ9LCOvN42SV6S8Fjbf4XHIYG5Kp48/98oJj +1pFsXNa8nn8fmN9zFw8yM/yd22iZdueCWQJAWMja//po/OrhniVpNDV5awKnOYtz +xSRF1JbnhxthseFeSUKylzE8sn4trnSlBvm2+5MclcqJSd7h13Z8nWVtUwJAZ0SA +Ve0+ov4TKAsnH2jTZnnWc4VdeIFcqq3vdRBwQQvy0G59D2W6ABsidSfO4auqcDHL +gf1TYRZr2LI5usnxiQJAGt3e0QC0q0WN4p49YhyXXVuHccx79RFgfN7V1lkAyYuL +2bPV41mEtD/AbSrObtqqvOaWZjwSr8JCyPdPZZAIMw== +-----END RSA PRIVATE KEY----- +`; +const EC = `-----BEGIN EC PRIVATE KEY----- +MHcCAQEEILpVcXOEZQXpH/q3ec3AsLg4x4Bld6uZLqDC56OYKQK4oAoGCCqGSM49 +AwEHoUQDQgAE0gwi0a/4kuVxjsO3vw79rT3QSxnPY7sooVlI+ENq35D7meWZv/dp +FTGFte7Bd8lFxaZ10dMo0tl++AOtt7umPg== +-----END EC PRIVATE KEY----- +`; + +function attempt(label: string, run: () => unknown): void { + try { + console.log(label, run()); + } catch (e) { + const err = e as NodeJS.ErrnoException; + console.log(label, "throws", err.name, err.code, err.message); + } +} + +const rsa = crypto.createPrivateKey(RSA); +const rsaPublic = crypto.createPublicKey(rsa); +const ec = crypto.createPrivateKey(EC); +const ecPublic = crypto.createPublicKey(ec); +const data = Buffer.from("signed message"); + +for (const digest of ["sha1", "sha224", "sha256", "SHA384", "RSA-SHA512", "sha256WithRSAEncryption"]) { + const signature = crypto.sign(digest, data, rsa); + console.log(digest, signature.toString("base64"), crypto.verify(digest, data, rsaPublic, signature)); +} +attempt("null digest", () => crypto.sign(null, data, RSA).equals(crypto.sign("sha256", data, rsa))); +attempt("private key verifies", () => crypto.verify("sha256", data, RSA, crypto.sign("sha256", data, rsa))); +attempt("wrong data", () => crypto.verify("sha256", Buffer.from("other"), rsaPublic, crypto.sign("sha256", data, rsa))); + +const pss = crypto.sign("sha256", data, { key: rsa, padding: crypto.constants.RSA_PKCS1_PSS_PADDING, saltLength: 32 }); +attempt("pss", () => [pss.length, crypto.verify("sha256", data, { key: rsaPublic, padding: crypto.constants.RSA_PKCS1_PSS_PADDING }, pss)].join()); +attempt("pss as pkcs1", () => crypto.verify("sha256", data, rsaPublic, pss)); +attempt("pss digest salt", () => + crypto.verify("sha256", data, { key: rsaPublic, padding: crypto.constants.RSA_PKCS1_PSS_PADDING, saltLength: crypto.constants.RSA_PSS_SALTLEN_DIGEST }, pss), +); + +const der = crypto.sign("sha256", data, ec); +attempt("ecdsa der", () => [der[0], crypto.verify("sha256", data, ecPublic, der)].join()); +const p1363 = crypto.sign("sha384", data, { key: EC, dsaEncoding: "ieee-p1363" }); +attempt("ecdsa p1363", () => [p1363.length, crypto.verify("sha384", data, { key: ecPublic, dsaEncoding: "ieee-p1363" }, p1363)].join()); +attempt("p1363 as der", () => crypto.verify("sha384", data, ecPublic, p1363)); +attempt("truncated", () => crypto.verify("sha256", data, ecPublic, der.subarray(0, 10))); + +const signer = crypto.createSign("SHA256"); +signer.update("signed "); +signer.update(Buffer.from("message")); +const hex = signer.sign(rsa, "hex"); +attempt("sign object", () => hex === crypto.sign("sha256", data, rsa).toString("hex")); +attempt("sign again", () => signer.sign(rsa)); +attempt("update after sign", () => signer.update("more")); +const verifier = crypto.createVerify("RSA-SHA256"); +verifier.update("signed message", "utf8"); +attempt("verify object", () => verifier.verify(RSA, Buffer.from(hex, "hex").toString("base64"), "base64")); +attempt("verify again", () => verifier.verify(rsaPublic, hex, "hex")); +const ecSigner = crypto.createSign("sha512").update("chained"); +const ecSignature = ecSigner.sign({ key: ec, dsaEncoding: "ieee-p1363" }); +attempt("chained", () => crypto.createVerify("sha512").update("chained").verify({ key: ecPublic, dsaEncoding: "ieee-p1363" }, ecSignature)); + +attempt("invalid digest", () => crypto.sign("nope", data, rsa)); +const longDigest = "unknown-digest-".repeat(40); +attempt("long digest", () => crypto.sign(longDigest, data, rsa)); +attempt("long verify digest", () => crypto.verify(longDigest, data, rsaPublic, Buffer.alloc(64))); +attempt("public key signs", () => crypto.sign("sha256", data, rsaPublic)); +attempt("invalid createSign", () => crypto.createSign("nope")); +attempt("invalid dsaEncoding", () => crypto.sign("sha256", data, { key: ec, dsaEncoding: "x" as "der" })); diff --git a/tests/corpus/7084-crypto-key-pairs.cjs b/tests/corpus/7084-crypto-key-pairs.cjs new file mode 100644 index 000000000..ca267d89f --- /dev/null +++ b/tests/corpus/7084-crypto-key-pairs.cjs @@ -0,0 +1,78 @@ +// generateKeyPairSync and generateKeyPair for RSA and EC keys: KeyObject +// results, PEM and DER encodings, curve names and NIST aliases, the +// callback's later macrotask, and Node's errors for invalid types and +// options. +const crypto = require("node:crypto"); + +function attempt(label, run) { + try { + console.log(label, run()); + } catch (e) { + console.log(label, "throws", e.name, e.code, e.message); + } +} + +const data = Buffer.from("key pair"); +for (const curve of ["prime256v1", "P-256", "secp384r1", "P-384", "secp521r1", "P-521", "secp256k1"]) { + const { publicKey, privateKey } = crypto.generateKeyPairSync("ec", { namedCurve: curve }); + const signature = crypto.sign("sha256", data, privateKey); + console.log(curve, privateKey.type, publicKey.type, privateKey.asymmetricKeyDetails, crypto.verify("sha256", data, publicKey, signature)); +} +const rsa = crypto.generateKeyPairSync("rsa", { modulusLength: 1024 }); +console.log("rsa", rsa.privateKey.asymmetricKeyDetails, Object.keys(rsa)); +const exponent = crypto.generateKeyPairSync("rsa", { modulusLength: 1024, publicExponent: 3 }); +console.log("exponent", exponent.publicKey.asymmetricKeyDetails); +const encoded = crypto.generateKeyPairSync("ec", { + namedCurve: "P-256", + publicKeyEncoding: { type: "spki", format: "pem" }, + privateKeyEncoding: { type: "sec1", format: "der" }, +}); +console.log("encoded", typeof encoded.publicKey, encoded.publicKey.split("\n")[0], Buffer.isBuffer(encoded.privateKey)); +const reparsed = crypto.createPrivateKey({ key: encoded.privateKey, format: "der", type: "sec1" }); +console.log("reparsed", crypto.createPublicKey(reparsed).export({ type: "spki", format: "pem" }) === encoded.publicKey); +const rsaPem = crypto.generateKeyPairSync("rsa", { + modulusLength: 1024, + publicKeyEncoding: { type: "pkcs1", format: "pem" }, + privateKeyEncoding: { type: "pkcs8", format: "pem" }, +}); +console.log("rsa pem", rsaPem.publicKey.split("\n")[0], rsaPem.privateKey.split("\n")[0]); +const jwk = crypto.generateKeyPairSync("ec", { namedCurve: "secp256k1", publicKeyEncoding: { format: "jwk" } }); +console.log("jwk", Object.keys(jwk.publicKey), jwk.publicKey.crv); + +attempt("no options", () => crypto.generateKeyPairSync("rsa")); +attempt("no modulusLength", () => crypto.generateKeyPairSync("rsa", {})); +attempt("string modulusLength", () => crypto.generateKeyPairSync("rsa", { modulusLength: "2048" })); +attempt("negative modulusLength", () => crypto.generateKeyPairSync("rsa", { modulusLength: -1 })); +attempt("unknown type", () => crypto.generateKeyPairSync("foo", {})); +attempt("number type", () => crypto.generateKeyPairSync(5, {})); +attempt("no curve", () => crypto.generateKeyPairSync("ec", {})); +attempt("unknown curve", () => crypto.generateKeyPairSync("ec", { namedCurve: "nope" })); +attempt("bad encoding format", () => crypto.generateKeyPairSync("ec", { namedCurve: "P-256", publicKeyEncoding: { type: "spki", format: "xml" } })); +attempt("bad encoding type", () => crypto.generateKeyPairSync("ec", { namedCurve: "P-256", privateKeyEncoding: { type: "spki", format: "pem" } })); +attempt("encoding not object", () => crypto.generateKeyPairSync("ec", { namedCurve: "P-256", publicKeyEncoding: "pem" })); +// Error messages hold the whole text at any length, and received values +// render as Node's validators print them. +attempt("long type", () => crypto.generateKeyPairSync("unknown-type-".repeat(20), {})); +attempt("long encoding type", () => crypto.generateKeyPairSync("ec", { namedCurve: "P-256", publicKeyEncoding: { type: "z".repeat(300), format: "pem" } })); +attempt("quoted encoding type", () => crypto.generateKeyPairSync("ec", { namedCurve: "P-256", publicKeyEncoding: { type: "it's\n", format: "pem" } })); +attempt("huge modulusLength", () => crypto.generateKeyPairSync("rsa", { modulusLength: 2 ** 40 })); +attempt("fractional modulusLength", () => crypto.generateKeyPairSync("rsa", { modulusLength: 1.5 })); +attempt("NaN modulusLength", () => crypto.generateKeyPairSync("rsa", { modulusLength: NaN })); +const key = rsa.privateKey; +attempt("long method", () => key.aMethodNameLongEnoughThatTheWholeMessageRunsPastOneHundredAndSixtyBytesOnceTheReceiverTextAndTheSuffixAreIncluded()); +attempt("getter", () => { + "use strict"; + key.asymmetricKeyType = "ec"; +}); + +let returned = false; +crypto.generateKeyPair("ec", { namedCurve: "P-384" }, (err, publicKey, privateKey) => { + console.log("callback", returned, err, publicKey.type, privateKey.asymmetricKeyDetails); + crypto.generateKeyPair("rsa", { modulusLength: 1024 }, (err2, publicKey2) => { + console.log("rsa callback", err2, publicKey2.asymmetricKeyDetails.modulusLength); + }); +}); +process.nextTick(() => console.log("tick")); +attempt("callback errors throw", () => crypto.generateKeyPair("ec", {}, () => console.log("never"))); +returned = true; +console.log("returned"); diff --git a/tests/corpus/7085-webcrypto-ecdsa-rsa.ts b/tests/corpus/7085-webcrypto-ecdsa-rsa.ts new file mode 100644 index 000000000..446ed7c63 --- /dev/null +++ b/tests/corpus/7085-webcrypto-ecdsa-rsa.ts @@ -0,0 +1,66 @@ +// WebCrypto ECDSA, RSASSA-PKCS1-v1_5 and RSA-PSS: importKey from spki, +// pkcs8, jwk and raw, exportKey to each format, sign and verify (RSA +// PKCS#1 v1.5 signatures are deterministic, so they compare with Node), +// generateKey for every curve and both RSA algorithms, and the CryptoKey +// members of these keys. +const RSA_PKCS8 = "MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBANZ6LgFEQmb7gLq7/4Xg0juRM5D2B/9r4Rx3ZJnF+ldG1dPRBU0wAjW5A2YHhptjO93UbZXZcqxMkUlwlaJuMDPnpTG3HGCBOlIBqUVlig+bod9pvAlv9haHQrQ6s0W92R5cssyIdCzGYhDaWsWjJxASlnj/LGMt7otAX4vv2TbLAgMBAAECgYAxZL/1kLfOKbuyxZsKRQWFrGH4z4B2BW05dhGYDG0j7m9OMcQLy7Omla4/sBoC7S6bXtHCAesT3SpUhHYhi0jUL0r1EtQ2UopU+qdJKavSqRdHbjpH9l3x6GjVdJIxfoqaduZGKNTH1HDUBsPZmZgKtew9/YewJ0f5IZDVlO6ecQJBAPT3R1MDbUnK79sAOCWqCLEkI8ihzqqnGy35bUut4ijhS+RKaCdFi+woOUzsU0j7p/uKAjrbkpcgCeXFWWYYtcMCQQDgI1V4/Tt+ZBQGzVo9D0sI683jZJXpLwWNt/hcchgbkqnjz/3ygmPWkWxc1ryefx+Y33MXDzIz/J3baJl254JZAkBYyNr/+mj86uGeJWk0NXlrAqc5i3PFJEXUlueHG2Gx4V5JQrKXMTyyfi2udKUG+bb7kxyVyolJ3uHXdnydZW1TAkBnRIBV7T6i/hMoCycfaNNmedZzhV14gVyqre91EHBBC/LQbn0PZboAGyJ1J87hq6pwMcuB/VNhFmvYsjm6yfGJAkAa3d7RALSrRY3inj1iHJddW4dxzHv1EWB83tXWWQDJi4vZs9XjWYS0P8BtKs5u2qq85pZmPBKvwkLI909lkAgz"; +const RSA_SPKI = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDWei4BREJm+4C6u/+F4NI7kTOQ9gf/a+Ecd2SZxfpXRtXT0QVNMAI1uQNmB4abYzvd1G2V2XKsTJFJcJWibjAz56UxtxxggTpSAalFZYoPm6HfabwJb/YWh0K0OrNFvdkeXLLMiHQsxmIQ2lrFoycQEpZ4/yxjLe6LQF+L79k2ywIDAQAB"; +const EC_PKCS8 = "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgulVxc4RlBekf+rd5zcCwuDjHgGV3q5kuoMLno5gpArihRANCAATSDCLRr/iS5XGOw7e/Dv2tPdBLGc9juyihWUj4Q2rfkPuZ5Zm/92kVMYW17sF3yUXFpnXR0yjS2X74A623u6Y+"; +const EC_SPKI = "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE0gwi0a/4kuVxjsO3vw79rT3QSxnPY7sooVlI+ENq35D7meWZv/dpFTGFte7Bd8lFxaZ10dMo0tl++AOtt7umPg=="; + +const bytes = (text: string): Uint8Array => new Uint8Array(Buffer.from(text, "base64")); +const hex = (buffer: ArrayBuffer): string => Buffer.from(buffer).toString("hex"); +const data = new TextEncoder().encode("webcrypto message"); + +const rsaPrivate = await crypto.subtle.importKey("pkcs8", bytes(RSA_PKCS8), { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" }, true, ["sign"]); +const rsaPublic = await crypto.subtle.importKey("spki", bytes(RSA_SPKI), { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" }, true, ["verify"]); +console.log(rsaPrivate.type, rsaPublic.type, rsaPrivate.extractable, rsaPublic.usages); +const rsaAlgorithm = rsaPrivate.algorithm as RsaHashedKeyAlgorithm; +console.log(rsaAlgorithm.name, rsaAlgorithm.modulusLength, Array.from(rsaAlgorithm.publicExponent), rsaAlgorithm.hash.name); +const rsaSignature = await crypto.subtle.sign("RSASSA-PKCS1-v1_5", rsaPrivate, data); +console.log("rsa signature", hex(rsaSignature)); +console.log("rsa verify", await crypto.subtle.verify({ name: "RSASSA-PKCS1-v1_5" }, rsaPublic, rsaSignature, data)); +console.log("rsa spki", Buffer.from(await crypto.subtle.exportKey("spki", rsaPublic)).toString("base64") === RSA_SPKI); +console.log("rsa pkcs8", Buffer.from(await crypto.subtle.exportKey("pkcs8", rsaPrivate)).toString("base64") === RSA_PKCS8); +const rsaJwk = await crypto.subtle.exportKey("jwk", rsaPublic); +console.log("rsa jwk", JSON.stringify(rsaJwk)); +const fromJwk = await crypto.subtle.importKey("jwk", rsaJwk, { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" }, false, ["verify"]); +console.log("rsa jwk verify", await crypto.subtle.verify("RSASSA-PKCS1-v1_5", fromJwk, rsaSignature, data), fromJwk.extractable); + +const ecPrivate = await crypto.subtle.importKey("pkcs8", bytes(EC_PKCS8), { name: "ECDSA", namedCurve: "P-256" }, true, ["sign"]); +const ecPublic = await crypto.subtle.importKey("spki", bytes(EC_SPKI), { name: "ECDSA", namedCurve: "P-256" }, true, ["verify"]); +console.log(JSON.stringify(ecPrivate.algorithm), ecPrivate.usages, ecPublic.usages); +console.log("ec jwk", JSON.stringify(await crypto.subtle.exportKey("jwk", ecPrivate))); +const point = await crypto.subtle.exportKey("raw", ecPublic); +console.log("ec raw", hex(point)); +const fromPoint = await crypto.subtle.importKey("raw", point, { name: "ECDSA", namedCurve: "P-256" }, true, ["verify"]); +for (const hash of ["SHA-1", "SHA-256", "SHA-384", "SHA-512"]) { + const signature = await crypto.subtle.sign({ name: "ECDSA", hash }, ecPrivate, data); + console.log("ecdsa", hash, signature.byteLength, await crypto.subtle.verify({ name: "ECDSA", hash: { name: hash } }, fromPoint, signature, data)); +} +const ecSignature = await crypto.subtle.sign({ name: "ECDSA", hash: "SHA-256" }, ecPrivate, data); +const tampered = new Uint8Array(ecSignature); +tampered[5] ^= 1; +console.log("tampered", await crypto.subtle.verify({ name: "ECDSA", hash: "SHA-256" }, ecPublic, tampered, data)); +console.log("other data", await crypto.subtle.verify({ name: "ECDSA", hash: "SHA-256" }, ecPublic, ecSignature, new Uint8Array([1]))); + +for (const namedCurve of ["P-256", "P-384", "P-521"]) { + const pair = await crypto.subtle.generateKey({ name: "ECDSA", namedCurve }, false, ["sign", "verify"]); + const signature = await crypto.subtle.sign({ name: "ECDSA", hash: "SHA-384" }, pair.privateKey, data); + console.log(namedCurve, Object.keys(pair).join(), pair.privateKey.extractable, pair.publicKey.extractable, signature.byteLength, + await crypto.subtle.verify({ name: "ECDSA", hash: "SHA-384" }, pair.publicKey, signature, data), + (await crypto.subtle.exportKey("raw", pair.publicKey)).byteLength); +} +for (const name of ["RSASSA-PKCS1-v1_5", "RSA-PSS"]) { + const pair = await crypto.subtle.generateKey({ name, modulusLength: 1024, publicExponent: new Uint8Array([1, 0, 1]), hash: "SHA-512" }, true, ["sign", "verify"]); + const params = { name, saltLength: 20 }; + const signature = await crypto.subtle.sign(params, pair.privateKey, data); + const jwk = await crypto.subtle.exportKey("jwk", pair.privateKey); + console.log(name, signature.byteLength, await crypto.subtle.verify(params, pair.publicKey, signature, data), jwk.alg, Object.keys(jwk).join()); +} +const pss = await crypto.subtle.importKey("pkcs8", bytes(RSA_PKCS8), { name: "RSA-PSS", hash: "SHA-256" }, false, ["sign"]); +const pssPublic = await crypto.subtle.importKey("spki", bytes(RSA_SPKI), { name: "RSA-PSS", hash: "SHA-256" }, false, ["verify"]); +const pssSignature = await crypto.subtle.sign({ name: "RSA-PSS", saltLength: 32 }, pss, data); +console.log("pss", await crypto.subtle.verify({ name: "RSA-PSS", saltLength: 32 }, pssPublic, pssSignature, data), + await crypto.subtle.verify({ name: "RSA-PSS", saltLength: 0 }, pssPublic, pssSignature, data)); +console.log(ecPrivate instanceof CryptoKey, String(ecPublic), Object.prototype.toString.call(rsaPublic)); diff --git a/tests/corpus/7086-webcrypto-ecdsa-rsa-errors.mjs b/tests/corpus/7086-webcrypto-ecdsa-rsa-errors.mjs new file mode 100644 index 000000000..62292fbdd --- /dev/null +++ b/tests/corpus/7086-webcrypto-ecdsa-rsa-errors.mjs @@ -0,0 +1,58 @@ +// WebCrypto ECDSA and RSA errors: Node's TypeErrors for missing algorithm +// members, and its DOMExceptions for curves, usages, key types, formats, +// extractability and JWK members, from generateKey, importKey, exportKey, +// sign and verify. + +const t = async (label, fn) => { try { const v = await fn(); console.log(label, "=>", v instanceof ArrayBuffer ? "AB" + v.byteLength : typeof v === "object" && v !== null ? JSON.stringify(v) : v); } catch (e) { console.log(label, "!!", e.name, e.code, e.message); } }; +const e3 = new Uint8Array([1, 0, 1]); +await t("rsa no hash", () => crypto.subtle.generateKey({ name: "RSASSA-PKCS1-v1_5", modulusLength: 1024, publicExponent: e3 }, true, ["sign"])); +await t("rsa no len", () => crypto.subtle.generateKey({ name: "RSASSA-PKCS1-v1_5", publicExponent: e3, hash: "SHA-256" }, true, ["sign"])); +await t("rsa no exp", () => crypto.subtle.generateKey({ name: "RSASSA-PKCS1-v1_5", modulusLength: 1024, hash: "SHA-256" }, true, ["sign"])); +await t("rsa bad usage", () => crypto.subtle.generateKey({ name: "RSASSA-PKCS1-v1_5", modulusLength: 1024, publicExponent: e3, hash: "SHA-256" }, true, ["encrypt"])); +await t("pss bad usage", () => crypto.subtle.generateKey({ name: "RSA-PSS", modulusLength: 1024, publicExponent: e3, hash: "SHA-256" }, true, ["encrypt"])); +await t("rsa bad hash", () => crypto.subtle.generateKey({ name: "RSASSA-PKCS1-v1_5", modulusLength: 1024, publicExponent: e3, hash: "MD5" }, true, ["sign"])); +await t("rsa exp 3", async () => (await crypto.subtle.generateKey({ name: "RSASSA-PKCS1-v1_5", modulusLength: 1024, publicExponent: new Uint8Array([3]), hash: "SHA-256" }, true, ["sign"])).publicKey.algorithm.publicExponent.length); +await t("rsa exp 4", () => crypto.subtle.generateKey({ name: "RSASSA-PKCS1-v1_5", modulusLength: 1024, publicExponent: new Uint8Array([4]), hash: "SHA-256" }, true, ["sign"])); +await t("rsa small", () => crypto.subtle.generateKey({ name: "RSASSA-PKCS1-v1_5", modulusLength: 256, publicExponent: e3, hash: "SHA-256" }, true, ["sign"])); +await t("ecdsa no curve", () => crypto.subtle.generateKey({ name: "ECDSA" }, true, ["sign"])); +await t("unknown alg", () => crypto.subtle.generateKey({ name: "FOO" }, true, ["sign"])); +const ec = await crypto.subtle.generateKey({ name: "ECDSA", namedCurve: "P-256" }, true, ["sign", "verify"]); +await t("export bad format", () => crypto.subtle.exportKey("foo", ec.publicKey)); +await t("export not key", () => crypto.subtle.exportKey("spki", {})); +await t("export spki private", () => crypto.subtle.exportKey("spki", ec.privateKey)); +await t("export pkcs8 public", () => crypto.subtle.exportKey("pkcs8", ec.publicKey)); +await t("import raw private", () => crypto.subtle.importKey("raw", new Uint8Array(32), { name: "ECDSA", namedCurve: "P-256" }, true, ["sign"])); +await t("import pkcs8 verify", async () => crypto.subtle.importKey("pkcs8", await crypto.subtle.exportKey("pkcs8", ec.privateKey), { name: "ECDSA", namedCurve: "P-256" }, true, ["verify"])); +await t("import pkcs8 empty", async () => crypto.subtle.importKey("pkcs8", await crypto.subtle.exportKey("pkcs8", ec.privateKey), { name: "ECDSA", namedCurve: "P-256" }, true, [])); +await t("import spki empty", async () => (await crypto.subtle.importKey("spki", await crypto.subtle.exportKey("spki", ec.publicKey), { name: "ECDSA", namedCurve: "P-256" }, true, [])).usages); +await t("import no curve", async () => crypto.subtle.importKey("spki", await crypto.subtle.exportKey("spki", ec.publicKey), { name: "ECDSA" }, true, ["verify"])); +await t("import rsa no hash", async () => crypto.subtle.importKey("spki", await crypto.subtle.exportKey("spki", ec.publicKey), { name: "RSASSA-PKCS1-v1_5" }, true, ["verify"])); +await t("import ec as rsa", async () => crypto.subtle.importKey("spki", await crypto.subtle.exportKey("spki", ec.publicKey), { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" }, true, ["verify"])); +await t("import jwk bad", () => crypto.subtle.importKey("jwk", { kty: "EC", crv: "P-256", x: "AA", y: "AA" }, { name: "ECDSA", namedCurve: "P-256" }, true, ["verify"])); +await t("import jwk crv mismatch", async () => crypto.subtle.importKey("jwk", await crypto.subtle.exportKey("jwk", ec.publicKey), { name: "ECDSA", namedCurve: "P-384" }, true, ["verify"])); +await t("import jwk kty", async () => crypto.subtle.importKey("jwk", { ...(await crypto.subtle.exportKey("jwk", ec.publicKey)), kty: "RSA" }, { name: "ECDSA", namedCurve: "P-256" }, true, ["verify"])); +await t("jwk ext false", async () => crypto.subtle.importKey("jwk", { ...(await crypto.subtle.exportKey("jwk", ec.publicKey)), ext: false }, { name: "ECDSA", namedCurve: "P-256" }, true, ["verify"])); +await t("jwk key_ops", async () => crypto.subtle.importKey("jwk", { ...(await crypto.subtle.exportKey("jwk", ec.publicKey)), key_ops: ["sign"] }, { name: "ECDSA", namedCurve: "P-256" }, true, ["verify"])); +const data = new Uint8Array([1, 2, 3]); +const spki = await crypto.subtle.exportKey("spki", ec.publicKey); +const rsa = await crypto.subtle.generateKey({ name: "RSASSA-PKCS1-v1_5", modulusLength: 1024, publicExponent: new Uint8Array([1, 0, 1]), hash: "SHA-256" }, false, ["sign", "verify"]); +const pss = await crypto.subtle.generateKey({ name: "RSA-PSS", modulusLength: 1024, publicExponent: new Uint8Array([1, 0, 1]), hash: "SHA-384" }, true, ["sign", "verify"]); +await t("export nonextractable", () => crypto.subtle.exportKey("pkcs8", rsa.privateKey)); +await t("export raw private", () => crypto.subtle.exportKey("raw", ec.privateKey)); +await t("sign with public", () => crypto.subtle.sign({ name: "ECDSA", hash: "SHA-256" }, ec.publicKey, data)); +await t("sign wrong alg", () => crypto.subtle.sign("RSASSA-PKCS1-v1_5", ec.privateKey, data)); +await t("ecdsa no hash", () => crypto.subtle.sign({ name: "ECDSA" }, ec.privateKey, data)); +await t("bad curve", () => crypto.subtle.generateKey({ name: "ECDSA", namedCurve: "P-999" }, true, ["sign"])); +await t("bad usage", () => crypto.subtle.generateKey({ name: "ECDSA", namedCurve: "P-256" }, true, ["encrypt"])); +await t("empty usages", () => crypto.subtle.generateKey({ name: "ECDSA", namedCurve: "P-256" }, true, [])); +await t("verify usage only", async () => { const k = await crypto.subtle.generateKey({ name: "ECDSA", namedCurve: "P-384" }, true, ["verify"]); return k.privateKey.usages + "|" + k.publicKey.usages; }); +await t("import wrong curve", () => crypto.subtle.importKey("spki", spki, { name: "ECDSA", namedCurve: "P-384" }, true, ["verify"])); +await t("import sign usage public", () => crypto.subtle.importKey("spki", spki, { name: "ECDSA", namedCurve: "P-256" }, true, ["sign"])); +await t("import garbage", () => crypto.subtle.importKey("spki", new Uint8Array([1, 2, 3]), { name: "ECDSA", namedCurve: "P-256" }, true, ["verify"])); +await t("pss no salt", () => crypto.subtle.sign({ name: "RSA-PSS" }, pss.privateKey, data)); +await t("verify bad sig len", () => crypto.subtle.verify({ name: "ECDSA", hash: "SHA-256" }, ec.publicKey, new Uint8Array(10), data)); +await t("verify with private", () => crypto.subtle.verify({ name: "ECDSA", hash: "SHA-256" }, ec.privateKey, new Uint8Array(64), data)); +await t("sign hmac with ec", () => crypto.subtle.sign("HMAC", ec.privateKey, data)); +await t("sign ec hash obj", async () => (await crypto.subtle.sign({ name: "ECDSA", hash: { name: "SHA-512" } }, ec.privateKey, data)).byteLength); +await t("p521", async () => (await crypto.subtle.sign({ name: "ECDSA", hash: "SHA-256" }, (await crypto.subtle.generateKey({ name: "ECDSA", namedCurve: "P-521" }, false, ["sign"])).privateKey, data)).byteLength); +await t("raw export p384", async () => (await crypto.subtle.exportKey("raw", (await crypto.subtle.generateKey({ name: "ECDSA", namedCurve: "P-384" }, true, ["sign","verify"])).publicKey)).byteLength); diff --git a/tests/corpus/7087-tls-socket-wrap.ts b/tests/corpus/7087-tls-socket-wrap.ts new file mode 100644 index 000000000..d1357a98e --- /dev/null +++ b/tests/corpus/7087-tls-socket-wrap.ts @@ -0,0 +1,104 @@ +// new tls.TLSSocket(socket, { isServer: true, ... }) over a net server's +// connection: the wrap is a new socket that runs the server handshake, with +// cert/key or a secureContext and ALPNProtocols. The wrapped socket stays +// open without I/O, destroy() on it closes both, and its 'close' fires just +// before the TLS socket's. A peer that closes, resets or sends bytes that +// are not TLS fails the handshake with Node's events and codes. +import { readFileSync } from "node:fs"; +import * as net from "node:net"; +import * as tls from "node:tls"; + +const certs = "tests/fixtures/server/certs"; +const ca = readFileSync(`${certs}/ca.pem`); +const cert = readFileSync(`${certs}/localhost.pem`); +const key = readFileSync(`${certs}/localhost-key.pem`); +const context = tls.createSecureContext({ cert, key }); + +// cert/key and ALPN: the events, the state of both sockets, and the data. +function certKey(next: () => void): void { + const server = net.createServer((raw) => { + raw.on("close", (hadError: boolean) => console.log("raw close", hadError, raw.destroyed)); + const s = new tls.TLSSocket(raw, { isServer: true, cert, key, ALPNProtocols: ["h2", "http/1.1"] }); + console.log("after wrap", raw.destroyed, raw.readyState, raw.writable, s === raw); + console.log("before handshake", s.destroyed, s.encrypted, s.alpnProtocol); + console.log("same peer", s.remoteAddress === raw.remoteAddress, s.remotePort === raw.remotePort); + s.on("connect", () => console.log("never: connect")); + s.on("secureConnect", () => console.log("never: secureConnect")); + s.on("secure", () => { + console.log("secure", s.getProtocol(), s.alpnProtocol, s.authorized, s.authorizationError); + }); + s.on("data", (chunk: Buffer) => { + console.log("server data", chunk.toString()); + s.end("pong"); + }); + s.on("end", () => console.log("server end")); + s.on("close", (hadError: boolean) => { + console.log("tls close", hadError, raw.destroyed, s.destroyed); + server.close(next); + }); + }); + server.listen(0, () => { + const port = (server.address() as net.AddressInfo).port; + const c = tls.connect({ port, host: "localhost", ca, ALPNProtocols: ["http/1.1"] }, () => { + console.log("client secureConnect", c.authorized, c.alpnProtocol); + c.write("ping"); + }); + c.on("secure", () => console.log("client secure")); + c.on("data", (chunk: Buffer) => console.log("client data", chunk.toString())); + c.on("close", () => console.log("client close")); + }); +} + +// secureContext, and destroy() on the wrapped socket. +function secureContext(next: () => void): void { + const server = net.createServer((raw) => { + raw.on("close", (hadError: boolean) => console.log("raw close", hadError)); + const s = new tls.TLSSocket(raw, { isServer: true, secureContext: context }); + s.on("secure", () => { + console.log("context secure", s.alpnProtocol); + raw.destroy(); + }); + s.on("close", (hadError: boolean) => { + console.log("tls close", hadError, s.destroyed); + server.close(next); + }); + }); + server.listen(0, () => { + const port = (server.address() as net.AddressInfo).port; + const c = tls.connect({ port, host: "localhost", ca }, () => console.log("client secureConnect")); + c.on("error", (err: Error & { code?: string }) => console.log("client error", err.code)); + c.on("close", () => console.log("client close")); + }); +} + +// Handshakes the peer fails. +function failure(mode: string, next: () => void): void { + const server = net.createServer((raw) => { + const s = new tls.TLSSocket(raw, { isServer: true, cert, key }); + s.on("secure", () => console.log("never: secure")); + s.on("error", (err: Error & { code?: string }) => console.log(mode, "error", err.code)); + s.on("end", () => console.log(mode, "end")); + s.on("close", (hadError: boolean) => { + console.log(mode, "close", hadError); + server.close(next); + }); + }); + server.listen(0, () => { + const port = (server.address() as net.AddressInfo).port; + const c = net.connect(port, "localhost", () => { + if (mode === "close") c.end(); + else if (mode === "garbage") c.end("hello there, this is not tls\r\n"); + else if (mode === "http") c.end("GET / HTTP/1.1\r\nHost: localhost\r\n\r\n"); + else c.resetAndDestroy(); + }); + c.on("error", () => {}); + }); +} + +certKey(() => + secureContext(() => + failure("close", () => + failure("garbage", () => failure("http", () => failure("reset", () => console.log("done")))), + ), + ), +); diff --git a/tests/corpus/7088-tls-socket-connect-tunnel.ts b/tests/corpus/7088-tls-socket-connect-tunnel.ts new file mode 100644 index 000000000..806575ddb --- /dev/null +++ b/tests/corpus/7088-tls-socket-connect-tunnel.ts @@ -0,0 +1,64 @@ +// An HTTPS-intercepting proxy: the http server answers CONNECT, then runs +// the TLS server side on the tunnel socket with new tls.TLSSocket and a +// secureContext; the client runs its handshake inside the tunnel with +// tls.connect({ socket }). Without a servername the client verifies against +// the host the tunnel socket dialed. +import { readFileSync } from "node:fs"; +import * as http from "node:http"; +import * as net from "node:net"; +import * as tls from "node:tls"; + +const certs = "tests/fixtures/server/certs"; +const ca = readFileSync(`${certs}/ca.pem`); +const context = tls.createSecureContext({ + cert: readFileSync(`${certs}/localhost.pem`), + key: readFileSync(`${certs}/localhost-key.pem`), +}); + +let tunnels = 0; +const proxy = http.createServer((_req, res) => { + res.end("plain"); +}); +proxy.on("connect", (req: http.IncomingMessage, socket: net.Socket, head: Buffer) => { + const id = ++tunnels; + console.log(id, "proxy connect", req.method, req.url, head.length); + socket.write("HTTP/1.1 200 Connection Established\r\n\r\n"); + const secure = new tls.TLSSocket(socket, { isServer: true, secureContext: context }); + secure.on("secure", () => console.log(id, "proxy secure", secure.getProtocol())); + secure.on("data", (chunk: Buffer) => { + console.log(id, "proxy got", chunk.toString()); + secure.end(`intercepted ${chunk.toString()}`); + }); + secure.on("close", () => { + console.log(id, "proxy tls close"); + if (id === 2) proxy.close(); + }); + socket.on("close", () => console.log(id, "tunnel close")); +}); + +function tunnel(port: number, servername: string | undefined, next: () => void): void { + const raw = net.connect(port, "localhost", () => { + raw.write("CONNECT example.test:443 HTTP/1.1\r\nHost: example.test:443\r\n\r\n"); + }); + raw.once("data", (chunk: Buffer) => { + console.log("client got", chunk.toString().split("\r\n")[0]); + const onSecure = (): void => { + console.log("client secureConnect", client.authorized); + client.write(servername ?? "no servername"); + }; + const client = + servername === undefined + ? tls.connect({ socket: raw, ca }, onSecure) + : tls.connect({ socket: raw, ca, servername }, onSecure); + client.on("data", (data: Buffer) => console.log("client data", data.toString())); + client.on("close", () => { + console.log("client close"); + next(); + }); + }); +} + +proxy.listen(0, () => { + const port = (proxy.address() as net.AddressInfo).port; + tunnel(port, "localhost", () => tunnel(port, undefined, () => console.log("done"))); +}); diff --git a/tests/corpus/7089-http-host-header-tunnels.ts b/tests/corpus/7089-http-host-header-tunnels.ts new file mode 100644 index 000000000..bc982b2d1 --- /dev/null +++ b/tests/corpus/7089-http-host-header-tunnels.ts @@ -0,0 +1,31 @@ +// An HTTP/1.1 request without a Host header gets 400 Bad Request, except +// the requests that leave the parser first: an upgrade an 'upgrade' +// listener takes, and a CONNECT (destroyed here, with no 'connect' +// listener). +import * as http from "node:http"; +import * as net from "node:net"; + +const server = http.createServer((_req, res) => res.end("request")); +server.on("upgrade", (req: http.IncomingMessage, socket: net.Socket) => { + console.log("upgrade", req.url, req.headers.host); + socket.end("HTTP/1.1 101 Switching Protocols\r\n\r\n"); +}); +function probe(head: string, next: () => void): void { + const port = (server.address() as net.AddressInfo).port; + const raw = net.connect(port, "127.0.0.1", () => raw.write(head)); + let got = ""; + raw.on("data", (d: Buffer) => { + got += d.toString(); + }); + raw.on("close", () => { + console.log("got", JSON.stringify(got.split("\r\n")[0])); + next(); + }); +} +server.listen(0, "127.0.0.1", () => { + probe("GET /ws HTTP/1.1\r\nConnection: upgrade\r\nUpgrade: websocket\r\n\r\n", () => + probe("CONNECT a:1 HTTP/1.1\r\n\r\n", () => + probe("GET / HTTP/1.1\r\nConnection: close\r\n\r\n", () => server.close()), + ), + ); +}); diff --git a/tests/corpus/7094-tls-socket-failed-handshakes.ts b/tests/corpus/7094-tls-socket-failed-handshakes.ts new file mode 100644 index 000000000..daad49a4e --- /dev/null +++ b/tests/corpus/7094-tls-socket-failed-handshakes.ts @@ -0,0 +1,49 @@ +// A TLSSocket whose handshake never completes still closes and releases +// its listeners, including a 'secure' listener that holds the socket: one +// peer closes before sending anything and the next sends a plain HTTP +// request. +import { readFileSync } from "node:fs"; +import * as net from "node:net"; +import * as tls from "node:tls"; + +const certs = "tests/fixtures/server/certs"; +const cert = readFileSync(`${certs}/localhost.pem`); +const key = readFileSync(`${certs}/localhost-key.pem`); +const context = tls.createSecureContext({ cert, key }); + +const peers: ((port: number) => void)[] = [ + (port) => { + const quiet = net.connect(port, "127.0.0.1", () => quiet.end()); + }, + (port) => { + const plain = net.connect(port, "127.0.0.1", () => plain.write("GET / HTTP/1.1\r\nHost: x\r\n\r\n")); + plain.on("error", () => {}); + plain.resume(); + }, + (port) => { + const quiet = net.connect(port, "127.0.0.1", () => quiet.destroy()); + }, +]; +let served = 0; +const server = net.createServer((raw) => { + served++; + const secure = + served === 3 + ? new tls.TLSSocket(raw, { isServer: true, secureContext: context }) + : new tls.TLSSocket(raw, { isServer: true, cert, key }); + secure.on("secure", () => console.log("secure", served, secure.alpnProtocol)); + secure.on("error", (err: NodeJS.ErrnoException) => console.log("error", served, err.code)); + secure.on("close", (hadError: boolean) => { + console.log("close", served, hadError); + next(); + }); +}); +function next(): void { + const peer = peers.shift(); + if (peer === undefined) { + server.close(() => console.log("server closed")); + return; + } + peer((server.address() as net.AddressInfo).port); +} +server.listen(0, "127.0.0.1", next); diff --git a/tests/corpus/7106-async-prefix-enter-with.ts b/tests/corpus/7106-async-prefix-enter-with.ts new file mode 100644 index 000000000..d70822171 --- /dev/null +++ b/tests/corpus/7106-async-prefix-enter-with.ts @@ -0,0 +1,119 @@ +// AsyncLocalStorage.enterWith in an async callee: before the callee's first +// await it runs in the caller's execution, so the caller sees the store +// after the call (Node's semantics), and the callee keeps it after its own +// await; after the first await it stays inside the callee. Covers callees +// that never suspend, stackless and fiber-backed callees, nested callees, +// run() scopes inside the prefix, and calls without an await. A store that +// an event-loop callback enters this way, or through a generator step, ends +// with that callback and does not reach the next one. +import { AsyncLocalStorage } from "node:async_hooks"; + +const store = new AsyncLocalStorage(); +const log: string[] = []; +const current = (): string => store.getStore() ?? "none"; + +async function neverSuspends(tag: string): Promise { + store.enterWith(`never-${tag}`); + return 1; +} +async function prefixThenAwait(tag: string): Promise { + store.enterWith(`prefix-${tag}`); + await null; + log.push(`callee ${tag} after its await: ${current()}`); + return 2; +} +async function afterAwait(tag: string): Promise { + await null; + store.enterWith(`after-${tag}`); + return 3; +} +async function nested(tag: string): Promise { + await neverSuspends(`inner-${tag}`); + log.push(`nested ${tag} after its callee: ${current()}`); + return 4; +} +async function runInPrefix(tag: string): Promise { + store.run(`run-${tag}`, () => store.enterWith("inside-run")); + return 5; +} +async function* ticks(): AsyncGenerator { + yield 1; +} +async function fibered(tag: string): Promise { + store.enterWith(`fibered-${tag}`); + let n = 0; + for await (const t of ticks()) n += t; + log.push(`fibered ${tag} after its loop: ${current()}`); + return n; +} + +function* steps(): Generator { + store.enterWith("generator-step"); + yield 1; +} +function callbacks(): Promise { + return new Promise((resolve) => { + setTimeout(() => { + void prefixThenAwait("9"); + log.push(`timer after the call: ${current()}`); + }, 0); + setTimeout(() => { + log.push(`second timer: ${current()}`); + steps().next(); + log.push(`second timer after a generator step: ${current()}`); + }, 0); + setTimeout(() => { + log.push(`third timer: ${current()}`); + void neverSuspends("10"); + log.push(`third timer after the call: ${current()}`); + }, 0); + setTimeout(() => { + log.push(`last timer: ${current()}`); + resolve(); + }, 0); + }); +} + +async function main(): Promise { + await store.run("A", async () => { + await neverSuspends("1"); + log.push(`A after neverSuspends: ${current()}`); + }); + await store.run("B", async () => { + const pending = prefixThenAwait("2"); + log.push(`B after the call: ${current()}`); + store.enterWith("B-later"); + await pending; + log.push(`B after its await: ${current()}`); + }); + await store.run("C", async () => { + await afterAwait("3"); + log.push(`C after afterAwait: ${current()}`); + }); + await store.run("D", async () => { + await nested("4"); + log.push(`D after nested: ${current()}`); + }); + await store.run("E", async () => { + await runInPrefix("5"); + log.push(`E after runInPrefix: ${current()}`); + }); + await store.run("F", async () => { + void neverSuspends("6"); + log.push(`F after an unawaited call: ${current()}`); + await null; + log.push(`F later: ${current()}`); + }); + store.run("G", () => { + void neverSuspends("7"); + log.push(`G synchronous caller: ${current()}`); + }); + await store.run("H", async () => { + await fibered("8"); + log.push(`H after fibered: ${current()}`); + }); + await callbacks(); + log.push(`main after the callbacks: ${current()}`); + for (const line of log) console.log(line); +} +void main(); diff --git a/tests/corpus/7107-for-await-context.ts b/tests/corpus/7107-for-await-context.ts new file mode 100644 index 000000000..8e21933b9 --- /dev/null +++ b/tests/corpus/7107-for-await-context.ts @@ -0,0 +1,37 @@ +// An async function's AsyncLocalStorage store across `for await` over an +// async generator: the consumer keeps its store after each resumption of +// the generator, whether it came from run(), from enterWith before the +// loop, or from enterWith after an earlier await. +import { AsyncLocalStorage } from "node:async_hooks"; + +const store = new AsyncLocalStorage(); +const current = (): string => store.getStore() ?? "none"; + +async function* numbers(n: number): AsyncGenerator { + for (let i = 0; i < n; i++) { + await null; + yield i; + } +} + +async function consume(label: string): Promise { + let sum = 0; + for await (const v of numbers(3)) { + sum += v; + console.log(`${label} step ${v}: ${current()}`); + } + console.log(`${label} after the loop (${sum}): ${current()}`); +} + +async function entered(): Promise { + await null; + store.enterWith("entered"); + await consume("entered"); +} + +async function main(): Promise { + await store.run("run", () => consume("run")); + await entered(); + console.log(`main: ${current()}`); +} +void main(); diff --git a/tests/corpus/7108-generator-step-context.ts b/tests/corpus/7108-generator-step-context.ts new file mode 100644 index 000000000..3e9a01a5f --- /dev/null +++ b/tests/corpus/7108-generator-step-context.ts @@ -0,0 +1,49 @@ +// A synchronous generator has no AsyncLocalStorage context of its own: each +// step runs in its resumer's execution, so it sees the store current at +// that next() call, and a store it enters is the resumer's after the step. +// Covers steps inside and outside run(), for-of, spread and early return. +import { AsyncLocalStorage } from "node:async_hooks"; + +const store = new AsyncLocalStorage(); +const current = (): string => store.getStore() ?? "none"; + +function* steps(): Generator { + console.log(`start sees ${current()}`); + yield "a"; + console.log(`step 2 sees ${current()}`); + store.enterWith("entered-in-step-2"); + yield "b"; + console.log(`step 3 sees ${current()}`); +} + +function* labels(): Generator { + yield current(); + yield current(); +} + +store.run("S1", () => { + const g = steps(); + g.next(); + store.run("S2", () => g.next()); + console.log(`caller after the S2 step: ${current()}`); + store.enterWith("S3"); + g.next(); + console.log(`caller after step 3: ${current()}`); +}); + +store.run("forof", () => { + const seen: string[] = []; + for (const label of labels()) seen.push(label); + console.log(`for-of: ${seen.join(",")}`); + console.log(`spread: ${[...labels()].join(",")}`); +}); + +store.run("outer", () => { + const g = steps(); + g.next(); + g.next(); + console.log(`caller after entering step: ${current()}`); + g.return(); + console.log(`caller after return: ${current()}`); +}); +console.log(`top level: ${current()}`); diff --git a/tests/corpus/7130-typed-message-listeners.ts b/tests/corpus/7130-typed-message-listeners.ts new file mode 100644 index 000000000..b98983824 --- /dev/null +++ b/tests/corpus/7130-typed-message-listeners.ts @@ -0,0 +1,80 @@ +import { MessageChannel } from "node:worker_threads"; + +// 'message' listeners with typed parameters on MessageChannel ports. A +// message with the parameter's exact layout, and one with other keys, +// shared values or other kinds, reach the listener with the same values as +// in Node. +type Item = { id: number; name: string; done: boolean }; +type Nested = { label: string; item: Item }; + +const { port1, port2 } = new MessageChannel(); +// Each port's lines print in delivery order once every message arrived. +const lines: string[] = []; +const nestedLines: string[] = []; +const otherLines: string[] = []; +let pending = 0; +function finish(): void { + if (--pending === 0) { + for (const line of [...lines, ...nestedLines, ...otherLines]) console.log(line); + port1.close(); + channel.port1.close(); + other.port1.close(); + } +} + +port1.on("message", (value: number | string | Item | null | undefined) => { + if (value === null || value === undefined) lines.push(`unit ${String(value)}`); + else if (typeof value === "number") lines.push(`number ${value.toFixed(2)}`); + else if (typeof value === "string") lines.push(`string ${value.length} ${value}`); + else lines.push(`item ${value.id} ${value.name} ${value.done} ${JSON.stringify(value)}`); + finish(); +}); + +const sent: unknown[] = [ + 1.5, + -0, + "", + "héllo wörld", + null, + undefined, + { id: 1, name: "first", done: false }, + { id: 2, name: "", done: true }, + // An extra key stays beside the record. + { id: 4, name: "extra", done: false, note: "kept" }, +]; +pending += sent.length; +for (const value of sent) port2.postMessage(value); + +// Nested records, a value shared by two fields, and a second listener +// that reads the message untyped. +const channel = new MessageChannel(); +channel.port1.on("message", (value: Nested) => { + nestedLines.push(`nested ${value.label} ${value.item.id} ${value.item.name} ${JSON.stringify(value)}`); + finish(); +}); +channel.port1.on("message", (value: unknown) => { + nestedLines.push(`untyped ${JSON.stringify(value)}`); + finish(); +}); +const shared = { id: 5, name: "shared", done: true }; +const nested: unknown[] = [ + { label: "plain", item: { id: 6, name: "six", done: false } }, + { label: "shared", item: shared, again: shared }, +]; +pending += 2 * nested.length; +for (const value of nested) channel.port2.postMessage(value); + +// once listeners, and a listener added while the port delivers. +const other = new MessageChannel(); +other.port1.once("message", (value: Item) => { + otherLines.push(`once ${value.id} ${value.name}`); + other.port1.on("message", (later: number) => { + otherLines.push(`later ${later}`); + finish(); + }); + finish(); +}); +pending += 3; +other.port2.postMessage({ id: 7, name: "seven", done: true }); +other.port2.postMessage(8); +other.port2.postMessage(9); diff --git a/tests/corpus/7130-webcrypto-wrap-key.ts b/tests/corpus/7130-webcrypto-wrap-key.ts new file mode 100644 index 000000000..8cf51fab4 --- /dev/null +++ b/tests/corpus/7130-webcrypto-wrap-key.ts @@ -0,0 +1,69 @@ +// crypto.subtle.wrapKey and unwrapKey: AES-KW wrapping matches RFC 3394's +// test vectors, raw and JSON Web Key payloads round-trip (AES-KW pads the +// JWK's JSON with spaces to a multiple of 8 bytes), and AES-GCM, AES-CBC +// and AES-CTR wrap keys the way encrypt does. jose's A128KW and +// PBES2-HS256+A128KW content-key wrapping is the shape used here. +const encoder = new TextEncoder(); + +function hex(buffer: ArrayBuffer): string { + return Buffer.from(buffer).toString("hex"); +} + +type Key = Awaited>; + +function describe(key: Key): string { + return JSON.stringify({ algorithm: key.algorithm, usages: key.usages, extractable: key.extractable }); +} + +// RFC 3394 section 4: 128-, 192- and 256-bit key data under each KEK size. +const vectors: [string, string, string][] = [ + ["000102030405060708090A0B0C0D0E0F", "00112233445566778899AABBCCDDEEFF", "1fa68b0a8112b447aef34bd8fb5a7b829d3e862371d2cfe5"], + ["000102030405060708090A0B0C0D0E0F1011121314151617", "00112233445566778899AABBCCDDEEFF", "96778b25ae6ca435f92b5b97c050aed2468ab8a17ad84e5d"], + ["000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F", "00112233445566778899AABBCCDDEEFF", "64e8c3f9ce0f5ba263e9777905818a2a93c8191e7d6e8ae7"], + ["000102030405060708090A0B0C0D0E0F1011121314151617", "00112233445566778899AABBCCDDEEFF0001020304050607", "031d33264e15d33268f24ec260743edce1c6c7ddee725a936ba814915c6762d2"], + ["000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F", "00112233445566778899AABBCCDDEEFF000102030405060708090A0B0C0D0E0F", "28c9f404c4b810f4cbccb35cfb87f8263f5786e2d80ed326cbc7f0e71a99f43bfb988b9b7a02dd21"], +]; +for (const [kekHex, dataHex, expected] of vectors) { + const kek = await crypto.subtle.importKey("raw", Buffer.from(kekHex, "hex"), "AES-KW", false, ["wrapKey", "unwrapKey"]); + const data = await crypto.subtle.importKey("raw", Buffer.from(dataHex, "hex"), "AES-CBC", true, ["encrypt"]); + const wrapped = await crypto.subtle.wrapKey("raw", data, kek, "AES-KW"); + console.log(hex(wrapped) === expected, hex(wrapped)); + const unwrapped = await crypto.subtle.unwrapKey("raw", wrapped, kek, { name: "aes-kw" }, "AES-CBC", true, ["decrypt"]); + console.log(describe(unwrapped), hex(await crypto.subtle.exportKey("raw", unwrapped))); +} + +// jose's content-key wrap: an HMAC key wrapped raw, then unwrapped and exported. +const kek = await crypto.subtle.importKey("raw", new Uint8Array(16).fill(7), "AES-KW", true, ["wrapKey", "unwrapKey"]); +const cek = await crypto.subtle.importKey("raw", new Uint8Array(32).fill(9), { name: "HMAC", hash: "SHA-256" }, true, ["sign"]); +const wrappedCek = await crypto.subtle.wrapKey("raw", cek, kek, "AES-KW"); +console.log(hex(wrappedCek)); +const back = await crypto.subtle.unwrapKey("raw", wrappedCek, kek, "AES-KW", { hash: "SHA-256", name: "HMAC" }, true, ["sign"]); +console.log(describe(back), hex(await crypto.subtle.exportKey("raw", back))); + +// JSON Web Key payloads, padded for AES-KW. +const wrappedJwk = await crypto.subtle.wrapKey("jwk", cek, kek, "AES-KW"); +console.log(wrappedJwk.byteLength, hex(wrappedJwk)); +const fromJwk = await crypto.subtle.unwrapKey("jwk", wrappedJwk, kek, "AES-KW", { name: "HMAC", hash: "SHA-256" }, false, ["sign"]); +console.log(describe(fromJwk)); +const asRaw = await crypto.subtle.unwrapKey("raw", wrappedJwk, kek, "AES-KW", { name: "HMAC", hash: "SHA-256" }, true, ["sign"]); +const padded = new TextDecoder().decode(await crypto.subtle.exportKey("raw", asRaw)); +console.log(JSON.stringify(padded), padded.length); + +// The AES ciphers as wrapping algorithms. +const gcmKey = await crypto.subtle.importKey("raw", new Uint8Array(16).fill(3), "AES-GCM", false, ["wrapKey", "unwrapKey"]); +const iv = new Uint8Array(12).fill(1); +const gcmWrapped = await crypto.subtle.wrapKey("jwk", cek, gcmKey, { name: "AES-GCM", iv }); +console.log(gcmWrapped.byteLength, hex(gcmWrapped).slice(0, 64)); +console.log(describe(await crypto.subtle.unwrapKey("jwk", gcmWrapped, gcmKey, { name: "AES-GCM", iv }, { name: "HMAC", hash: "SHA-256" }, true, ["sign"]))); +const cbcKey = await crypto.subtle.importKey("raw", new Uint8Array(32).fill(4), "AES-CBC", false, ["wrapKey", "unwrapKey"]); +const cbcWrapped = await crypto.subtle.wrapKey("raw", cek, cbcKey, { name: "AES-CBC", iv: new Uint8Array(16) }); +console.log(hex(cbcWrapped)); +console.log(describe(await crypto.subtle.unwrapKey("raw", cbcWrapped, cbcKey, { name: "AES-CBC", iv: new Uint8Array(16) }, "AES-GCM", false, ["encrypt"]))); +const ctrKey = await crypto.subtle.importKey("raw", new Uint8Array(24).fill(5), "AES-CTR", false, ["wrapKey", "unwrapKey"]); +const ctr = { name: "AES-CTR", counter: new Uint8Array(16), length: 64 }; +const ctrWrapped = await crypto.subtle.wrapKey("raw", cek, ctrKey, ctr); +console.log(hex(ctrWrapped)); +const fromCtr = await crypto.subtle.unwrapKey("raw", ctrWrapped, ctrKey, ctr, { name: "HMAC", hash: "SHA-1" }, true, ["verify"]); +console.log(describe(fromCtr)); +const signature = await crypto.subtle.sign("HMAC", back, encoder.encode("message")); +console.log(hex(signature).slice(0, 32)); diff --git a/tests/corpus/7131-typed-worker-messages.ts b/tests/corpus/7131-typed-worker-messages.ts new file mode 100644 index 000000000..86197b4de --- /dev/null +++ b/tests/corpus/7131-typed-worker-messages.ts @@ -0,0 +1,54 @@ +import { MessageChannel, MessagePort, Worker, isMainThread, parentPort } from "node:worker_threads"; + +// 'message' listeners with typed parameters on a worker and its +// parentPort: records and numbers in both directions, and a message that +// carries a transferred MessagePort. +type Job = { id: number; text: string; urgent: boolean }; +type Reply = { id: number; length: number; upper: string }; + +if (isMainThread) { + const worker = new Worker(new URL(import.meta.url)); + // The worker's replies, then the port's, each in delivery order. + const replies: string[] = []; + const portLines: string[] = []; + let expected = 5; + const done = (): void => { + if (--expected > 0) return; + for (const line of [...replies, ...portLines]) console.log(line); + void worker.terminate(); + }; + worker.on("message", (reply: Reply | number) => { + if (typeof reply === "number") replies.push(`count ${reply}`); + else replies.push(`reply ${reply.id} ${reply.length} ${reply.upper}`); + done(); + }); + const jobs: Job[] = [ + { id: 1, text: "alpha", urgent: false }, + { id: 2, text: "βeta", urgent: true }, + { id: 3, text: "", urgent: false }, + ]; + for (const job of jobs) worker.postMessage(job); + worker.postMessage(jobs.length); + // A port in the message: the worker answers through it. + const { port1, port2 } = new MessageChannel(); + port1.on("message", (text: string) => { + portLines.push(`port ${text}`); + port1.close(); + done(); + }); + worker.postMessage({ port: port2 }, [port2]); +} else { + let count = 0; + parentPort!.on("message", (message: Job | number | { port: MessagePort }) => { + if (typeof message === "number") { + parentPort!.postMessage(count + message); + } else if ("port" in message) { + message.port.postMessage("through the port"); + message.port.close(); + } else { + count++; + const text = message.urgent ? message.text.toUpperCase() : message.text; + parentPort!.postMessage({ id: message.id, length: message.text.length, upper: text }); + } + }); +} diff --git a/tests/corpus/7131-webcrypto-wrap-key-errors.mjs b/tests/corpus/7131-webcrypto-wrap-key-errors.mjs new file mode 100644 index 000000000..23cb3e10d --- /dev/null +++ b/tests/corpus/7131-webcrypto-wrap-key-errors.mjs @@ -0,0 +1,65 @@ +// Rejections of crypto.crypto.subtle.wrapKey and unwrapKey in Node's order: +// argument conversion, wrap algorithm normalization (AES-KW, then the +// encrypt registry), the wrapping key's algorithm and usages, the export +// or import of the wrapped key, and the cipher's OperationErrors (AES-KW's +// length rules and integrity check), which reject on a later turn. + +async function attempt(label, run) { + try { + const value = await run(); + const shown = value instanceof ArrayBuffer ? `ArrayBuffer(${value.byteLength})` : value instanceof CryptoKey ? JSON.stringify([value.algorithm, value.usages]) : value; + console.log(label, "resolves", shown); + } catch (e) { + console.log(label, e.name, e.code, e.message); + } +} + +const kek = await crypto.subtle.importKey("raw", new Uint8Array(16).fill(1), "AES-KW", true, ["wrapKey", "unwrapKey"]); +const unwrapOnly = await crypto.subtle.importKey("raw", new Uint8Array(16).fill(1), "AES-KW", true, ["unwrapKey"]); +const gcm = await crypto.subtle.importKey("raw", new Uint8Array(16), "AES-GCM", true, ["wrapKey", "unwrapKey"]); +const hmac20 = await crypto.subtle.importKey("raw", new Uint8Array(20).fill(2), { name: "HMAC", hash: "SHA-256" }, true, ["sign"]); +const hmac32 = await crypto.subtle.importKey("raw", new Uint8Array(32).fill(2), { name: "HMAC", hash: "SHA-256" }, true, ["sign"]); +const locked = await crypto.subtle.importKey("raw", new Uint8Array(16), "AES-CBC", false, ["encrypt"]); +const password = await crypto.subtle.importKey("raw", new Uint8Array(4), "PBKDF2", false, ["deriveBits"]); +const hs256 = { name: "HMAC", hash: "SHA-256" }; + +await attempt("wrap too few", () => crypto.subtle.wrapKey("raw", hmac32, kek)); +await attempt("wrap bad format", () => crypto.subtle.wrapKey("pem", hmac32, kek, "AES-KW")); +await attempt("wrap key not a key", () => crypto.subtle.wrapKey("raw", JSON.parse("{}"), kek, "AES-KW")); +await attempt("wrap wrapping key not a key", () => crypto.subtle.wrapKey("raw", hmac32, JSON.parse("{}"), "AES-KW")); +await attempt("wrap unknown algorithm", () => crypto.subtle.wrapKey("raw", hmac32, kek, "AES-XTS")); +await attempt("wrap no name", () => crypto.subtle.wrapKey("raw", hmac32, kek, {})); +await attempt("wrap gcm without iv", () => crypto.subtle.wrapKey("raw", hmac32, gcm, "AES-GCM")); +await attempt("wrap mismatch", () => crypto.subtle.wrapKey("raw", hmac32, gcm, "AES-KW")); +await attempt("wrap ctr on gcm key", () => crypto.subtle.wrapKey("raw", hmac32, gcm, { name: "AES-CTR", counter: new Uint8Array(16), length: 64 })); +await attempt("wrap missing usage", () => crypto.subtle.wrapKey("raw", hmac32, unwrapOnly, "AES-KW")); +await attempt("wrap pbkdf2 key", () => crypto.subtle.wrapKey("raw", password, kek, "AES-KW")); +await attempt("wrap non-extractable", () => crypto.subtle.wrapKey("raw", locked, kek, "AES-KW")); +await attempt("wrap spki", () => crypto.subtle.wrapKey("spki", hmac32, kek, "AES-KW")); +await attempt("wrap 20 bytes with AES-KW", () => crypto.subtle.wrapKey("raw", hmac20, kek, "AES-KW")); +await attempt("wrap 20 bytes with AES-GCM", () => crypto.subtle.wrapKey("raw", hmac20, gcm, { name: "AES-GCM", iv: new Uint8Array(12) })); + +const wrapped = new Uint8Array(await crypto.subtle.wrapKey("raw", hmac32, kek, "AES-KW")); +const tampered = wrapped.slice(); +tampered[3] ^= 1; +const notJson = await crypto.subtle.wrapKey("raw", hmac32, kek, "AES-KW"); +await attempt("unwrap too few", () => crypto.subtle.unwrapKey("raw", wrapped, kek, "AES-KW", hs256, true)); +await attempt("unwrap data string", () => crypto.subtle.unwrapKey("raw", "x", kek, "AES-KW", hs256, true, ["sign"])); +await attempt("unwrap not a key", () => crypto.subtle.unwrapKey("raw", wrapped, JSON.parse("{}"), "AES-KW", hs256, true, ["sign"])); +await attempt("unwrap bad usage name", () => crypto.subtle.unwrapKey("raw", wrapped, kek, "AES-KW", hs256, true, ["fly"])); +await attempt("unwrap hmac without hash", () => crypto.subtle.unwrapKey("raw", wrapped, kek, "AES-KW", { name: "HMAC" }, true, ["sign"])); +await attempt("unwrap unknown key algorithm", () => crypto.subtle.unwrapKey("raw", wrapped, kek, "AES-KW", "TWOFISH", true, ["sign"])); +await attempt("unwrap mismatch", () => crypto.subtle.unwrapKey("raw", wrapped, gcm, "AES-KW", hs256, true, ["sign"])); +await attempt("unwrap missing usage", async () => crypto.subtle.unwrapKey("raw", wrapped, await crypto.subtle.importKey("raw", new Uint8Array(16).fill(1), "AES-KW", true, ["wrapKey"]), "AES-KW", hs256, true, ["sign"])); +await attempt("unwrap tampered", () => crypto.subtle.unwrapKey("raw", tampered, kek, "AES-KW", hs256, true, ["sign"])); +await attempt("unwrap 16 bytes", () => crypto.subtle.unwrapKey("raw", new Uint8Array(16), kek, "AES-KW", hs256, true, ["sign"])); +await attempt("unwrap 28 bytes", () => crypto.subtle.unwrapKey("raw", new Uint8Array(28), kek, "AES-KW", hs256, true, ["sign"])); +await attempt("unwrap bad key usage", () => crypto.subtle.unwrapKey("raw", wrapped, kek, "AES-KW", hs256, true, ["encrypt"])); +await attempt("unwrap empty usages", () => crypto.subtle.unwrapKey("raw", wrapped, kek, "AES-KW", hs256, true, [])); +await attempt("unwrap as pbkdf2", () => crypto.subtle.unwrapKey("raw", wrapped, kek, "AES-KW", "PBKDF2", false, ["deriveBits"])); +await attempt("unwrap as extractable pbkdf2", () => crypto.subtle.unwrapKey("raw", wrapped, kek, "AES-KW", "PBKDF2", true, ["deriveBits"])); +await attempt("unwrap raw bytes as jwk", () => crypto.subtle.unwrapKey("jwk", notJson, kek, "AES-KW", hs256, true, ["sign"])); +const jwkWrapped = await crypto.subtle.wrapKey("jwk", hmac32, kek, "AES-KW"); +await attempt("unwrap jwk with other usages", () => crypto.subtle.unwrapKey("jwk", jwkWrapped, kek, "AES-KW", hs256, true, ["sign", "verify"])); +await attempt("unwrap jwk as AES", () => crypto.subtle.unwrapKey("jwk", jwkWrapped, kek, "AES-KW", "AES-GCM", true, ["encrypt"])); +await attempt("unwrap jwk ok", () => crypto.subtle.unwrapKey("jwk", jwkWrapped, kek, "AES-KW", hs256, false, ["sign"])); diff --git a/tests/corpus/7132-jose-pbes2/main.mjs b/tests/corpus/7132-jose-pbes2/main.mjs new file mode 100644 index 000000000..1c0f1df13 --- /dev/null +++ b/tests/corpus/7132-jose-pbes2/main.mjs @@ -0,0 +1,50 @@ +// The real jose@6.2.12 PBES2 and AES key-wrap paths: EncryptJWT and +// jwtDecrypt from jose's public jose/jwt/encrypt, jose/jwt/decrypt and +// jose/errors entry points, over crypto.subtle importKey, deriveBits, +// wrapKey, unwrapKey, exportKey, encrypt and decrypt. node_modules/jose +// vendors the dist/webapi files those entry points load and their +// declarations, unmodified (MIT, LICENSE.md), with a package.json that +// exports only those entry points. Content keys and IVs are random, so +// tokens are checked by shape and by what they decrypt to. +import { EncryptJWT } from "jose/jwt/encrypt"; +import { jwtDecrypt } from "jose/jwt/decrypt"; +import * as errors from "jose/errors"; + +const encoder = new TextEncoder(); +const password = encoder.encode("correct horse battery staple"); +const kek = new Uint8Array(32); +for (let i = 0; i < kek.length; i++) kek[i] = i * 13 + 5; + +async function roundTrip(label, alg, enc, key, header = {}) { + const token = await new EncryptJWT({ sub: "alice", scope: "read", n: 1 }) + .setProtectedHeader({ alg, enc, ...header }) + .setIssuedAt(1_799_999_000) + .encrypt(key); + const parts = token.split("."); + const { payload, protectedHeader } = await jwtDecrypt(token, key, { + keyManagementAlgorithms: [alg], + currentDate: new Date(1_800_000_000_000), + }); + console.log(label, parts.length, parts[1].length > 0, payload.sub, payload.scope, payload.iat); + console.log(label, protectedHeader.alg, protectedHeader.enc, protectedHeader.p2c ?? "-"); + return token; +} + +const pbes = await roundTrip("pbes2 256", "PBES2-HS256+A128KW", "A256GCM", password); +await roundTrip("pbes2 384", "PBES2-HS384+A192KW", "A128CBC-HS256", password); +await roundTrip("pbes2 512", "PBES2-HS512+A256KW", "A256CBC-HS512", password); +await roundTrip("a128kw", "A128KW", "A128GCM", kek.subarray(0, 16)); +await roundTrip("a256kw", "A256KW", "A256CBC-HS512", kek); + +try { + await jwtDecrypt(pbes, encoder.encode("wrong password"), { keyManagementAlgorithms: ["PBES2-HS256+A128KW"] }); + console.log("wrong password accepted"); +} catch (e) { + console.log("wrong password", e instanceof errors.JWEDecryptionFailed, e.code); +} +try { + await jwtDecrypt(pbes, password); + console.log("unlisted algorithm accepted"); +} catch (e) { + console.log("unlisted algorithm", e instanceof errors.JOSEAlgNotAllowed, e.code); +} diff --git a/tests/corpus/7132-jose-pbes2/node_modules/jose/LICENSE.md b/tests/corpus/7132-jose-pbes2/node_modules/jose/LICENSE.md new file mode 100644 index 000000000..d0ec038a8 --- /dev/null +++ b/tests/corpus/7132-jose-pbes2/node_modules/jose/LICENSE.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2018 Filip Skokan + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/types/jwt/decrypt.d.ts b/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/types/jwt/decrypt.d.ts new file mode 100644 index 000000000..cdf6a372a --- /dev/null +++ b/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/types/jwt/decrypt.d.ts @@ -0,0 +1,34 @@ +import type * as t from '../types.d.ts'; +/** JWE decryption and JWT Claims Set validation options. */ +export interface JWTDecryptOptions extends t.DecryptOptions, t.JWTClaimVerificationOptions { +} +/** + * Resolves a JWT decryption key. No token components have been authenticated when this function is + * called. + */ +export interface JWTDecryptGetKey extends t.GetKeyFunction { +} +/** + * Decrypts a Compact JWE-formatted JWT and validates its Claims Set. + * + * @param key Private key or shared secret to decrypt and verify the JWT with. See + * {@link https://github.com/panva/jose/issues/210#jwe-alg Algorithm Key Requirements}. + */ +export declare function jwtDecrypt(jwt: string | Uint8Array, key: t.KeyInput, options?: JWTDecryptOptions): Promise>; +/** + * Decrypts a JWT and validates its claims, returning the dynamically + * {@link t.ResolvedKey.key resolved key}. + * + * @param getKey Function resolving a private key or shared secret to decrypt and verify the JWT + * with. See {@link https://github.com/panva/jose/issues/210#jwe-alg Algorithm Key Requirements}. + */ +export declare function jwtDecrypt(jwt: string | Uint8Array, getKey: JWTDecryptGetKey, options?: JWTDecryptOptions): Promise & t.ResolvedKey>; +/** + * Accepts a key or key resolver. Use this overload when forwarding either form; the result includes + * `key` only when a resolver was used. + * + * @param key Private key or shared secret, or a function resolving one, to decrypt and verify the + * JWT with. See + * {@link https://github.com/panva/jose/issues/210#jwe-alg Algorithm Key Requirements}. + */ +export declare function jwtDecrypt(jwt: string | Uint8Array, key: t.KeyInput | JWTDecryptGetKey, options?: JWTDecryptOptions): Promise & Partial>; diff --git a/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/types/jwt/encrypt.d.ts b/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/types/jwt/encrypt.d.ts new file mode 100644 index 000000000..c39e4c994 --- /dev/null +++ b/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/types/jwt/encrypt.d.ts @@ -0,0 +1,57 @@ +import type * as t from '../types.d.ts'; +/** @param payload Initial JWT Claims Set. Defaults to an empty object. */ +declare const EncryptJWT_base: new (payload?: t.JWTPayload) => t.ProduceJWT; +/** Builds and encrypts Compact JWE-formatted JSON Web Tokens. */ +export declare class EncryptJWT extends EncryptJWT_base { + #private; + /** + * Sets the JWE Protected Header. May only be called once. + * + * @param protectedHeader JWE Protected Header. Must contain an "alg" (JWE Algorithm) and "enc" + * (JWE Encryption Algorithm) properties. + */ + setProtectedHeader(protectedHeader: t.CompactJWEHeaderParameters): this; + /** + * Sets JWE Key Management parameters, such as ECDH-ES "apu" and "apv" or PBES2 "p2c", and adds + * them to the appropriate JOSE Header. Use this instead of the header setters for algorithm + * inputs. May only be called once. + */ + setKeyManagementParameters(parameters: t.JWEKeyManagementHeaderParameters): this; + /** + * Sets the content encryption key. By default, a suitable random key is generated for the JWE + * "enc" (Encryption Algorithm). May only be called once. + * + * @deprecated For testing and vector validation only; allow random generation in production. + */ + setContentEncryptionKey(cek: Uint8Array): this; + /** + * Sets the Initialization Vector for content encryption. By default, a suitable random IV is + * generated for the JWE "enc" (Encryption Algorithm). May only be called once. + * + * @deprecated For testing and vector validation only; allow random generation in production. + */ + setInitializationVector(iv: Uint8Array): this; + /** + * Replicates the "iss" (Issuer) Claim in the JWE Protected Header, exposing it without + * decryption. + */ + replicateIssuerAsHeader(): this; + /** + * Replicates the "sub" (Subject) Claim in the JWE Protected Header, exposing it without + * decryption. + */ + replicateSubjectAsHeader(): this; + /** + * Replicates the "aud" (Audience) Claim in the JWE Protected Header, exposing it without + * decryption. + */ + replicateAudienceAsHeader(): this; + /** + * Encrypts and returns the JWT. + * + * @param key Public key or shared secret to encrypt the JWT with. See + * {@link https://github.com/panva/jose/issues/210#jwe-alg Algorithm Key Requirements}. + */ + encrypt(key: t.KeyInput, options?: t.EncryptOptions): Promise; +} +export {}; diff --git a/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/types/types.d.ts b/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/types/types.d.ts new file mode 100644 index 000000000..9815c018d --- /dev/null +++ b/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/types/types.d.ts @@ -0,0 +1,736 @@ +/** Supported JWS "alg" (Algorithm) Header Parameter values. Availability depends on the runtime. */ +export type JWSAlgorithm = + | 'HS256' + | 'HS384' + | 'HS512' + | 'RS256' + | 'RS384' + | 'RS512' + | 'PS256' + | 'PS384' + | 'PS512' + | 'ES256' + | 'ES384' + | 'ES512' + | 'EdDSA' + | 'Ed25519' + | 'ML-DSA-44' + | 'ML-DSA-65' + | 'ML-DSA-87' + | (string & {}) + +/** Supported JWE "alg" (Algorithm) Header Parameter values. Availability depends on the runtime. */ +export type JWEKeyManagementAlgorithm = + | 'dir' + | 'A128KW' + | 'A192KW' + | 'A256KW' + | 'A128GCMKW' + | 'A192GCMKW' + | 'A256GCMKW' + | 'ECDH-ES' + | 'ECDH-ES+A128KW' + | 'ECDH-ES+A192KW' + | 'ECDH-ES+A256KW' + | 'RSA-OAEP' + | 'RSA-OAEP-256' + | 'RSA-OAEP-384' + | 'RSA-OAEP-512' + | 'PBES2-HS256+A128KW' + | 'PBES2-HS384+A192KW' + | 'PBES2-HS512+A256KW' + | (string & {}) + +/** + * Supported JWE "enc" (Encryption Algorithm) Header Parameter values. Availability depends on the + * runtime. + */ +export type JWEContentEncryptionAlgorithm = + | 'A128CBC-HS256' + | 'A192CBC-HS384' + | 'A256CBC-HS512' + | 'A128GCM' + | 'A192GCM' + | 'A256GCM' + | (string & {}) + +/** JWK "kty" (Key Type) Parameter values supported by this module. */ +export type JWKKeyType = 'EC' | 'RSA' | 'OKP' | 'AKP' | 'oct' | (string & {}) + +/** + * Generic JSON Web Key Parameters. + * + * > Note: This is declared as a type alias rather than an interface so that it satisfies the implicit index + * > signature of the `JsonWebKey` types shipped by `@types/node` and `lib.dom`. + */ +export type JWKParameters = { + /** JWK "kty" (Key Type) Parameter */ + kty?: JWKKeyType + /** JWK "alg" (Algorithm) Parameter */ + alg?: JWSAlgorithm | JWEKeyManagementAlgorithm | JWEContentEncryptionAlgorithm + /** Permitted key operations. */ + key_ops?: string[] + /** Whether the key may be exported. */ + ext?: boolean + + /** JWK "use" (Public Key Use) Parameter */ + use?: 'sig' | 'enc' | (string & {}) + /** X.509 certificate chain. */ + x5c?: string[] + /** X.509 certificate SHA-1 thumbprint. */ + x5t?: string + /** X.509 certificate SHA-256 thumbprint. */ + 'x5t#S256'?: string + /** X.509 certificate URL. */ + x5u?: string + + /** JWK "kid" (Key ID) Parameter */ + kid?: string +} + +/** Convenience interface for public OKP JSON Web Keys. */ +export interface JWK_OKP_Public extends JWKParameters { + /** Key pair subtype. */ + crv: string + /** Public key. */ + x: string +} + +/** Convenience interface for private OKP JSON Web Keys. */ +export interface JWK_OKP_Private extends JWK_OKP_Public { + /** Private key. */ + d: string +} + +/** Convenience interface for public AKP JSON Web Keys. */ +export interface JWK_AKP_Public extends JWKParameters { + /** JWK "alg" (Algorithm) Parameter */ + alg: string + + /** AKP JWK "pub" (The Public key) Parameter */ + pub: string +} + +/** Convenience interface for private AKP JSON Web Keys. */ +export interface JWK_AKP_Private extends JWK_AKP_Public { + /** AKP JWK "priv" (The Private Key) Parameter */ + priv: string +} + +/** Convenience interface for public EC JSON Web Keys. */ +export interface JWK_EC_Public extends JWKParameters { + /** Curve. */ + crv: string + /** Public key X coordinate. */ + x: string + /** Public key Y coordinate. */ + y: string +} + +/** Convenience interface for private EC JSON Web Keys. */ +export interface JWK_EC_Private extends JWK_EC_Public { + /** Private key. */ + d: string +} + +/** Convenience interface for public RSA JSON Web Keys. */ +export interface JWK_RSA_Public extends JWKParameters { + /** Public exponent. */ + e: string + /** Modulus. */ + n: string +} + +/** Convenience interface for private RSA JSON Web Keys. */ +export interface JWK_RSA_Private extends JWK_RSA_Public { + /** Private exponent. */ + d: string + /** First factor CRT exponent. */ + dp: string + /** Second factor CRT exponent. */ + dq: string + /** First prime factor. */ + p: string + /** Second prime factor. */ + q: string + /** First CRT coefficient. */ + qi: string +} + +/** Convenience interface for "oct" JSON Web Keys. */ +export interface JWK_oct extends JWKParameters { + /** Symmetric key value. */ + k: string +} + +/** + * JSON Web Key ({@link https://www.rfc-editor.org/info/rfc7517/ JWK}). "RSA", "EC", "OKP", "AKP", + * and "oct" key types are supported. + * + * > Note: This is declared as a type alias rather than an interface so that it satisfies the implicit index + * > signature of the `JsonWebKey` types shipped by `@types/node` and `lib.dom`. It spells out the + * > {@link JWKParameters} members rather than intersecting them so that every JWK member is documented + * > in one place. + */ +export type JWK = { + /** JWK "kty" (Key Type) Parameter */ + kty?: JWKKeyType + /** JWK "alg" (Algorithm) Parameter */ + alg?: JWSAlgorithm | JWEKeyManagementAlgorithm | JWEContentEncryptionAlgorithm + /** Permitted key operations. */ + key_ops?: string[] + /** Whether the key may be exported. */ + ext?: boolean + + /** JWK "use" (Public Key Use) Parameter */ + use?: 'sig' | 'enc' | (string & {}) + /** X.509 certificate chain. */ + x5c?: string[] + /** X.509 certificate SHA-1 thumbprint. */ + x5t?: string + /** X.509 certificate SHA-256 thumbprint. */ + 'x5t#S256'?: string + /** X.509 certificate URL. */ + x5u?: string + + /** JWK "kid" (Key ID) Parameter */ + kid?: string + /** EC curve or OKP key pair subtype. */ + crv?: string + /** Private RSA exponent, EC key, or OKP key. */ + d?: string + /** RSA first factor CRT exponent. */ + dp?: string + /** RSA second factor CRT exponent. */ + dq?: string + /** RSA public exponent. */ + e?: string + /** Symmetric key value. */ + k?: string + /** RSA modulus. */ + n?: string + /** RSA first prime factor. */ + p?: string + /** RSA second prime factor. */ + q?: string + /** RSA first CRT coefficient. */ + qi?: string + /** EC public key X coordinate or OKP public key. */ + x?: string + /** EC public key Y coordinate. */ + y?: string + + /** AKP JWK "pub" (Public Key) Parameter */ + pub?: string + + /** AKP JWK "priv" (Private key) Parameter */ + priv?: string + /** Additional RSA prime factors. */ + oth?: Array<{ + /** Factor CRT exponent. */ + d?: string + /** Prime factor. */ + r?: string + /** Factor CRT coefficient. */ + t?: string + }> +} + +/** Discriminated union of supported JSON Web Key shapes, narrowed by the "kty" (Key Type) Parameter. */ +// Intersect "kty" into each member separately: typedoc omits the parentheses in `X & (A | B)`. +export type AnyJWK = + | (JWK_EC_Private & { kty: 'EC' }) + | (JWK_EC_Public & { kty: 'EC' }) + | (JWK_RSA_Private & { kty: 'RSA' }) + | (JWK_RSA_Public & { kty: 'RSA' }) + | (JWK_OKP_Private & { kty: 'OKP' }) + | (JWK_OKP_Public & { kty: 'OKP' }) + | (JWK_AKP_Private & { kty: 'AKP' }) + | (JWK_AKP_Public & { kty: 'AKP' }) + | (JWK_oct & { kty: 'oct' }) + +/** Key or secret input accepted by all sign, verify, encrypt, and decrypt operations. */ +export type KeyInput = CryptoKey | KeyObject | JWK | Uint8Array + +/** Dynamic key resolver for consuming operations. */ +export interface GetKeyFunction { + /** + * Resolves a key for an unverified token. Throw if no suitable key can be resolved. + * + * @param token The consumed JWE or JWS token; none of its components have been verified. + */ + (protectedHeader: IProtectedHeader, token: IToken): Promise | KeyTypes +} + +/** Flattened JWS verification input. */ +export interface FlattenedJWSInput { + /** JWS Unprotected Header as a JSON object. Not integrity protected; omit when empty. */ + header?: JWSHeaderParameters + + /** Base64url-encoded payload; with `b64: false`, supply an unencoded string or Uint8Array. */ + payload: string | Uint8Array + + /** Base64url-encoded UTF-8 JWS Protected Header. Integrity protected; omit when empty. */ + protected?: string + + /** Base64url-encoded signature or MAC. */ + signature: string +} + +/** General JWS verification input. */ +export interface GeneralJWSInput { + /** Base64url-encoded payload; with `b64: false`, supply an unencoded string or Uint8Array. */ + payload: string | Uint8Array + + /** + * The "signatures" member value MUST be an array of JSON objects. Each object represents a + * signature or MAC over the JWS Payload and the JWS Protected Header. + */ + signatures: Omit[] +} + +/** + * Flattened JWS JSON Serialization token. The payload is an empty string when the + * {@link https://www.rfc-editor.org/info/rfc7797/ unencoded payload option} is used. + */ +export interface FlattenedJWS extends Partial { + payload: string + signature: string +} + +/** + * General JWS JSON Serialization token. The payload is an empty string when the + * {@link https://www.rfc-editor.org/info/rfc7797/ unencoded payload option} is used. + */ +export interface GeneralJWS { + payload: string + signatures: Omit[] +} + +/** Header Parameters common to JWE and JWS. */ +export interface JoseHeaderParameters { + /** "kid" (Key ID) Header Parameter */ + kid?: string + + /** X.509 certificate SHA-1 thumbprint. */ + x5t?: string + + /** X.509 certificate chain. */ + x5c?: string[] + + /** X.509 certificate URL. */ + x5u?: string + + /** JWK Set URL. */ + jku?: string + + /** Public JWK only; private and symmetric key parameters are not permitted. */ + jwk?: Omit + + /** "typ" (Type) Header Parameter */ + typ?: string + + /** Content type. */ + cty?: string +} + +/** Recognized JWS Header Parameters; additional members may also be present. */ +export interface JWSHeaderParameters extends JoseHeaderParameters { + /** JWS "alg" (Algorithm) Header Parameter */ + alg?: JWSAlgorithm + + /** + * Controls payload encoding and the JWS signing input as defined by + * {@link https://www.rfc-editor.org/info/rfc7797/ RFC7797}. Set to `false` and list `b64` in + * `crit` to use an unencoded payload. + */ + b64?: boolean + + /** Extension parameters that must be recognized. */ + crit?: string[] + + /** Any other JWS Header member. */ + [propName: string]: unknown +} + +/** Recognized JWE Key Management-related Header Parameters. */ +export interface JWEKeyManagementHeaderParameters { + /** ECDH-ES Agreement PartyUInfo bytes, used in ConcatKDF and added to the JOSE header. */ + apu?: Uint8Array + + /** ECDH-ES Agreement PartyVInfo bytes, used in ConcatKDF and added to the JOSE header. */ + apv?: Uint8Array + + /** PBES2 PBKDF2 iteration count, added to the JOSE header. */ + p2c?: number + + /** @deprecated For testing and vector validation only. */ + p2s?: Uint8Array + /** @deprecated For testing and vector validation only. */ + iv?: Uint8Array + /** @deprecated For testing and vector validation only. */ + epk?: CryptoKey | KeyObject +} + +/** Flattened JWE JSON Serialization token. */ +export interface FlattenedJWE { + /** + * Base64url-encoded additional authenticated data; integrity protected but not encrypted. Omit + * when empty. + */ + aad?: string + + /** Base64url-encoded ciphertext. */ + ciphertext: string + + /** Base64url-encoded encrypted key. Omit when empty. */ + encrypted_key?: string + + /** + * JWE Per-Recipient Unprotected Header as a JSON object. Not integrity protected; omit when + * empty. + */ + header?: JWEHeaderParameters + + /** Base64url-encoded initialization vector. Omit when empty. */ + iv?: string + + /** Base64url-encoded UTF-8 JWE Protected Header. Integrity protected; omit when empty. */ + protected?: string + + /** Base64url-encoded authentication tag. Omit when empty. */ + tag?: string + + /** JWE Shared Unprotected Header as a JSON object. Not integrity protected; omit when empty. */ + unprotected?: JWEHeaderParameters +} + +/** General JWE JSON Serialization token. */ +export interface GeneralJWE extends Omit { + recipients: Pick[] +} + +/** Recognized JWE Header Parameters; additional members may also be present. */ +export interface JWEHeaderParameters extends JoseHeaderParameters { + /** JWE "alg" (Algorithm) Header Parameter */ + alg?: JWEKeyManagementAlgorithm + + /** JWE "enc" (Encryption Algorithm) Header Parameter */ + enc?: JWEContentEncryptionAlgorithm + + /** Extension parameters that must be recognized. */ + crit?: string[] + + /** + * JWE compression algorithm. Only `"DEF"` (DEFLATE) is supported, requiring the runtime's + * `CompressionStream` / `DecompressionStream` APIs. + */ + zip?: 'DEF' | (string & {}) + + /** Any other JWE Header member. */ + [propName: string]: unknown +} + +/** Shared "crit" option for signing, verification, encryption, and decryption. */ +export interface CritOption { + /** + * Recognized "crit" (Critical) Header Parameter names. Set each value to `true` to require + * integrity protection, or `false` when protection is optional. The JWS `b64` extension is always + * recognized and processed. + * + * > Warning: Other extensions are only checked for syntax and optional integrity protection. Their presence + * > is not required by this option. You must check their presence and process them according to the + * > profile's validation steps after the operation succeeds. + */ + crit?: { + [propName: string]: boolean + } +} + +/** JWE Decryption options. */ +export interface DecryptOptions extends CritOption { + /** + * Accepted JWE "alg" (Algorithm) Header Parameter values. Defaults to all algorithms applicable + * to the key or secret except PBES2, which must be explicitly allowed. + */ + keyManagementAlgorithms?: JWEKeyManagementAlgorithm[] + + /** + * Accepted JWE "enc" (Encryption Algorithm) Header Parameter values. Defaults to all algorithms + * applicable to the key or secret. + */ + contentEncryptionAlgorithms?: JWEContentEncryptionAlgorithm[] + + /** + * Maximum "p2c" (PBES2 Count) Header Parameter value, limiting PBKDF2 iterations and their + * computational expense. Defaults to 10000; must be a positive safe integer or `Infinity` to + * disable the limit. + */ + maxPBES2Count?: number + + /** + * Maximum decompressed plaintext size in bytes. Defaults to 250000; `0` rejects compressed JWEs, + * and `Infinity` disables the limit. Other values must be positive safe integers. + */ + maxDecompressedLength?: number +} + +/** JWE Encryption options. */ +export interface EncryptOptions extends CritOption {} + +/** JWT Claims Set verification options. */ +export interface JWTClaimVerificationOptions { + /** Expected JWT "aud" (Audience) Claim value(s). Requires the claim to be present. */ + audience?: string | string[] + + /** + * Clock skew tolerance in seconds or a duration string (e.g. "5 seconds"). Applies to the "nbf" + * (Not Before) and "exp" (Expiration Time) claims, and to "iat" (Issued At) when + * {@link maxTokenAge} is set. + */ + clockTolerance?: string | number + + /** Expected JWT "iss" (Issuer) Claim value(s). Requires the claim to be present. */ + issuer?: string | string[] + + /** + * Maximum time since the JWT "iat" (Issued At) Claim, in seconds or a duration string (e.g. "2 + * hours"). Requires the claim to be present. + */ + maxTokenAge?: string | number + + /** Expected JWT "sub" (Subject) Claim value. Requires the claim to be present. */ + subject?: string + + /** Expected JWT "typ" (Type) Header Parameter value. Requires the parameter to be present. */ + typ?: string + + /** Date for NumericDate comparisons. Defaults to `new Date()`. */ + currentDate?: Date + + /** + * Additional claim names required in the JWT Claims Set. The {@link issuer}, {@link audience}, + * {@link subject}, and {@link maxTokenAge} options independently require "iss", "aud", "sub", and + * "iat", respectively. + */ + requiredClaims?: string[] +} + +/** JWS Verification options. */ +export interface VerifyOptions extends CritOption { + /** + * Accepted JWS "alg" (Algorithm) Header Parameter values. Defaults to all algorithms applicable + * to the key or secret. Unsecured JWTs (`alg: "none"`) are never accepted. + */ + algorithms?: JWSAlgorithm[] +} + +/** JWS Signing options. */ +export interface SignOptions extends CritOption {} + +/** Recognized JWT Claims Set members; additional members may also be present. */ +export interface JWTPayload { + /** JWT Issuer */ + iss?: string + + /** JWT Subject */ + sub?: string + + /** JWT Audience */ + aud?: string | string[] + + /** JWT ID */ + jti?: string + + /** Not valid before this Unix timestamp in seconds. */ + nbf?: number + + /** Expiration Unix timestamp in seconds. */ + exp?: number + + /** Issued-at Unix timestamp in seconds. */ + iat?: number + + /** Any other JWT Claim Set member. */ + [propName: string]: unknown +} + +/** Flattened JWE JSON Serialization decryption result. */ +export interface FlattenedDecryptResult { + /** JWE Additional Authenticated Data, integrity protected but not encrypted. */ + additionalAuthenticatedData?: Uint8Array + + /** Plaintext. */ + plaintext: Uint8Array + + /** JWE Protected Header. */ + protectedHeader?: JWEHeaderParameters + + /** JWE Shared Unprotected Header. Not integrity protected. */ + sharedUnprotectedHeader?: JWEHeaderParameters + + /** JWE Per-Recipient Unprotected Header. Not integrity protected. */ + unprotectedHeader?: JWEHeaderParameters +} + +/** General JWE JSON Serialization decryption result. */ +export interface GeneralDecryptResult extends FlattenedDecryptResult {} + +/** Compact JWE decryption result. */ +export interface CompactDecryptResult { + /** Plaintext. */ + plaintext: Uint8Array + + /** JWE Protected Header. */ + protectedHeader: CompactJWEHeaderParameters +} + +/** Flattened JWS JSON Serialization verification result. */ +export interface FlattenedVerifyResult { + /** JWS Payload. */ + payload: Uint8Array + + /** JWS Protected Header. */ + protectedHeader?: JWSHeaderParameters + + /** JWS Unprotected Header. Not integrity protected. */ + unprotectedHeader?: JWSHeaderParameters +} + +/** General JWS JSON Serialization verification result. */ +export interface GeneralVerifyResult extends FlattenedVerifyResult {} + +/** Compact JWS verification result. */ +export interface CompactVerifyResult { + /** JWS Payload. */ + payload: Uint8Array + + /** JWS Protected Header. */ + protectedHeader: CompactJWSHeaderParameters +} + +/** Signed JSON Web Token (JWT) verification result. */ +export interface JWTVerifyResult { + /** JWT Claims Set. */ + payload: PayloadType & + JWTPayload & + ([PayloadType] extends [object] ? unknown : unknown extends PayloadType ? unknown : never) + + /** JWS Protected Header. */ + protectedHeader: JWTHeaderParameters +} + +/** Encrypted JSON Web Token (JWT) decryption result. */ +export interface JWTDecryptResult { + /** JWT Claims Set. */ + payload: PayloadType & + JWTPayload & + ([PayloadType] extends [object] ? unknown : unknown extends PayloadType ? unknown : never) + + /** JWE Protected Header. */ + protectedHeader: CompactJWEHeaderParameters +} + +/** Key resolver result metadata. */ +export interface ResolvedKey { + /** The resolved key, present only when a key resolver is used. */ + key: KeyType +} + +/** Recognized Compact JWS Header Parameters; additional members may also be present. */ +export interface CompactJWSHeaderParameters extends JWSHeaderParameters { + alg: JWSAlgorithm +} + +/** Recognized signed JWT Header Parameters; additional members may also be present. */ +export interface JWTHeaderParameters extends CompactJWSHeaderParameters { + b64?: boolean +} + +/** Recognized Compact JWE Header Parameters; additional members may also be present. */ +export interface CompactJWEHeaderParameters extends JWEHeaderParameters { + alg: JWEKeyManagementAlgorithm + enc: JWEContentEncryptionAlgorithm +} + +/** JSON Web Key Set. */ +export interface JSONWebKeySet { + keys: JWK[] +} + +/** Node.js {@link !KeyObject} representation accepted as key input. */ +export interface KeyObject { + type: string +} + +/** The runtime's Web Crypto {@link !CryptoKey} representation accepted as key input. */ +export type CryptoKey = typeof globalThis extends { + crypto: { subtle: { generateKey(...args: any[]): Promise } } +} + ? Extract + : CryptoKeyStructuralFallback + +/** Structural fallback used when the host {@link !CryptoKey} type cannot be inferred. */ +export interface CryptoKeyStructuralFallback { + readonly algorithm: { name: string } + readonly extractable: boolean + readonly type: string + readonly usages: string[] +} + +/** Shared fluent API for JWT-producing classes. */ +export interface ProduceJWT { + /** + * Set the "iss" (Issuer) Claim. + * + * @param issuer "Issuer" Claim value to set on the JWT Claims Set. + */ + setIssuer(issuer: string): this + + /** + * Set the "sub" (Subject) Claim. + * + * @param subject "sub" (Subject) Claim value to set on the JWT Claims Set. + */ + setSubject(subject: string): this + + /** + * Set the "aud" (Audience) Claim. + * + * @param audience "aud" (Audience) Claim value to set on the JWT Claims Set. + */ + setAudience(audience: string | string[]): this + + /** + * Set the "jti" (JWT ID) Claim. + * + * @param jwtId "jti" (JWT ID) Claim value to set on the JWT Claims Set. + */ + setJti(jwtId: string): this + + /** + * Set the "nbf" (Not Before) Claim. Numbers are Unix timestamps in seconds; Dates are converted + * to seconds. Strings are relative to now, using seconds, minutes, hours, days, weeks, or years + * (365.25 days; no months). Prefix `-` or suffix `ago` subtracts the duration. + * + * @param input "nbf" (Not Before) Claim value as a timestamp, Date, or relative duration. + */ + setNotBefore(input: number | string | Date): this + + /** + * Set the "exp" (Expiration Time) Claim. Accepts a Unix timestamp in seconds, a Date, or a + * duration relative to now using the same formats as {@link setNotBefore}. + * + * @param input "exp" (Expiration Time) Claim value as a timestamp, Date, or relative duration. + */ + setExpirationTime(input: number | string | Date): this + + /** + * Set the "iat" (Issued At) Claim. Defaults to the current Unix timestamp in seconds. Accepts a + * Unix timestamp in seconds, a Date, or a duration relative to now using the same formats as + * {@link setNotBefore}. + * + * @param input "iat" (Issued At) Claim value as a timestamp, Date, or relative duration. + */ + setIssuedAt(input?: number | string | Date): this +} diff --git a/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/types/util/errors.d.ts b/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/types/util/errors.d.ts new file mode 100644 index 000000000..930a29b9f --- /dev/null +++ b/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/types/util/errors.d.ts @@ -0,0 +1,182 @@ +import type * as t from '../types.d.ts'; +/** Stable error codes used by this module. */ +export type JOSEErrorCode = 'ERR_JOSE_ALG_NOT_ALLOWED' | 'ERR_JOSE_GENERIC' | 'ERR_JOSE_NOT_SUPPORTED' | 'ERR_JWE_DECRYPTION_FAILED' | 'ERR_JWE_INVALID' | 'ERR_JWK_INVALID' | 'ERR_JWKS_INVALID' | 'ERR_JWKS_MULTIPLE_MATCHING_KEYS' | 'ERR_JWKS_NO_MATCHING_KEY' | 'ERR_JWKS_TIMEOUT' | 'ERR_JWS_INVALID' | 'ERR_JWS_SIGNATURE_VERIFICATION_FAILED' | 'ERR_JWT_CLAIM_VALIDATION_FAILED' | 'ERR_JWT_EXPIRED' | 'ERR_JWT_INVALID'; +/** Shared details of JWT claim or header validation failures. */ +export interface JWTClaimValidationFailure { + /** Claim or header that failed validation. */ + claim: string; + /** Reason code for the validation failure. */ + reason: JWTClaimValidationReason; + /** The parsed JWT Claims Set; validation of other claims may be incomplete. */ + payload: t.JWTPayload; +} +/** Reason codes produced by JWT Claims Set validation. */ +export type JWTClaimValidationReason = 'check_failed' | 'invalid' | 'mismatch' | 'missing' | 'unspecified' | (string & {}); +/** Base class for JOSE errors. */ +export declare class JOSEError extends Error { + /** Stable code identifying the error class. */ + static code: JOSEErrorCode | (string & {}); + /** Stable code identifying the error class. Use {@link AnyJOSEError} to narrow subclasses by code. */ + code: JOSEErrorCode | (string & {}); + constructor(message?: string, options?: { + cause?: unknown; + }); +} +/** + * Thrown when JWT claim or header validation fails. Expiration is reported separately as + * {@link JWTExpired}. + */ +export declare class JWTClaimValidationFailed extends JOSEError implements JWTClaimValidationFailure { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); + /** The {@link JWTClaimValidationFailure} details carried by this error. */ + cause: JWTClaimValidationFailure; + /** Claim or header that failed validation. */ + claim: string; + /** Reason code for the validation failure. */ + reason: JWTClaimValidationReason; + /** + * The parsed JWT Claims Set; validation of other claims may be incomplete. With + * {@link jwt/verify.jwtVerify jwtVerify} and {@link jwt/decrypt.jwtDecrypt jwtDecrypt}, token + * authentication precedes claim validation. {@link jwt/unsecured.UnsecuredJWT.decode} does not + * authenticate tokens. + */ + payload: t.JWTPayload; + constructor(message: string, payload: t.JWTPayload, claim?: string, reason?: JWTClaimValidationReason); +} +/** Thrown when a JWT has expired or exceeds the configured maximum token age. */ +export declare class JWTExpired extends JOSEError implements JWTClaimValidationFailure { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); + /** The {@link JWTClaimValidationFailure} details carried by this error. */ + cause: JWTClaimValidationFailure; + /** Claim or header that failed validation. */ + claim: string; + /** Reason code for the validation failure. */ + reason: JWTClaimValidationReason; + /** + * The parsed JWT Claims Set; validation of other claims may be incomplete. With + * {@link jwt/verify.jwtVerify jwtVerify} and {@link jwt/decrypt.jwtDecrypt jwtDecrypt}, token + * authentication precedes claim validation. {@link jwt/unsecured.UnsecuredJWT.decode} does not + * authenticate tokens. + */ + payload: t.JWTPayload; + constructor(message: string, payload: t.JWTPayload, claim?: string, reason?: JWTClaimValidationReason); +} +/** Thrown when an algorithm is disallowed by configuration. */ +export declare class JOSEAlgNotAllowed extends JOSEError { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); +} +/** Thrown when a feature or algorithm is unsupported. */ +export declare class JOSENotSupported extends JOSEError { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); +} +/** Thrown when JWE ciphertext decryption or authentication fails. */ +export declare class JWEDecryptionFailed extends JOSEError { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); + constructor(message?: string, options?: { + cause?: unknown; + }); +} +/** Thrown when a JWE is invalid. */ +export declare class JWEInvalid extends JOSEError { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); +} +/** Thrown when a JWS is invalid. */ +export declare class JWSInvalid extends JOSEError { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); +} +/** Thrown when a JWT is invalid. */ +export declare class JWTInvalid extends JOSEError { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); +} +/** Thrown when a JWK is invalid. */ +export declare class JWKInvalid extends JOSEError { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); +} +/** Thrown when a JWKS is invalid. */ +export declare class JWKSInvalid extends JOSEError { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); +} +/** Thrown when no keys match in a JWKS. */ +export declare class JWKSNoMatchingKey extends JOSEError { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); + constructor(message?: string, options?: { + cause?: unknown; + }); +} +/** Thrown when multiple keys match in a JWKS. */ +export declare class JWKSMultipleMatchingKeys extends JOSEError { + /** + * Yields public keys matching the JWS Header so verification can be attempted with each. See the + * {@link jwks/remote.createRemoteJWKSet createRemoteJWKSet} and + * {@link jwks/local.createLocalJWKSet createLocalJWKSet} examples. Empty on manually constructed + * errors. + */ + [Symbol.asyncIterator]: () => AsyncIterableIterator; + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); + constructor(message?: string, options?: { + cause?: unknown; + }); +} +/** Thrown when fetching a remote JWKS times out. */ +export declare class JWKSTimeout extends JOSEError { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); + constructor(message?: string, options?: { + cause?: unknown; + }); +} +/** Thrown when JWS signature verification fails. */ +export declare class JWSSignatureVerificationFailed extends JOSEError { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); + constructor(message?: string, options?: { + cause?: unknown; + }); +} +/** Errors thrown during JWT Claims Set validation. */ +export type JWTClaimValidationError = JWTClaimValidationFailed | JWTExpired; +/** + * Discriminated union of specific {@link JOSEError} subclasses, narrowed by `code`. Excludes base + * {@link JOSEError} instances; use `instanceof JOSEError` to catch all module errors. + */ +export type AnyJOSEError = (JOSEAlgNotAllowed & { + code: 'ERR_JOSE_ALG_NOT_ALLOWED'; +}) | (JOSENotSupported & { + code: 'ERR_JOSE_NOT_SUPPORTED'; +}) | (JWEDecryptionFailed & { + code: 'ERR_JWE_DECRYPTION_FAILED'; +}) | (JWEInvalid & { + code: 'ERR_JWE_INVALID'; +}) | (JWKInvalid & { + code: 'ERR_JWK_INVALID'; +}) | (JWKSInvalid & { + code: 'ERR_JWKS_INVALID'; +}) | (JWKSMultipleMatchingKeys & { + code: 'ERR_JWKS_MULTIPLE_MATCHING_KEYS'; +}) | (JWKSNoMatchingKey & { + code: 'ERR_JWKS_NO_MATCHING_KEY'; +}) | (JWKSTimeout & { + code: 'ERR_JWKS_TIMEOUT'; +}) | (JWSInvalid & { + code: 'ERR_JWS_INVALID'; +}) | (JWSSignatureVerificationFailed & { + code: 'ERR_JWS_SIGNATURE_VERIFICATION_FAILED'; +}) | (JWTClaimValidationFailed & { + code: 'ERR_JWT_CLAIM_VALIDATION_FAILED'; +}) | (JWTExpired & { + code: 'ERR_JWT_EXPIRED'; +}) | (JWTInvalid & { + code: 'ERR_JWT_INVALID'; +}); diff --git a/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/jwt/decrypt.js b/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/jwt/decrypt.js new file mode 100644 index 000000000..cfbe4730c --- /dev/null +++ b/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/jwt/decrypt.js @@ -0,0 +1,13 @@ +import { prepareDecrypt, decryptCompact } from "../lib/jwe_decrypt.js"; +import { validateClaimsSet } from "../lib/jwt_claims_set.js"; +import { JWTClaimValidationFailed } from "../util/errors.js"; +async function jwtDecrypt(jwt, key, options) { + const { plaintext, ...result } = await decryptCompact(jwt, prepareDecrypt(options), key), { protectedHeader } = result, payload = validateClaimsSet(protectedHeader, plaintext, options); + for (const claim of ["iss", "sub", "aud"]) + if (protectedHeader[claim] !== void 0 && (claim === "aud" ? JSON.stringify(protectedHeader.aud) !== JSON.stringify(payload.aud) : protectedHeader[claim] !== payload[claim])) + throw new JWTClaimValidationFailed(`replicated "${claim}" claim header parameter mismatch`, payload, claim, "mismatch"); + return { payload, ...result }; +} +export { + jwtDecrypt +}; diff --git a/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/jwt/encrypt.js b/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/jwt/encrypt.js new file mode 100644 index 000000000..c26908857 --- /dev/null +++ b/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/jwt/encrypt.js @@ -0,0 +1,45 @@ +import { compactJWE, createJWE } from "../lib/jwe_encrypt.js"; +import { JWTClaimsBuilder, jwtClaim, jwtData } from "../lib/jwt_claims_set.js"; +import { assertNotSet } from "../lib/validate.js"; +const EncryptJWT_base = JWTClaimsBuilder; +class EncryptJWT extends EncryptJWT_base { + #input = [void 0]; + #replicateIssuerAsHeader; + #replicateSubjectAsHeader; + #replicateAudienceAsHeader; + setProtectedHeader(protectedHeader) { + return assertNotSet(this.#input[1], "setProtectedHeader"), this.#input[1] = protectedHeader, this; + } + setKeyManagementParameters(parameters) { + return assertNotSet(this.#input[7], "setKeyManagementParameters"), this.#input[7] = parameters, this; + } + setContentEncryptionKey(cek) { + return assertNotSet(this.#input[5], "setContentEncryptionKey"), this.#input[5] = cek, this; + } + setInitializationVector(iv) { + return assertNotSet(this.#input[6], "setInitializationVector"), this.#input[6] = iv, this; + } + replicateIssuerAsHeader() { + return this.#replicateIssuerAsHeader = !0, this; + } + replicateSubjectAsHeader() { + return this.#replicateSubjectAsHeader = !0, this; + } + replicateAudienceAsHeader() { + return this.#replicateAudienceAsHeader = !0, this; + } + async encrypt(key, options) { + const plaintext = jwtData(this); + this.#input[1] && (this.#replicateIssuerAsHeader || this.#replicateSubjectAsHeader || this.#replicateAudienceAsHeader) && (this.#input[1] = { + ...this.#input[1], + iss: this.#replicateIssuerAsHeader ? jwtClaim(this, "iss") : void 0, + sub: this.#replicateSubjectAsHeader ? jwtClaim(this, "sub") : void 0, + aud: this.#replicateAudienceAsHeader ? jwtClaim(this, "aud") : void 0 + }); + const input = [...this.#input]; + return input[0] = plaintext, compactJWE(await createJWE(input, key, options)); + } +} +export { + EncryptJWT +}; diff --git a/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/lib/buffer_utils.js b/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/lib/buffer_utils.js new file mode 100644 index 000000000..b3cad85a9 --- /dev/null +++ b/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/lib/buffer_utils.js @@ -0,0 +1,75 @@ +const encoder = new TextEncoder(), decoder = new TextDecoder(), strictDecoder = new TextDecoder("utf-8", { fatal: !0 }), MAX_INT32 = 2 ** 32; +function concat(...buffers) { + const size = buffers.reduce((acc, { length }) => acc + length, 0), buf = new Uint8Array(size); + let i = 0; + for (const buffer of buffers) + buf.set(buffer, i), i += buffer.length; + return buf; +} +function writeUInt32BE(buf, value, offset) { + if (value < 0 || value >= MAX_INT32) + throw new RangeError(`value must be >= 0 and <= ${MAX_INT32 - 1}. Received ${value}`); + buf.set([value >>> 24, value >>> 16, value >>> 8, value & 255], offset); +} +function uint64be(value) { + const high = Math.floor(value / MAX_INT32), low = value % MAX_INT32, buf = new Uint8Array(8); + return writeUInt32BE(buf, high, 0), writeUInt32BE(buf, low, 4), buf; +} +function uint32be(value) { + const buf = new Uint8Array(4); + return writeUInt32BE(buf, value), buf; +} +const NON_ASCII = /[^\x00-\x7f]/; +function encode(string) { + if (typeof string == "string" && string.length >= 128) { + if (NON_ASCII.test(string)) + throw new TypeError("non-ASCII string encountered in encode()"); + return encoder.encode(string); + } + const bytes = new Uint8Array(string.length); + for (let i = 0; i < string.length; i++) { + const code = string.charCodeAt(i); + if (code > 127) + throw new TypeError("non-ASCII string encountered in encode()"); + bytes[i] = code; + } + return bytes; +} +function encodeBase64(input, url = !1) { + if (Uint8Array.prototype.toBase64) + return input.toBase64({ alphabet: url ? "base64url" : "base64", omitPadding: url }); + const CHUNK_SIZE = 32768, arr = []; + for (let i = 0; i < input.length; i += CHUNK_SIZE) + arr.push(String.fromCharCode.apply(null, input.subarray(i, i + CHUNK_SIZE))); + const encoded = btoa(arr.join("")); + return url ? encoded.replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_") : encoded; +} +function decodeBase64(encoded, url = !1) { + if (Uint8Array.fromBase64) + return Uint8Array.fromBase64(encoded, { alphabet: url ? "base64url" : "base64" }); + if (url) { + if (encoded.includes("+") || encoded.includes("/")) + throw new TypeError("Invalid base64url"); + encoded = encoded.replace(/-/g, "+").replace(/_/g, "/"); + } + const binary = atob(encoded), bytes = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i++) + bytes[i] = binary.charCodeAt(i); + return bytes; +} +async function digest(algorithm, data) { + const subtleDigest = `SHA-${algorithm.slice(-3)}`; + return new Uint8Array(await crypto.subtle.digest(subtleDigest, data)); +} +export { + concat, + decodeBase64, + decoder, + digest, + encode, + encodeBase64, + encoder, + strictDecoder, + uint32be, + uint64be +}; diff --git a/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/lib/content_encryption.js b/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/lib/content_encryption.js new file mode 100644 index 000000000..1ea355e91 --- /dev/null +++ b/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/lib/content_encryption.js @@ -0,0 +1,90 @@ +import { concat, uint64be } from "./buffer_utils.js"; +import { rawKey, invalidKeyInput, isCryptoKey } from "./key.js"; +import { JWEDecryptionFailed, JWEInvalid } from "../util/errors.js"; +const generateCek = (enc) => crypto.getRandomValues(new Uint8Array(enc.cekBits >> 3)); +function checkCekLength(cek, expected) { + const actual = cek.byteLength << 3; + if (actual !== expected) + throw new JWEInvalid(`Invalid Content Encryption Key length. Expected ${expected} bits, got ${actual} bits`); +} +const generateIv = (enc) => crypto.getRandomValues(new Uint8Array(enc.ivBits >> 3)); +function checkIvLength(enc, iv) { + if (iv.length << 3 !== enc.ivBits) + throw new JWEInvalid("Invalid Initialization Vector length"); +} +async function cbcKeySetup(enc, cek, usage) { + if (!(cek instanceof Uint8Array)) + throw new TypeError(invalidKeyInput(cek, "Uint8Array")); + const keySize = enc.cekBits >> 1, encKey = await crypto.subtle.importKey("raw", cek.subarray(keySize >> 3), "AES-CBC", !1, [usage]), macKey = await crypto.subtle.importKey("raw", cek.subarray(0, keySize >> 3), { + hash: `SHA-${keySize << 1}`, + name: "HMAC" + }, !1, ["sign"]); + return [encKey, macKey, keySize]; +} +async function cbcHmacTag(macKey, macData, keySize) { + return new Uint8Array((await crypto.subtle.sign("HMAC", macKey, macData)).slice(0, keySize >> 3)); +} +async function cbcEncrypt(enc, plaintext, cek, iv, aad) { + const [encKey, macKey, keySize] = await cbcKeySetup(enc, cek, "encrypt"), ciphertext = new Uint8Array(await crypto.subtle.encrypt({ + iv, + name: "AES-CBC" + }, encKey, plaintext)), macData = concat(aad, iv, ciphertext, uint64be(aad.length * 8)), tag = await cbcHmacTag(macKey, macData, keySize); + return { ciphertext, tag, iv }; +} +async function timingSafeEqual(a, b) { + const algorithm = { name: "HMAC", hash: "SHA-256" }, key = await crypto.subtle.generateKey(algorithm, !1, ["sign", "verify"]), aHmac = await crypto.subtle.sign(algorithm, key, a); + return crypto.subtle.verify(algorithm, key, aHmac, b); +} +async function cbcDecrypt(enc, cek, ciphertext, iv, tag, aad) { + const [encKey, macKey, keySize] = await cbcKeySetup(enc, cek, "decrypt"), macData = concat(aad, iv, ciphertext, uint64be(aad.length * 8)), expectedTag = await cbcHmacTag(macKey, macData, keySize); + try { + if (await timingSafeEqual(tag, expectedTag)) + return new Uint8Array(await crypto.subtle.decrypt({ iv, name: "AES-CBC" }, encKey, ciphertext)); + } catch { + } + throw new JWEDecryptionFailed(); +} +async function encrypt(enc, plaintext, cek, iv, aad) { + if (!isCryptoKey(cek) && !(cek instanceof Uint8Array)) + throw new TypeError(invalidKeyInput(cek, "CryptoKey", "KeyObject", "Uint8Array", "JSON Web Key")); + if (iv ? checkIvLength(enc, iv) : iv = generateIv(enc), cek instanceof Uint8Array && checkCekLength(cek, enc.cekBits), enc.cbc) + return cbcEncrypt(enc, plaintext, cek, iv, aad); + const encKey = await rawKey(cek, enc.subtle, "encrypt"), encrypted = new Uint8Array(await crypto.subtle.encrypt({ + additionalData: aad, + iv, + name: "AES-GCM", + tagLength: 128 + }, encKey, plaintext)); + return { ciphertext: encrypted.subarray(0, -16), tag: encrypted.subarray(-16), iv }; +} +async function decrypt(enc, cek, ciphertext, iv, tag, aad) { + if (!isCryptoKey(cek) && !(cek instanceof Uint8Array)) + throw new TypeError(invalidKeyInput(cek, "CryptoKey", "KeyObject", "Uint8Array", "JSON Web Key")); + if (!iv) + throw new JWEInvalid("JWE Initialization Vector missing"); + if (!tag) + throw new JWEInvalid("JWE Authentication Tag missing"); + if (!enc.cbc && tag.length !== 16) + throw new JWEInvalid("Invalid Authentication Tag length"); + if (checkIvLength(enc, iv), cek instanceof Uint8Array && checkCekLength(cek, enc.cekBits), enc.cbc) + return cbcDecrypt(enc, cek, ciphertext, iv, tag, aad); + const encKey = await rawKey(cek, enc.subtle, "decrypt"); + try { + return new Uint8Array(await crypto.subtle.decrypt({ + additionalData: aad, + iv, + name: "AES-GCM", + tagLength: 128 + }, encKey, concat(ciphertext, tag))); + } catch { + throw new JWEDecryptionFailed(); + } +} +export { + checkCekLength, + checkIvLength, + decrypt, + encrypt, + generateCek, + generateIv +}; diff --git a/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/lib/deflate.js b/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/lib/deflate.js new file mode 100644 index 000000000..27ffba278 --- /dev/null +++ b/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/lib/deflate.js @@ -0,0 +1,40 @@ +import { JOSENotSupported, JWEInvalid } from "../util/errors.js"; +import { concat } from "./buffer_utils.js"; +function validateZip(joseHeader, protectedHeader) { + if (joseHeader.zip !== void 0 && joseHeader.zip !== "DEF") + throw new JOSENotSupported('Unsupported JWE "zip" (Compression Algorithm) Header Parameter value.'); + if (joseHeader.zip !== void 0 && !protectedHeader?.zip) + throw new JWEInvalid('JWE "zip" (Compression Algorithm) Header Parameter MUST be in a protected header.'); +} +function supported(name) { + if (typeof globalThis[name] > "u") + throw new JOSENotSupported(`JWE "zip" (Compression Algorithm) Header Parameter requires the ${name} API.`); +} +async function transform(stream, input, maxLength = 1 / 0) { + const writer = stream.writable.getWriter(); + writer.write(input).catch(() => { + }), writer.close().catch(() => { + }); + const chunks = []; + let length = 0; + const reader = stream.readable.getReader(); + for (; ; ) { + const { value, done } = await reader.read(); + if (done) + break; + if (chunks.push(value), length += value.byteLength, maxLength !== 1 / 0 && length > maxLength) + throw new JWEInvalid("Decompressed plaintext exceeded the configured limit"); + } + return concat(...chunks); +} +async function compress(input) { + return supported("CompressionStream"), transform(new CompressionStream("deflate-raw"), input); +} +async function decompress(input, maxLength) { + return supported("DecompressionStream"), transform(new DecompressionStream("deflate-raw"), input, maxLength); +} +export { + compress, + decompress, + validateZip +}; diff --git a/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/lib/jwe_algorithms.js b/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/lib/jwe_algorithms.js new file mode 100644 index 000000000..a87af4eb5 --- /dev/null +++ b/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/lib/jwe_algorithms.js @@ -0,0 +1,116 @@ +import { JOSENotSupported } from "../util/errors.js"; +import { table } from "./key_descriptor.js"; +const wrap = [ + ["encrypt", "wrapKey"], + ["decrypt", "unwrapKey"] +], derive = [[], ["deriveBits"]], none = [[], []]; +function rsaes(bits) { + return { + kty: ["RSA"], + mode: "key-encryption", + subtle: { name: "RSA-OAEP", hash: `SHA-${bits}` }, + usages: wrap, + ops: ["wrapKey", "unwrapKey"] + }; +} +function ecdh(mode) { + return { + kty: ["EC", "OKP"], + mode, + subtle: { name: "ECDH" }, + resolve: ({ kty, crv, asymmetricKeyType }) => { + if (crv === "X25519" || asymmetricKeyType === "x25519") + return { name: "X25519" }; + if (kty === "OKP") + throw new JOSENotSupported('Invalid or unsupported JWK "alg" (Algorithm) Parameter value'); + return { name: "ECDH", namedCurve: crv }; + }, + usages: derive, + ops: [void 0, "deriveBits"] + }; +} +function aeskw(bits, gcm = !1) { + return { + kty: ["oct"], + mode: "key-wrapping", + secret: !0, + subtle: { name: gcm ? "AES-GCM" : "AES-KW", length: bits }, + usages: none, + ops: gcm ? ["encrypt", "decrypt"] : ["wrapKey", "unwrapKey"] + }; +} +function pbes2() { + return { + kty: ["oct"], + mode: "key-wrapping", + secret: !0, + subtle: { name: "PBKDF2" }, + usages: none, + ops: ["deriveBits", "deriveBits"] + }; +} +const JWE = table({ + dir: { + kty: ["oct"], + mode: "direct-encryption", + secret: !0, + subtle: { name: "AES-GCM" }, + usages: none, + ops: ["encrypt", "decrypt"] + }, + "RSA-OAEP": rsaes(1), + "RSA-OAEP-256": rsaes(256), + "RSA-OAEP-384": rsaes(384), + "RSA-OAEP-512": rsaes(512), + "ECDH-ES": ecdh("direct-key-agreement"), + "ECDH-ES+A128KW": ecdh("key-agreement-with-key-wrapping"), + "ECDH-ES+A192KW": ecdh("key-agreement-with-key-wrapping"), + "ECDH-ES+A256KW": ecdh("key-agreement-with-key-wrapping"), + A128KW: aeskw(128), + A192KW: aeskw(192), + A256KW: aeskw(256), + A128GCMKW: aeskw(128, !0), + A192GCMKW: aeskw(192, !0), + A256GCMKW: aeskw(256, !0), + "PBES2-HS256+A128KW": pbes2(), + "PBES2-HS384+A192KW": pbes2(), + "PBES2-HS512+A256KW": pbes2() +}), contentOps = ["encrypt", "decrypt"]; +function contentEncryption(bits, cbc = !1) { + return { + kty: ["oct"], + secret: !0, + subtle: { name: cbc ? "AES-CBC" : "AES-GCM", length: bits }, + usages: none, + ops: contentOps, + cekBits: bits, + ivBits: cbc ? 128 : 96, + cbc + }; +} +const ENC = table({ + A128GCM: contentEncryption(128), + A192GCM: contentEncryption(192), + A256GCM: contentEncryption(256), + "A128CBC-HS256": contentEncryption(256, !0), + "A192CBC-HS384": contentEncryption(384, !0), + "A256CBC-HS512": contentEncryption(512, !0) +}); +function unsupported(parameter, name) { + throw new JOSENotSupported(`Invalid or unsupported "${parameter}" (JWE ${name}) header value`); +} +function jweAlgorithm(alg) { + return (typeof alg == "string" ? JWE[alg] : void 0) ?? unsupported("alg", "Algorithm"); +} +function isJWECEKTransport(algorithm) { + return algorithm.mode === "key-wrapping" || algorithm.mode === "key-encryption" || algorithm.mode === "key-agreement-with-key-wrapping"; +} +function jweEncryption(enc) { + return (typeof enc == "string" ? ENC[enc] : void 0) ?? unsupported("enc", "Encryption Algorithm"); +} +export { + JWE, + isJWECEKTransport, + jweAlgorithm, + jweEncryption +}; diff --git a/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/lib/jwe_decrypt.js b/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/lib/jwe_decrypt.js new file mode 100644 index 000000000..e5453b9ba --- /dev/null +++ b/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/lib/jwe_decrypt.js @@ -0,0 +1,187 @@ +import { decrypt, generateCek } from "./content_encryption.js"; +import { decodeBase64url, encodeBase64url, parseJoseHeader, isDisjoint, isObject, validateCrit, validateAlgorithms, JWE_RECOGNIZED } from "./validate.js"; +import { JOSEAlgNotAllowed, JOSENotSupported, JWEInvalid } from "../util/errors.js"; +import { decryptKeyManagement } from "./key_management.js"; +import { decoder } from "./buffer_utils.js"; +import { prepareKey } from "./key.js"; +import { JWE, isJWECEKTransport, jweAlgorithm, jweEncryption } from "./jwe_algorithms.js"; +import { decompress, validateZip } from "./deflate.js"; +function snapshotSharedJWE(jwe) { + const { aad, ciphertext, iv, protected: encodedProtected, tag, unprotected } = jwe; + if (iv !== void 0 && (typeof iv != "string" || !iv)) + throw new JWEInvalid("JWE Initialization Vector incorrect type"); + if (typeof ciphertext != "string") + throw new JWEInvalid("JWE Ciphertext missing or incorrect type"); + if (tag !== void 0 && (typeof tag != "string" || !tag)) + throw new JWEInvalid("JWE Authentication Tag incorrect type"); + if (encodedProtected !== void 0 && typeof encodedProtected != "string") + throw new JWEInvalid("JWE Protected Header incorrect type"); + if (aad !== void 0 && (typeof aad != "string" || !aad)) + throw new JWEInvalid("JWE AAD incorrect type"); + if (unprotected !== void 0 && !isObject(unprotected)) + throw new JWEInvalid("JWE Shared Unprotected Header incorrect type"); + return { + aad, + ciphertext, + iv, + protected: encodedProtected, + tag, + unprotected: unprotected === void 0 ? void 0 : { ...unprotected } + }; +} +function snapshotRecipientJWE(recipient) { + let header, headerAlg; + try { + const { header: inputHeader } = recipient; + if (isObject(inputHeader)) { + headerAlg = inputHeader.alg; + const parameters = Object.keys(inputHeader); + parameters.includes("alg") || (headerAlg = void 0), header = Object.fromEntries(parameters.map((parameter) => [ + parameter, + parameter === "alg" ? headerAlg : inputHeader[parameter] + ])); + } else + header = inputHeader; + const { encrypted_key: encryptedKey } = recipient; + return [{ encrypted_key: encryptedKey, header }, headerAlg]; + } catch (error) { + return [void 0, headerAlg, error]; + } +} +function checkRecipient(jwe) { + const { encrypted_key: encryptedKey, header } = jwe; + if (encryptedKey !== void 0 && typeof encryptedKey != "string") + throw new JWEInvalid("JWE Encrypted Key incorrect type"); + if (header !== void 0 && !isObject(header)) + throw new JWEInvalid("JWE Per-Recipient Unprotected Header incorrect type"); + if (jwe.protected === void 0 && header === void 0 && jwe.unprotected === void 0) + throw new JWEInvalid("JOSE Header missing"); +} +function shareJWE(jwe) { + const { protected: encodedProtected, ciphertext, iv, tag, aad } = jwe; + let parsedProt; + return encodedProtected !== void 0 && (parsedProt = parseJoseHeader(encodedProtected, JWEInvalid, "JWE Protected Header is invalid")), [ + parsedProt, + decodeBase64url(ciphertext, "ciphertext", JWEInvalid), + iv !== void 0 ? decodeBase64url(iv, "iv", JWEInvalid) : void 0, + tag !== void 0 ? decodeBase64url(tag, "tag", JWEInvalid) : void 0, + encodeBase64url((encodedProtected ?? "") + (aad !== void 0 ? `.${aad}` : ""), "aad", JWEInvalid) + ]; +} +function prepareDecrypt(options) { + return [ + options && validateAlgorithms("keyManagementAlgorithms", options.keyManagementAlgorithms), + options && validateAlgorithms("contentEncryptionAlgorithms", options.contentEncryptionAlgorithms), + options?.crit, + options?.maxPBES2Count, + options?.maxDecompressedLength + ]; +} +async function decryptJWE(jwe, shared, key, token = shareJWE(jwe)) { + const [parsedProt, ciphertext, iv, tag, additionalData] = token, { header, unprotected, aad } = jwe; + let joseHeader; + if (header !== void 0 || unprotected !== void 0) { + if (!isDisjoint(parsedProt, header, unprotected)) + throw new JWEInvalid("JWE Protected, JWE Unprotected Header, and JWE Per-Recipient Unprotected Header Parameter names must be disjoint"); + joseHeader = { ...parsedProt, ...header, ...unprotected }; + } else + joseHeader = parsedProt ?? {}; + const [keyManagementAlgorithms, contentEncryptionAlgorithms, crit, maxPBES2Count, maxDecompressedLength] = shared, { encrypted_key: encodedKey } = jwe; + validateCrit(JWEInvalid, JWE_RECOGNIZED, crit, parsedProt, joseHeader), validateZip(joseHeader, parsedProt); + const { alg, enc } = joseHeader; + if (typeof alg != "string" || !alg) + throw new JWEInvalid("missing JWE Algorithm (alg) in JWE Header"); + const selected = JWE[alg]; + if (encodedKey === "" && (!selected || !isJWECEKTransport(selected))) + throw new JWEInvalid("JWE Encrypted Key incorrect type"); + const integrated = selected?.mode === "integrated-encryption"; + if (!integrated && (typeof enc != "string" || !enc)) + throw new JWEInvalid("missing JWE Encryption Algorithm (enc) in JWE Header"); + if (keyManagementAlgorithms && !keyManagementAlgorithms.has(alg) || !keyManagementAlgorithms && alg.startsWith("PBES2")) + throw new JOSEAlgNotAllowed('"alg" (Algorithm) Header Parameter value not allowed'); + let encEntry; + if (integrated) { + if (enc !== void 0) + throw new JWEInvalid('JWE "enc" (Encryption Algorithm) Header Parameter must not be present for integrated encryption'); + if (iv?.byteLength) + throw new JWEInvalid("JWE Initialization Vector must be empty for integrated encryption"); + if (tag?.byteLength) + throw new JWEInvalid("JWE Authentication Tag must be empty for integrated encryption"); + } else { + if (contentEncryptionAlgorithms && !contentEncryptionAlgorithms.has(enc)) + throw new JOSEAlgNotAllowed('"enc" (Encryption Algorithm) Header Parameter value not allowed'); + encEntry = jweEncryption(enc); + } + let encryptedKey; + if (encodedKey !== void 0) + try { + encryptedKey = decodeBase64url(encodedKey, "encrypted_key", JWEInvalid); + } catch (error) { + if (!selected || !isJWECEKTransport(selected)) + throw error; + encryptedKey = new Uint8Array(); + } + let resolvedKey = !1; + typeof key == "function" && (key = await key(parsedProt, jwe), resolvedKey = !0); + const algEntry = selected ?? jweAlgorithm(alg); + isJWECEKTransport(algEntry) && encryptedKey === void 0 && (encryptedKey = new Uint8Array()); + const k = await prepareKey(algEntry.mode === "direct-encryption" ? encEntry : algEntry, key, "decrypt"); + let plaintext; + if (algEntry.mode === "integrated-encryption") + plaintext = await algEntry.decrypt(k, encryptedKey, ciphertext, additionalData, parsedProt, joseHeader); + else { + const encryption = encEntry; + let cek; + try { + cek = await decryptKeyManagement(algEntry, encryption, k, encryptedKey, joseHeader, maxPBES2Count), isJWECEKTransport(algEntry) && cek instanceof Uint8Array && cek.byteLength << 3 !== encryption.cekBits && (cek = generateCek(encryption)); + } catch (err) { + if (err instanceof TypeError || err instanceof JWEInvalid || err instanceof JOSENotSupported) + throw err; + cek = generateCek(encryption); + } + plaintext = await decrypt(encryption, cek, ciphertext, iv, tag, additionalData); + } + if (joseHeader.zip === "DEF") { + const decompressionLimit = maxDecompressedLength ?? 25e4; + if (decompressionLimit === 0) + throw new JOSENotSupported('JWE "zip" (Compression Algorithm) Header Parameter is not supported.'); + if (decompressionLimit !== 1 / 0 && (!Number.isSafeInteger(decompressionLimit) || decompressionLimit < 1)) + throw new TypeError("maxDecompressedLength must be 0, a positive safe integer, or Infinity"); + plaintext = await decompress(plaintext, decompressionLimit).catch((cause) => { + throw cause instanceof JWEInvalid ? cause : new JWEInvalid("Failed to decompress plaintext", { cause }); + }); + } + return { + plaintext, + ...parsedProt && { protectedHeader: parsedProt }, + ...aad !== void 0 && { + additionalAuthenticatedData: decodeBase64url(aad, "aad", JWEInvalid) + }, + ...unprotected && { sharedUnprotectedHeader: unprotected }, + ...header && { unprotectedHeader: header }, + ...resolvedKey && { key: k } + }; +} +async function decryptCompact(jwe, shared, key) { + if (jwe instanceof Uint8Array && (jwe = decoder.decode(jwe)), typeof jwe != "string") + throw new JWEInvalid("Compact JWE must be a string or Uint8Array"); + const { 0: protectedHeader, 1: encryptedKey, 2: iv, 3: ciphertext, 4: tag, length } = jwe.split("."); + if (length !== 5) + throw new JWEInvalid("Invalid Compact JWE"); + return decryptJWE({ + ciphertext, + iv: iv || void 0, + protected: protectedHeader, + tag: tag || void 0, + encrypted_key: encryptedKey || void 0 + }, shared, key); +} +export { + checkRecipient, + decryptCompact, + decryptJWE, + prepareDecrypt, + shareJWE, + snapshotRecipientJWE, + snapshotSharedJWE +}; diff --git a/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/lib/jwe_encrypt.js b/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/lib/jwe_encrypt.js new file mode 100644 index 000000000..5ab33766b --- /dev/null +++ b/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/lib/jwe_encrypt.js @@ -0,0 +1,75 @@ +import { encode as b64u } from "../util/base64url.js"; +import { encrypt } from "./content_encryption.js"; +import { encryptKeyManagement } from "./key_management.js"; +import { JWEInvalid } from "../util/errors.js"; +import { assertUint8Array, isDisjoint, isObject, serializeJoseHeader, validateCrit, validateCritDuplicates, JWE_RECOGNIZED } from "./validate.js"; +import { encode } from "./buffer_utils.js"; +import { prepareKey } from "./key.js"; +import { JWE, jweAlgorithm, jweEncryption } from "./jwe_algorithms.js"; +import { compress, validateZip } from "./deflate.js"; +function checkDisjoint(protectedHeader, unprotectedHeader, sharedUnprotectedHeader) { + if (!isDisjoint(protectedHeader, unprotectedHeader, sharedUnprotectedHeader)) + throw new JWEInvalid("JWE Protected, JWE Shared Unprotected and JWE Per-Recipient Header Parameter names must be disjoint"); +} +function checkEncryptHeaders(input, options, sharedHeadersNormalized = !1) { + if (!input[1] && !input[2] && !input[3]) + throw new JWEInvalid("either setProtectedHeader, setUnprotectedHeader, or sharedUnprotectedHeader must be called before #encrypt()"); + options !== void 0 && (input[8] = options?.crit); + let [, protectedHeader, unprotectedHeader, sharedUnprotectedHeader, aad, cek, iv, keyManagementParameters, crit] = input; + if (aad !== void 0 && assertUint8Array(aad, "JWE Additional Authenticated Data"), cek !== void 0 && assertUint8Array(cek, "JWE Content Encryption Key"), iv !== void 0 && assertUint8Array(iv, "JWE Initialization Vector"), !sharedHeadersNormalized && protectedHeader !== void 0 && (protectedHeader = serializeJoseHeader(JWEInvalid, protectedHeader)[0], input[1] = protectedHeader), unprotectedHeader !== void 0 && (unprotectedHeader = serializeJoseHeader(JWEInvalid, unprotectedHeader)[0], input[2] = unprotectedHeader), !sharedHeadersNormalized && sharedUnprotectedHeader !== void 0 && (sharedUnprotectedHeader = serializeJoseHeader(JWEInvalid, sharedUnprotectedHeader)[0], input[3] = sharedUnprotectedHeader), keyManagementParameters !== void 0 && !isObject(keyManagementParameters)) + throw new TypeError("JWE Key Management Parameters must be an object"); + checkDisjoint(protectedHeader, unprotectedHeader, sharedUnprotectedHeader); + const joseHeader = { + ...protectedHeader, + ...unprotectedHeader, + ...sharedUnprotectedHeader + }; + validateCritDuplicates(JWEInvalid, protectedHeader), validateCrit(JWEInvalid, JWE_RECOGNIZED, crit, protectedHeader, joseHeader), validateZip(joseHeader, protectedHeader); + const { alg, enc } = joseHeader; + if (typeof alg != "string" || !alg) + throw new JWEInvalid('JWE "alg" (Algorithm) Header Parameter missing or invalid'); + const algEntry = JWE[alg]; + if (algEntry?.mode === "integrated-encryption") { + if (enc !== void 0) + throw new JWEInvalid('JWE "enc" (Encryption Algorithm) Header Parameter must not be present for integrated encryption'); + if (cek !== void 0) + throw new TypeError(`setContentEncryptionKey cannot be called with JWE "alg" (Algorithm) Header ${alg}`); + if (iv !== void 0) + throw new TypeError(`setInitializationVector cannot be called with JWE "alg" (Algorithm) Header ${alg}`); + return [joseHeader, void 0, algEntry]; + } + if (typeof enc != "string" || !enc) + throw new JWEInvalid('JWE "enc" (Encryption Algorithm) Header Parameter missing or invalid'); + return [joseHeader, jweEncryption(enc), algEntry]; +} +async function encryptJWE(input, checked, key) { + const [joseHeader, encEntry, selected] = checked, [inputPlaintext, inputProtectedHeader, inputUnprotectedHeader, sharedUnprotectedHeader, aad, providedCek, inputIv, keyManagementParameters, , unprotectedParameters] = input; + let protectedHeader = inputProtectedHeader, unprotectedHeader = inputUnprotectedHeader; + const algEntry = selected ?? jweAlgorithm(joseHeader.alg); + let encryptedKey, parameters, cek; + algEntry.mode === "integrated-encryption" ? cek = await prepareKey(algEntry, key, "encrypt") : [cek, encryptedKey, parameters] = await encryptKeyManagement(algEntry, encEntry, key, joseHeader, providedCek, keyManagementParameters), parameters && (unprotectedParameters ? unprotectedHeader = unprotectedHeader ? { ...unprotectedHeader, ...parameters } : parameters : protectedHeader = protectedHeader ? { ...protectedHeader, ...parameters } : parameters, checkDisjoint(protectedHeader, unprotectedHeader, sharedUnprotectedHeader)); + const protectedHeaderS = protectedHeader ? b64u(JSON.stringify(protectedHeader)) : "", aadMember = aad?.byteLength ? b64u(aad) : void 0, additionalData = encode(aadMember ? `${protectedHeaderS}.${aadMember}` : protectedHeaderS); + let plaintext = inputPlaintext; + joseHeader.zip === "DEF" && (plaintext = await compress(plaintext).catch((cause) => { + throw new JWEInvalid("Failed to compress plaintext", { cause }); + })); + let ciphertext, tag, iv; + algEntry.mode === "integrated-encryption" ? [encryptedKey, ciphertext] = await algEntry.encrypt(cek, plaintext, additionalData, protectedHeader, joseHeader, keyManagementParameters) : { ciphertext, tag, iv } = await encrypt(encEntry, plaintext, cek, inputIv, additionalData); + const jwe = { + ciphertext: b64u(ciphertext) + }; + return iv && (jwe.iv = b64u(iv)), tag && (jwe.tag = b64u(tag)), encryptedKey?.byteLength && (jwe.encrypted_key = b64u(encryptedKey)), aadMember && (jwe.aad = aadMember), protectedHeader && (jwe.protected = protectedHeaderS), sharedUnprotectedHeader && (jwe.unprotected = sharedUnprotectedHeader), unprotectedHeader && (jwe.header = unprotectedHeader), jwe; +} +async function createJWE(input, key, options) { + return encryptJWE(input, checkEncryptHeaders(input, options), key); +} +function compactJWE(jwe) { + return [jwe.protected, jwe.encrypted_key, jwe.iv, jwe.ciphertext, jwe.tag].join("."); +} +export { + checkDisjoint, + checkEncryptHeaders, + compactJWE, + createJWE, + encryptJWE +}; diff --git a/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/lib/jwt_claims_set.js b/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/lib/jwt_claims_set.js new file mode 100644 index 000000000..b99943f9c --- /dev/null +++ b/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/lib/jwt_claims_set.js @@ -0,0 +1,146 @@ +import { JWTClaimValidationFailed, JWTExpired, JWTInvalid } from "../util/errors.js"; +import { encoder, strictDecoder } from "./buffer_utils.js"; +import { isObject } from "./validate.js"; +const epoch = (date) => Math.floor(date.getTime() / 1e3), multipliers = { + s: 1, + m: 60, + h: 3600, + d: 86400, + w: 604800, + y: 31557600 +}, REGEX = /^(\+|\-)? ?(\d+|\d+\.\d+) ?(seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|years?|yrs?|y)(?: (ago|from now))?$/i, checkFailed = "check_failed"; +function invalidDuration() { + throw new TypeError("Invalid time period format"); +} +function secs(str) { + typeof str != "string" && invalidDuration(); + const matched = REGEX.exec(str); + (!matched || matched[4] && matched[1]) && invalidDuration(); + const value = parseFloat(matched[2]), numericDate2 = Math.round(value * multipliers[matched[3][0].toLowerCase()]); + return Number.isFinite(numericDate2) || invalidDuration(), matched[1] === "-" || matched[4] === "ago" ? -numericDate2 : numericDate2; +} +function validateInput(label, input) { + if (!Number.isFinite(input)) + throw new TypeError(`Invalid ${label} input`); + return input; +} +function validateStringClaim(claim, value) { + if (typeof value != "string") + throw new TypeError(`"${claim}" claim must be a string`); +} +function validateAudienceClaim(value) { + if (typeof value != "string" && (!Array.isArray(value) || Array.from(value).some((member) => typeof member != "string"))) + throw new TypeError('"aud" claim must be a string or an array of strings'); +} +function numericDate(value, label) { + return typeof value == "number" ? validateInput(label, value) : value instanceof Date ? validateInput(label, epoch(value)) : epoch(/* @__PURE__ */ new Date()) + secs(value); +} +const normalizeTyp = (value) => { + const normalized = value.toLowerCase(); + return value.includes("/") ? normalized : `application/${normalized}`; +}, checkAudiencePresence = (audPayload, audOption) => typeof audPayload == "string" ? audOption.includes(audPayload) : Array.isArray(audPayload) ? audOption.some((aud) => audPayload.includes(aud)) : !1; +function validateNumericDate(payload, claim, required = !1) { + const value = payload[claim]; + if (!(value === void 0 && !required)) { + if (typeof value != "number") + throw new JWTClaimValidationFailed(`"${claim}" claim must be a number`, payload, claim, "invalid"); + return value; + } +} +function unexpectedClaim(payload, claim) { + throw new JWTClaimValidationFailed(`unexpected "${claim}" claim value`, payload, claim, checkFailed); +} +function validateClaimsSet(protectedHeader, encodedPayload, options = {}) { + let payload; + try { + payload = JSON.parse(strictDecoder.decode(encodedPayload)); + } catch { + } + if (!isObject(payload)) + throw new JWTInvalid("JWT Claims Set must be a top-level JSON object"); + const { typ } = options; + if (typ !== void 0 && (typeof protectedHeader.typ != "string" || normalizeTyp(protectedHeader.typ) !== normalizeTyp(typ))) + throw new JWTClaimValidationFailed('unexpected "typ" JWT header value', payload, "typ", checkFailed); + const { requiredClaims = [], issuer, subject, audience, maxTokenAge } = options, presenceCheck = [...requiredClaims]; + maxTokenAge !== void 0 && presenceCheck.push("iat"), audience !== void 0 && presenceCheck.push("aud"), subject !== void 0 && presenceCheck.push("sub"), issuer !== void 0 && presenceCheck.push("iss"); + for (const claim of new Set(presenceCheck.reverse())) + if (!Object.hasOwn(payload, claim)) + throw new JWTClaimValidationFailed(`missing required "${claim}" claim`, payload, claim, "missing"); + issuer !== void 0 && !(Array.isArray(issuer) ? issuer : [issuer]).includes(payload.iss) && unexpectedClaim(payload, "iss"), subject !== void 0 && payload.sub !== subject && unexpectedClaim(payload, "sub"), audience !== void 0 && !checkAudiencePresence(payload.aud, typeof audience == "string" ? [audience] : audience) && unexpectedClaim(payload, "aud"); + const { clockTolerance } = options; + let tolerance = 0; + if (typeof clockTolerance == "string") + tolerance = secs(clockTolerance); + else if (clockTolerance !== void 0) { + if (typeof clockTolerance != "number") + throw new TypeError("Invalid clockTolerance option type"); + tolerance = clockTolerance; + } + validateInput("clockTolerance option", tolerance); + const { currentDate } = options, now = validateInput("currentDate option", epoch(currentDate === void 0 ? /* @__PURE__ */ new Date() : currentDate)), iat = validateNumericDate(payload, "iat", maxTokenAge !== void 0), nbf = validateNumericDate(payload, "nbf"); + if (nbf !== void 0 && nbf > now + tolerance) + throw new JWTClaimValidationFailed('"nbf" claim timestamp check failed', payload, "nbf", checkFailed); + const exp = validateNumericDate(payload, "exp"); + if (exp !== void 0 && exp <= now - tolerance) + throw new JWTExpired('"exp" claim timestamp check failed', payload, "exp", checkFailed); + if (maxTokenAge !== void 0) { + const age = now - iat, max = validateInput("maxTokenAge option", typeof maxTokenAge == "number" ? maxTokenAge : secs(maxTokenAge)); + if (age - tolerance > max) + throw new JWTExpired('"iat" claim timestamp check failed (too far in the past)', payload, "iat", checkFailed); + if (age < -tolerance) + throw new JWTClaimValidationFailed('"iat" claim timestamp check failed (it should be in the past)', payload, "iat", checkFailed); + } + return payload; +} +let producerPayloads; +function producerPayload(producer) { + return producerPayloads.get(producer); +} +function jwtData(producer) { + const payload = producerPayload(producer); + for (const claim of ["iat", "nbf", "exp"]) { + const value = payload[claim]; + if (typeof value == "number" && !Number.isFinite(value)) + throw new TypeError(`"${claim}" claim must be a finite number`); + } + return encoder.encode(JSON.stringify(payload)); +} +function jwtClaim(producer, claim) { + return producerPayload(producer)[claim]; +} +class JWTClaimsBuilder { + constructor(payload = {}) { + if (!isObject(payload)) + throw new TypeError("JWT Claims Set MUST be an object"); + (producerPayloads ||= /* @__PURE__ */ new WeakMap()).set(this, structuredClone(payload)); + } + setIssuer(value) { + return validateStringClaim("iss", value), producerPayload(this).iss = value, this; + } + setSubject(value) { + return validateStringClaim("sub", value), producerPayload(this).sub = value, this; + } + setAudience(value) { + return validateAudienceClaim(value), producerPayload(this).aud = value, this; + } + setJti(value) { + return validateStringClaim("jti", value), producerPayload(this).jti = value, this; + } + setNotBefore(value) { + return producerPayload(this).nbf = numericDate(value, "setNotBefore"), this; + } + setExpirationTime(value) { + return producerPayload(this).exp = numericDate(value, "setExpirationTime"), this; + } + setIssuedAt(value) { + const payload = producerPayload(this); + return value === void 0 ? payload.iat = epoch(/* @__PURE__ */ new Date()) : typeof value == "string" ? payload.iat = validateInput("setIssuedAt", epoch(/* @__PURE__ */ new Date()) + secs(value)) : payload.iat = numericDate(value, "setIssuedAt"), this; + } +} +export { + JWTClaimsBuilder, + jwtClaim, + jwtData, + secs, + validateClaimsSet +}; diff --git a/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/lib/key.js b/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/lib/key.js new file mode 100644 index 000000000..7dc5602f1 --- /dev/null +++ b/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/lib/key.js @@ -0,0 +1,155 @@ +import { isObject } from "./validate.js"; +import { decode } from "../util/base64url.js"; +import { JOSENotSupported } from "../util/errors.js"; +const tag = (key) => key[Symbol.toStringTag], jwkMatchesOp = (entry, key, usage) => { + const { alg } = entry; + if (key.use !== void 0) { + const expected = usage === "sign" || usage === "verify" ? "sig" : "enc"; + if (key.use !== expected) + throw new TypeError(`Invalid key for this operation, its "use" must be "${expected}" when present`); + } + if (key.alg !== void 0 && key.alg !== alg) + throw new TypeError(`Invalid key for this operation, its "alg" must be "${alg}" when present`); + if (Array.isArray(key.key_ops)) { + const expectedKeyOp = usage === "encrypt" || usage === "decrypt" ? entry.ops?.[usage === "encrypt" ? 0 : 1] : usage; + if (expectedKeyOp && !key.key_ops.includes(expectedKeyOp)) + throw new TypeError(`Invalid key for this operation, its "key_ops" must include "${expectedKeyOp}" when present`); + } +}; +async function prepareKey(entry, key, usage) { + const { alg, secret } = entry, privateKey = usage === "decrypt" || usage === "sign"; + if (secret && key instanceof Uint8Array) + return key; + let normalized, keyObject; + if (isObject(key)) { + if (normalized = normalizeJwk(key), typeof normalized.kty != "string") + throw invalidKeyType(alg, key, secret); + if (!(secret ? normalized.kty === "oct" && typeof normalized.k == "string" : normalized.kty !== "oct" && (privateKey ? normalized.kty === "AKP" && typeof normalized.priv == "string" || typeof normalized.d == "string" : normalized.d === void 0 && normalized.priv === void 0))) + throw new TypeError(secret ? 'JSON Web Key for symmetric algorithms must have JWK "kty" (Key Type) equal to "oct" and the JWK "k" (Key Value) present' : `JSON Web Key for this operation must be a ${privateKey ? "private" : "public"} JWK`); + if (jwkMatchesOp(entry, normalized, usage), normalized.kty === "oct") + return decode(normalized.k); + if (!Object.isFrozen(key)) { + const { key_ops } = key; + Array.isArray(key_ops) && Object.freeze(key_ops), Object.freeze(key); + } + } else { + if (!isKeyLike(key)) + throw invalidKeyType(alg, key, secret); + const expectedType = secret ? "secret" : privateKey ? "private" : "public"; + if (key.type !== expectedType && (secret || ["secret", "public", "private"].includes(key.type))) + throw new TypeError(`${tag(key)} instances must be of type "${expectedType}" for the ${alg} algorithm`); + if (isCryptoKey(key)) + return key; + if (keyObject = key, keyObject.type === "secret") + return keyObject.export(); + } + cache ||= /* @__PURE__ */ new WeakMap(); + const cacheKey = key; + let cached = cache.get(cacheKey); + if (cached?.[alg]) + return cached[alg]; + if (cached || cache.set(cacheKey, cached = {}), keyObject && typeof keyObject.toCryptoKey == "function") { + const isPublic = keyObject.type === "public", crv = nist[keyObject.asymmetricKeyDetails?.namedCurve], params = entry.resolve?.({ crv, asymmetricKeyType: keyObject.asymmetricKeyType }) ?? entry.subtle; + return cached[alg] = keyObject.toCryptoKey(params, isPublic, entry.usages[isPublic ? 0 : 1]); + } + return normalized ??= keyObject.export({ format: "jwk" }), normalized.alg = alg, cached[alg] = await jwkToKey(entry, normalized); +} +let cache; +const nist = { + __proto__: null, + prime256v1: "P-256", + secp384r1: "P-384", + secp521r1: "P-521" +}; +function assertCryptoKey(key) { + if (!isCryptoKey(key)) + throw new Error("CryptoKey instance expected"); +} +const isCryptoKey = (key) => { + if (key?.[Symbol.toStringTag] === "CryptoKey") + return !0; + try { + return key instanceof CryptoKey; + } catch { + return !1; + } +}, isKeyObject = (key) => key?.[Symbol.toStringTag] === "KeyObject", isKeyLike = (key) => isCryptoKey(key) || isKeyObject(key); +function message(msg, actual, ...types) { + if (types.length > 2) { + const last = types.pop(); + msg += `one of type ${types.join(", ")}, or ${last}.`; + } else types.length === 2 ? msg += `one of type ${types[0]} or ${types[1]}.` : msg += `of type ${types[0]}.`; + return actual == null ? msg += ` Received ${actual}` : typeof actual == "function" && actual.name ? msg += ` Received function ${actual.name}` : typeof actual == "object" && actual != null && actual.constructor?.name && (msg += ` Received an instance of ${actual.constructor.name}`), msg; +} +const invalidKeyInput = (actual, ...types) => message("Key must be ", actual, ...types); +function invalidKeyType(alg, actual, secret) { + const types = ["CryptoKey", "KeyObject", "JSON Web Key"]; + return secret && types.push("Uint8Array"), new TypeError(message(`Key for the ${alg} algorithm must be `, actual, ...types)); +} +const unusable = (name, prop = "algorithm.name") => new TypeError(`CryptoKey does not support this operation, its ${prop} must be ${name}`); +function checkUsage(key, usage) { + if (usage && !key.usages.includes(usage)) + throw new TypeError(`CryptoKey does not support this operation, its usages must include ${usage}.`); +} +function checkModulusLength(alg, key) { + const { modulusLength } = key.algorithm; + if (typeof modulusLength != "number" || modulusLength < 2048) + throw new TypeError(`${alg} requires key modulusLength to be 2048 bits or larger`); +} +function checkCryptoKey(key, expected, usage) { + const algorithm = key.algorithm; + if (algorithm.name !== expected.name) + throw unusable(expected.name); + if (expected.hash && algorithm.hash?.name !== expected.hash) + throw unusable(expected.hash, "algorithm.hash"); + if (expected.namedCurve && algorithm.namedCurve !== expected.namedCurve) + throw unusable(expected.namedCurve, "algorithm.namedCurve"); + if (expected.length !== void 0 && algorithm.length !== expected.length) + throw unusable(expected.length, "algorithm.length"); + checkUsage(key, usage); +} +function snapshotJwk(jwk) { + return { __proto__: null, ...jwk }; +} +function normalizeJwk(jwk) { + const normalized = snapshotJwk(jwk); + if (normalized.ext !== void 0 && typeof normalized.ext != "boolean") + throw new TypeError('"ext" (Extractable) Parameter must be a boolean'); + if (normalized.key_ops !== void 0) { + const value = normalized.key_ops, keyOps = Array.isArray(value) ? [...value] : void 0; + if (!keyOps || keyOps.some((operation) => typeof operation != "string") || new Set(keyOps).size !== keyOps.length) + throw new TypeError('"key_ops" (Key Operations) Parameter must be an array of unique strings'); + normalized.key_ops = keyOps; + } + return normalized; +} +function validateExtractableOption(extractable) { + if (extractable !== void 0 && typeof extractable != "boolean") + throw new TypeError('"extractable" option must be a boolean'); + return extractable; +} +async function jwkToKey(entry, jwk, extractable) { + if (!entry.kty.includes(jwk.kty)) + throw new JOSENotSupported('Invalid or unsupported JWK "alg" (Algorithm) Parameter value'); + const algorithm = entry.resolve?.({ kty: jwk.kty, crv: jwk.crv }) ?? entry.subtle, isPrivate = !!(jwk.d || jwk.priv), keyData = { ...jwk, ext: extractable ?? jwk.ext }; + return keyData.kty !== "AKP" && delete keyData.alg, delete keyData.use, crypto.subtle.importKey("jwk", keyData, algorithm, keyData.ext ?? !isPrivate, jwk.key_ops ?? entry.usages[isPrivate ? 1 : 0]); +} +async function rawKey(key, expected, usage, extractable = !1) { + return key instanceof Uint8Array && (key = await crypto.subtle.importKey("raw", key, expected, extractable, [usage])), checkCryptoKey(key, expected, usage), key; +} +export { + assertCryptoKey, + checkCryptoKey, + checkModulusLength, + checkUsage, + invalidKeyInput, + isCryptoKey, + isKeyLike, + isKeyObject, + jwkToKey, + normalizeJwk, + prepareKey, + rawKey, + snapshotJwk, + validateExtractableOption +}; diff --git a/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/lib/key_descriptor.js b/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/lib/key_descriptor.js new file mode 100644 index 000000000..cf0afbd90 --- /dev/null +++ b/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/lib/key_descriptor.js @@ -0,0 +1,9 @@ +function table(entries) { + const out = { __proto__: null }; + for (const alg in entries) + out[alg] = { ...entries[alg], alg }; + return out; +} +export { + table +}; diff --git a/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/lib/key_management.js b/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/lib/key_management.js new file mode 100644 index 000000000..7b7533b70 --- /dev/null +++ b/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/lib/key_management.js @@ -0,0 +1,196 @@ +import { encode as b64u } from "../util/base64url.js"; +import { prepareKey, rawKey, jwkToKey, checkCryptoKey, checkModulusLength, checkUsage, assertCryptoKey } from "./key.js"; +import { jweAlgorithm, jweEncryption, isJWECEKTransport } from "./jwe_algorithms.js"; +import { JOSENotSupported, JWEInvalid } from "../util/errors.js"; +import { decodeBase64url, assertUint8Array, isObject } from "./validate.js"; +import { digest, concat, encode, uint32be } from "./buffer_utils.js"; +import { checkCekLength, generateCek, encrypt, decrypt } from "./content_encryption.js"; +function checkEcdhCryptoKey(key, usage) { + if (key.algorithm.name !== "ECDH" && key.algorithm.name !== "X25519") + throw new TypeError("CryptoKey does not support this operation, its algorithm.name must be ECDH or X25519"); + checkUsage(key, usage); +} +async function aeskwWrap(alg, key, cek) { + const cryptoKey = await rawKey(key, jweAlgorithm(alg).subtle, "wrapKey", !0), cryptoKeyCek = await crypto.subtle.importKey("raw", cek, { hash: "SHA-256", name: "HMAC" }, !0, ["sign"]); + return new Uint8Array(await crypto.subtle.wrapKey("raw", cryptoKeyCek, cryptoKey, "AES-KW")); +} +async function aeskwUnwrap(alg, key, encryptedKey) { + const cryptoKey = await rawKey(key, jweAlgorithm(alg).subtle, "unwrapKey", !0), cryptoKeyCek = await crypto.subtle.unwrapKey("raw", encryptedKey, cryptoKey, "AES-KW", { hash: "SHA-256", name: "HMAC" }, !0, ["sign"]); + return new Uint8Array(await crypto.subtle.exportKey("raw", cryptoKeyCek)); +} +function checkRsaKey(alg, key, usage) { + checkCryptoKey(key, jweAlgorithm(alg).subtle, usage), checkModulusLength(alg, key); +} +async function deriveKey(p2s, alg, p2c, key) { + if (!(p2s instanceof Uint8Array) || p2s.length < 8) + throw new JWEInvalid("PBES2 Salt Input must be 8 or more octets"); + if (!Number.isSafeInteger(p2c) || Math.sign(p2c) !== 1) + throw new JWEInvalid("PBES2 Count Input must be a positive integer"); + const salt = concat(encode(alg), Uint8Array.of(0), p2s), keylen = parseInt(alg.slice(13, 16), 10), subtleAlg = { + hash: `SHA-${alg.slice(8, 11)}`, + iterations: p2c, + name: "PBKDF2", + salt + }, cryptoKey = await rawKey(key, jweAlgorithm(alg).subtle, "deriveBits"); + return new Uint8Array(await crypto.subtle.deriveBits(subtleAlg, cryptoKey, keylen)); +} +function lengthAndInput(input) { + return concat(uint32be(input.length), input); +} +async function concatKdf(Z, L, OtherInfo) { + const dkLen = L >> 3, hashLen = 32, reps = Math.ceil(dkLen / hashLen), dk = new Uint8Array(reps * hashLen); + for (let i = 1; i <= reps; i++) { + const hashResult = await digest("sha256", concat(uint32be(i), Z, OtherInfo)); + dk.set(hashResult, (i - 1) * hashLen); + } + return dk.slice(0, dkLen); +} +async function ecdhesDeriveKey(publicKey, privateKey, algorithm, keyLength, apu = new Uint8Array(), apv = new Uint8Array()) { + checkEcdhCryptoKey(publicKey), checkEcdhCryptoKey(privateKey, "deriveBits"); + const otherInfo = concat(lengthAndInput(encode(algorithm)), lengthAndInput(apu), lengthAndInput(apv), uint32be(keyLength)), Z = new Uint8Array(await crypto.subtle.deriveBits({ + name: publicKey.algorithm.name, + public: publicKey + }, privateKey, publicKey.algorithm.name === "X25519" ? 256 : Math.ceil(parseInt(publicKey.algorithm.namedCurve.slice(-3), 10) / 8) << 3)); + return concatKdf(Z, keyLength, otherInfo); +} +function assertEcdhKey(key) { + assertCryptoKey(key); + const curve = key.algorithm.namedCurve; + if (curve !== "P-256" && curve !== "P-384" && curve !== "P-521" && key.algorithm.name !== "X25519") + throw new JOSENotSupported("ECDH with the provided key is not allowed or not supported by your javascript runtime"); +} +function partyInfo(joseHeader, name) { + const value = joseHeader[name]; + if (value !== void 0) { + if (typeof value != "string") + throw new JWEInvalid(`JOSE Header "${name}" (Agreement Party${name === "apu" ? "U" : "V"}Info) invalid`); + return decodeBase64url(value, name, JWEInvalid); + } +} +function checkPartyInfo(apu, apv) { + if (!(apu === void 0 || apv === void 0 || apu.byteLength !== apv.byteLength)) { + for (let i = 0; i < apu.byteLength; i++) + if (apu[i] !== apv[i]) + return; + throw new JWEInvalid('JOSE Header "apu" and "apv" values must be distinct'); + } +} +function assertEncryptedKey(encryptedKey) { + if (encryptedKey === void 0) + throw new JWEInvalid("JWE Encrypted Key missing"); +} +function assertNoEncryptedKey(encryptedKey) { + if (encryptedKey !== void 0) + throw new JWEInvalid("Encountered unexpected JWE Encrypted Key"); +} +function validateMaxPBES2Count(value) { + if (value !== void 0 && value !== 1 / 0 && (!Number.isSafeInteger(value) || value < 1)) + throw new TypeError("maxPBES2Count must be a positive safe integer or Infinity"); +} +async function decryptKeyManagement(entry, enc, key, encryptedKey, joseHeader, maxPBES2Count) { + const { alg } = entry, mode = entry.mode; + if (mode === "direct-encryption") + return assertNoEncryptedKey(encryptedKey), key; + const direct = mode === "direct-key-agreement"; + switch (direct ? assertNoEncryptedKey(encryptedKey) : assertEncryptedKey(encryptedKey), entry.subtle.name) { + case "ECDH": { + const { epk } = joseHeader; + if (!isObject(epk) || ["d", "k", "p", "q", "dp", "dq", "qi", "oth", "priv"].some((parameter) => Object.hasOwn(epk, parameter))) + throw new JWEInvalid('JOSE Header "epk" (Ephemeral Public Key) missing or invalid'); + assertEcdhKey(key); + const ephemeralPublicKey = await jwkToKey(entry, epk), partyUInfo = partyInfo(joseHeader, "apu"), partyVInfo = partyInfo(joseHeader, "apv"); + checkPartyInfo(partyUInfo, partyVInfo); + const sharedSecret = await ecdhesDeriveKey(ephemeralPublicKey, key, direct ? enc.alg : alg, direct ? enc.cekBits : parseInt(alg.slice(-5, -2), 10), partyUInfo, partyVInfo); + if (direct) + return sharedSecret; + key = sharedSecret; + break; + } + case "RSA-OAEP": + return assertCryptoKey(key), checkRsaKey(alg, key, "decrypt"), new Uint8Array(await crypto.subtle.decrypt("RSA-OAEP", key, encryptedKey)); + case "PBKDF2": { + if (typeof joseHeader.p2c != "number") + throw new JWEInvalid('JOSE Header "p2c" (PBES2 Count) missing or invalid'); + validateMaxPBES2Count(maxPBES2Count); + const p2cLimit = maxPBES2Count ?? 1e4; + if (joseHeader.p2c > p2cLimit) + throw new JWEInvalid('JOSE Header "p2c" (PBES2 Count) out is of acceptable bounds'); + if (typeof joseHeader.p2s != "string") + throw new JWEInvalid('JOSE Header "p2s" (PBES2 Salt) missing or invalid'); + const p2s = decodeBase64url(joseHeader.p2s, "p2s", JWEInvalid); + key = await deriveKey(p2s, alg, joseHeader.p2c, key); + break; + } + case "AES-GCM": { + if (typeof joseHeader.iv != "string") + throw new JWEInvalid('JOSE Header "iv" (Initialization Vector) missing or invalid'); + if (typeof joseHeader.tag != "string") + throw new JWEInvalid('JOSE Header "tag" (Authentication Tag) missing or invalid'); + const iv = decodeBase64url(joseHeader.iv, "iv", JWEInvalid), tag = decodeBase64url(joseHeader.tag, "tag", JWEInvalid); + if (iv.byteLength !== 12) + throw new JWEInvalid("Invalid Initialization Vector length"); + if (tag.byteLength !== 16) + throw new JWEInvalid("Invalid Authentication Tag length"); + return decrypt(jweEncryption(alg.slice(0, -2)), key, encryptedKey, iv, tag, new Uint8Array()); + } + } + return aeskwUnwrap(alg.slice(-6), key, encryptedKey); +} +async function encryptKeyManagement(entry, enc, inputKey, joseHeader, providedCek, providedParameters = {}) { + const { alg, mode } = entry, transport = isJWECEKTransport(entry); + if (providedCek !== void 0 && !transport) + throw new TypeError(`setContentEncryptionKey cannot be called with JWE "alg" (Algorithm) Header ${alg}`); + let key = await prepareKey(mode === "direct-encryption" ? enc : entry, inputKey, "encrypt"); + if (mode === "direct-encryption") + return [key, void 0, void 0]; + const cek = transport ? providedCek ?? generateCek(enc) : void 0; + cek && checkCekLength(cek, enc.cekBits); + let encryptedKey, parameters; + switch (entry.subtle.name) { + case "ECDH": { + assertEcdhKey(key); + const { apu: providedApu, apv: providedApv } = providedParameters; + providedApu !== void 0 && assertUint8Array(providedApu, '"apu"'), providedApv !== void 0 && assertUint8Array(providedApv, '"apv"'); + const apu = providedApu ?? partyInfo(joseHeader, "apu"), apv = providedApv ?? partyInfo(joseHeader, "apv"); + checkPartyInfo(apu, apv); + let ephemeralKey; + providedParameters.epk !== void 0 ? ephemeralKey = await prepareKey(entry, providedParameters.epk, "decrypt") : ephemeralKey = (await crypto.subtle.generateKey(key.algorithm, !0, ["deriveBits"])).privateKey; + const subtle = crypto.subtle; + let exportableEpk = ephemeralKey; + if (!exportableEpk.extractable) { + if (typeof subtle.getPublicKey != "function") + throw new TypeError('CryptoKey for "epk" must be extractable'); + exportableEpk = await subtle.getPublicKey(ephemeralKey, []); + } + const { x, y, crv, kty } = await subtle.exportKey("jwk", exportableEpk), direct = mode === "direct-key-agreement", sharedSecret = await ecdhesDeriveKey(key, ephemeralKey, direct ? enc.alg : alg, direct ? enc.cekBits : parseInt(alg.slice(-5, -2), 10), apu, apv), epk = { x, crv, kty }; + if (kty === "EC" && (epk.y = y), parameters = { epk }, providedApu !== void 0 && (parameters.apu = b64u(providedApu)), providedApv !== void 0 && (parameters.apv = b64u(providedApv)), direct) + return [sharedSecret, void 0, parameters]; + key = sharedSecret; + break; + } + case "RSA-OAEP": { + assertCryptoKey(key), checkRsaKey(alg, key, "encrypt"), encryptedKey = new Uint8Array(await crypto.subtle.encrypt("RSA-OAEP", key, cek)); + break; + } + case "PBKDF2": { + const { p2c = 2048, p2s = crypto.getRandomValues(new Uint8Array(16)) } = providedParameters; + key = await deriveKey(p2s, alg, p2c, key), parameters = { p2c, p2s: b64u(p2s) }; + break; + } + case "AES-GCM": { + const iv = providedParameters.iv === void 0 ? crypto.getRandomValues(new Uint8Array(12)) : providedParameters.iv; + if (!(iv instanceof Uint8Array)) + throw new TypeError('"iv" must be an instance of Uint8Array'); + const wrapped = await encrypt(jweEncryption(alg.slice(0, -2)), cek, key, iv, new Uint8Array()); + encryptedKey = wrapped.ciphertext, parameters = { iv: b64u(wrapped.iv), tag: b64u(wrapped.tag) }; + } + } + if (encryptedKey ??= await aeskwWrap(alg.slice(-6), key, cek), !(encryptedKey instanceof Uint8Array) || !encryptedKey.byteLength) + throw new TypeError("JWE key management algorithm did not produce an Encrypted Key"); + return [cek, encryptedKey, parameters]; +} +export { + decryptKeyManagement, + encryptKeyManagement, + validateMaxPBES2Count +}; diff --git a/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/lib/validate.js b/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/lib/validate.js new file mode 100644 index 000000000..51b7dfe89 --- /dev/null +++ b/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/lib/validate.js @@ -0,0 +1,122 @@ +import { JOSENotSupported, JWSInvalid } from "../util/errors.js"; +import { decode } from "../util/base64url.js"; +import { encode, strictDecoder } from "./buffer_utils.js"; +function assertUint8Array(input, label) { + if (!(input instanceof Uint8Array)) + throw new TypeError(`${label} must be an instance of Uint8Array`); +} +function isObject(input) { + if (typeof input != "object" || input === null || Object.prototype.toString.call(input) !== "[object Object]") + return !1; + const prototype = Object.getPrototypeOf(input); + return prototype === null || Object.getPrototypeOf(prototype) === null; +} +function isJwkSet(input) { + return isObject(input) && Array.isArray(input.keys) && Array.from(input.keys).every(isObject); +} +function isDisjoint(...headers) { + const parameters = /* @__PURE__ */ new Set(); + for (const header of headers) + if (header) + for (const parameter of Object.keys(header)) { + if (parameters.has(parameter)) + return !1; + parameters.add(parameter); + } + return !0; +} +function assertNotSet(value, name) { + if (value !== void 0) + throw new TypeError(`${name} can only be called once`); +} +function decodeBase64url(value, label, ErrorClass) { + try { + return decode(value); + } catch { + throw new ErrorClass(`Failed to base64url decode the ${label}`); + } +} +function encodeBase64url(value, label, ErrorClass) { + try { + return encode(value); + } catch { + throw new ErrorClass(`The ${label} is not a valid base64url string`); + } +} +function parseJoseHeader(b64, ErrorClass, message) { + let parsed; + try { + parsed = JSON.parse(strictDecoder.decode(decode(b64))); + } catch { + throw new ErrorClass(message); + } + if (!isObject(parsed)) + throw new ErrorClass(message); + return parsed; +} +const JWS_RECOGNIZED = { __proto__: null, b64: !0 }, JWE_RECOGNIZED = { __proto__: null }; +function validateAlgorithms(option, algorithms) { + if (algorithms !== void 0 && (!Array.isArray(algorithms) || algorithms.some((s) => typeof s != "string"))) + throw new TypeError(`"${option}" option must be an array of strings`); + return algorithms === void 0 ? void 0 : new Set(algorithms); +} +function validateCritDuplicates(Err, protectedHeader) { + const { crit } = protectedHeader ?? {}; + if (Array.isArray(crit) && new Set(crit).size !== crit.length) + throw new Err('"crit" (Critical) Header Parameter MUST NOT contain duplicate values'); +} +function validateCrit(Err, recognizedDefault, recognizedOption, protectedHeader, joseHeader) { + if (joseHeader.crit !== void 0 && protectedHeader?.crit === void 0) + throw new Err('"crit" (Critical) Header Parameter MUST be integrity protected'); + if (!protectedHeader || protectedHeader.crit === void 0) + return []; + if (!Array.isArray(protectedHeader.crit) || protectedHeader.crit.length === 0 || protectedHeader.crit.some((input) => typeof input != "string" || input.length === 0)) + throw new Err('"crit" (Critical) Header Parameter MUST be an array of non-empty strings when present'); + const recognized = recognizedOption === void 0 ? recognizedDefault : { __proto__: null, ...recognizedOption, ...recognizedDefault }; + for (const parameter of protectedHeader.crit) { + if (!(parameter in recognized)) + throw new JOSENotSupported(`Extension Header Parameter "${parameter}" is not recognized`); + if (!Object.hasOwn(joseHeader, parameter) || joseHeader[parameter] === void 0) + throw new Err(`Extension Header Parameter "${parameter}" is missing`); + if (recognized[parameter] && (!Object.hasOwn(protectedHeader, parameter) || protectedHeader[parameter] === void 0)) + throw new Err(`Extension Header Parameter "${parameter}" MUST be integrity protected`); + } + return protectedHeader.crit; +} +function validateB64(protectedHeader, extensions) { + if (extensions.includes("b64")) { + const b64 = protectedHeader.b64; + if (typeof b64 != "boolean") + throw new JWSInvalid('The "b64" (base64url-encode payload) Header Parameter must be a boolean'); + return b64; + } + return !0; +} +function serializeJoseHeader(Err, header) { + let serialized, parsed; + try { + serialized = JSON.stringify(header), parsed = JSON.parse(serialized); + } catch (cause) { + throw new Err("JOSE Header is not valid JSON", { cause }); + } + if (!isObject(parsed)) + throw new Err("JOSE Header is not a JSON object"); + return [parsed, serialized]; +} +export { + JWE_RECOGNIZED, + JWS_RECOGNIZED, + assertNotSet, + assertUint8Array, + decodeBase64url, + encodeBase64url, + isDisjoint, + isJwkSet, + isObject, + parseJoseHeader, + serializeJoseHeader, + validateAlgorithms, + validateB64, + validateCrit, + validateCritDuplicates +}; diff --git a/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/util/base64url.js b/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/util/base64url.js new file mode 100644 index 000000000..4447baa6d --- /dev/null +++ b/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/util/base64url.js @@ -0,0 +1,16 @@ +import { encoder, decoder, encodeBase64, decodeBase64 } from "../lib/buffer_utils.js"; +const invalid = "The input to be decoded is not correctly encoded."; +function decode(input) { + try { + return decodeBase64(typeof input == "string" ? input : decoder.decode(input), !0); + } catch (cause) { + throw new TypeError(invalid, { cause }); + } +} +function encode(input) { + return encodeBase64(typeof input == "string" ? encoder.encode(input) : input, !0); +} +export { + decode, + encode +}; diff --git a/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/util/errors.js b/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/util/errors.js new file mode 100644 index 000000000..b7d579f51 --- /dev/null +++ b/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/util/errors.js @@ -0,0 +1,109 @@ +class JOSEError extends Error { + static code = "ERR_JOSE_GENERIC"; + code = "ERR_JOSE_GENERIC"; + constructor(message, options) { + super(message, options), this.name = this.constructor.name, Error.captureStackTrace?.(this, this.constructor); + } +} +class JWTClaimValidationFailed extends JOSEError { + static code = "ERR_JWT_CLAIM_VALIDATION_FAILED"; + code = "ERR_JWT_CLAIM_VALIDATION_FAILED"; + claim; + reason; + payload; + constructor(message, payload, claim = "unspecified", reason = "unspecified") { + super(message, { cause: { claim, reason, payload } }), this.claim = claim, this.reason = reason, this.payload = payload; + } +} +class JWTExpired extends JOSEError { + static code = "ERR_JWT_EXPIRED"; + code = "ERR_JWT_EXPIRED"; + claim; + reason; + payload; + constructor(message, payload, claim = "unspecified", reason = "unspecified") { + super(message, { cause: { claim, reason, payload } }), this.claim = claim, this.reason = reason, this.payload = payload; + } +} +class JOSEAlgNotAllowed extends JOSEError { + static code = "ERR_JOSE_ALG_NOT_ALLOWED"; + code = "ERR_JOSE_ALG_NOT_ALLOWED"; +} +class JOSENotSupported extends JOSEError { + static code = "ERR_JOSE_NOT_SUPPORTED"; + code = "ERR_JOSE_NOT_SUPPORTED"; +} +class JWEDecryptionFailed extends JOSEError { + static code = "ERR_JWE_DECRYPTION_FAILED"; + code = "ERR_JWE_DECRYPTION_FAILED"; + constructor(message = "decryption operation failed", options) { + super(message, options); + } +} +class JWEInvalid extends JOSEError { + static code = "ERR_JWE_INVALID"; + code = "ERR_JWE_INVALID"; +} +class JWSInvalid extends JOSEError { + static code = "ERR_JWS_INVALID"; + code = "ERR_JWS_INVALID"; +} +class JWTInvalid extends JOSEError { + static code = "ERR_JWT_INVALID"; + code = "ERR_JWT_INVALID"; +} +class JWKInvalid extends JOSEError { + static code = "ERR_JWK_INVALID"; + code = "ERR_JWK_INVALID"; +} +class JWKSInvalid extends JOSEError { + static code = "ERR_JWKS_INVALID"; + code = "ERR_JWKS_INVALID"; +} +class JWKSNoMatchingKey extends JOSEError { + static code = "ERR_JWKS_NO_MATCHING_KEY"; + code = "ERR_JWKS_NO_MATCHING_KEY"; + constructor(message = "no applicable key found in the JSON Web Key Set", options) { + super(message, options); + } +} +class JWKSMultipleMatchingKeys extends JOSEError { + [Symbol.asyncIterator] = async function* () { + }; + static code = "ERR_JWKS_MULTIPLE_MATCHING_KEYS"; + code = "ERR_JWKS_MULTIPLE_MATCHING_KEYS"; + constructor(message = "multiple matching keys found in the JSON Web Key Set", options) { + super(message, options); + } +} +class JWKSTimeout extends JOSEError { + static code = "ERR_JWKS_TIMEOUT"; + code = "ERR_JWKS_TIMEOUT"; + constructor(message = "request timed out", options) { + super(message, options); + } +} +class JWSSignatureVerificationFailed extends JOSEError { + static code = "ERR_JWS_SIGNATURE_VERIFICATION_FAILED"; + code = "ERR_JWS_SIGNATURE_VERIFICATION_FAILED"; + constructor(message = "signature verification failed", options) { + super(message, options); + } +} +export { + JOSEAlgNotAllowed, + JOSEError, + JOSENotSupported, + JWEDecryptionFailed, + JWEInvalid, + JWKInvalid, + JWKSInvalid, + JWKSMultipleMatchingKeys, + JWKSNoMatchingKey, + JWKSTimeout, + JWSInvalid, + JWSSignatureVerificationFailed, + JWTClaimValidationFailed, + JWTExpired, + JWTInvalid +}; diff --git a/tests/corpus/7132-jose-pbes2/node_modules/jose/package.json b/tests/corpus/7132-jose-pbes2/node_modules/jose/package.json new file mode 100644 index 000000000..bcb353430 --- /dev/null +++ b/tests/corpus/7132-jose-pbes2/node_modules/jose/package.json @@ -0,0 +1,17 @@ +{ + "name": "jose", + "version": "6.2.12", + "license": "MIT", + "type": "module", + "sideEffects": false, + "exports": { + "./jwt/*": { + "types": "./dist/types/jwt/*.d.ts", + "default": "./dist/webapi/jwt/*.js" + }, + "./errors": { + "types": "./dist/types/util/errors.d.ts", + "default": "./dist/webapi/util/errors.js" + } + } +} diff --git a/tests/corpus/7139-message-typed-records.ts b/tests/corpus/7139-message-typed-records.ts new file mode 100644 index 000000000..35d0d1588 --- /dev/null +++ b/tests/corpus/7139-message-typed-records.ts @@ -0,0 +1,85 @@ +import { MessageChannel } from "node:worker_threads"; + +// Typed records posted on a MessageChannel arrive as plain objects with +// the same keys, order and values as in Node: optional fields present and +// absent, nested records, a record reached twice, a record nested deeper +// than the encoder recurses, a record parsed with undeclared keys, and a +// function-valued field, which postMessage rejects. +type Point = { x: number; y: number }; +type Item = { id: number; name: string; done: boolean; pos: Point; note?: string }; +type Pair = { left: Point; right: Point }; +type Link = { value: number; next?: Link }; + +const { port1, port2 } = new MessageChannel(); +const lines: string[] = []; +const expected = 7; +port1.on("message", (value: unknown) => { + let checks = ""; + if (!Array.isArray(value)) { + const object = value as Record; + if ("left" in object) checks += ` same ${object["left"] === object["right"]}`; + const pos = object["pos"] as Record | undefined; + if (pos !== undefined) checks += ` negative zero ${Object.is(pos["x"], -0)}`; + } + lines.push(JSON.stringify(value) + checks); + if (lines.length === expected) { + for (const line of lines) console.log(line); + port1.close(); + } +}); + +const item: Item = { id: 1, name: "first", done: false, pos: { x: 1, y: 2 } }; +port2.postMessage(item); +const noted: Item = { id: 2, name: "", done: true, pos: { x: -0, y: 0.5 }, note: "n" }; +port2.postMessage(noted); + +const shared: Point = { x: 3, y: 4 }; +const pair: Pair = { left: shared, right: shared }; +port2.postMessage(pair); + +let chain: Link = { value: 0 }; +for (let i = 1; i < 40; i++) chain = { value: i, next: chain }; +port2.postMessage(chain); + +const parsed = JSON.parse('{"y":6,"extra":true,"x":5}') as Point; +port2.postMessage(parsed); +port2.postMessage({ x: 7, y: 8 } as Point); +port2.postMessage([item.pos, noted.pos]); + +type WithFunction = { id: number; run: () => number }; +const bad: WithFunction = { id: 9, run: () => 1 }; +try { + port2.postMessage(bad); +} catch (error) { + console.log("rejected", (error as Error).name); +} + +// Fields that hold objects with getters: each field's value is read +// completely, its getters included, before the next field's, as in Node. +type Holder = { id: number; first: unknown; second: unknown }; +const reads: string[] = []; +const getters: Holder = { + id: 1, + first: { + get a() { + reads.push("first.a"); + return 1; + }, + nested: { + get b() { + reads.push("first.nested.b"); + return 2; + }, + }, + }, + second: { + get c() { + reads.push("second.c"); + return 3; + }, + }, +}; +const { port1: getterPort, port2: getterSender } = new MessageChannel(); +getterSender.postMessage(getters); +console.log("getter order", reads.join(",")); +getterPort.close(); diff --git a/tests/corpus/7141-comoving-loop-counters.ts b/tests/corpus/7141-comoving-loop-counters.ts new file mode 100644 index 000000000..a756b7a46 --- /dev/null +++ b/tests/corpus/7141-comoving-loop-counters.ts @@ -0,0 +1,144 @@ +// For loops with a second counter that steps beside the tested one +// (`i++, j += 4`): pixel strides over typed arrays, interleaving into number +// arrays, descending and inclusive loops, three counters, early exits, +// starts and limits that are fractions, negative zero, NaN, infinite or +// near 2^53, counters that live across a yield or an await, and counters a +// closure captures, the body writes or the condition reads. +function luminance(px: Uint8Array, count: number): number[] { + const out: number[] = []; + for (let i = 0, p = 0; i < count; i++, p += 4) { + out.push((px[p]! * 77 + px[p + 1]! * 150 + px[p + 2]! * 29) >> 8); + } + return out; +} +const px = new Uint8Array(32); +for (let k = 0; k < px.length; k++) px[k] = (k * 37 + 11) & 255; +console.log(luminance(px, 8).join(",")); +// Past the end, reads are undefined and the sums NaN, which `>> 8` makes 0. +console.log(luminance(px, 10).join(",")); +console.log(luminance(px, 0).length, luminance(px, -3).length, luminance(px, 2.5).join(",")); +console.log(luminance(px, NaN).length); + +function interleave(a: number[], b: number[]): number[] { + const out: number[] = new Array(a.length * 2).fill(-1); + const n = a.length; + for (let i = 0, j = 0; i < n; i++, j += 2) { + out[j] = a[i]!; + out[j + 1] = b[i]!; + } + return out; +} +console.log(interleave([1, 2, 3, 4], [10, 20, 30, 40]).join(",")); +console.log(interleave([], []).length); + +// The second counter's start is not proven: charCodeAt truncates its index, +// and the counter's own values keep JS semantics. +function codes(s: string, count: number, from: number, step: number): string { + const out: (number | string)[] = []; + if (step === 3) { + for (let i = 0, j = from; i < count; i++, j += 3) out.push(s.charCodeAt(j), j); + } else { + for (let i = 0, j = from; i < count; i++, j -= 2) out.push(s.charCodeAt(j), j); + } + return out.join(","); +} +const word = "abcdefghijklmnop"; +console.log(codes(word, 4, 0, 3), "|", codes(word, 4, 15, -2)); +console.log(codes(word, 4, 0.5, 3), "|", codes(word, 3, 9.75, -2)); +const negativeZero = -0; +console.log(codes(word, 2, negativeZero, 3), Object.is(negativeZero, -0)); +console.log(codes(word, 2, NaN, 3), "|", codes(word, 2, Infinity, -2), "|", codes(word, 2, -Infinity, 3)); +// Near 2^52 the run-time test passes or fails on the trip count; near 2^53 +// the double counter rounds (odd values above 2^53 are not representable). +console.log(codes(word, 4, 2 ** 52 - 16, 3)); +console.log(codes(word, 6, 2 ** 52 - 4, 3)); +console.log(codes(word, 6, 2 ** 53 - 10, 3)); +console.log(codes(word, 4, -(2 ** 53) + 3, -2)); +// A limit that is not finite or not an integer. +function capped(s: string, limit: number): string { + const out: number[] = []; + for (let i = 0, j = 1; i < limit; i++, j += 2) { + if (j >= s.length) break; + out.push(s.charCodeAt(j)); + } + return out.join(","); +} +console.log(capped(word, Infinity), "|", capped(word, 3.5), "|", capped(word, NaN), "|", capped(word, -Infinity)); + +// A descending driver with an ascending follower, inclusive bounds and +// larger steps. +function mirror(s: string): string { + const out: string[] = []; + for (let i = s.length - 1, j = 0; i >= 0; i--, j += 2) out.push(`${s.charCodeAt(i)}@${j}`); + return out.join(" "); +} +console.log(mirror("scriptc")); +function stepped(bytes: Uint8Array, last: number): string { + const out: number[] = []; + for (let i = 0, j = bytes.length - 1; i <= last; i += 3, j -= 5) + out.push(bytes[i]! | 0, bytes[j]! | 0, j); + return out.join(","); +} +console.log(stepped(px, 9), "|", stepped(px, 30)); + +// Three counters, continue and break. +function triple(a: Uint8Array, n: number): string { + const out: number[] = []; + for (let i = 0, j = 10, k = 100; i < n; i++, j -= 1, k += 7) { + if (i % 3 === 1) continue; + if (k > 150) break; + out.push(a[i]!, a[j]!, k); + } + return out.join(","); +} +console.log(triple(px, 12), "|", triple(px, 4)); + +// A counted limit with a known range and a follower used by a remainder. +let rotated = 0; +for (let i = 0, j = 5; i < 1000; i++, j += 3) rotated = (rotated * 31 + px[j % 32]!) % 1000003; +console.log(rotated); + +// The follower starting from a driver's start of unproven exactness. +function window(bytes: Uint8Array, from: number, to: number): string { + const out: number[] = []; + for (let i = from, j = 0; i < to; i++, j += 2) out.push(bytes[i]! | 0, bytes[j]! | 0, i, j); + return out.join(","); +} +console.log(window(px, 2, 5), "|", window(px, 1.5, 4), "|", window(px, -0, 2)); + +// Counters a closure captures keep one binding per iteration; a follower the +// body writes, one the condition reads, and a start that reads another +// counter keep their JS values. +const reads: (() => number)[] = []; +for (let i = 0, j = 0; i < 3; i++, j += 4) { + reads.push(() => j); + void px[j]; +} +console.log(reads.map((read) => read()).join(",")); +const written: number[] = []; +for (let i = 0, j = 0; i < 5; i++, j += 2) { + if (i === 2) j += 7; + written.push(px[j]!, j); +} +console.log(written.join(",")); +const chased: number[] = []; +for (let i = 0, j = 5; i < j; i += 2, j++) chased.push(px[i]!, i, j); +console.log(chased.join(",")); +const offset: number[] = []; +for (let i = 1, j = i + 1; i < 4; i++, j += 2) offset.push(px[j]!, j); +console.log(offset.join(",")); + +// Counters that live across a yield or an await. +function* strided(bytes: Uint8Array, n: number): Generator { + for (let i = 0, j = 1; i < n; i++, j += 3) yield bytes[j]! + i; +} +console.log([...strided(px, 6)].join(",")); +async function weighted(bytes: Uint8Array, n: number): Promise { + let sum = 0; + for (let i = 0, j = 2; i < n; i++, j += 5) { + await null; + sum += bytes[j]! * (i + 1); + } + return sum; +} +void (async () => console.log(await weighted(px, 6), await weighted(px, 3)))(); diff --git a/tests/corpus/7143-optional-read-integer-index.ts b/tests/corpus/7143-optional-read-integer-index.ts new file mode 100644 index 000000000..43fd59a6e --- /dev/null +++ b/tests/corpus/7143-optional-read-integer-index.ts @@ -0,0 +1,70 @@ +// Element reads that may be undefined, at indices with a proven integer +// range: loop counters with an unknown limit, `i | 0`, `x >>> 0`, negative +// counters, -0, indices past the length and the capacity, and copies +// `a[x] = b[y]!` that meet holes. +function copy(src: number[], n: number): number[] { + const out: number[] = new Array(n); + for (let i = 0; i < n; i++) out[i] = src[i]!; + return out; +} +const src: number[] = [1, 2, 3, 4]; +const copied = copy(src, 6); +console.log(copied.length, copied.join(","), 4 in copied, copied[5] === undefined); + +const holes: number[] = new Array(5); +holes[0] = 10; +holes[3] = 13; +const fromHoles = copy(holes, 5); +console.log(fromHoles.join(","), 1 in fromHoles, 3 in fromHoles); + +function show(a: (string | undefined)[], from: number, to: number): string { + const out: string[] = []; + for (let i = from; i < to; i++) { + const v = a[i]; + out.push(v === undefined ? "_" : v); + } + return out.join(","); +} +const words: (string | undefined)[] = ["a", undefined, "c"]; +words[5] = "f"; +console.log(show(words, -3, 8)); +console.log(show(words, 0, 0), show(words, 2, 1)); + +function masked(a: number[], keys: number[]): string { + const out: string[] = []; + for (const k of keys) { + const v = a[k | 0]; + const w = a[k >>> 0]; + out.push(`${v === undefined ? "-" : v}/${w === undefined ? "-" : w}`); + } + return out.join(" "); +} +const values: number[] = [5, 6, 7]; +console.log(masked(values, [0, 1.9, 2, 3, -1, -0, 4294967297, 2 ** 31])); + +const negativeZero = -0; +console.log(values[negativeZero], values[0 * -1], Object.is(negativeZero, -0)); + +// A counter that starts below zero and one that passes the capacity. +let total = 0; +for (let i = -2; i < values.length + 3; i++) total += values[i] ?? 100; +console.log(total); +const big: number[] = []; +for (let k = 0; k < 20; k++) big.push(k); +big.length = 10; +let seen = 0; +for (let i = 0; i < 40; i++) if (big[i] !== undefined) seen++; +console.log(seen, big.length); + +// Rotations through the read, with a hole that becomes undefined. +function rotate(p: number[], r: number): void { + const first = p[0]!; + for (let i = 0; i < r; i++) p[i] = p[i + 1]!; + p[r] = first; +} +const ring: number[] = new Array(4); +ring[0] = 0; +ring[1] = 1; +ring[3] = 3; +rotate(ring, 3); +console.log(ring.join(","), 1 in ring, ring[1] === undefined); diff --git a/tests/corpus/7150-cipher-legacy-modes.ts b/tests/corpus/7150-cipher-legacy-modes.ts new file mode 100644 index 000000000..80edfd029 --- /dev/null +++ b/tests/corpus/7150-cipher-legacy-modes.ts @@ -0,0 +1,74 @@ +// createCipheriv and createDecipheriv in the legacy modes: AES in ECB, +// CFB, CFB8, CFB1 and OFB, triple DES (three- and two-key) in CBC, ECB, +// CFB, CFB8, CFB1 and OFB, and bare chacha20. Fixed keys, IVs and inputs +// give Node's exact ciphertexts; chunked updates, string encodings, +// disabled padding and chacha20's 64-bit counter carry round-trip. +import { createCipheriv, createDecipheriv } from "node:crypto"; + +function bytes(length: number, seed: number): Buffer { + const out = Buffer.alloc(length); + for (let i = 0; i < length; i++) out[i] = (i * 29 + seed) & 0xff; + return out; +} + +const plaintext = Buffer.from("The five boxing wizards jump quickly; sphinx of black quartz, judge my vow."); + +function encrypt(name: string, key: Buffer, iv: Buffer | null, data: Buffer, chunk: number): Buffer { + const cipher = createCipheriv(name, key, iv); + const parts: Buffer[] = []; + for (let off = 0; off < data.length; off += chunk) parts.push(cipher.update(data.subarray(off, off + chunk))); + parts.push(cipher.final()); + return Buffer.concat(parts); +} + +function decrypt(name: string, key: Buffer, iv: Buffer | null, data: Buffer, chunk: number): Buffer { + const decipher = createDecipheriv(name, key, iv); + const parts: Buffer[] = []; + for (let off = 0; off < data.length; off += chunk) parts.push(decipher.update(data.subarray(off, off + chunk))); + parts.push(decipher.final()); + return Buffer.concat(parts); +} + +const cases: [string, number, number][] = []; +for (const bits of [128, 192, 256]) + for (const mode of ["ecb", "cfb", "cfb8", "cfb1", "ofb"]) cases.push([`aes-${bits}-${mode}`, bits / 8, mode === "ecb" ? 0 : 16]); +for (const name of ["des-ede3-cbc", "des3", "des-ede3", "des-ede3-ecb", "des-ede3-cfb", "des-ede3-cfb8", "des-ede3-cfb1", "des-ede3-ofb"]) + cases.push([name, 24, name === "des-ede3" || name === "des-ede3-ecb" ? 0 : 8]); +for (const name of ["des-ede-cbc", "des-ede", "des-ede-ecb", "des-ede-cfb", "des-ede-ofb"]) + cases.push([name, 16, name === "des-ede" || name === "des-ede-ecb" ? 0 : 8]); +cases.push(["chacha20", 32, 16]); + +for (const [name, keyLength, ivLength] of cases) { + const key = bytes(keyLength, keyLength + 3); + const iv = ivLength ? bytes(ivLength, 11) : null; + const sealed = encrypt(name, key, iv, plaintext, plaintext.length); + const chunked = encrypt(name, key, iv, plaintext, 7); + const opened = decrypt(name, key, iv, sealed, 5); + console.log(name.padEnd(14), sealed.length, sealed.toString("hex").slice(0, 48), chunked.equals(sealed), opened.equals(plaintext)); +} + +// String input and output encodings through a stream mode. +const ofb = createCipheriv("aes-128-ofb", bytes(16, 1), bytes(16, 2)); +const encoded = ofb.update("68656c6c6f20776f726c64", "hex", "base64") + ofb.final("base64"); +const unofb = createDecipheriv("aes-128-ofb", bytes(16, 1), bytes(16, 2)); +console.log(encoded, unofb.update(encoded, "base64", "utf8") + unofb.final("utf8")); + +// ECB and triple-DES CBC without padding over whole blocks. +for (const [name, keyLength, ivLength, block] of [["aes-256-ecb", 32, 0, 16], ["des-ede3-cbc", 24, 8, 8]] as const) { + const cipher = createCipheriv(name, bytes(keyLength, 5), ivLength ? bytes(ivLength, 6) : null); + cipher.setAutoPadding(false); + const sealed = Buffer.concat([cipher.update(bytes(block * 3, 7)), cipher.final()]); + const decipher = createDecipheriv(name, bytes(keyLength, 5), ivLength ? bytes(ivLength, 6) : null); + decipher.setAutoPadding(false); + const opened = Buffer.concat([decipher.update(sealed), decipher.final()]); + console.log(name, "unpadded", sealed.length, sealed.toString("hex").slice(0, 32), opened.equals(bytes(block * 3, 7))); +} + +// chacha20's counter is the IV's first 8 bytes, little-endian: the low +// word carries into the high word, which wraps. +const chachaKey = bytes(32, 9); +for (const counter of ["ffffffff00000000", "ffffffffffffffff", "fffffffe01000000"]) { + const iv = Buffer.from(counter + "0102030405060708", "hex"); + const stream = encrypt("chacha20", chachaKey, iv, Buffer.alloc(200), 50); + console.log("chacha20", counter, stream.toString("hex").slice(120, 152), stream.subarray(64, 96).toString("hex")); +} diff --git a/tests/corpus/7151-cipher-legacy-errors.cjs b/tests/corpus/7151-cipher-legacy-errors.cjs new file mode 100644 index 000000000..fe9c59bcc --- /dev/null +++ b/tests/corpus/7151-cipher-legacy-errors.cjs @@ -0,0 +1,83 @@ +// createCipheriv errors for the legacy modes, in Node's order: the IV +// length each mode takes (none for ECB), the key length, OpenSSL's +// "unsupported" for the ciphers its legacy provider holds, and the block +// modes' final-block and padding failures; the AEAD-only methods throw +// Node's invalid-state errors on the stream modes. +const crypto = require("crypto"); + +function attempt(label, run) { + try { + const value = run(); + console.log(label, "ok", Buffer.isBuffer(value) ? value.toString("hex") : value); + } catch (e) { + console.log(label, e.name, e.code, e.message, e.library, e.reason, JSON.stringify(e.opensslErrorStack)); + } +} + +const k16 = Buffer.alloc(16, 1), k24 = Buffer.alloc(24, 1), k32 = Buffer.alloc(32, 1); +attempt("ecb null iv", () => crypto.createCipheriv("aes-128-ecb", k16, null).final()); +attempt("ecb empty iv", () => crypto.createCipheriv("aes-128-ecb", k16, Buffer.alloc(0)).final()); +attempt("ecb 16-byte iv", () => crypto.createCipheriv("aes-128-ecb", k16, Buffer.alloc(16))); +attempt("cfb 15-byte iv", () => crypto.createCipheriv("aes-128-cfb", k16, Buffer.alloc(15))); +attempt("cfb null iv", () => crypto.createCipheriv("aes-128-cfb", k16, null)); +attempt("ofb key 15", () => crypto.createCipheriv("aes-128-ofb", Buffer.alloc(15), Buffer.alloc(16))); +attempt("des3 16-byte iv", () => crypto.createCipheriv("des-ede3-cbc", k24, Buffer.alloc(16))); +attempt("des3 16-byte key", () => crypto.createCipheriv("des-ede3-cbc", k16, Buffer.alloc(8))); +attempt("des-ede 24-byte key", () => crypto.createCipheriv("des-ede-cbc", k24, Buffer.alloc(8))); +attempt("des-ede3 ecb with iv", () => crypto.createCipheriv("des-ede3", k24, Buffer.alloc(8))); +attempt("chacha20 12-byte iv", () => crypto.createCipheriv("chacha20", k32, Buffer.alloc(12))); +attempt("chacha20 authTagLength ignored", () => crypto.createCipheriv("chacha20", k32, Buffer.alloc(16), { authTagLength: 16 }).update("x")); +attempt("upper-case name", () => crypto.createCipheriv("DES-EDE3-CBC", k24, Buffer.alloc(8)).final()); +attempt("des-cbc", () => crypto.createCipheriv("des-cbc", Buffer.alloc(8), Buffer.alloc(8))); +attempt("des-cbc bad iv", () => crypto.createCipheriv("des-cbc", Buffer.alloc(8), Buffer.alloc(3))); +attempt("des-cbc bad key", () => crypto.createCipheriv("des-cbc", Buffer.alloc(3), Buffer.alloc(8))); +attempt("bf-cbc", () => crypto.createDecipheriv("bf-cbc", k16, Buffer.alloc(8))); +attempt("rc4", () => crypto.createCipheriv("rc4", k16, null)); +attempt("rc4 with iv", () => crypto.createCipheriv("rc4", k16, Buffer.alloc(8))); +attempt("seed-ecb", () => crypto.createCipheriv("seed-ecb", k16, null)); +attempt("unknown cipher", () => crypto.createCipheriv("aes-128-xyz", k16, Buffer.alloc(16))); + +attempt("cfb getAuthTag", () => { + const c = crypto.createCipheriv("aes-128-cfb", k16, Buffer.alloc(16)); + c.update("x"); + c.final(); + return c.getAuthTag(); +}); +attempt("ofb setAAD", () => crypto.createCipheriv("aes-128-ofb", k16, Buffer.alloc(16)).setAAD(Buffer.alloc(1))); +attempt("chacha20 setAuthTag", () => crypto.createDecipheriv("chacha20", k32, Buffer.alloc(16)).setAuthTag(Buffer.alloc(16))); +attempt("cfb update sizes", () => { + const c = crypto.createCipheriv("aes-128-cfb", k16, Buffer.alloc(16)); + return [c.update("abc").length, c.update("defghijklmnopqrstu").length, c.final().length].join(","); +}); +attempt("cfb1 setAutoPadding(false)", () => { + const c = crypto.createCipheriv("aes-128-cfb1", k16, Buffer.alloc(16)); + c.setAutoPadding(false); + return Buffer.concat([c.update("abc"), c.final()]); +}); +attempt("ecb unpadded short", () => { + const c = crypto.createCipheriv("aes-128-ecb", k16, null); + c.setAutoPadding(false); + c.update(Buffer.alloc(31, 7)); + return c.final(); +}); +attempt("des3 unpadded short", () => { + const c = crypto.createCipheriv("des-ede3-cbc", k24, Buffer.alloc(8)); + c.setAutoPadding(false); + c.update(Buffer.alloc(7)); + return c.final(); +}); +attempt("des3 bad decrypt", () => { + const d = crypto.createDecipheriv("des-ede3-cbc", k24, Buffer.alloc(8)); + d.update(Buffer.alloc(16, 9)); + return d.final(); +}); +attempt("ecb decrypt 15 bytes", () => { + const d = crypto.createDecipheriv("aes-128-ecb", k16, null); + d.update(Buffer.alloc(15)); + return d.final(); +}); +attempt("ofb update after final", () => { + const c = crypto.createCipheriv("aes-128-ofb", k16, Buffer.alloc(16)); + c.final(); + return c.update("x"); +}); diff --git a/tests/corpus/7152-crypto-prime.ts b/tests/corpus/7152-crypto-prime.ts new file mode 100644 index 000000000..79edcc502 --- /dev/null +++ b/tests/corpus/7152-crypto-prime.ts @@ -0,0 +1,96 @@ +// checkPrimeSync, checkPrime, generatePrimeSync and generatePrime: known +// primes, composites and strong pseudoprimes as bigints, Buffers, typed +// arrays, ArrayBuffers and DataViews, the checks option, and generated +// primes' length, top bits, primality, residues and safety, as +// ArrayBuffers and bigints, synchronously and through the callbacks (whose +// error parameter, typed Error | null, receives null where Node passes +// undefined). The small sizes and the add and rem forms have one possible +// result. +import { checkPrime, checkPrimeSync, generatePrime, generatePrimeSync } from "node:crypto"; + +const mersenne61 = 2n ** 61n - 1n; +const mersenne127 = 2n ** 127n - 1n; +const mersenne521 = 2n ** 521n - 1n; +const known: [string, bigint][] = [ + ["0", 0n], + ["1", 1n], + ["2", 2n], + ["3", 3n], + ["4", 4n], + ["9", 9n], + ["17863", 17863n], + ["17881", 17881n], + ["561 (Carmichael)", 561n], + ["2047 (strong pseudoprime to 2)", 2047n], + ["3215031751 (strong pseudoprime to 2, 3, 5 and 7)", 3215031751n], + ["2^61 - 1", mersenne61], + ["2^61 + 1", mersenne61 + 2n], + ["2^127 - 1", mersenne127], + ["(2^61 - 1)(2^127 - 1)", mersenne61 * mersenne127], + ["2^521 - 1", mersenne521], + ["2^521 + 1", mersenne521 + 2n], +]; +for (const [label, value] of known) console.log(label, checkPrimeSync(value)); + +console.log("Buffer 13", checkPrimeSync(Buffer.from([0, 0, 13]))); +console.log("Uint8Array 15", checkPrimeSync(new Uint8Array([15]))); +const words = new ArrayBuffer(8); +new DataView(words).setBigUint64(0, mersenne61); +console.log("ArrayBuffer 2^61 - 1", checkPrimeSync(words)); +console.log("DataView 2^61 - 1", checkPrimeSync(new DataView(words))); +console.log("empty", checkPrimeSync(new Uint8Array(0))); +console.log("checks 1", checkPrimeSync(97n, { checks: 1 })); +console.log("checks 200", checkPrimeSync(3215031751n, { checks: 200 })); + +const bitLength = (n: bigint): number => n.toString(2).length; +const fromBuffer = (buffer: ArrayBuffer): bigint => BigInt("0x" + Buffer.from(buffer).toString("hex")); + +for (const bits of [2, 3, 4]) console.log("size", bits, generatePrimeSync(bits, { bigint: true })); +const five = generatePrimeSync(5, { bigint: true }); +console.log("size 5", five === 29n || five === 31n); +for (const bits of [8, 16, 31, 32, 33, 64, 127, 256, 512]) { + const p = generatePrimeSync(bits, { bigint: true }); + console.log("size", bits, bitLength(p) === bits, p >> BigInt(bits - 2), checkPrimeSync(p)); +} +const buffer = generatePrimeSync(96); +console.log("ArrayBuffer", buffer instanceof ArrayBuffer, buffer.byteLength, checkPrimeSync(buffer), bitLength(fromBuffer(buffer))); +console.log("bigint false", generatePrimeSync(40, { bigint: false }).byteLength); + +for (const bits of [3, 6, 7]) console.log("safe", bits, generatePrimeSync(bits, { safe: true, bigint: true })); +for (const bits of [16, 64, 128]) { + const p = generatePrimeSync(bits, { safe: true, bigint: true }); + console.log("safe", bits, bitLength(p) === bits, p >> BigInt(bits - 2), checkPrimeSync(p), checkPrimeSync((p - 1n) / 2n)); +} + +console.log("add 200 rem 1", generatePrimeSync(8, { add: 200n, rem: 1n, bigint: true })); +console.log("add 128", generatePrimeSync(8, { add: 128n, bigint: true })); +console.log("add 16 rem 3", generatePrimeSync(5, { add: 16n, rem: 3n, bigint: true })); +const safe4 = generatePrimeSync(5, { add: 4n, safe: true, bigint: true }); +console.log("add 4 safe", safe4 === 23n || safe4 === 47n); +for (const [label, add, rem] of [ + ["add 12 rem 11", 12n, 11n], + ["add 6", 6n, undefined], + ["add 1000 rem 7", 1000n, 7n], +] as const) { + const p = generatePrimeSync(48, rem === undefined ? { add, bigint: true } : { add, rem, bigint: true }); + console.log(label, p % add, checkPrimeSync(p), bitLength(p) >= 48); +} +const viaBuffer = generatePrimeSync(64, { add: Buffer.from([0x01, 0x00]), rem: new Uint8Array([0x0f]), bigint: true }); +console.log("add and rem as bytes", viaBuffer % 256n, checkPrimeSync(viaBuffer)); +const safeAdd = generatePrimeSync(64, { add: 24n, rem: 23n, safe: true, bigint: true }); +console.log("safe add 24 rem 23", safeAdd % 24n, checkPrimeSync((safeAdd - 1n) / 2n)); +console.log("rem without add", generatePrimeSync(16, { rem: 5n, bigint: true }) < 65536n); + +generatePrime(64, (err, prime) => { + console.log("callback", err ?? "no error", prime instanceof ArrayBuffer, prime.byteLength, checkPrimeSync(prime)); + generatePrime(128, { safe: true, bigint: true }, (err2, safe) => { + console.log("callback safe", err2 ?? "no error", bitLength(safe), checkPrimeSync((safe - 1n) / 2n)); + checkPrime(safe, (err3, result) => { + console.log("checkPrime", err3 ?? "no error", result); + checkPrime(Buffer.from([1, 1]), { checks: 3 }, (err4, result2) => { + console.log("checkPrime 257", err4 ?? "no error", result2); + }); + }); + }); +}); +console.log("sync done"); diff --git a/tests/corpus/7153-crypto-prime-errors.cjs b/tests/corpus/7153-crypto-prime-errors.cjs new file mode 100644 index 000000000..ff32e6e09 --- /dev/null +++ b/tests/corpus/7153-crypto-prime-errors.cjs @@ -0,0 +1,80 @@ +// checkPrime, checkPrimeSync, generatePrime and generatePrimeSync errors from +// JavaScript: argument types and ranges, the options bag, add and rem, and +// OpenSSL's generation failures, synchronously and through the callbacks. +const crypto = require("crypto"); + +function attempt(label, run) { + try { + const value = run(); + console.log(label, "ok", typeof value === "bigint" ? `${value}n` : value instanceof ArrayBuffer ? `ArrayBuffer(${value.byteLength})` : value); + } catch (e) { + console.log(label, e.name, e.code, e.message, JSON.stringify(Object.keys(e))); + } +} + +attempt("check string", () => crypto.checkPrimeSync("7")); +attempt("check number", () => crypto.checkPrimeSync(7)); +attempt("check negative", () => crypto.checkPrimeSync(-7n)); +attempt("check large negative", () => crypto.checkPrimeSync(-(2n ** 40n))); +attempt("check options null", () => crypto.checkPrimeSync(7n, null)); +attempt("check options array", () => crypto.checkPrimeSync(7n, [])); +attempt("check options string", () => crypto.checkPrimeSync(7n, "x")); +attempt("check checks string", () => crypto.checkPrimeSync(7n, { checks: "1" })); +attempt("check checks fraction", () => crypto.checkPrimeSync(7n, { checks: 1.5 })); +attempt("check checks -1", () => crypto.checkPrimeSync(7n, { checks: -1 })); +attempt("check checks 2^31", () => crypto.checkPrimeSync(7n, { checks: 2 ** 31 })); +attempt("check checks -0", () => crypto.checkPrimeSync(7n, { checks: -0 })); +attempt("check checks 10", () => crypto.checkPrimeSync(7n, { checks: 10 })); +attempt("check no callback", () => crypto.checkPrime(7n)); +attempt("check callback string", () => crypto.checkPrime(7n, {}, "cb")); +attempt("check async string", () => crypto.checkPrime("7", () => {})); +attempt("check async options null", () => crypto.checkPrime(7n, null, () => {})); + +attempt("size string", () => crypto.generatePrimeSync("8")); +attempt("size fraction", () => crypto.generatePrimeSync(8.5)); +attempt("size 0", () => crypto.generatePrimeSync(0)); +attempt("size 2^31", () => crypto.generatePrimeSync(2 ** 31)); +attempt("size NaN", () => crypto.generatePrimeSync(NaN)); +attempt("size 1", () => crypto.generatePrimeSync(1)); +attempt("size 2 safe", () => crypto.generatePrimeSync(2, { safe: true })); +attempt("size 4 safe", () => crypto.generatePrimeSync(4, { safe: true })); +attempt("size 5 safe", () => crypto.generatePrimeSync(5, { safe: true })); +attempt("options null", () => crypto.generatePrimeSync(8, null)); +attempt("options number", () => crypto.generatePrimeSync(8, 1)); +attempt("safe string", () => crypto.generatePrimeSync(8, { safe: "yes" })); +attempt("bigint number", () => crypto.generatePrimeSync(8, { bigint: 1 })); +attempt("add number", () => crypto.generatePrimeSync(8, { add: 6 })); +attempt("add string", () => crypto.generatePrimeSync(8, { add: "6" })); +attempt("add negative", () => crypto.generatePrimeSync(8, { add: -6n })); +attempt("rem number", () => crypto.generatePrimeSync(8, { add: 6n, rem: 1 })); +attempt("rem negative", () => crypto.generatePrimeSync(8, { add: 6n, rem: -1n })); +attempt("add too long", () => crypto.generatePrimeSync(8, { add: 1024n })); +attempt("add zero", () => crypto.generatePrimeSync(8, { add: 0n })); +attempt("add zero bytes", () => crypto.generatePrimeSync(8, { add: new Uint8Array(0) })); +attempt("rem equal add", () => crypto.generatePrimeSync(8, { add: 6n, rem: 6n })); +attempt("rem above add", () => crypto.generatePrimeSync(8, { add: 6n, rem: 7n })); +attempt("rem without add", () => crypto.generatePrimeSync(8, { rem: 1000n, bigint: true }) < 256n); +attempt("add 1 bigint", () => crypto.generatePrimeSync(8, { add: 1n, bigint: true }) < 256n); +attempt("gen no callback", () => crypto.generatePrime(8)); +attempt("gen options callback", () => crypto.generatePrime(8, {})); +attempt("gen size string", () => crypto.generatePrime("8", () => {})); +attempt("gen add too long", () => crypto.generatePrime(8, { add: 1024n }, () => {})); +attempt("gen options null", () => crypto.generatePrime(8, null, () => {})); + +// The callback forms' generation errors and results, one after another. +crypto.generatePrime(1, function (err, prime) { + console.log("gen 1 callback", arguments.length, err.name, err.code, err.message, JSON.stringify(Object.keys(err)), prime); + crypto.generatePrime(8, { add: 0n }, function (err2, prime2) { + console.log("gen add 0 callback", arguments.length, err2.message, prime2); + crypto.generatePrime(3, { safe: true, bigint: true }, function (err3, prime3) { + console.log("gen 3 safe callback", arguments.length, err3, prime3); + crypto.checkPrime(7n, function (err4, result) { + console.log("check 7 callback", arguments.length, err4, result); + crypto.checkPrime(new Uint8Array([0, 0, 9]), { checks: 3 }, function (err5, result2) { + console.log("check 9 callback", arguments.length, err5, result2); + }); + }); + }); + }); +}); +console.log("sync end"); diff --git a/tests/corpus/7180-generic-local-binding-early-uses.ts b/tests/corpus/7180-generic-local-binding-early-uses.ts new file mode 100644 index 000000000..f3f7816b5 --- /dev/null +++ b/tests/corpus/7180-generic-local-binding-early-uses.ts @@ -0,0 +1,69 @@ +// Generic arrow bindings declared inside functions, used by code that runs +// before or after the declaration: closures created above the declaration +// and hoisted function declarations read the binding like any const, so a +// use before the declaration throws a ReferenceError and a later one sees +// the function. +function describe(error: unknown): string { + return error instanceof Error ? `${error.name}: ${error.message}` : String(error); +} + +function make(): (x: number) => number { + const read = (): ((x: number) => number) => id; + const id: (x: number) => number = (x: T): T => x; + return read(); +} +console.log(make()(7)); + +function outer(flag: boolean): number { + if (flag) return helper(); + const id: (x: number) => number = (x: T): T => x; + return helper(); + + function helper(): number { + return id(1); + } +} +console.log(outer(false)); +try { + console.log(outer(true)); +} catch (error) { + console.log(describe(error)); +} + +function run(): void { + const early = (): number => id(2); + try { + early(); + } catch (error) { + console.log(describe(error)); + } + const id: (x: number) => number = (x: T): T => x; + console.log(early()); +} +run(); + +function counter(): () => number { + let total = 0; + const step = (): number => (total = add(total, 1)); + const add: (a: number, b: number) => number = (a: T, b: T): number => a + b; + step(); + step(); + return step; +} +console.log(counter()()); + +// Each evaluation of the declaration makes a new function object. +const first = make(); +console.log(first === make(), first === first, first(8)); +function looped(): string { + const seen: ((x: string) => string)[] = []; + for (let i = 0; i < 2; i++) { + const read = (): ((x: string) => string) => tag; + const tag: (x: string) => string = function (x: T): T { + return x; + }; + seen.push(read()); + } + return `${seen[0] === seen[1]} ${seen[1]!("p")}`; +} +console.log(looped()); diff --git a/tests/corpus/7181-generic-module-binding-tdz.ts b/tests/corpus/7181-generic-module-binding-tdz.ts new file mode 100644 index 000000000..d2d2915fb --- /dev/null +++ b/tests/corpus/7181-generic-module-binding-tdz.ts @@ -0,0 +1,49 @@ +// A module-scope generic arrow binding is in its temporal dead zone until +// its declaration runs: a hoisted function or an earlier closure that calls +// it during module evaluation throws a ReferenceError, and calls made after +// the declaration reach the function. A `var` binding is undefined until +// then: the call's arguments run, then it throws "is not a function". +function describe(error: unknown): string { + return error instanceof Error ? `${error.name}: ${error.message}` : String(error); +} + +function early(): number { + return id(1); +} +const before = (): string => wrap("b"); + +try { + console.log(early()); +} catch (error) { + console.log(describe(error)); +} +try { + console.log(before()); +} catch (error) { + console.log(describe(error)); +} + +const id = (x: T): T => x; +let wrap = (x: T): string => `[${String(x)}]`; + +console.log(early(), before(), id("direct"), wrap(2)); + +function later(): string { + return wrap(id(3)); +} +console.log(later()); + +function callMissing(): number { + return missing(log("argument")); +} +function log(text: string): number { + console.log(text); + return 1; +} +try { + console.log(callMissing()); +} catch (error) { + console.log(describe(error)); +} +var missing = (x: T): T => x; +console.log(callMissing(), missing(5)); diff --git a/tests/corpus/7182-generic-binding-direct-calls.ts b/tests/corpus/7182-generic-binding-direct-calls.ts new file mode 100644 index 000000000..c7b5397e1 --- /dev/null +++ b/tests/corpus/7182-generic-binding-direct-calls.ts @@ -0,0 +1,42 @@ +// Generic arrow bindings whose uses all run after the declaration: later +// statements, closures created after it, later declarators, recursion and +// function declarations that only later code calls. These calls go +// straight to the compiled instances. +const pair = (x: T): [T, T] => [x, x]; +const doubled = pair(2), labels = pair("a"); +console.log(doubled.join(","), labels.join(",")); + +function useLater(): string { + return pair(true).join(","); +} +console.log(useLater()); + +export function exported(n: number): number { + return pair(n)[0] + 1; +} +console.log(exported(4)); + +function local(): string { + var twice = (x: T): number => x * 2; + const call = (): number => twice(4); + const depth = (xs: T[], n: number): number => (n <= 0 ? xs.length : depth(xs, n - 1) + 1); + return `${twice(1)} ${call()} ${depth([1, 2], 3)} ${helper()}`; + + function helper(): string { + return pair("h").join(""); + } +} +console.log(local()); + +function loop(): string[] { + const out: string[] = []; + const later: (() => string)[] = []; + for (let i = 0; i < 3; i++) { + const tag = (x: T): string => `#${String(x)}`; + later.push(() => tag("late")); + out.push(tag(i * 10)); + } + for (const f of later) out.push(f()); + return out; +} +console.log(loop().join(" ")); diff --git a/tests/corpus/7184-reduce-runtime-accumulator.ts b/tests/corpus/7184-reduce-runtime-accumulator.ts new file mode 100644 index 000000000..bd2c8e4d9 --- /dev/null +++ b/tests/corpus/7184-reduce-runtime-accumulator.ts @@ -0,0 +1,24 @@ +// Array reduce keeps the values its callback and initial value produce at +// run time: an initial value read from a missing element is undefined, a +// callback can produce another primitive through `??`, and a parenthesized +// callback widens like a bare one. +const noStrings: string[] = []; +const noNumbers: number[] = []; + +const fromMissingInit = ([] as number[]).reduce((_acc, _value) => "ok", noStrings[0]); +console.log(typeof fromMissingInit, fromMissingInit); + +const nonEmpty = [1, 2].reduce((acc, value) => acc + value, noNumbers[0]); +console.log(typeof nonEmpty, nonEmpty); + +const fallback = [1, 2].reduce((_acc, _value) => noNumbers[0] ?? "fallback"); +console.log(typeof fallback, fallback); + +const parenthesized = [1, 2].reduce((acc, x) => (x === 1 ? noStrings[0] : acc + "!"), ""); +console.log(parenthesized); + +const wrapped = [1, 2].reduce(((acc, x) => (x === 1 ? noStrings[0] : acc + "!")), ""); +console.log(wrapped); + +const right = ["a", "b"].reduceRight((acc, s) => acc + s, noStrings[0]); +console.log(right); diff --git a/tests/corpus/7185-array-callback-arguments-length.ts b/tests/corpus/7185-array-callback-arguments-length.ts new file mode 100644 index 000000000..ae5a8d847 --- /dev/null +++ b/tests/corpus/7185-array-callback-arguments-length.ts @@ -0,0 +1,42 @@ +// `arguments.length` in a function-expression callback counts every +// argument the array method passes: value, index and array, plus the +// accumulator for reduce. One stored in a fixed-arity function type counts +// that type's parameters, which every call passes. +const seen: number[] = []; +[10, 20].forEach(function (value) { + seen.push(arguments.length, value); +}); +console.log(seen.join(",")); + +console.log( + [1, 2] + .map(function (value) { + return `${value}:${arguments.length}`; + }) + .join(" "), +); + +console.log([5].filter(function () { + return arguments.length === 3; +}).length); + +console.log([1].reduce(function (acc, value) { + return acc + value + arguments.length; +}, 0)); + +console.log(["x", "y"].reduceRight(function (acc: string, value: string) { + return `${acc}${value}${arguments.length}`; +}, "")); + +console.log([3, 4].some(function (value) { + return arguments.length === 3 && value === 4; +})); + +const sum: (a: number, b: number) => number = function (a) { + return a + arguments.length; +}; +console.log(sum(1, 2)); + +console.log([3, 1, 2].sort(function (a, b) { + return arguments.length === 2 ? a - b : 0; +}).join(",")); diff --git a/tests/corpus/7240-objectmode-streams.ts b/tests/corpus/7240-objectmode-streams.ts new file mode 100644 index 000000000..47f799eee --- /dev/null +++ b/tests/corpus/7240-objectmode-streams.ts @@ -0,0 +1,161 @@ +// Typed object-mode streams: records through Readable.from, Transform +// and Writable in pipeline(), typed 'data' listeners, a Transform with +// readableObjectMode parsing bytes into records, an object-mode Duplex, +// a PassThrough's object-counted high-water mark, and Readable.from over +// a Map, a Set of records and an array mixing strings and Buffers, and +// byte-mode chunk parameters typed Buffer | string. +import { Duplex, PassThrough, Readable, Transform, Writable } from "node:stream"; +import { pipeline } from "node:stream/promises"; + +interface Row { + id: number; + name: string; +} + +interface Total { + count: number; + ids: number[]; +} + +async function records(): Promise { + const source = Readable.from([ + { id: 1, name: "ada" }, + { id: 2, name: "bo" }, + { id: 3, name: "cy" }, + ]); + const shout = new Transform({ + objectMode: true, + transform(row: Row, _enc: string, cb: (err: Error | null, out?: Row) => void) { + if (row.id === 2) { + cb(null); + return; + } + cb(null, { id: row.id * 10, name: row.name.toUpperCase() }); + }, + }); + const seen: Row[] = []; + await pipeline( + source, + shout, + new Writable({ + objectMode: true, + write(row: Row, enc: string | null | undefined, cb: () => void) { + seen.push(row); + console.log("write", row.id, enc); + cb(); + }, + }), + ); + console.log(seen.map((r) => `${r.id}:${r.name}`).join(" ")); +} + +function counters(): void { + const counter = new Readable({ objectMode: true, read() {} }); + console.log("hwm", counter.readableHighWaterMark, counter.readableObjectMode); + counter.on("data", (n: number) => console.log("data", n + 1)); + counter.on("end", () => console.log("counter end")); + counter.push(1); + counter.push(0); + counter.push(41); + counter.push(null); +} + +function sided(): void { + // bytes in, records out + const parse = new Transform({ + readableObjectMode: true, + transform(chunk: Buffer, _enc: string, cb: (err: Error | null, out?: Total) => void) { + const ids = chunk.toString().split(",").map((s) => Number(s)); + cb(null, { count: ids.length, ids }); + }, + }); + console.log( + "sided", + parse.readableObjectMode, + parse.writableObjectMode, + parse.readableHighWaterMark, + parse.writableHighWaterMark, + ); + parse.on("data", (t: Total) => console.log("total", t.count, t.ids.join("+"))); + parse.write("1,2,3"); + parse.end("4"); +} + +function duplex(): void { + const echo = new Duplex({ + objectMode: true, + read() {}, + write(chunk: Row, _enc: string, cb: () => void) { + this.push({ id: chunk.id + 1, name: chunk.name }); + cb(); + }, + final(cb: () => void) { + this.push(null); + cb(); + }, + }); + echo.on("data", (r: Row) => console.log("echo", r.id, r.name)); + echo.write({ id: 1, name: "x" }); + echo.end({ id: 5, name: "y" }); +} + +function passthrough(): void { + const pt = new PassThrough({ objectMode: true, highWaterMark: 2 }); + const ok1 = pt.write({ id: 1, name: "a" }); + const ok2 = pt.write({ id: 2, name: "b" }); + console.log("backpressure", ok1, ok2, pt.writableLength, pt.readableLength); + pt.on("data", (r: Row) => console.log("pt", r.name)); + pt.end(); +} + +async function collections(): Promise { + const prices = new Map([ + ["tea", 3], + ["cake", 5], + ]); + for await (const entry of Readable.from(prices)) console.log("entry", JSON.stringify(entry)); + const mixed: (string | Buffer)[] = ["text", Buffer.from("bytes")]; + for await (const chunk of Readable.from(mixed)) console.log("mixed", typeof chunk, String(chunk)); + const tags = new Set([{ tag: "a" }, { tag: "b" }]); + for await (const t of Readable.from(tags)) console.log("set", JSON.stringify(t)); +} + +// Byte-mode sides pass Buffers: a chunk parameter typed as a supertype +// of Buffer takes them, inline and as an underscore method, and +// super(undefined) leaves the stream in byte mode. +class Collector extends Writable { + constructor() { + super(undefined); + } + _write(chunk: Buffer | string, _encoding: BufferEncoding, cb: (err?: Error | null) => void): void { + console.log("collector", Buffer.isBuffer(chunk), typeof chunk === "string" ? chunk : chunk.toString()); + cb(); + } +} + +async function byteChunks(): Promise { + const inline = new Writable({ + write(chunk: Buffer | string, _encoding: BufferEncoding, cb: (err?: Error | null) => void): void { + console.log("inline", Buffer.isBuffer(chunk), typeof chunk === "string" ? chunk : chunk.toString()); + cb(); + }, + }); + inline.write("one"); + inline.end(Buffer.from("two")); + await new Promise((resolve) => inline.on("finish", () => resolve())); + const collector = new Collector(); + collector.end("three"); + await new Promise((resolve) => collector.on("finish", () => resolve())); +} + +async function main(): Promise { + await records(); + await collections(); + counters(); + sided(); + duplex(); + passthrough(); + await new Promise((resolve) => setTimeout(resolve, 5)); + await byteChunks(); +} +main(); diff --git a/tests/corpus/7241-objectmode-streams.mjs b/tests/corpus/7241-objectmode-streams.mjs new file mode 100644 index 000000000..6101930df --- /dev/null +++ b/tests/corpus/7241-objectmode-streams.mjs @@ -0,0 +1,155 @@ +// Object-mode streams in JavaScript: any value as a chunk through +// Readable, Writable, Duplex, Transform and PassThrough, pipe() and +// pipeline() between them, unshift(), the per-side modes and +// their high-water marks, and the encoding argument write() and end() +// hand to _write and _transform. +import { Duplex, PassThrough, Readable, Transform, Writable } from "node:stream"; +import { pipeline } from "node:stream/promises"; + +const show = (v) => (typeof v === "object" ? JSON.stringify(v) : `${typeof v}:${String(v)}`); + +async function values() { + const seen = []; + await pipeline( + Readable.from([1, "two", { three: 3 }, [4], true, 0, ""]), + new Transform({ + objectMode: true, + transform(chunk, enc, cb) { + cb(null, { wrapped: chunk }); + }, + }), + new Writable({ + objectMode: true, + write(chunk, enc, cb) { + seen.push(show(chunk.wrapped)); + cb(); + }, + }), + ); + console.log("values:", seen.join(" ")); +} + +function lines() { + let rest = ""; + const split = new Transform({ + readableObjectMode: true, + transform(chunk, enc, cb) { + const parts = (rest + chunk.toString()).split("\n"); + rest = parts.pop(); + for (const p of parts) this.push({ line: p, length: p.length }); + cb(); + }, + flush(cb) { + if (rest) this.push({ line: rest, length: rest.length }); + cb(); + }, + }); + console.log("split modes:", split.readableObjectMode, split.writableObjectMode); + split.on("data", (row) => console.log("line:", row.line, row.length)); + split.on("end", () => console.log("split end")); + split.write("alpha\nbe"); + split.write("ta\ngamma"); + split.end(); +} + +function encodings() { + const w = new Writable({ + objectMode: true, + write(chunk, enc, cb) { + console.log("write enc:", show(chunk), enc); + cb(); + }, + }); + w.write(1, () => console.log("write cb")); + w.write(2, null); + w.write(3, "utf8"); + w.write("four", "latin1"); + w.write(5, "anything"); + w.end(6, () => console.log("end cb")); + const t = new Transform({ + objectMode: true, + transform(chunk, enc, cb) { + console.log("transform enc:", show(chunk), enc); + cb(); + }, + }); + t.write(7); + t.end(8, "hex"); + const bytes = new Writable({ + write(chunk, enc, cb) { + console.log("byte write:", chunk.toString("hex"), enc); + cb(); + }, + }); + bytes.write("hi", "latin1"); + bytes.write("6869", "hex"); + try { + bytes.write("x", "bogus"); + } catch (e) { + console.log("byte encoding:", e.code); + } + bytes.end(); +} + +function unshiftFront() { + const r = new Readable({ objectMode: true, read() {} }); + r.push({ id: 1 }); + r.push({ id: 2 }); + r.push(null); + console.log("length:", r.readableLength, "hwm:", r.readableHighWaterMark); + r.unshift({ id: 0 }); + r.on("data", (v) => console.log("after unshift:", show(v))); + r.on("end", () => console.log("read end")); + // strings stay strings on an object-mode side, whatever the encoding + const strings = new Readable({ objectMode: true, read() {} }); + strings.push("b"); + strings.unshift("a"); + strings.push("6869", "hex"); + strings.push(null); + strings.on("data", (v) => console.log("string entry:", show(v))); +} + +function piped() { + const src = new PassThrough({ objectMode: true }); + const echo = new Duplex({ + objectMode: true, + read() {}, + write(chunk, enc, cb) { + this.push({ echoed: chunk }); + cb(); + }, + final(cb) { + this.push(null); + cb(); + }, + }); + src.pipe(echo).on("data", (v) => console.log("piped:", show(v))); + src.write({ n: 1 }); + src.write([2, 3]); + src.end("last"); +} + +function byteModeValues() { + const r = new Readable({ read() {} }); + r.on("error", (e) => console.log("byte push:", e.code)); + r.push(undefined); + r.push(42); +} + +async function forAwait() { + const pt = new PassThrough({ objectMode: true, highWaterMark: 2 }); + const ok = [pt.write(1), pt.write(2), pt.write(3)]; + console.log("backpressure:", ok.join(" "), pt.writableLength); + pt.end({ done: true }); + const got = []; + for await (const v of pt) got.push(show(v)); + console.log("for await:", got.join(" ")); +} + +await values(); +lines(); +encodings(); +unshiftFront(); +piped(); +byteModeValues(); +await forAwait(); diff --git a/tests/corpus/7242-objectmode-dynamic.cjs b/tests/corpus/7242-objectmode-dynamic.cjs new file mode 100644 index 000000000..fad06ad98 --- /dev/null +++ b/tests/corpus/7242-objectmode-dynamic.cjs @@ -0,0 +1,93 @@ +// Object-mode streams through dynamic values: options objects held in +// variables, constructor functions with util.inherits (Transform.call +// and Writable.call with objectMode), streams passed around as plain +// values, and Readable.from over values of mixed types. +const stream = require("stream"); +const util = require("util"); + +const show = (v) => (v !== null && typeof v === "object" ? JSON.stringify(v) : `${typeof v}:${String(v)}`); + +function Tagger(tag) { + stream.Transform.call(this, { objectMode: true }); + this.tag = tag; +} +util.inherits(Tagger, stream.Transform); +Tagger.prototype._transform = function (chunk, enc, cb) { + console.log("tagger enc:", enc); + if (chunk === 3) return cb(); + cb(null, { tag: this.tag, value: chunk }); +}; +Tagger.prototype._flush = function (cb) { + this.push({ tag: this.tag, value: "flushed" }); + cb(); +}; + +function Collector() { + stream.Writable.call(this, { objectMode: true, highWaterMark: 2 }); + this.items = []; +} +util.inherits(Collector, stream.Writable); +Collector.prototype._write = function (chunk, enc, cb) { + this.items.push(show(chunk)); + cb(); +}; + +function viaConstructors(next) { + const t = new Tagger("x"); + const c = new Collector(); + console.log("modes:", t.readableObjectMode, t.writableObjectMode, c.writableObjectMode, c.writableHighWaterMark); + t.pipe(c); + c.on("finish", () => { + console.log("collected:", c.items.join(" ")); + next(); + }); + t.write(1); + t.write("two", () => console.log("write cb")); + t.write(3); + t.end({ four: 4 }); +} + +function viaOptionObjects(next) { + const readableOpts = { objectMode: true, read() {} }; + const r = new stream.Readable(readableOpts); + const transformOpts = { + objectMode: true, + transform(chunk, enc, cb) { + cb(null, Array.isArray(chunk) ? chunk.length : chunk); + }, + }; + const t = new stream.Transform(transformOpts); + const seen = []; + const writableOpts = { + objectMode: true, + write(chunk, enc, cb) { + seen.push(show(chunk) + "/" + enc); + cb(); + }, + }; + const w = new stream.Writable(writableOpts); + stream.pipeline(r, t, w, (err) => { + console.log("option objects:", err === undefined ? "ok" : err, seen.join(" ")); + next(); + }); + r.push([1, 2, 3]); + r.push(false); + r.push({ k: "v" }); + r.push(null); +} + +async function viaFrom() { + const values = [0, "s", undefined, [1], { a: { b: 2 } }, 1.5]; + const out = []; + for await (const v of stream.Readable.from(values)) out.push(show(v)); + console.log("from:", out.join(" ")); + const pt = new stream.PassThrough({ objectMode: true }); + const passAlong = (s, v) => s.write(v); + passAlong(pt, { through: true }); + passAlong(pt, 7); + pt.end(); + pt.on("data", (v) => console.log("passed:", show(v))); + await new Promise((resolve) => pt.on("end", resolve)); +} + +viaConstructors(() => viaOptionObjects(() => viaFrom())); diff --git a/tests/corpus/7243-for-await-close-order.mjs b/tests/corpus/7243-for-await-close-order.mjs new file mode 100644 index 000000000..b3cc700be --- /dev/null +++ b/tests/corpus/7243-for-await-close-order.mjs @@ -0,0 +1,58 @@ +// for await over a stream completes through eos: a readable-only stream +// that will emit 'close' (autoDestroy and emitClose) ends the loop after +// 'close', a duplex after 'end', interleaved with other loops' ticks. +import { PassThrough, Readable } from "node:stream"; + +async function interleaved(objectMode) { + const log = []; + let on = true; + (async () => { + for (let i = 0; on && i < 30; i++) { + log.push("t" + i); + await null; + } + })(); + const nums = objectMode ? new PassThrough({ objectMode: true }) : new PassThrough({ highWaterMark: 4 }); + for (let k = 0; k < 6; k++) nums.write(objectMode ? k : "n" + k); + nums.end(); + nums.on("end", () => log.push("N-END")); + nums.on("close", () => log.push("N-CLOSE")); + nums.on("finish", () => log.push("N-FINISH")); + const fr = Readable.from(["a", "b", "c"]); + fr.on("end", () => log.push("F-END")); + fr.on("close", () => log.push("F-CLOSE")); + const a = (async () => { + for await (const n of nums) log.push("n" + String(n).length); + log.push("n-done"); + })(); + const b = (async () => { + for await (const v of fr) log.push("f" + v); + log.push("f-done"); + })(); + await Promise.all([a, b]); + on = false; + console.log(objectMode ? "object:" : "bytes:", log.join(" ")); +} + +async function closeBeforeDone() { + const log = []; + const r = new Readable({ objectMode: true, read() {} }); + r.on("end", () => log.push("end")); + r.on("close", () => log.push("close")); + r.push({ v: 1 }); + r.push(null); + for await (const v of r) log.push("v" + v.v); + log.push("done " + r.destroyed); + const keep = new Readable({ objectMode: true, autoDestroy: false, read() {} }); + keep.on("end", () => log.push("keep end")); + keep.on("close", () => log.push("keep close")); + keep.push(1); + keep.push(null); + for await (const v of keep) log.push("k" + v); + log.push("keep done " + keep.destroyed); + console.log("close order:", log.join(" ")); +} + +await interleaved(true); +await interleaved(false); +await closeBeforeDone(); diff --git a/tests/corpus/7244-objectmode-subclasses.ts b/tests/corpus/7244-objectmode-subclasses.ts new file mode 100644 index 000000000..5fcb05a24 --- /dev/null +++ b/tests/corpus/7244-objectmode-subclasses.ts @@ -0,0 +1,79 @@ +// Typed object-mode stream subclasses: Transform, Writable and Readable +// classes that pass objectMode to super(), _transform/_write/_read with +// record chunks, encoding parameters typed string | null | undefined, and +// push() of records and other values. +import { Readable, Transform, Writable } from "node:stream"; +import { pipeline } from "node:stream/promises"; + +interface Order { + id: number; + items: string[]; +} + +interface Summary { + id: number; + count: number; +} + +class Orders extends Readable { + private next = 1; + constructor() { + super({ objectMode: true }); + } + _read(): void { + if (this.next > 3) { + this.push(null); + return; + } + const id = this.next++; + this.push({ id, items: Array.from({ length: id }, (_, i) => `item${i}`) }); + } +} + +class Summarize extends Transform { + constructor() { + super({ objectMode: true }); + } + _transform( + order: Order, + encoding: string | null | undefined, + cb: (err: Error | null, data?: Summary) => void, + ): void { + console.log("summarize", order.id, encoding); + cb(null, { id: order.id, count: order.items.length }); + } +} + +class Report extends Writable { + lines: string[] = []; + constructor() { + super({ objectMode: true, highWaterMark: 1 }); + } + _write(s: Summary, _encoding: string, cb: (err?: Error | null) => void): void { + this.lines.push(`#${s.id}=${s.count}`); + cb(); + } +} + +async function main(): Promise { + const report = new Report(); + console.log("report", report.writableObjectMode, report.writableHighWaterMark); + await pipeline(new Orders(), new Summarize(), report); + console.log("lines", report.lines.join(" ")); + + const direct = new Summarize(); + direct.on("data", (s: Summary) => console.log("direct", s.id, s.count)); + direct.write({ id: 9, items: ["a"] }, "utf8"); + direct.write({ id: 10, items: [] }, () => console.log("written")); + direct.end(); + + const values = new Readable({ objectMode: true, read() {} }); + values.push(1.5); + values.push("text"); + values.push([1, 2]); + values.push(null); + const seen: string[] = []; + for await (const v of values) seen.push(JSON.stringify(v)); + console.log("values", seen.join(" ")); +} +main(); diff --git a/tests/corpus/7245-writable-callback-order.mjs b/tests/corpus/7245-writable-callback-order.mjs new file mode 100644 index 000000000..454e67fe0 --- /dev/null +++ b/tests/corpus/7245-writable-callback-order.mjs @@ -0,0 +1,71 @@ +// Writable callback and 'finish' timing: buffered writes completing +// inside clearBuffer keep their callback order, a deferred afterWrite +// with a user callback holds 'finish' for its own tick, a _final +// callback schedules 'finish' at once, and cork()/uncork() with +// callbacks. +import { PassThrough, Transform, Writable } from "node:stream"; + +function ticks(log, count) { + let n = 0; + const tick = () => { + log.push("t" + n); + if (++n < count) process.nextTick(tick); + }; + process.nextTick(tick); +} + +async function bufferedCallbacks() { + const log = []; + const nums = new PassThrough({ highWaterMark: 4 }); + for (let k = 0; k < 6; k++) log.push("w" + nums.write("n" + k, () => log.push("cb" + k))); + nums.end(() => log.push("end-cb")); + nums.on("finish", () => log.push("finish")); + nums.on("end", () => log.push("end")); + for await (const chunk of nums) log.push("r" + chunk.length); + log.push("done"); + console.log("buffered:", log.join(" ")); +} + +function finishTiming() { + return new Promise((resolve) => { + const log = []; + ticks(log, 10); + const plain = new Writable({ write(c, e, cb) { log.push("pw" + c); cb(); } }); + plain.write("1", () => log.push("pcb1")); + plain.write("2"); + plain.end("3", () => log.push("pend")); + plain.on("finish", () => log.push("pfinish")); + const withFinal = new Writable({ + write(c, e, cb) { log.push("fw" + c); cb(); }, + final(cb) { log.push("final"); cb(); }, + }); + withFinal.write("1", () => log.push("fcb1")); + withFinal.end(() => log.push("fend")); + withFinal.on("finish", () => log.push("ffinish")); + const corked = new Writable({ write(c, e, cb) { log.push("cw" + c); cb(); } }); + corked.cork(); + corked.write("a", () => log.push("ccb-a")); + corked.write("b", () => log.push("ccb-b")); + process.nextTick(() => { + corked.uncork(); + corked.end(() => log.push("cend")); + }); + corked.on("finish", () => log.push("cfinish")); + const t = new Transform({ + transform(c, e, cb) { cb(null, c); }, + flush(cb) { log.push("flush"); cb(); }, + }); + t.resume(); + t.write("q", () => log.push("tcb")); + t.end(() => log.push("tend")); + t.on("finish", () => log.push("tfinish")); + t.on("end", () => log.push("tread-end")); + setTimeout(() => { + console.log("timing:", log.join(" ")); + resolve(undefined); + }, 10); + }); +} + +await bufferedCallbacks(); +await finishTiming(); diff --git a/tests/corpus/7280-named-reads-hole-receivers.ts b/tests/corpus/7280-named-reads-hole-receivers.ts new file mode 100644 index 000000000..59f0c4820 --- /dev/null +++ b/tests/corpus/7280-named-reads-hole-receivers.ts @@ -0,0 +1,50 @@ +// Named reads and method calls through a parameter narrowed to one arm of a +// wide union, when an array hole reaches the narrowed branch. +type Bag = number | string | number[] | { a?: number; f?: () => string }; +function isBag(value: Bag): value is { a?: number; f?: () => string } { + return !Array.isArray(value) && typeof value !== "string" && typeof value !== "number"; +} +function peek(value: Bag): string { + if (isBag(value)) return typeof value.a; + return "-"; +} +function call(value: Bag): string { + if (isBag(value)) return value.f?.() ?? "none"; + return "-"; +} +class Shape { + side: number; + constructor(side: number) { + this.side = side; + } + area(): number { + return this.side * this.side; + } +} +type Item = Shape | string | number; +function area(item: Item): number { + if (item instanceof Shape || (typeof item !== "string" && typeof item !== "number")) return item.area(); + return 0; +} +const bags: Bag[] = [{ a: 1, f: () => "f" }, "s", [1]]; +bags.length = 4; +const items: Item[] = [new Shape(2), "x"]; +items.length = 3; +const out: string[] = []; +for (let i = 0; i < bags.length; i++) { + for (const which of [0, 1]) { + try { + out.push(which === 0 ? peek(bags[i]) : call(bags[i])); + } catch (e) { + out.push(e instanceof TypeError ? e.message : "other"); + } + } +} +for (let i = 0; i < items.length; i++) { + try { + out.push(String(area(items[i]))); + } catch (e) { + out.push(e instanceof TypeError ? e.message : "other"); + } +} +console.log(out.join("\n")); diff --git a/tests/corpus/7300-callback-captured-scalars.ts b/tests/corpus/7300-callback-captured-scalars.ts new file mode 100644 index 000000000..c8e8b28bd --- /dev/null +++ b/tests/corpus/7300-callback-captured-scalars.ts @@ -0,0 +1,147 @@ +// Callbacks that update captured `let` numbers and booleans from loops: typed +// array and array methods, an authored higher-order function called with one +// function at several calls and with two functions at one, closures of one +// function over different bindings, a callback that throws part way, nested +// callbacks, callbacks declared in the loop body, and reads after the loop. + +function each(values: number[], visit: (v: number, i: number) => void): void { + for (let i = 0; i < values.length; i++) visit(values[i]!, i); +} + +function until(limit: number, step: (i: number) => boolean): number { + let i = 0; + while (i < limit) { + if (!step(i)) break; + i++; + } + return i; +} + +function twice(values: Int16Array, f: (v: number) => void): void { + let i = 0; + do { + for (let j = 0; j < values.length; j++) f(values[j]!); + i++; + } while (i < 2); +} + +const ints = new Int32Array([5, -3, 8, 0, 12, -7, 4]); +let total = 0; +let negatives = 0; +let sawZero = false; +ints.forEach((v) => { + total = (total + v) | 0; + if (v < 0) negatives++; + if (v === 0) sawZero = true; +}); +console.log("forEach", total, negatives, sawZero); + +let max = -Infinity; +let maxAt = -1; +const floats = new Float64Array([0.5, 9.25, -2, 9.25, 3]); +floats.forEach((v, i) => { + if (v > max) { + max = v; + maxAt = i; + } +}); +console.log("max", max, maxAt); + +let calls = 0; +const doubled = ints.map((v) => { + calls++; + return v * 2 + calls; +}); +console.log("map", doubled.join(","), calls); + +let seen = 0; +const big = [3, 14, 15, 92, 65].filter((v) => { + seen += v; + return v > 10; +}); +console.log("filter", big.join(","), seen); + +let sum = 0; +let count = 0; +const summing = (v: number): void => { + sum += v; + count++; +}; +each([1, 2, 3], summing); +each([10, 20], summing); +console.log("each shared", sum, count); + +let a = 0; +let b = 0; +each([1, 2], (v) => { + a += v; +}); +each([1, 2], (v, i) => { + b += v * (i + 1); +}); +console.log("each two", a, b); + +const counters: (() => number)[] = []; +for (let k = 0; k < 3; k++) { + let local = k * 100; + each([1, 1, 1], () => { + local++; + }); + counters.push(() => local); +} +console.log("closures", counters.map((c) => c()).join(",")); + +let steps = 0; +console.log("until", until(10, (i) => (steps += i, steps < 12)), steps); + +let shorts = 0; +twice(new Int16Array([7, -8, 9]), (v) => { + shorts = shorts * 2 + v; +}); +console.log("twice", shorts); + +let partial = 0; +try { + floats.forEach((v) => { + if (v < 0) throw new RangeError(`at ${partial}`); + partial += v; + }); +} catch (error) { + console.log("throw", (error as Error).message, partial); +} + +let outer = 0; +let inner = 0; +[1, 2, 3].forEach((x) => { + outer += x; + ints.forEach((y) => { + if (y > x) inner++; + }); +}); +console.log("nested", outer, inner); + +let flag = false; +let flips = 0; +new Uint8Array([1, 0, 1, 1]).forEach((v) => { + if (v === 1) flag = !flag; + if (flag) flips++; +}); +console.log("bool", flag, flips); + +// Callbacks declared in the loop body: each iteration binds its own. +let strides = 0; +for (let i = 0; i < 10; i++) { + const bump = (): void => { + i += 2; + strides++; + }; + bump(); +} +let countdown = 0; +let k = 3; +while (k > 0) { + const tick = (n: number): number => (n > 0 ? tick(n - 1) + 1 : 0); + countdown += tick(k); + k--; +} +console.log("declared in loop", strides, countdown); diff --git a/tests/corpus/7301-running-extrema-branches.ts b/tests/corpus/7301-running-extrema-branches.ts new file mode 100644 index 000000000..9941ce538 --- /dev/null +++ b/tests/corpus/7301-running-extrema-branches.ts @@ -0,0 +1,169 @@ +// Running maximum and minimum scans over ascending, descending, alternating, +// pseudo-random and NaN-holding data: plain for, while, do-while and for-of +// loops, `<=` and `>=` ties, an argmax that records its index, typed array +// and array element reads, callbacks that update captured bindings, a scan +// nested in an outer loop, an update nested in another branch, and the clamp +// and saturating updates that are not running extrema. + +function shapes(n: number): [string, Float64Array][] { + const ascending = new Float64Array(n); + const descending = new Float64Array(n); + const alternating = new Float64Array(n); + const random = new Float64Array(n); + const holes = new Float64Array(n); + let x = 12345; + for (let i = 0; i < n; i++) { + ascending[i] = i; + descending[i] = n - i; + alternating[i] = i % 2 === 0 ? 0 : i; + x = (x * 1103515245 + 12345) % 2147483648; + random[i] = (x % 1000) - 500; + holes[i] = i % 7 === 3 ? NaN : (x % 97) - 48; + } + return [ + ["ascending", ascending], + ["descending", descending], + ["alternating", alternating], + ["random", random], + ["nan", holes], + ]; +} + +function argmax(a: Float64Array): string { + let max = -Infinity; + let at = -1; + for (let i = 0; i < a.length; i++) { + const v = a[i]!; + if (v > max) { + max = v; + at = i; + } + } + return `${max}@${at}`; +} + +function lastMin(a: Float64Array): string { + let min = Infinity; + let at = -1; + let i = 0; + while (i < a.length) { + if (a[i]! <= min) { + min = a[i]!; + at = i; + } + i++; + } + return `${min}@${at}`; +} + +function bothEnds(values: number[]): string { + let lo = Infinity; + let hi = -Infinity; + for (const v of values) { + if (lo > v) lo = v; + if (v >= hi) hi = v; + } + return `${lo}..${hi}`; +} + +function firstDrop(a: Float64Array): number { + let floor = a[0]!; + let i = 0; + do { + if (a[i]! < floor) floor = a[i]!; + i++; + } while (i < a.length); + return floor; +} + +function callbackPeak(a: Float64Array): string { + let max = -Infinity; + let at = -1; + a.forEach((v, i) => { + if (v > max) { + max = v; + at = i; + } + }); + return `${max}@${at}`; +} + +function arrayCallbackMin(values: number[]): number { + let min = Infinity; + values.forEach((v) => { + if (v < min) min = v; + }); + return min; +} + +function rowMaxima(a: Float64Array, width: number): string { + const out: number[] = []; + for (let row = 0; row * width < a.length; row++) { + let best = -Infinity; + for (let j = row * width; j < Math.min(a.length, (row + 1) * width); j++) { + if (a[j]! > best) best = a[j]!; + } + out.push(best); + } + return out.join(","); +} + +function longestNonNegativeRun(a: Float64Array): number { + let longest = 0; + let run = 0; + for (let i = 0; i < a.length; i++) { + if (a[i]! >= 0) { + run++; + if (run > longest) longest = run; + } else run = 0; + } + return longest; +} + +function clampSum(a: Float64Array, hi: number): number { + let s = 0; + for (let i = 0; i < a.length; i++) { + let v = a[i]!; + if (hi < v) v = hi; + s += v; + } + return s; +} + +function saturate(a: Float64Array, hi: number): number { + let v = 0; + for (let i = 0; i < a.length; i++) { + v += a[i]!; + if (hi < v) v = hi; + } + return v; +} + +for (const [name, a] of shapes(40)) { + const values = Array.from(a); + console.log( + name, + argmax(a), + lastMin(a), + bothEnds(values), + firstDrop(a), + callbackPeak(a), + arrayCallbackMin(values), + ); + console.log( + " rows", + rowMaxima(a, 7), + "run", + longestNonNegativeRun(a), + "clamp", + clampSum(a, 5), + "saturate", + saturate(a, 30), + ); +} + +const signed = new Float64Array([0, -0, 0, -0]); +let top = -Infinity; +for (let i = 0; i < signed.length; i++) if (signed[i]! >= top) top = signed[i]!; +console.log("signed zero", Object.is(top, -0), argmax(signed), lastMin(signed)); +console.log("empty", argmax(new Float64Array(0)), bothEnds([]), arrayCallbackMin([])); diff --git a/tests/corpus/7320-array-dense-loop-invariant-limits.ts b/tests/corpus/7320-array-dense-loop-invariant-limits.ts new file mode 100644 index 000000000..878245e60 --- /dev/null +++ b/tests/corpus/7320-array-dense-loop-invariant-limits.ts @@ -0,0 +1,101 @@ +// Index loops bounded by an invariant limit other than the array's own length +// (a local copy of a length, a literal, an inclusive bound), over arrays the +// loop never resizes: class instances whose number and boolean fields the loop +// writes, and number arrays that are dense, short, holey or partially filled. + +class Body { + x: number; + v = 0; + hit = false; + next: Body | null = null; + + constructor(x: number) { + this.x = x; + } +} + +function pairs(bodies: Body[]): string { + const n = bodies.length; + for (let i = 0; i < n; i++) { + const bi = bodies[i]!; + let vi = bi.v; + for (let j = i + 1; j < n; j++) { + const bj = bodies[j]!; + const d = bi.x - bj.x; + const f = 1 / (d * d + 1); + vi -= d * f; + bj.v += d * f; + bj.hit = f > 0.1; + } + bi.v = vi; + } + return bodies.map((b) => `${b.v.toFixed(6)}${b.hit ? "*" : ""}`).join(" "); +} + +function firstThree(bodies: Body[]): number { + let s = 0; + for (let j = 0; j < 3; j++) s += bodies[j]!.x; + return s; +} + +function inclusive(bodies: Body[]): number { + const last = bodies.length - 1; + let s = 0; + for (let j = 1; j <= last; j++) s += bodies[j]!.x * j; + return s; +} + +function linked(bodies: Body[]): string { + const n = bodies.length; + // A reference field write: the loop keeps the ordinary copy. + for (let j = 0; j < n - 1; j++) bodies[j]!.next = bodies[j + 1]!; + let count = 0; + for (let b: Body | null = bodies[0]!; b !== null; b = b.next) count++; + return `${count}`; +} + +function sumTo(a: number[], limit: number): string { + const n = limit; + let s = 0; + let missing = 0; + for (let i = 0; i < n; i++) { + const v = a[i]; + if (v === undefined) missing++; + else s += v; + } + return `${s} ${missing}`; +} + +function scaleTo(a: number[], out: number[]): string { + const n = a.length; + for (let i = 0; i < n; i++) out[i] = a[i] * 2; + return `${out.length}:${out.join(",")}`; +} + +function window(a: number[]): number { + let s = 0; + for (let i = 2; i < 6; i++) s += a[i] * i; + return s; +} + +const bodies = [new Body(0), new Body(1.5), new Body(-2), new Body(4), new Body(0.25)]; +console.log(pairs(bodies)); +console.log(pairs(bodies)); +console.log(firstThree(bodies), inclusive(bodies), inclusive([new Body(7)])); +console.log(linked([new Body(1), new Body(2), new Body(3)])); + +const dense = [1, 2, 3, 4, 5, 6, 7, 8]; +console.log(sumTo(dense, 8), sumTo(dense, 5), sumTo(dense, 0), sumTo(dense, -3)); +// Limits past the length or the filled prefix take the checked loop. +console.log(sumTo(dense, 10), sumTo([], 2)); +const holey = [1]; +holey[3] = 4; +console.log(sumTo(holey, 4), sumTo(holey, 1)); +const shortened = [1, 2, 3, 4, 5, 6]; +shortened.length = 3; +shortened.length = 6; +console.log(sumTo(shortened, 6), sumTo(shortened, 3)); +console.log(scaleTo(dense, new Array(8).fill(0))); +console.log(scaleTo(dense, [9, 9])); +console.log(scaleTo([1, 2, 3], [])); +console.log(window(dense), window([1, 2, 3, 4, 5, 6])); diff --git a/tests/corpus/7321-speculative-loop-bail-sites.ts b/tests/corpus/7321-speculative-loop-bail-sites.ts new file mode 100644 index 000000000..460595feb --- /dev/null +++ b/tests/corpus/7321-speculative-loop-bail-sites.ts @@ -0,0 +1,111 @@ +// Speculative integer loops whose checks fail: on the first statement of +// every entry (fractional field data), after an effect earlier in the same +// iteration, before a `continue`, `break` or `return`, and on entries +// that alternate between integer and fractional data. A site that keeps +// failing stops trying its integer copy; every result is Node's. + +class Point { + x: number; + y: number; + w = 0; + + constructor(x: number, y: number) { + this.x = x; + this.y = y; + } +} + +function show(label: string, value: number): void { + console.log(label, Object.is(value, -0) ? "-0" : String(value)); +} + +// Each inner entry fails at its first statement when the fields hold +// fractions, and runs no iteration at all for the last point. +function pairs(points: Point[]): number { + let total = 0; + for (let i = 0; i < points.length; i++) { + const p = points[i]!; + let acc = p.w; + for (let j = i + 1; j < points.length; j++) { + const q = points[j]!; + const dx = p.x - q.x; + const dy = p.y - q.y; + const d2 = dx * dx + dy * dy; + acc += d2; + q.w += d2 * 0.5; + } + p.w = acc; + total += acc; + } + return total; +} + +const fractional = [ + new Point(0.5, 1), + new Point(1.25, -2), + new Point(3, 4.75), + new Point(-1.5, 0), +]; +const integral = [new Point(0, 1), new Point(2, -2), new Point(3, 4), new Point(-1, 0)]; +for (let round = 0; round < 12; round++) { + show(`pairs fractional ${round}`, pairs(fractional)); +} +// The same site, now with integer data after it stopped speculating. +show("pairs integral", pairs(integral)); +show("pairs integral again", pairs(integral)); + +// A field write comes before the failing statement: the iteration resumes +// after it and must not repeat it. +function afterEffect(points: Point[], scale: number): number { + let s = 0; + for (let i = 0; i < points.length; i++) { + const p = points[i]!; + p.w += 1; + const k = p.x * scale; + s += k * k; + } + return s; +} +const marks = [new Point(1, 0), new Point(2, 0), new Point(3, 0)]; +show("after effect 2", afterEffect(marks, 2)); +show("after effect 0.5", afterEffect(marks, 0.5)); +show("after effect 3", afterEffect(marks, 3)); +console.log(marks.map((p) => p.w).join(" ")); + +// The failing statement is followed by `continue`, `break` and `return`. +function control(n: number, step: number): number { + let acc = 0; + let i = 0; + while (i < n) { + const v = i * step; + i++; + if (v * v > 400) break; + if ((i & 1) === 0) continue; + acc += v * 3; + if (acc > 1000) return -acc; + } + return acc; +} +for (const step of [1, 0.5, 2, 0.75, 7, 1.5, 3, 0.25, 5, 1.75, 4]) { + show(`control ${step}`, control(40, step)); +} + +// do-while: the check fails part-way through, on some entries only. +function steps(start: number, factor: number): number { + let v = start; + let k = 0; + do { + v = v * factor - k; + k++; + } while (k < 6); + return v; +} +for (let r = 0; r < 20; r++) show(`steps ${r}`, steps(r % 3 === 0 ? r + 0.5 : r, 3)); + +// Overflow past 2^53 on later entries only. +function powers(base: number, n: number): number { + let p = 1; + for (let i = 0; i < n; i++) p = p * base; + return p; +} +for (const n of [3, 10, 40, 80, 5, 60, 2, 70, 1, 90, 0, 4]) show(`powers ${n}`, powers(3, n)); diff --git a/tests/corpus/7322-union-scalar-equality.ts b/tests/corpus/7322-union-scalar-equality.ts new file mode 100644 index 000000000..82e0564fc --- /dev/null +++ b/tests/corpus/7322-union-scalar-equality.ts @@ -0,0 +1,68 @@ +// Strict equality and inequality between a `number | undefined` or +// `boolean | undefined` value and a number or boolean: element reads of +// holes, past the end and of present undefined, NaN, -0 and +0, and +// Object.is, in branches, loops and expressions. + +function zeros(a: number[], at: number[]): string { + const hits: string[] = []; + for (const i of at) { + const v = a[i]; + hits.push(`${i}:${v === 0 ? "eq" : "ne"}${v !== 0 ? "!" : ""}${0 === v ? "r" : ""}`); + } + return hits.join(" "); +} + +const holey: number[] = [0, -0, 1]; +holey[5] = 0; +console.log(zeros(holey, [0, 1, 2, 3, 4, 5, 6, -1])); +console.log(zeros([NaN, 0.5, -1, 0], [0, 1, 2, 3])); + +function nanAndZeros(a: (number | undefined)[]): string { + return a + .map((v) => `${v === -0}${v === 0}${Object.is(v, -0)}${Object.is(v, 0)}${Object.is(v, NaN)}`) + .join(" "); +} +console.log(nanAndZeros([0, -0, NaN, undefined, 1])); + +function flags(a: (boolean | undefined)[]): string { + let t = 0; + let f = 0; + let notTrue = 0; + for (let i = 0; i < a.length + 2; i++) { + if (a[i] === true) t++; + if (a[i] === false) f++; + if (a[i] !== true) notTrue++; + } + return `${t} ${f} ${notTrue}`; +} +const bools: (boolean | undefined)[] = [true, false, undefined, true]; +bools[7] = false; +console.log(flags(bools)); + +// A permutation walk that stops at a zero element, as the flip loops of a +// counting benchmark do. +function walk(p: number[], start: number): number { + let steps = 0; + let at = start; + while (p[at] !== 0 && steps < 20) { + at = p[at]!; + steps++; + } + return steps; +} +console.log(walk([0, 3, 1, 2], 1), walk([1, 2, 3, 0], 0), walk([2, 2, 2], 0)); + +class Slot { + value: number | undefined = undefined; + on: boolean | undefined = undefined; +} +const slots = [new Slot(), new Slot(), new Slot()]; +slots[1]!.value = 0; +slots[1]!.on = true; +slots[2]!.value = -0; +slots[2]!.on = false; +console.log( + slots + .map((s) => `${s.value === 0}${s.value !== 0}${s.on === true}${s.on === false}${s.on !== false}`) + .join(" "), +); diff --git a/tests/corpus/7323-loop-bound-parameter-versions.ts b/tests/corpus/7323-loop-bound-parameter-versions.ts new file mode 100644 index 000000000..cdbb8cfc4 --- /dev/null +++ b/tests/corpus/7323-loop-bound-parameter-versions.ts @@ -0,0 +1,88 @@ +// Functions whose number parameters bound their loops, called with values +// no caller proves to be int32s: integers, fractions, NaN, -0, infinities, +// and integers past the int32 range. Every call prints Node's result, +// including -0 read back through 1 / n, a reassigned parameter, a thrown +// error, a void result and a function passed as a value. + +function show(label: string, value: number): void { + console.log(label, Object.is(value, -0) ? "-0" : String(value)); +} + +function sumTo(n: number): number { + let s = 0; + for (let i = 0; i < n; i++) s += i; + return s; +} + +function fill(n: number, out: number[]): void { + for (let i = 0; i < n; i++) out.push(i * 2); +} + +function sign(n: number): number { + let steps = 0; + for (let i = 0; i < n && steps < 4; i++) steps++; + return 1 / n + steps; +} + +function countdown(n: number, step: number): number { + let k = 0; + while (n > 0) { + n -= step; + k++; + if (k > 100) break; + } + return k * 1000 + n; +} + +function checked(n: number, limit: number): number { + let s = 0; + let over = -1; + for (let i = 0; i <= n; i++) { + if (i > limit && over < 0) over = i; + s += i; + } + if (over >= 0) throw new RangeError(`past ${limit} at ${over}`); + return s; +} + +function fillTo(n: number, out: number[]): number { + for (let i = 0; i < n; i++) out[i] = i * 3; + return out.length; +} + +function window2(a: number[], from: number, to: number): string { + const parts: string[] = []; + for (let i = from; i < to; i++) parts.push(String(a[i])); + return parts.join(","); +} + +// Values arrive as strings and JSON, so no caller proves them int32s. +const inputs: number[] = JSON.parse("[0, 1, 5, 7.5, -3, -0, 2147483647, 2147483648, -2147483649, 1e300]"); +inputs.push(NaN, Infinity, -Infinity, Number("12"), Number.parseFloat("3.25")); + +for (const n of inputs) { + const small = Math.abs(n) > 100 ? (n > 0 ? 3 : -3) : n; + show(`sumTo ${n}`, sumTo(small)); + show(`sign ${n}`, sign(n)); + show(`countdown ${n}`, countdown(small, 2)); + const out: number[] = []; + fill(small, out); + const dense: number[] = []; + const length = fillTo(small, dense); + console.log(`fill ${n}`, out.join(" "), length, dense.join(" ")); +} + +for (const n of [3, 4.5, 6]) { + try { + show(`checked ${n}`, checked(n, 4)); + } catch (e) { + console.log(`checked ${n} threw`, (e as Error).message); + } +} + +const data = [10, 20, 30, 40, 50]; +for (const [from, to] of JSON.parse("[[0, 2], [1.5, 4], [-1, 1], [3, 9], [2, 2]]") as [number, number][]) + console.log(`window ${from}..${to}`, window2(data, from, to)); + +const fns: ((n: number) => number)[] = [sumTo, (n) => sumTo(n) * 2]; +console.log(fns.map((f) => f(Number("4"))).join(" "), [1, 2.5, 3].map(sumTo).join(" ")); diff --git a/tests/corpus/7324-loop-bound-parameter-window-sums.ts b/tests/corpus/7324-loop-bound-parameter-window-sums.ts new file mode 100644 index 000000000..094e9848c --- /dev/null +++ b/tests/corpus/7324-loop-bound-parameter-window-sums.ts @@ -0,0 +1,33 @@ +// Sliding-window sums whose length and width are number parameters read +// from JSON, so no caller proves them int32s: int32 sizes run the +// function's int32 version, and a fractional width, a length past the +// int32 range, -0 and NaN run its ordinary body. + +function windowSums(n: number, width: number, values: number[], out: number[]): number { + let total = 0; + for (let i = 0; i + width <= n; i++) { + let s = 0; + for (let k = 0; k < width; k++) s += values[i + k]!; + out[i] = s; + total += s; + } + return total; +} + +const values: number[] = []; +for (let i = 0; i < 4000; i++) values.push((i * 37) % 101); + +const runs = JSON.parse( + "[[4000, 64, 3], [4000, 1, 2], [100, 2.5, 2], [10, 0, 1], [-0, 3, 1], [4000, 3000000000, 1], [3000000000, 4, 1], [12, 20, 1]]", +) as [number, number, number][]; +runs.push([NaN, 2, 1], [Number("250"), Number("8"), 2]); +for (const [n, width, rounds] of runs) { + const out: number[] = []; + const length = Math.min(Number.isFinite(n) ? n : 0, 4000); + for (let i = 0; i < length; i++) out.push(0); + let total = 0; + // A length past the values reads holes: keep that run within them. + const limit = n > 4000 ? 4000 : n; + for (let r = 0; r < rounds; r++) total += windowSums(limit, width, values, out); + console.log(`${n} ${width} ${rounds}:`, total, out.length, out.slice(0, 3).join(",")); +} diff --git a/tests/corpus/7360-worker-typed-records.ts b/tests/corpus/7360-worker-typed-records.ts new file mode 100644 index 000000000..6749398f3 --- /dev/null +++ b/tests/corpus/7360-worker-typed-records.ts @@ -0,0 +1,37 @@ +import { Worker, isMainThread, parentPort, workerData } from "node:worker_threads"; + +// Typed records posted to a worker and back keep their keys, order and +// values, as in Node: records with optional fields, nested records and a +// record in workerData. +type Point = { x: number; y: number }; +type Job = { id: number; label: string; urgent: boolean; at: Point; note?: string }; +type Reply = { id: number; sum: number; label: string }; + +if (isMainThread) { + const start: Point = { x: 10, y: 20 }; + const worker = new Worker(new URL(import.meta.url), { workerData: start }); + const replies: string[] = []; + worker.on("message", (reply: Reply) => { + replies.push(`${JSON.stringify(reply)} ${Object.keys(reply).join(",")}`); + if (replies.length === 3) { + for (const line of replies) console.log(line); + void worker.terminate(); + } + }); + const jobs: Job[] = [ + { id: 1, label: "a", urgent: false, at: { x: 1, y: 2 } }, + { id: 2, label: "b", urgent: true, at: { x: 3, y: 4 }, note: "n" }, + { id: 3, label: "", urgent: false, at: { x: -1, y: 0.25 } }, + ]; + for (const job of jobs) worker.postMessage(job); +} else { + const origin = workerData as Point; + parentPort!.on("message", (job: Job) => { + const reply: Reply = { + id: job.id, + sum: job.at.x + job.at.y + origin.x + origin.y, + label: `${job.label}${job.urgent ? "!" : ""}${job.note ?? ""}`, + }; + parentPort!.postMessage(reply); + }); +} diff --git a/tests/corpus/7361-message-listeners-share-value.ts b/tests/corpus/7361-message-listeners-share-value.ts new file mode 100644 index 000000000..2a4e287df --- /dev/null +++ b/tests/corpus/7361-message-listeners-share-value.ts @@ -0,0 +1,45 @@ +import { MessageChannel, type MessagePort } from "node:worker_threads"; + +// Listeners of one message share its value, as in Node: a typed listener +// that runs after an untyped one sees the changes the untyped one made, +// and a listener on a discriminated union reads the arm its discriminator +// names. +type Item = { id: number; name: string }; +type Shape = { kind: "circle"; size: number } | { kind: "square"; size: number; corner: string }; + +const lines: string[] = []; +let pending = 0; +const ports: MessagePort[] = []; +function finish(): void { + if (--pending > 0) return; + for (const line of lines) console.log(line); + for (const port of ports) port.close(); +} + +const shared = new MessageChannel(); +ports.push(shared.port1); +shared.port1.on("message", (value: Item) => { + lines.push(`typed first ${value.id} ${value.name}`); +}); +shared.port1.on("message", (value: any) => { + value.id = value.id * 10; + value.name = `${value.name}!`; +}); +shared.port1.on("message", (value: Item) => { + lines.push(`typed after ${value.id} ${value.name}`); + finish(); +}); +pending += 2; +shared.port2.postMessage({ id: 1, name: "a" }); +shared.port2.postMessage({ id: 2, name: "b" }); + +const shapes = new MessageChannel(); +ports.push(shapes.port1); +shapes.port1.on("message", (shape: Shape) => { + if (shape.kind === "circle") lines.push(`circle ${shape.size}`); + else lines.push(`square ${shape.size} ${shape.corner}`); + finish(); +}); +pending += 2; +shapes.port2.postMessage({ kind: "circle", size: 3 }); +shapes.port2.postMessage({ kind: "square", size: 4, corner: "round" }); diff --git a/tests/corpus/class-quoted-accessor-keys.mjs b/tests/corpus/class-quoted-accessor-keys.mjs new file mode 100644 index 000000000..637300b43 --- /dev/null +++ b/tests/corpus/class-quoted-accessor-keys.mjs @@ -0,0 +1,26 @@ +// Methods whose quoted names begin with "get:" or "set:", next to an +// accessor of the property after the prefix, read and called through +// untyped values. +class Quoted { + "get:size"() { + return "method get:size"; + } + "set:size"(value) { + return `method set:size ${value}`; + } + get size() { + return 3; + } +} +class Sized { + constructor() { + this.size = 5; + } +} +const values = [new Quoted(), new Sized(), { "get:size": () => "record get:size", size: 1 }]; +for (const value of values) { + const getter = value["get:size"]; + console.log(typeof getter, getter?.call(value)); + console.log(typeof value["set:size"], value.size); + if (value instanceof Quoted) console.log(value["get:size"](), value["set:size"](7)); +} diff --git a/tests/coverage-fixtures/blocked-signature-body.ts b/tests/coverage-fixtures/blocked-signature-body.ts new file mode 100644 index 000000000..2c37484e0 --- /dev/null +++ b/tests/coverage-fixtures/blocked-signature-body.ts @@ -0,0 +1,19 @@ +// Functions whose signatures cannot compile: the report still lists what +// their bodies would report. A use of the blocked parameter names the +// declaration; the body's own unsupported statement reports itself, with +// the reached diagnostics for a function the entry calls and as unreached +// for one nothing calls. +function measure(target: WeakRef, label: string): number { + const extra = new WeakRef({ label }); + console.log(target); + return label.length + (extra === undefined ? 0 : 1); +} +function unused(target: WeakRef): number { + const extra = new WeakRef({ target }); + return extra === undefined ? 0 : 1; +} +console.log(measure(new WeakRef({}), "abc")); +// A declared `this` parameter gives the body its receiver. +function described(this: { label: string }, target: WeakRef): string { + return this.label + String(target); +} diff --git a/tests/coverage-fixtures/startup-fences.js b/tests/coverage-fixtures/startup-fences.js new file mode 100644 index 000000000..22aad4e0a --- /dev/null +++ b/tests/coverage-fixtures/startup-fences.js @@ -0,0 +1,9 @@ +// Deferred fences in a module's top-level code run when the module loads; +// a fence inside a function runs when the function is called. +"use strict"; +var value = JSON.parse("{}"); +console.log(value instanceof Object); +function check(other) { + return other instanceof Object; +} +console.log(check([])); diff --git a/tests/diagnostics/arguments-length-self-call.ts b/tests/diagnostics/arguments-length-self-call.ts new file mode 100644 index 000000000..3afc2cb33 --- /dev/null +++ b/tests/diagnostics/arguments-length-self-call.ts @@ -0,0 +1,8 @@ +// `arguments.length` in a function expression stored in a slot with more +// parameters counts that slot's arguments, but a call through the +// expression's own name passes only the declared ones: no constant answers +// both, so the read fences. +const count: (a: number, b: number) => number = function self(a: number): number { + return a > 0 ? self(a - 1) + arguments.length : arguments.length; +}; +console.log(count(2, 0)); diff --git a/tests/diagnostics/generic-value-bindings.ts b/tests/diagnostics/generic-value-bindings.ts index a7de7aa1b..7c786c67c 100644 --- a/tests/diagnostics/generic-value-bindings.ts +++ b/tests/diagnostics/generic-value-bindings.ts @@ -60,3 +60,15 @@ function throughParam(a: Amb): void { console.log(a.m(4)); } throughParam(amb); + +// A `var` binding inside a function, declared in a branch and called from +// a closure: when the branch does not run, Node finds the binding undefined. +// The compiled instances have no per-call flag for that, so the call fences. +function branchVar(flag: boolean): number { + if (flag) { + var picked = (x: T): T => x; + } + const invoke = (): number => picked(1); + return invoke(); +} +console.log(branchVar(true)); diff --git a/tests/diagnostics/jsx-factory-mismatch/main.ts b/tests/diagnostics/jsx-factory-mismatch/main.ts new file mode 100644 index 000000000..47f9ff433 --- /dev/null +++ b/tests/diagnostics/jsx-factory-mismatch/main.ts @@ -0,0 +1,3 @@ +// The factory call a JSX element lowers to must type-check: h takes string children. +import { view } from "./view.js"; +console.log(view); diff --git a/tests/diagnostics/jsx-factory-mismatch/tsconfig.json b/tests/diagnostics/jsx-factory-mismatch/tsconfig.json new file mode 100644 index 000000000..4fcab4d21 --- /dev/null +++ b/tests/diagnostics/jsx-factory-mismatch/tsconfig.json @@ -0,0 +1 @@ +{ "compilerOptions": { "strict": true, "jsx": "react", "jsxFactory": "h" } } diff --git a/tests/diagnostics/jsx-factory-mismatch/view.tsx b/tests/diagnostics/jsx-factory-mismatch/view.tsx new file mode 100644 index 000000000..02bc0a95b --- /dev/null +++ b/tests/diagnostics/jsx-factory-mismatch/view.tsx @@ -0,0 +1,13 @@ +export function h(tag: string, props: object | null, ...children: string[]): string { + return `<${tag}>${children.join("")}`; +} +declare global { + namespace JSX { + type Element = string; + interface IntrinsicElements { + [name: string]: object; + } + } +} +const count = 3; +export const view =

    count: {count}

    ; diff --git a/tests/diagnostics/jsx-preserve/main.ts b/tests/diagnostics/jsx-preserve/main.ts new file mode 100644 index 000000000..1a5a8a0bc --- /dev/null +++ b/tests/diagnostics/jsx-preserve/main.ts @@ -0,0 +1,3 @@ +// "jsx": "preserve" emits JSX unchanged, so there is no call to compile. +import { view } from "./view.js"; +console.log(view); diff --git a/tests/diagnostics/jsx-preserve/tsconfig.json b/tests/diagnostics/jsx-preserve/tsconfig.json new file mode 100644 index 000000000..8bef9f7f4 --- /dev/null +++ b/tests/diagnostics/jsx-preserve/tsconfig.json @@ -0,0 +1 @@ +{ "compilerOptions": { "strict": true, "jsx": "preserve" } } diff --git a/tests/diagnostics/jsx-preserve/view.tsx b/tests/diagnostics/jsx-preserve/view.tsx new file mode 100644 index 000000000..48ce38147 --- /dev/null +++ b/tests/diagnostics/jsx-preserve/view.tsx @@ -0,0 +1,9 @@ +declare global { + namespace JSX { + type Element = string; + interface IntrinsicElements { + [name: string]: object; + } + } +} +export const view =

    kept as JSX

    ; diff --git a/tests/diagnostics/jsx-type-errors/main.ts b/tests/diagnostics/jsx-type-errors/main.ts new file mode 100644 index 000000000..00c4da53b --- /dev/null +++ b/tests/diagnostics/jsx-type-errors/main.ts @@ -0,0 +1,3 @@ +// TypeScript errors in JSX are reported with the positions of the file the program compiles. +import { view } from "./view.js"; +console.log(view); diff --git a/tests/diagnostics/jsx-type-errors/tsconfig.json b/tests/diagnostics/jsx-type-errors/tsconfig.json new file mode 100644 index 000000000..4fcab4d21 --- /dev/null +++ b/tests/diagnostics/jsx-type-errors/tsconfig.json @@ -0,0 +1 @@ +{ "compilerOptions": { "strict": true, "jsx": "react", "jsxFactory": "h" } } diff --git a/tests/diagnostics/jsx-type-errors/view.tsx b/tests/diagnostics/jsx-type-errors/view.tsx new file mode 100644 index 000000000..8a9be99ba --- /dev/null +++ b/tests/diagnostics/jsx-type-errors/view.tsx @@ -0,0 +1,15 @@ +export function h(tag: string | ((props: { count: number }) => string), props: object | null, ...children: string[]): string { + return typeof tag === "string" ? `<${tag}>${children.join("")}` : tag(props as { count: number }); +} +declare global { + namespace JSX { + type Element = string; + interface IntrinsicElements { + p: { title?: string }; + } + } +} +function Count(props: { count: number }): string { + return String(props.count); +} +export const view =

    text

    ; diff --git a/tests/diagnostics/stream-byte-mode-chunk-fences.ts b/tests/diagnostics/stream-byte-mode-chunk-fences.ts new file mode 100644 index 000000000..6afd1d794 --- /dev/null +++ b/tests/diagnostics/stream-byte-mode-chunk-fences.ts @@ -0,0 +1,36 @@ +// A byte-mode stream's write chunks are Buffers: an underscore method +// whose chunk parameter has another type is fenced, and super(undefined) +// passes no options, so the stream is in byte mode. A parameter that +// admits the Buffer (Buffer | string) is not fenced, inline or as a +// method. +import { Writable } from "node:stream"; + +class Numbers extends Writable { + constructor() { + super(undefined); + } + _write(chunk: number, _encoding: BufferEncoding, cb: (err?: Error | null) => void): void { + console.log(chunk); + cb(); + } +} +new Numbers().end("x"); + +const accepted = new Writable({ + write(chunk: Buffer | string, _encoding: BufferEncoding, cb: (err?: Error | null) => void): void { + console.log(typeof chunk === "string" ? chunk : chunk.toString()); + cb(); + }, +}); +accepted.end("z"); + +class Wide extends Writable { + constructor() { + super(undefined); + } + _write(chunk: Buffer | string, _encoding: BufferEncoding, cb: (err?: Error | null) => void): void { + console.log(typeof chunk === "string" ? chunk : chunk.toString()); + cb(); + } +} +new Wide().end("w"); diff --git a/tests/diagnostics/tls-socket-fences.ts b/tests/diagnostics/tls-socket-fences.ts new file mode 100644 index 000000000..948a8312a --- /dev/null +++ b/tests/diagnostics/tls-socket-fences.ts @@ -0,0 +1,20 @@ +// new tls.TLSSocket forms that have no lowering. +import { readFileSync } from "node:fs"; +import * as net from "node:net"; +import * as tls from "node:tls"; + +const cert = readFileSync("cert.pem"); +const key = readFileSync("key.pem"); +const context = tls.createSecureContext({ cert, key }); +function alpn(): string[] { + return ["h2"]; +} + +net.createServer((socket) => { + // The client mode: Node starts no handshake for it. + new tls.TLSSocket(socket, { cert, key }); + // A member after secureContext with side effects. + new tls.TLSSocket(socket, { isServer: true, secureContext: context, ALPNProtocols: alpn() }); + // A secureContext that is not a SecureContext. + new tls.TLSSocket(socket, { isServer: true, secureContext: { cert, key } }); +}); diff --git a/tests/diagnostics/webcrypto-refusals.ts b/tests/diagnostics/webcrypto-refusals.ts index 309e70c4e..030aa03c0 100644 --- a/tests/diagnostics/webcrypto-refusals.ts +++ b/tests/diagnostics/webcrypto-refusals.ts @@ -1,8 +1,8 @@ -// The native WebCrypto surface is crypto.subtle's secret-key slice, called -// directly on crypto.subtle. crypto.subtle or its methods taken as values -// are refused by name; the bundled declarations omit the other -// SubtleCrypto methods (node-types/fenced.ts pins their fence under -// @types/node). +// The native WebCrypto surface is crypto.subtle's secret-key slice and its +// ECDSA, Ed25519 and RSA algorithms, called directly on crypto.subtle. +// crypto.subtle or its methods taken as values are refused by name; the +// bundled declarations omit the other SubtleCrypto methods +// (node-types/fenced.ts pins their fence under @types/node). const subtle = crypto.subtle; console.log(typeof subtle); diff --git a/tests/fixtures/node-types/dh.ts b/tests/fixtures/node-types/dh.ts new file mode 100644 index 000000000..0b20b1056 --- /dev/null +++ b/tests/fixtures/node-types/dh.ts @@ -0,0 +1,22 @@ +import { + type DiffieHellman, + type DiffieHellmanGroup, + createDiffieHellman, + createDiffieHellmanGroup, + getDiffieHellman, +} from "node:crypto"; + +const group: DiffieHellmanGroup = getDiffieHellman("modp2"); +const alice: DiffieHellman = createDiffieHellman(group.getPrime(), group.getGenerator()); +const bob: DiffieHellman = createDiffieHellman(group.getPrime("hex"), "hex", 2); +alice.setPrivateKey(Buffer.from("0a0b0c", "hex")); +bob.setPrivateKey("0d0e0f", "hex"); +const alicePublic: Buffer = alice.generateKeys(); +const bobPublic: string = bob.generateKeys("base64"); +const fromAlice: string = alice.computeSecret(bobPublic, "base64", "hex"); +const fromBob: Buffer = bob.computeSecret(alicePublic); +console.log(fromAlice === fromBob.toString("hex"), fromAlice.slice(0, 16), alice.verifyError); +const named: DiffieHellmanGroup = createDiffieHellmanGroup("modp5"); +console.log(named.getPrime().length, named.getGenerator("hex"), bob.getPrivateKey("hex")); +const generated: string = named.generateKeys("hex"); +console.log(generated.length <= 384, named.getPublicKey("hex") === generated); diff --git a/tests/fixtures/node-types/fenced.ts b/tests/fixtures/node-types/fenced.ts index 274d57272..27e68a618 100644 --- a/tests/fixtures/node-types/fenced.ts +++ b/tests/fixtures/node-types/fenced.ts @@ -69,13 +69,13 @@ http2.createSecureServer({ http2.createSecureServer({ allowHTTP1: true, cert: "pem", key: "pem", streamResetBurst: 1 + 0 }); http2.connect("https://localhost"); /* The crypto surface beyond the lowered slice (randomness, the hash - * chain, the introspection statics): asymmetric-key operations name the - * missing public-key stack, symmetric ciphers the missing cipher stack, - * prime checks theirs, and setFips the FIPS truth (getFips() lowers to 0). */ -import { checkPrimeSync, createCipheriv, generateKeyPair, setFips } from "node:crypto"; -generateKeyPair("rsa", { modulusLength: 2048 }, () => {}); + * chain, the introspection statics): RSA encryption names the lowered + * asymmetric surface, symmetric ciphers the missing cipher stack, the + * secure heap the generic hint, and setFips the FIPS truth (getFips() lowers to 0). */ +import { createCipheriv, publicEncrypt, secureHeapUsed, setFips } from "node:crypto"; +publicEncrypt("key", Buffer.alloc(1)); createCipheriv("aes-128-ccm", Buffer.alloc(16), Buffer.alloc(12), { authTagLength: 16 }); -checkPrimeSync(7n); +secureHeapUsed(); setFips(false); fetch("https://example.invalid/", { integrity: "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", @@ -99,13 +99,19 @@ void callbackified; import { createECDH } from "node:crypto"; createECDH("sect163k1"); createECDH("prime256v1").computeSecret(Buffer.alloc(65), "hex"); +/* DiffieHellman's Object.prototype members, and crypto.diffieHellman over + * asymmetric KeyObjects. */ +import { createDiffieHellman, diffieHellman, type KeyObject } from "node:crypto"; +createDiffieHellman(Buffer.from([23])).toString(); +declare const dhKey: KeyObject; +diffieHellman({ privateKey: dhKey, publicKey: dhKey }); /* WebCrypto beyond the secret-key slice: @types/node declares the whole * SubtleCrypto surface, and the methods outside the slice, such as - * wrapKey, fence by name. */ -async function wrapWithWebCrypto(key: import("node:crypto").webcrypto.CryptoKey): Promise { - await crypto.subtle.wrapKey("raw", key, key, "AES-KW"); + * getPublicKey, fence by name. */ +async function publicKeyWithWebCrypto(key: import("node:crypto").webcrypto.CryptoKey): Promise { + await crypto.subtle.getPublicKey(key, ["verify"]); } -void wrapWithWebCrypto; +void publicKeyWithWebCrypto; /* Node's navigator global: typeof navigator and navigator.userAgent * compile; the other members fence by name. */ const cores = navigator.hardwareConcurrency; diff --git a/tests/fixtures/node-types/keys.ts b/tests/fixtures/node-types/keys.ts new file mode 100644 index 000000000..ba91f5d18 --- /dev/null +++ b/tests/fixtures/node-types/keys.ts @@ -0,0 +1,43 @@ +import { + type KeyObject, + createPrivateKey, + createPublicKey, + createSign, + createVerify, + generateKeyPair, + generateKeyPairSync, + sign, + verify, +} from "node:crypto"; + +const { publicKey, privateKey } = generateKeyPairSync("ec", { namedCurve: "P-256" }); +const data = Buffer.from("typed keys"); +const signature: Buffer = sign("sha256", data, privateKey); +console.log(publicKey.type, privateKey.asymmetricKeyType, verify("sha256", data, publicKey, signature)); +const pem = privateKey.export({ type: "pkcs8", format: "pem" }); +const parsed: KeyObject = createPrivateKey(pem); +console.log(parsed.equals(privateKey), createPublicKey(parsed).equals(publicKey)); +const pems = generateKeyPairSync("rsa", { + modulusLength: 1024, + publicKeyEncoding: { type: "spki", format: "pem" }, + privateKeyEncoding: { type: "pkcs8", format: "pem" }, +}); +const signer = createSign("SHA256"); +signer.update("typed"); +const hex: string = signer.sign(pems.privateKey, "hex"); +const verifier = createVerify("SHA256"); +verifier.update("typed"); +console.log(pems.publicKey.split("\n")[0], verifier.verify(pems.publicKey, hex, "hex")); +const ed = generateKeyPairSync("ed25519"); +const edSignature: Buffer = sign(null, data, ed.privateKey); +console.log(ed.publicKey.asymmetricKeyType, edSignature.length, verify(null, data, ed.publicKey, edSignature)); +const edJwk = ed.publicKey.export({ format: "jwk" }); +console.log(edJwk.kty, edJwk.crv, createPublicKey({ key: edJwk, format: "jwk" }).equals(ed.publicKey)); +const edDer = generateKeyPairSync("ed25519", { + publicKeyEncoding: { type: "spki", format: "der" }, + privateKeyEncoding: { type: "pkcs8", format: "der" }, +}); +console.log(edDer.publicKey.length, edDer.privateKey.length); +generateKeyPair("rsa", { modulusLength: 1024 }, (err, rsaPublic, rsaPrivate) => { + console.log(err, rsaPublic.asymmetricKeyDetails?.modulusLength, rsaPrivate.type); +}); diff --git a/tests/fixtures/node-types/tls-socket-wrap.ts b/tests/fixtures/node-types/tls-socket-wrap.ts new file mode 100644 index 000000000..4f1272b33 --- /dev/null +++ b/tests/fixtures/node-types/tls-socket-wrap.ts @@ -0,0 +1,31 @@ +// Under @types/node: an http CONNECT handler terminates TLS on the tunnel +// socket with new tls.TLSSocket, and the client runs its handshake inside +// the tunnel with tls.connect({ socket }). +import { readFileSync } from "node:fs"; +import * as http from "node:http"; +import * as net from "node:net"; +import * as tls from "node:tls"; + +const ca = readFileSync("tests/fixtures/server/certs/ca.pem"); +const cert = readFileSync("tests/fixtures/server/certs/localhost.pem"); +const key = readFileSync("tests/fixtures/server/certs/localhost-key.pem"); + +const proxy = http.createServer(); +proxy.on("connect", (req: http.IncomingMessage, socket: net.Socket) => { + socket.write("HTTP/1.1 200 Connection Established\r\n\r\n"); + const secure = new tls.TLSSocket(socket, { isServer: true, cert, key, ALPNProtocols: ["http/1.1"] }); + secure.on("secure", () => console.log("proxy secure", req.url, secure.alpnProtocol)); + secure.on("data", (chunk: Buffer) => secure.end(`intercepted ${chunk.toString()}`)); + secure.on("close", () => proxy.close()); +}); +proxy.listen(0, "127.0.0.1", () => { + const port = (proxy.address() as net.AddressInfo).port; + const raw = net.connect(port, "127.0.0.1", () => raw.write("CONNECT example.test:443 HTTP/1.1\r\n\r\n")); + raw.once("data", () => { + const client = tls.connect({ socket: raw, ca, servername: "localhost", ALPNProtocols: ["http/1.1"] }, () => { + console.log("client secureConnect", client.authorized, client.alpnProtocol); + client.write("hello"); + }); + client.on("data", (data: Buffer) => console.log("client data", data.toString())); + }); +}); diff --git a/tests/fixtures/node-types/webcrypto-ecdsa.ts b/tests/fixtures/node-types/webcrypto-ecdsa.ts new file mode 100644 index 000000000..2ec7cc4a7 --- /dev/null +++ b/tests/fixtures/node-types/webcrypto-ecdsa.ts @@ -0,0 +1,34 @@ +/* WebCrypto ECDSA, Ed25519 and RSA under @types/node: generateKey, exportKey, + * importKey, sign and verify lower to the same native values as under the + * bundled declarations. */ +import type { webcrypto } from "node:crypto"; + +async function main(): Promise { + const data = new TextEncoder().encode("node-types"); + const params: webcrypto.EcKeyGenParams = { name: "ECDSA", namedCurve: "P-384" }; + const pair: webcrypto.CryptoKeyPair = await crypto.subtle.generateKey(params, true, ["sign", "verify"]); + const ecdsa: webcrypto.EcdsaParams = { name: "ECDSA", hash: "SHA-384" }; + const signature = await crypto.subtle.sign(ecdsa, pair.privateKey, data); + console.log(pair.privateKey.type, signature.byteLength, await crypto.subtle.verify(ecdsa, pair.publicKey, signature, data)); + const spki = await crypto.subtle.exportKey("spki", pair.publicKey); + const imported = await crypto.subtle.importKey("spki", spki, { name: "ECDSA", namedCurve: "P-384" }, false, ["verify"]); + const algorithm = imported.algorithm as webcrypto.EcKeyAlgorithm; + console.log(algorithm.name, algorithm.namedCurve, await crypto.subtle.verify(ecdsa, imported, signature, data)); + const jwk: webcrypto.JsonWebKey = await crypto.subtle.exportKey("jwk", pair.privateKey); + console.log(jwk.kty, jwk.crv, jwk.key_ops?.join(",")); + const rsa = await crypto.subtle.generateKey( + { name: "RSA-PSS", modulusLength: 1024, publicExponent: new Uint8Array([1, 0, 1]), hash: "SHA-256" }, + false, + ["sign", "verify"], + ); + const pss: webcrypto.RsaPssParams = { name: "RSA-PSS", saltLength: 32 }; + const rsaSignature = await crypto.subtle.sign(pss, rsa.privateKey, data); + console.log(rsaSignature.byteLength, await crypto.subtle.verify(pss, rsa.publicKey, rsaSignature, data)); + const ed = (await crypto.subtle.generateKey("Ed25519", true, ["sign", "verify"])) as webcrypto.CryptoKeyPair; + const edSignature = await crypto.subtle.sign("Ed25519", ed.privateKey, data); + const raw = await crypto.subtle.exportKey("raw", ed.publicKey); + const edPublic = await crypto.subtle.importKey("raw", raw, "Ed25519", true, ["verify"]); + console.log(edPublic.algorithm.name, raw.byteLength, edSignature.byteLength, await crypto.subtle.verify("Ed25519", edPublic, edSignature, data)); +} + +void main(); diff --git a/tests/harness/__snapshots__/arguments-length-self-call.ts.txt b/tests/harness/__snapshots__/arguments-length-self-call.ts.txt new file mode 100644 index 000000000..7e94b56c6 --- /dev/null +++ b/tests/harness/__snapshots__/arguments-length-self-call.ts.txt @@ -0,0 +1,6 @@ +arguments-length-self-call.ts:6:32 - error SC1090: 'arguments.length' in a function expression that is called both through its own name and through a function type with more parameters (the count varies per call) are not supported yet + + 5 | const count: (a: number, b: number) => number = function self(a: number): number { + 6 | return a > 0 ? self(a - 1) + arguments.length : arguments.length; + | ^~~~~~~~~~~~~~~~ + 7 | }; \ No newline at end of file diff --git a/tests/harness/__snapshots__/coverage-js-builtin-callable-alias.txt b/tests/harness/__snapshots__/coverage-js-builtin-callable-alias.txt index 53eac6c3c..6291c291f 100644 --- a/tests/harness/__snapshots__/coverage-js-builtin-callable-alias.txt +++ b/tests/harness/__snapshots__/coverage-js-builtin-callable-alias.txt @@ -3,7 +3,7 @@ scriptc coverage tests/coverage-fixtures/js-builtin-callable-alias.js statements analyzed 3 compile statically 2 (66%) - deferred to runtime 1 site (JS statements that throw their fence if executed) + runs at startup 1 site (JS statements in module top-level code, which runs when the module loads) ×1 SC1090 a builtin function alias as an escaping value (call 'ext' directly) at tests/coverage-fixtures/js-builtin-callable-alias.js:8:13 hint: this form is valid TypeScript but does not compile yet — rewrite it with a supported equivalent of the construct the message names (https://scriptc.dev/limitations lists the boundary) diff --git a/tests/harness/__snapshots__/generic-value-bindings.ts.txt b/tests/harness/__snapshots__/generic-value-bindings.ts.txt index 04fa9d069..835b84fa7 100644 --- a/tests/harness/__snapshots__/generic-value-bindings.ts.txt +++ b/tests/harness/__snapshots__/generic-value-bindings.ts.txt @@ -92,4 +92,11 @@ generic-value-bindings.ts:60:15 - error SC1090: calls of the generic method 'm' 59 | function throughParam(a: Amb): void { 60 | console.log(a.m(4)); | ^~~~~~ - 61 | } \ No newline at end of file + 61 | } + +generic-value-bindings.ts:71:32 - error SC1090: generic arrow/function-expression bindings declared inside functions that code can call before their declaration runs (the binding 'picked' is a 'var' in a branch or block, or is called from a place that can run first — declare it with const before its uses) are not supported yet + + 70 | } + 71 | const invoke = (): number => picked(1); + | ^~~~~~ + 72 | return invoke(); \ No newline at end of file diff --git a/tests/harness/__snapshots__/jsx-factory-mismatch/main.ts.txt b/tests/harness/__snapshots__/jsx-factory-mismatch/main.ts.txt new file mode 100644 index 000000000..e0a19b654 --- /dev/null +++ b/tests/harness/__snapshots__/jsx-factory-mismatch/main.ts.txt @@ -0,0 +1,6 @@ +jsx-factory-mismatch/view.tsx:13:45 - error SC0001: Argument of type 'number' is not assignable to parameter of type 'string'. (in the call this JSX lowers to) + + 12 | const count = 3; + 13 | export const view = h("p", null, "count: ", count); + | ^~~~~ + 14 | \ No newline at end of file diff --git a/tests/harness/__snapshots__/jsx-preserve/main.ts.txt b/tests/harness/__snapshots__/jsx-preserve/main.ts.txt new file mode 100644 index 000000000..9e0239088 --- /dev/null +++ b/tests/harness/__snapshots__/jsx-preserve/main.ts.txt @@ -0,0 +1,8 @@ +jsx-preserve/view.tsx:9:21 - error SC1090: JSX with "jsx": "preserve" is not supported: that setting keeps JSX in the output instead of lowering it to calls + + 8 | } + 9 | export const view =

    kept as JSX

    ; + | ^~~~~~~~~~~~~~~~~~ + 10 | + + hint: set "jsx" to "react", "react-jsx" or "react-jsxdev" in the project tsconfig \ No newline at end of file diff --git a/tests/harness/__snapshots__/jsx-type-errors/main.ts.txt b/tests/harness/__snapshots__/jsx-type-errors/main.ts.txt new file mode 100644 index 000000000..c87c719a8 --- /dev/null +++ b/tests/harness/__snapshots__/jsx-type-errors/main.ts.txt @@ -0,0 +1,13 @@ +jsx-type-errors/view.tsx:15:30 - error SC0001: Type 'number' is not assignable to type 'string'. + + 14 | } + 15 | export const view = h("p", { title: 1 }, "text ", h(Count, { count: "two" })); + | ^~~~~~~ + 16 | + +jsx-type-errors/view.tsx:15:62 - error SC0001: Type 'string' is not assignable to type 'number'. + + 14 | } + 15 | export const view = h("p", { title: 1 }, "text ", h(Count, { count: "two" })); + | ^~~~~~~ + 16 | \ No newline at end of file diff --git a/tests/harness/__snapshots__/node-types-fenced.txt b/tests/harness/__snapshots__/node-types-fenced.txt index 3198db366..cabce49c4 100644 --- a/tests/harness/__snapshots__/node-types-fenced.txt +++ b/tests/harness/__snapshots__/node-types-fenced.txt @@ -115,36 +115,36 @@ fenced.ts:69:71 - error SC2020: 'createSecureServer option 'streamResetBurst' wi hint: h2 session-tuning options are accepted with literal values but are not applied yet -fenced.ts:76:1 - error SC2020: 'crypto.generateKeyPair' is typed by @types/node but has no scriptc lowering yet +fenced.ts:76:1 - error SC2020: 'crypto.publicEncrypt' is typed by @types/node but has no scriptc lowering yet - 75 | import { checkPrimeSync, createCipheriv, generateKeyPair, setFips } from "node:crypto"; - 76 | generateKeyPair("rsa", { modulusLength: 2048 }, () => {}); - | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 75 | import { createCipheriv, publicEncrypt, secureHeapUsed, setFips } from "node:crypto"; + 76 | publicEncrypt("key", Buffer.alloc(1)); + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 77 | createCipheriv("aes-128-ccm", Buffer.alloc(16), Buffer.alloc(12), { authTagLength: 16 }); - hint: asymmetric-key operations need a public-key stack (bignum, RSA/EC/EdDSA math) and public and private KeyObjects — neither exists in the static runtime, so no faithful lowering can be small; the lowered crypto surface is hashing, randomness, secret KeyObjects, and the introspection statics + hint: RSA encryption has no lowering — the asymmetric crypto surface is RSA and EC KeyObjects (generateKeyPair, createPrivateKey, createPublicKey), sign, verify, createSign, createVerify, createECDH, and createDiffieHellman fenced.ts:77:16 - error SC2020: 'crypto.createCipheriv with cipher 'aes-128-ccm'' is part of the standard library types but has no scriptc lowering yet - 76 | generateKeyPair("rsa", { modulusLength: 2048 }, () => {}); + 76 | publicEncrypt("key", Buffer.alloc(1)); 77 | createCipheriv("aes-128-ccm", Buffer.alloc(16), Buffer.alloc(12), { authTagLength: 16 }); | ^~~~~~~~~~~~~ - 78 | checkPrimeSync(7n); + 78 | secureHeapUsed(); - hint: the supported ciphers are aes-128-cbc, aes-192-cbc, aes-256-cbc, aes-128-ctr, aes-192-ctr, aes-256-ctr, aes-128-gcm, aes-192-gcm, aes-256-gcm, and chacha20-poly1305 + hint: the supported ciphers are aes-128, aes-192 and aes-256 in cbc, ctr, gcm, ecb, cfb, cfb8, cfb1 and ofb modes, des-ede3 in cbc, ecb, cfb, cfb8, cfb1 and ofb modes, des-ede in cbc, ecb, cfb and ofb modes, chacha20, and chacha20-poly1305 -fenced.ts:78:1 - error SC2020: 'crypto.checkPrimeSync' is typed by @types/node but has no scriptc lowering yet +fenced.ts:78:1 - error SC2020: 'crypto.secureHeapUsed' is typed by @types/node but has no scriptc lowering yet 77 | createCipheriv("aes-128-ccm", Buffer.alloc(16), Buffer.alloc(12), { authTagLength: 16 }); - 78 | checkPrimeSync(7n); - | ^~~~~~~~~~~~~~~~~~ + 78 | secureHeapUsed(); + | ^~~~~~~~~~~~~~~~ 79 | setFips(false); hint: the type checker sees everything @types/node declares, but only the supported surface compiles (https://scriptc.dev/limitations) fenced.ts:79:1 - error SC2020: 'crypto.setFips' is typed by @types/node but has no scriptc lowering yet - 78 | checkPrimeSync(7n); + 78 | secureHeapUsed(); 79 | setFips(false); | ^~~~~~~~~~~~~~ 80 | fetch("https://example.invalid/", { @@ -219,24 +219,42 @@ fenced.ts:101:1 - error SC2020: 'ECDH.computeSecret(key, encoding) with a Buffer 100 | createECDH("sect163k1"); 101 | createECDH("prime256v1").computeSecret(Buffer.alloc(65), "hex"); | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 102 | /* WebCrypto beyond the secret-key slice: @types/node declares the whole + 102 | /* DiffieHellman's Object.prototype members, and crypto.diffieHellman over hint: Node reads the second argument as the key's input encoding and answers a Buffer; pass the output encoding third: computeSecret(key, undefined, encoding) -fenced.ts:106:9 - error SC2020: 'crypto.subtle.wrapKey' is part of the standard library types but has no scriptc lowering yet +fenced.ts:105:1 - error SC2020: 'DiffieHellman.toString' is part of the standard library types but has no scriptc lowering yet - 105 | async function wrapWithWebCrypto(key: import("node:crypto").webcrypto.CryptoKey): Promise { - 106 | await crypto.subtle.wrapKey("raw", key, key, "AES-KW"); - | ^~~~~~~~~~~~~~~~~~~~~ - 107 | } + 104 | import { createDiffieHellman, diffieHellman, type KeyObject } from "node:crypto"; + 105 | createDiffieHellman(Buffer.from([23])).toString(); + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 106 | declare const dhKey: KeyObject; - hint: the native WebCrypto surface is crypto.subtle's importKey, generateKey and exportKey for HMAC and AES keys (importKey also for PBKDF2 and HKDF keys), encrypt and decrypt with AES-GCM, AES-CBC or AES-CTR, deriveBits and deriveKey with PBKDF2 or HKDF, sign and verify with HMAC, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle + hint: generateKeys(), computeSecret(), getPrime(), getGenerator(), getPublicKey(), getPrivateKey(), setPublicKey(), setPrivateKey() and verifyError are supported -fenced.ts:111:15 - error SC2020: 'navigator.hardwareConcurrency' is typed by @types/node but has no scriptc lowering yet +fenced.ts:107:1 - error SC2020: 'crypto.diffieHellman' is typed by @types/node but has no scriptc lowering yet - 110 | * compile; the other members fence by name. */ - 111 | const cores = navigator.hardwareConcurrency; + 106 | declare const dhKey: KeyObject; + 107 | diffieHellman({ privateKey: dhKey, publicKey: dhKey }); + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 108 | /* WebCrypto beyond the secret-key slice: @types/node declares the whole + + hint: crypto.diffieHellman takes DH, EC or X25519 KeyObjects and has no lowering; the objects of createDiffieHellman, getDiffieHellman and createECDH compute shared secrets + +fenced.ts:112:9 - error SC2020: 'crypto.subtle.getPublicKey' is part of the standard library types but has no scriptc lowering yet + + 111 | async function publicKeyWithWebCrypto(key: import("node:crypto").webcrypto.CryptoKey): Promise { + 112 | await crypto.subtle.getPublicKey(key, ["verify"]); + | ^~~~~~~~~~~~~~~~~~~~~~~~~~ + 113 | } + + hint: the native WebCrypto surface is crypto.subtle's importKey, generateKey and exportKey for HMAC and AES keys (importKey also for PBKDF2 and HKDF keys), encrypt and decrypt with AES-GCM, AES-CBC or AES-CTR, wrapKey and unwrapKey of secret keys with AES-KW or those ciphers, deriveBits and deriveKey with PBKDF2 or HKDF, sign and verify with HMAC, generateKey, importKey, exportKey, sign and verify for ECDSA, Ed25519, RSASSA-PKCS1-v1_5 and RSA-PSS, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle + +fenced.ts:117:15 - error SC2020: 'navigator.hardwareConcurrency' is typed by @types/node but has no scriptc lowering yet + + 116 | * compile; the other members fence by name. */ + 117 | const cores = navigator.hardwareConcurrency; | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 112 | void cores; + 118 | void cores; hint: of Node's navigator global only typeof navigator and navigator.userAgent compile \ No newline at end of file diff --git a/tests/harness/__snapshots__/stream-byte-mode-chunk-fences.ts.txt b/tests/harness/__snapshots__/stream-byte-mode-chunk-fences.ts.txt new file mode 100644 index 000000000..39a06dfd7 --- /dev/null +++ b/tests/harness/__snapshots__/stream-byte-mode-chunk-fences.ts.txt @@ -0,0 +1,8 @@ +stream-byte-mode-chunk-fences.ts:10:5 - error SC2020: 'a '_write' method whose parameter 0 is 'number' where Node passes 'Uint8Array'' is part of the standard library types but has no scriptc lowering yet + + 9 | constructor() { + 10 | super(undefined); + | ^~~~~~~~~~~~~~~~ + 11 | } + + hint: annotate the parameter with the Node signature's type or a supertype of it (chunks are Buffers here) \ No newline at end of file diff --git a/tests/harness/__snapshots__/tls-socket-fences.ts.txt b/tests/harness/__snapshots__/tls-socket-fences.ts.txt new file mode 100644 index 000000000..e912b7649 --- /dev/null +++ b/tests/harness/__snapshots__/tls-socket-fences.ts.txt @@ -0,0 +1,26 @@ +tls-socket-fences.ts:15:29 - error SC2020: 'new tls.TLSSocket in client mode' is part of the standard library types but has no scriptc lowering yet + + 14 | // The client mode: Node starts no handshake for it. + 15 | new tls.TLSSocket(socket, { cert, key }); + | ^~~~~~~~~~~~~ + 16 | // A member after secureContext with side effects. + + hint: Node starts no handshake for a client-mode TLSSocket — pass isServer: true for the server side; tls.connect makes client sockets + +tls-socket-fences.ts:17:71 - error SC2020: 'a new tls.TLSSocket options literal with side effects after secureContext' is part of the standard library types but has no scriptc lowering yet + + 16 | // A member after secureContext with side effects. + 17 | new tls.TLSSocket(socket, { isServer: true, secureContext: context, ALPNProtocols: alpn() }); + | ^~~~~~~~~~~~~~~~~~~~~ + 18 | // A secureContext that is not a SecureContext. + + hint: secureContext is read after the other members — move it to the end of the object + +tls-socket-fences.ts:19:62 - error SC2020: 'a '{ cert: Uint8Array; key: Uint8Array }' secureContext' is part of the standard library types but has no scriptc lowering yet + + 18 | // A secureContext that is not a SecureContext. + 19 | new tls.TLSSocket(socket, { isServer: true, secureContext: { cert, key } }); + | ^~~~~~~~~~~~~ + 20 | }); + + hint: pass a tls.createSecureContext({ cert, key }) result \ No newline at end of file diff --git a/tests/harness/__snapshots__/webcrypto-refusals.ts.txt b/tests/harness/__snapshots__/webcrypto-refusals.ts.txt index e46099f69..7242d2966 100644 --- a/tests/harness/__snapshots__/webcrypto-refusals.ts.txt +++ b/tests/harness/__snapshots__/webcrypto-refusals.ts.txt @@ -1,11 +1,11 @@ webcrypto-refusals.ts:6:16 - error SC2020: 'crypto.subtle as a value' is part of the standard library types but has no scriptc lowering yet - 5 | // @types/node). + 5 | // (node-types/fenced.ts pins their fence under @types/node). 6 | const subtle = crypto.subtle; | ^~~~~~~~~~~~~ 7 | console.log(typeof subtle); - hint: the native WebCrypto surface is crypto.subtle's importKey, generateKey and exportKey for HMAC and AES keys (importKey also for PBKDF2 and HKDF keys), encrypt and decrypt with AES-GCM, AES-CBC or AES-CTR, deriveBits and deriveKey with PBKDF2 or HKDF, sign and verify with HMAC, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle + hint: the native WebCrypto surface is crypto.subtle's importKey, generateKey and exportKey for HMAC and AES keys (importKey also for PBKDF2 and HKDF keys), encrypt and decrypt with AES-GCM, AES-CBC or AES-CTR, wrapKey and unwrapKey of secret keys with AES-KW or those ciphers, deriveBits and deriveKey with PBKDF2 or HKDF, sign and verify with HMAC, generateKey, importKey, exportKey, sign and verify for ECDSA, Ed25519, RSASSA-PKCS1-v1_5 and RSA-PSS, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle webcrypto-refusals.ts:9:20 - error SC2020: 'crypto.subtle.sign as a value' is part of the standard library types but has no scriptc lowering yet @@ -14,4 +14,4 @@ webcrypto-refusals.ts:9:20 - error SC2020: 'crypto.subtle.sign as a value' is pa | ^~~~~~~~~~~~~~~~~~ 10 | - hint: the native WebCrypto surface is crypto.subtle's importKey, generateKey and exportKey for HMAC and AES keys (importKey also for PBKDF2 and HKDF keys), encrypt and decrypt with AES-GCM, AES-CBC or AES-CTR, deriveBits and deriveKey with PBKDF2 or HKDF, sign and verify with HMAC, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle \ No newline at end of file + hint: the native WebCrypto surface is crypto.subtle's importKey, generateKey and exportKey for HMAC and AES keys (importKey also for PBKDF2 and HKDF keys), encrypt and decrypt with AES-GCM, AES-CBC or AES-CTR, wrapKey and unwrapKey of secret keys with AES-KW or those ciphers, deriveBits and deriveKey with PBKDF2 or HKDF, sign and verify with HMAC, generateKey, importKey, exportKey, sign and verify for ECDSA, Ed25519, RSASSA-PKCS1-v1_5 and RSA-PSS, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle \ No newline at end of file diff --git a/tests/harness/bootstrap-seed.test.ts b/tests/harness/bootstrap-seed.test.ts index 2e8b3bad9..76b1f6de5 100644 --- a/tests/harness/bootstrap-seed.test.ts +++ b/tests/harness/bootstrap-seed.test.ts @@ -78,6 +78,10 @@ test("CI restores each seed only into its matching instrumented bootstrap lane", const execute = checks.steps.find((step) => step.run?.split("\n").includes("node scripts/ci-native-bootstrap.mjs"), )!; + // The sanitized seed's build needs more memory than the runner has: swap backs it. + const swap = seed.steps.findIndex((step) => step.run?.includes("swapon /mnt/swapfile")); + expect(seed.steps[swap]?.if).toBe("matrix.flavor == 'san'"); + expect(swap).toBeLessThan(seed.steps.indexOf(prepare)); expect(prepare.env?.SCRIPTC_BOOTSTRAP_SEED_DIRECTORY).toBe("${{ runner.temp }}/native-bootstrap"); expect(execute.env?.SCRIPTC_BOOTSTRAP_SEED_DIRECTORY).toBe( prepare.env?.SCRIPTC_BOOTSTRAP_SEED_DIRECTORY, diff --git a/tests/harness/corpus-costs/4638-contract-program-lifetime-arrays.ts.json b/tests/harness/corpus-costs/4638-contract-program-lifetime-arrays.ts.json new file mode 100644 index 000000000..8512b117c --- /dev/null +++ b/tests/harness/corpus-costs/4638-contract-program-lifetime-arrays.ts.json @@ -0,0 +1 @@ +{ "plain": 2800, "san": 4600 } diff --git a/tests/harness/corpus-costs/4639-contract-program-lifetime-maps.ts.json b/tests/harness/corpus-costs/4639-contract-program-lifetime-maps.ts.json new file mode 100644 index 000000000..d12d0c3a4 --- /dev/null +++ b/tests/harness/corpus-costs/4639-contract-program-lifetime-maps.ts.json @@ -0,0 +1 @@ +{ "plain": 2500, "san": 3600 } diff --git a/tests/harness/corpus-costs/6706-dh-groups.ts.json b/tests/harness/corpus-costs/6706-dh-groups.ts.json new file mode 100644 index 000000000..b051df274 --- /dev/null +++ b/tests/harness/corpus-costs/6706-dh-groups.ts.json @@ -0,0 +1 @@ +{ "plain": 4855, "san": 8228 } diff --git a/tests/harness/corpus-costs/6707-dh-explicit-params.ts.json b/tests/harness/corpus-costs/6707-dh-explicit-params.ts.json new file mode 100644 index 000000000..95ab0f1b2 --- /dev/null +++ b/tests/harness/corpus-costs/6707-dh-explicit-params.ts.json @@ -0,0 +1 @@ +{ "plain": 6328, "san": 10420 } diff --git a/tests/harness/corpus-costs/6708-dh-errors.js.json b/tests/harness/corpus-costs/6708-dh-errors.js.json new file mode 100644 index 000000000..743216cb9 --- /dev/null +++ b/tests/harness/corpus-costs/6708-dh-errors.js.json @@ -0,0 +1 @@ +{ "plain": 8044, "san": 10499 } diff --git a/tests/harness/corpus-costs/6709-dh-generate.ts.json b/tests/harness/corpus-costs/6709-dh-generate.ts.json new file mode 100644 index 000000000..1e00ad64d --- /dev/null +++ b/tests/harness/corpus-costs/6709-dh-generate.ts.json @@ -0,0 +1 @@ +{ "plain": 6999, "san": 9388 } diff --git a/tests/harness/corpus-costs/6710-dh-encodings.cjs.json b/tests/harness/corpus-costs/6710-dh-encodings.cjs.json new file mode 100644 index 000000000..15e5385e0 --- /dev/null +++ b/tests/harness/corpus-costs/6710-dh-encodings.cjs.json @@ -0,0 +1 @@ +{ "plain": 6048, "san": 8117 } diff --git a/tests/harness/corpus-costs/6711-dh-handles.ts.json b/tests/harness/corpus-costs/6711-dh-handles.ts.json new file mode 100644 index 000000000..ecf09ecb1 --- /dev/null +++ b/tests/harness/corpus-costs/6711-dh-handles.ts.json @@ -0,0 +1 @@ +{ "plain": 5664, "san": 7161 } diff --git a/tests/harness/corpus-costs/6712-dh-peer-keys.js.json b/tests/harness/corpus-costs/6712-dh-peer-keys.js.json new file mode 100644 index 000000000..429593dc5 --- /dev/null +++ b/tests/harness/corpus-costs/6712-dh-peer-keys.js.json @@ -0,0 +1 @@ +{ "plain": 9427, "san": 12410 } diff --git a/tests/harness/corpus-costs/6822-jsx-classic-runtime/main.ts.json b/tests/harness/corpus-costs/6822-jsx-classic-runtime/main.ts.json new file mode 100644 index 000000000..dbc0fa779 --- /dev/null +++ b/tests/harness/corpus-costs/6822-jsx-classic-runtime/main.ts.json @@ -0,0 +1 @@ +{ "plain": 3400, "san": 2900 } diff --git a/tests/harness/corpus-costs/6824-jsx-automatic-runtime/main.ts.json b/tests/harness/corpus-costs/6824-jsx-automatic-runtime/main.ts.json new file mode 100644 index 000000000..2a685047e --- /dev/null +++ b/tests/harness/corpus-costs/6824-jsx-automatic-runtime/main.ts.json @@ -0,0 +1 @@ +{ "plain": 3600, "san": 3000 } diff --git a/tests/harness/corpus-costs/6825-jsx-automatic-development/main.ts.json b/tests/harness/corpus-costs/6825-jsx-automatic-development/main.ts.json new file mode 100644 index 000000000..79e32d960 --- /dev/null +++ b/tests/harness/corpus-costs/6825-jsx-automatic-development/main.ts.json @@ -0,0 +1 @@ +{ "plain": 3000, "san": 2500 } diff --git a/tests/harness/corpus-costs/6870-error-constructor-values.js.json b/tests/harness/corpus-costs/6870-error-constructor-values.js.json new file mode 100644 index 000000000..c608ce33a --- /dev/null +++ b/tests/harness/corpus-costs/6870-error-constructor-values.js.json @@ -0,0 +1 @@ +{ "plain": 1200, "san": 3400 } diff --git a/tests/harness/corpus-costs/6871-worker-data-destructuring.cjs.json b/tests/harness/corpus-costs/6871-worker-data-destructuring.cjs.json new file mode 100644 index 000000000..d6c0cb9ee --- /dev/null +++ b/tests/harness/corpus-costs/6871-worker-data-destructuring.cjs.json @@ -0,0 +1 @@ +{ "plain": 1300, "san": 3490 } diff --git a/tests/harness/corpus-costs/6872-regex-start-filter.ts.json b/tests/harness/corpus-costs/6872-regex-start-filter.ts.json new file mode 100644 index 000000000..c9d5dd264 --- /dev/null +++ b/tests/harness/corpus-costs/6872-regex-start-filter.ts.json @@ -0,0 +1 @@ +{ "plain": 2430, "san": 8860 } diff --git a/tests/harness/corpus-costs/6997-typed-array-invariant-index-terms.ts.json b/tests/harness/corpus-costs/6997-typed-array-invariant-index-terms.ts.json new file mode 100644 index 000000000..ed05a3c5d --- /dev/null +++ b/tests/harness/corpus-costs/6997-typed-array-invariant-index-terms.ts.json @@ -0,0 +1 @@ +{ "plain": 3400, "san": 4800 } diff --git a/tests/harness/corpus-costs/6998-typed-array-while-loop-versions.ts.json b/tests/harness/corpus-costs/6998-typed-array-while-loop-versions.ts.json new file mode 100644 index 000000000..204a01a83 --- /dev/null +++ b/tests/harness/corpus-costs/6998-typed-array-while-loop-versions.ts.json @@ -0,0 +1 @@ +{ "plain": 3700, "san": 5000 } diff --git a/tests/harness/corpus-costs/7002-empty-array-contextual-slots/main.ts.json b/tests/harness/corpus-costs/7002-empty-array-contextual-slots/main.ts.json new file mode 100644 index 000000000..9495bb65c --- /dev/null +++ b/tests/harness/corpus-costs/7002-empty-array-contextual-slots/main.ts.json @@ -0,0 +1 @@ +{ "plain": 2282, "san": 1173 } diff --git a/tests/harness/corpus-costs/7005-dyn-constructor-instanceof/main.ts.json b/tests/harness/corpus-costs/7005-dyn-constructor-instanceof/main.ts.json new file mode 100644 index 000000000..f84e21437 --- /dev/null +++ b/tests/harness/corpus-costs/7005-dyn-constructor-instanceof/main.ts.json @@ -0,0 +1 @@ +{ "plain": 2308, "san": 955 } diff --git a/tests/harness/corpus-costs/7008-js-unit-initialized-properties/main.ts.json b/tests/harness/corpus-costs/7008-js-unit-initialized-properties/main.ts.json new file mode 100644 index 000000000..3a6df58d2 --- /dev/null +++ b/tests/harness/corpus-costs/7008-js-unit-initialized-properties/main.ts.json @@ -0,0 +1 @@ +{ "plain": 3065, "san": 2623 } diff --git a/tests/harness/corpus-costs/7011-for-in-dictionary-deletes.ts.json b/tests/harness/corpus-costs/7011-for-in-dictionary-deletes.ts.json new file mode 100644 index 000000000..e172eed8a --- /dev/null +++ b/tests/harness/corpus-costs/7011-for-in-dictionary-deletes.ts.json @@ -0,0 +1 @@ +{ "plain": 1527, "san": 2135 } diff --git a/tests/harness/corpus-costs/7080-crypto-ed25519.mjs.json b/tests/harness/corpus-costs/7080-crypto-ed25519.mjs.json new file mode 100644 index 000000000..8a1e96b68 --- /dev/null +++ b/tests/harness/corpus-costs/7080-crypto-ed25519.mjs.json @@ -0,0 +1 @@ +{ "plain": 4906, "san": 50709 } diff --git a/tests/harness/corpus-costs/7081-webcrypto-ed25519.mjs.json b/tests/harness/corpus-costs/7081-webcrypto-ed25519.mjs.json new file mode 100644 index 000000000..47b28eca2 --- /dev/null +++ b/tests/harness/corpus-costs/7081-webcrypto-ed25519.mjs.json @@ -0,0 +1 @@ +{ "plain": 7958, "san": 51204 } diff --git a/tests/harness/corpus-costs/7082-crypto-key-objects.ts.json b/tests/harness/corpus-costs/7082-crypto-key-objects.ts.json new file mode 100644 index 000000000..b0d8e50c9 --- /dev/null +++ b/tests/harness/corpus-costs/7082-crypto-key-objects.ts.json @@ -0,0 +1 @@ +{ "plain": 4405, "san": 7280 } diff --git a/tests/harness/corpus-costs/7083-crypto-sign-verify.ts.json b/tests/harness/corpus-costs/7083-crypto-sign-verify.ts.json new file mode 100644 index 000000000..650393982 --- /dev/null +++ b/tests/harness/corpus-costs/7083-crypto-sign-verify.ts.json @@ -0,0 +1 @@ +{ "plain": 3631, "san": 5127 } diff --git a/tests/harness/corpus-costs/7084-crypto-key-pairs.cjs.json b/tests/harness/corpus-costs/7084-crypto-key-pairs.cjs.json new file mode 100644 index 000000000..363a8de39 --- /dev/null +++ b/tests/harness/corpus-costs/7084-crypto-key-pairs.cjs.json @@ -0,0 +1 @@ +{ "plain": 6267, "san": 19680 } diff --git a/tests/harness/corpus-costs/7085-webcrypto-ecdsa-rsa.ts.json b/tests/harness/corpus-costs/7085-webcrypto-ecdsa-rsa.ts.json new file mode 100644 index 000000000..73c07085d --- /dev/null +++ b/tests/harness/corpus-costs/7085-webcrypto-ecdsa-rsa.ts.json @@ -0,0 +1 @@ +{ "plain": 5184, "san": 40791 } diff --git a/tests/harness/corpus-costs/7086-webcrypto-ecdsa-rsa-errors.mjs.json b/tests/harness/corpus-costs/7086-webcrypto-ecdsa-rsa-errors.mjs.json new file mode 100644 index 000000000..a74af7ae1 --- /dev/null +++ b/tests/harness/corpus-costs/7086-webcrypto-ecdsa-rsa-errors.mjs.json @@ -0,0 +1 @@ +{ "plain": 8513, "san": 13054 } diff --git a/tests/harness/corpus-costs/7087-tls-socket-wrap.ts.json b/tests/harness/corpus-costs/7087-tls-socket-wrap.ts.json new file mode 100644 index 000000000..b4372e38c --- /dev/null +++ b/tests/harness/corpus-costs/7087-tls-socket-wrap.ts.json @@ -0,0 +1 @@ +{ "plain": 4491, "san": 82505 } diff --git a/tests/harness/corpus-costs/7088-tls-socket-connect-tunnel.ts.json b/tests/harness/corpus-costs/7088-tls-socket-connect-tunnel.ts.json new file mode 100644 index 000000000..8e6e4a3d0 --- /dev/null +++ b/tests/harness/corpus-costs/7088-tls-socket-connect-tunnel.ts.json @@ -0,0 +1 @@ +{ "plain": 4100, "san": 56643 } diff --git a/tests/harness/corpus-costs/7089-http-host-header-tunnels.ts.json b/tests/harness/corpus-costs/7089-http-host-header-tunnels.ts.json new file mode 100644 index 000000000..349a68037 --- /dev/null +++ b/tests/harness/corpus-costs/7089-http-host-header-tunnels.ts.json @@ -0,0 +1 @@ +{ "plain": 3401, "san": 48875 } diff --git a/tests/harness/corpus-costs/7094-tls-socket-failed-handshakes.ts.json b/tests/harness/corpus-costs/7094-tls-socket-failed-handshakes.ts.json new file mode 100644 index 000000000..ec5193be7 --- /dev/null +++ b/tests/harness/corpus-costs/7094-tls-socket-failed-handshakes.ts.json @@ -0,0 +1 @@ +{ "plain": 2618, "san": 3905 } diff --git a/tests/harness/corpus-costs/7106-async-prefix-enter-with.ts.json b/tests/harness/corpus-costs/7106-async-prefix-enter-with.ts.json new file mode 100644 index 000000000..74a3101e8 --- /dev/null +++ b/tests/harness/corpus-costs/7106-async-prefix-enter-with.ts.json @@ -0,0 +1 @@ +{ "plain": 3100, "san": 4000 } diff --git a/tests/harness/corpus-costs/7107-for-await-context.ts.json b/tests/harness/corpus-costs/7107-for-await-context.ts.json new file mode 100644 index 000000000..111fd2885 --- /dev/null +++ b/tests/harness/corpus-costs/7107-for-await-context.ts.json @@ -0,0 +1 @@ +{ "plain": 2200, "san": 3000 } diff --git a/tests/harness/corpus-costs/7108-generator-step-context.ts.json b/tests/harness/corpus-costs/7108-generator-step-context.ts.json new file mode 100644 index 000000000..a5e618da8 --- /dev/null +++ b/tests/harness/corpus-costs/7108-generator-step-context.ts.json @@ -0,0 +1 @@ +{ "plain": 2400, "san": 3100 } diff --git a/tests/harness/corpus-costs/7130-typed-message-listeners.ts.json b/tests/harness/corpus-costs/7130-typed-message-listeners.ts.json new file mode 100644 index 000000000..bf7b0d199 --- /dev/null +++ b/tests/harness/corpus-costs/7130-typed-message-listeners.ts.json @@ -0,0 +1 @@ +{ "plain": 2200, "san": 2200 } diff --git a/tests/harness/corpus-costs/7130-webcrypto-wrap-key.ts.json b/tests/harness/corpus-costs/7130-webcrypto-wrap-key.ts.json new file mode 100644 index 000000000..56b3458ca --- /dev/null +++ b/tests/harness/corpus-costs/7130-webcrypto-wrap-key.ts.json @@ -0,0 +1 @@ +{ "plain": 5334, "san": 8182 } diff --git a/tests/harness/corpus-costs/7131-typed-worker-messages.ts.json b/tests/harness/corpus-costs/7131-typed-worker-messages.ts.json new file mode 100644 index 000000000..953e2af8f --- /dev/null +++ b/tests/harness/corpus-costs/7131-typed-worker-messages.ts.json @@ -0,0 +1 @@ +{ "plain": 1700, "san": 1600 } diff --git a/tests/harness/corpus-costs/7131-webcrypto-wrap-key-errors.mjs.json b/tests/harness/corpus-costs/7131-webcrypto-wrap-key-errors.mjs.json new file mode 100644 index 000000000..5cb958a53 --- /dev/null +++ b/tests/harness/corpus-costs/7131-webcrypto-wrap-key-errors.mjs.json @@ -0,0 +1 @@ +{ "plain": 5838, "san": 8032 } diff --git a/tests/harness/corpus-costs/7132-jose-pbes2/main.mjs.json b/tests/harness/corpus-costs/7132-jose-pbes2/main.mjs.json new file mode 100644 index 000000000..3ad38d83d --- /dev/null +++ b/tests/harness/corpus-costs/7132-jose-pbes2/main.mjs.json @@ -0,0 +1 @@ +{ "plain": 30866, "san": 122276 } diff --git a/tests/harness/corpus-costs/7139-message-typed-records.ts.json b/tests/harness/corpus-costs/7139-message-typed-records.ts.json new file mode 100644 index 000000000..f0ed6cbc2 --- /dev/null +++ b/tests/harness/corpus-costs/7139-message-typed-records.ts.json @@ -0,0 +1 @@ +{ "plain": 4300, "san": 4400 } diff --git a/tests/harness/corpus-costs/7141-comoving-loop-counters.ts.json b/tests/harness/corpus-costs/7141-comoving-loop-counters.ts.json new file mode 100644 index 000000000..7c0606885 --- /dev/null +++ b/tests/harness/corpus-costs/7141-comoving-loop-counters.ts.json @@ -0,0 +1 @@ +{ "plain": 4260, "san": 6600 } diff --git a/tests/harness/corpus-costs/7143-optional-read-integer-index.ts.json b/tests/harness/corpus-costs/7143-optional-read-integer-index.ts.json new file mode 100644 index 000000000..16148d8ce --- /dev/null +++ b/tests/harness/corpus-costs/7143-optional-read-integer-index.ts.json @@ -0,0 +1 @@ +{ "plain": 3500, "san": 5400 } diff --git a/tests/harness/corpus-costs/7150-cipher-legacy-modes.ts.json b/tests/harness/corpus-costs/7150-cipher-legacy-modes.ts.json new file mode 100644 index 000000000..40196decf --- /dev/null +++ b/tests/harness/corpus-costs/7150-cipher-legacy-modes.ts.json @@ -0,0 +1 @@ +{ "plain": 6099, "san": 8251 } diff --git a/tests/harness/corpus-costs/7151-cipher-legacy-errors.cjs.json b/tests/harness/corpus-costs/7151-cipher-legacy-errors.cjs.json new file mode 100644 index 000000000..6a82dd081 --- /dev/null +++ b/tests/harness/corpus-costs/7151-cipher-legacy-errors.cjs.json @@ -0,0 +1 @@ +{ "plain": 7845, "san": 9581 } diff --git a/tests/harness/corpus-costs/7152-crypto-prime.ts.json b/tests/harness/corpus-costs/7152-crypto-prime.ts.json new file mode 100644 index 000000000..4df2be436 --- /dev/null +++ b/tests/harness/corpus-costs/7152-crypto-prime.ts.json @@ -0,0 +1 @@ +{ "plain": 7273, "san": 10284 } diff --git a/tests/harness/corpus-costs/7153-crypto-prime-errors.cjs.json b/tests/harness/corpus-costs/7153-crypto-prime-errors.cjs.json new file mode 100644 index 000000000..d20dc198a --- /dev/null +++ b/tests/harness/corpus-costs/7153-crypto-prime-errors.cjs.json @@ -0,0 +1 @@ +{ "plain": 5893, "san": 5691 } diff --git a/tests/harness/corpus-costs/7180-generic-local-binding-early-uses.ts.json b/tests/harness/corpus-costs/7180-generic-local-binding-early-uses.ts.json new file mode 100644 index 000000000..929b49d23 --- /dev/null +++ b/tests/harness/corpus-costs/7180-generic-local-binding-early-uses.ts.json @@ -0,0 +1 @@ +{ "plain": 2542, "san": 2911 } diff --git a/tests/harness/corpus-costs/7181-generic-module-binding-tdz.ts.json b/tests/harness/corpus-costs/7181-generic-module-binding-tdz.ts.json new file mode 100644 index 000000000..321663d8b --- /dev/null +++ b/tests/harness/corpus-costs/7181-generic-module-binding-tdz.ts.json @@ -0,0 +1 @@ +{ "plain": 2500, "san": 2870 } diff --git a/tests/harness/corpus-costs/7182-generic-binding-direct-calls.ts.json b/tests/harness/corpus-costs/7182-generic-binding-direct-calls.ts.json new file mode 100644 index 000000000..e9da8d46e --- /dev/null +++ b/tests/harness/corpus-costs/7182-generic-binding-direct-calls.ts.json @@ -0,0 +1 @@ +{ "plain": 2686, "san": 3069 } diff --git a/tests/harness/corpus-costs/7184-reduce-runtime-accumulator.ts.json b/tests/harness/corpus-costs/7184-reduce-runtime-accumulator.ts.json new file mode 100644 index 000000000..c480bea3f --- /dev/null +++ b/tests/harness/corpus-costs/7184-reduce-runtime-accumulator.ts.json @@ -0,0 +1 @@ +{ "plain": 2541, "san": 3260 } diff --git a/tests/harness/corpus-costs/7185-array-callback-arguments-length.ts.json b/tests/harness/corpus-costs/7185-array-callback-arguments-length.ts.json new file mode 100644 index 000000000..b86a9e70a --- /dev/null +++ b/tests/harness/corpus-costs/7185-array-callback-arguments-length.ts.json @@ -0,0 +1 @@ +{ "plain": 2528, "san": 3201 } diff --git a/tests/harness/corpus-costs/7240-objectmode-streams.ts.json b/tests/harness/corpus-costs/7240-objectmode-streams.ts.json new file mode 100644 index 000000000..2695df1a4 --- /dev/null +++ b/tests/harness/corpus-costs/7240-objectmode-streams.ts.json @@ -0,0 +1 @@ +{ "plain": 2084, "san": 2980 } diff --git a/tests/harness/corpus-costs/7241-objectmode-streams.mjs.json b/tests/harness/corpus-costs/7241-objectmode-streams.mjs.json new file mode 100644 index 000000000..bd04ea049 --- /dev/null +++ b/tests/harness/corpus-costs/7241-objectmode-streams.mjs.json @@ -0,0 +1 @@ +{ "plain": 2190, "san": 3093 } diff --git a/tests/harness/corpus-costs/7242-objectmode-dynamic.cjs.json b/tests/harness/corpus-costs/7242-objectmode-dynamic.cjs.json new file mode 100644 index 000000000..3bc4d9211 --- /dev/null +++ b/tests/harness/corpus-costs/7242-objectmode-dynamic.cjs.json @@ -0,0 +1 @@ +{ "plain": 2430, "san": 3287 } diff --git a/tests/harness/corpus-costs/7243-for-await-close-order.mjs.json b/tests/harness/corpus-costs/7243-for-await-close-order.mjs.json new file mode 100644 index 000000000..e5826334d --- /dev/null +++ b/tests/harness/corpus-costs/7243-for-await-close-order.mjs.json @@ -0,0 +1 @@ +{ "plain": 2402, "san": 3174 } diff --git a/tests/harness/corpus-costs/7244-objectmode-subclasses.ts.json b/tests/harness/corpus-costs/7244-objectmode-subclasses.ts.json new file mode 100644 index 000000000..d555a94ab --- /dev/null +++ b/tests/harness/corpus-costs/7244-objectmode-subclasses.ts.json @@ -0,0 +1 @@ +{ "plain": 1917, "san": 2782 } diff --git a/tests/harness/corpus-costs/7245-writable-callback-order.mjs.json b/tests/harness/corpus-costs/7245-writable-callback-order.mjs.json new file mode 100644 index 000000000..0226cbb35 --- /dev/null +++ b/tests/harness/corpus-costs/7245-writable-callback-order.mjs.json @@ -0,0 +1 @@ +{ "plain": 2144, "san": 2747 } diff --git a/tests/harness/corpus-costs/7280-named-reads-hole-receivers.ts.json b/tests/harness/corpus-costs/7280-named-reads-hole-receivers.ts.json new file mode 100644 index 000000000..db9f23551 --- /dev/null +++ b/tests/harness/corpus-costs/7280-named-reads-hole-receivers.ts.json @@ -0,0 +1 @@ +{ "plain": 4700, "san": 2400 } diff --git a/tests/harness/corpus-costs/7300-callback-captured-scalars.ts.json b/tests/harness/corpus-costs/7300-callback-captured-scalars.ts.json new file mode 100644 index 000000000..18495cd4e --- /dev/null +++ b/tests/harness/corpus-costs/7300-callback-captured-scalars.ts.json @@ -0,0 +1 @@ +{ "plain": 6000, "san": 7900 } diff --git a/tests/harness/corpus-costs/7301-running-extrema-branches.ts.json b/tests/harness/corpus-costs/7301-running-extrema-branches.ts.json new file mode 100644 index 000000000..74df88f0c --- /dev/null +++ b/tests/harness/corpus-costs/7301-running-extrema-branches.ts.json @@ -0,0 +1 @@ +{ "plain": 5300, "san": 5800 } diff --git a/tests/harness/corpus-costs/7320-array-dense-loop-invariant-limits.ts.json b/tests/harness/corpus-costs/7320-array-dense-loop-invariant-limits.ts.json new file mode 100644 index 000000000..2ea93101c --- /dev/null +++ b/tests/harness/corpus-costs/7320-array-dense-loop-invariant-limits.ts.json @@ -0,0 +1 @@ +{ "plain": 2600, "san": 31600 } diff --git a/tests/harness/corpus-costs/7321-speculative-loop-bail-sites.ts.json b/tests/harness/corpus-costs/7321-speculative-loop-bail-sites.ts.json new file mode 100644 index 000000000..1e5609a66 --- /dev/null +++ b/tests/harness/corpus-costs/7321-speculative-loop-bail-sites.ts.json @@ -0,0 +1 @@ +{ "plain": 2000, "san": 2600 } diff --git a/tests/harness/corpus-costs/7322-union-scalar-equality.ts.json b/tests/harness/corpus-costs/7322-union-scalar-equality.ts.json new file mode 100644 index 000000000..2df20a0ed --- /dev/null +++ b/tests/harness/corpus-costs/7322-union-scalar-equality.ts.json @@ -0,0 +1 @@ +{ "plain": 2100, "san": 2800 } diff --git a/tests/harness/corpus-costs/7323-loop-bound-parameter-versions.ts.json b/tests/harness/corpus-costs/7323-loop-bound-parameter-versions.ts.json new file mode 100644 index 000000000..ee18f5f11 --- /dev/null +++ b/tests/harness/corpus-costs/7323-loop-bound-parameter-versions.ts.json @@ -0,0 +1 @@ +{ "plain": 2000, "san": 2800 } diff --git a/tests/harness/corpus-costs/7324-loop-bound-parameter-window-sums.ts.json b/tests/harness/corpus-costs/7324-loop-bound-parameter-window-sums.ts.json new file mode 100644 index 000000000..9622c5beb --- /dev/null +++ b/tests/harness/corpus-costs/7324-loop-bound-parameter-window-sums.ts.json @@ -0,0 +1 @@ +{ "plain": 2000, "san": 3000 } diff --git a/tests/harness/corpus-costs/7360-worker-typed-records.ts.json b/tests/harness/corpus-costs/7360-worker-typed-records.ts.json new file mode 100644 index 000000000..cf0403d1d --- /dev/null +++ b/tests/harness/corpus-costs/7360-worker-typed-records.ts.json @@ -0,0 +1 @@ +{ "plain": 2700, "san": 2900 } diff --git a/tests/harness/corpus-costs/7361-message-listeners-share-value.ts.json b/tests/harness/corpus-costs/7361-message-listeners-share-value.ts.json new file mode 100644 index 000000000..c428eb8f1 --- /dev/null +++ b/tests/harness/corpus-costs/7361-message-listeners-share-value.ts.json @@ -0,0 +1 @@ +{ "plain": 3000, "san": 4600 } diff --git a/tests/harness/corpus-costs/class-quoted-accessor-keys.mjs.json b/tests/harness/corpus-costs/class-quoted-accessor-keys.mjs.json new file mode 100644 index 000000000..81da85b90 --- /dev/null +++ b/tests/harness/corpus-costs/class-quoted-accessor-keys.mjs.json @@ -0,0 +1 @@ +{ "plain": 3900, "san": 4000 } diff --git a/tests/harness/coverage.test.ts b/tests/harness/coverage.test.ts index 86d202b46..3ed562aea 100644 --- a/tests/harness/coverage.test.ts +++ b/tests/harness/coverage.test.ts @@ -182,6 +182,57 @@ test("statements of a signature-blocked function count as not static", () => { expect(coverage.stats.statementsFailed + (un?.statementsFailed ?? 0)).toBe(5); }); +test("the body of a signature-blocked function reports its diagnostics", () => { + const { coverage } = analyze(fixture("blocked-signature-body.ts")); + const sites = (diags: readonly { code: string; message: string }[]) => + diags.map((d) => `${d.code} ${d.message}`).sort(); + // measure is called: its body's diagnostics join the reached ones. + expect(sites(coverage.diagnostics)).toEqual([ + "SC2004 uses of 'extra' inherit the blocker on its declaration", + "SC2004 uses of 'measure' inherit the blocker on its declaration", + "SC2004 uses of 'target' inherit the blocker on its declaration", + "SC2020 'WeakRef' is part of the standard library types but has no scriptc lowering yet", + "SC2020 'new WeakRef' is part of the standard library types but has no scriptc lowering yet", + ]); + // Nothing calls unused or described: their signatures and bodies report + // as unreached. described's declared `this` is its receiver, so + // `this.label` reports nothing. + expect(sites(coverage.unreached?.diagnostics ?? [])).toEqual([ + "SC2004 uses of 'extra' inherit the blocker on its declaration", + "SC2004 uses of 'target' inherit the blocker on its declaration", + "SC2020 'WeakRef' is part of the standard library types but has no scriptc lowering yet", + "SC2020 'WeakRef' is part of the standard library types but has no scriptc lowering yet", + "SC2020 'new WeakRef' is part of the standard library types but has no scriptc lowering yet", + ]); + // The bodies' statements still count once each, as not static. + const un = coverage.unreached?.stats; + expect(coverage.stats.functionsSkipped + (un?.functionsSkipped ?? 0)).toBe(3); + expect(coverage.stats.statementsTotal + (un?.statementsTotal ?? 0)).toBe(7); + expect(coverage.stats.statementsFailed + (un?.statementsFailed ?? 0)).toBe(7); +}); + +test("deferred fences in a module's top-level code report as running at startup", () => { + const { coverage, sourceTexts } = analyze(fixture("startup-fences.js")); + expect( + (coverage.runtimeFences ?? []).map((d) => [d.code, d.loc.start, d.startup === true]), + ).toEqual([ + ["SC1090", expect.any(Number), true], + ["SC1090", expect.any(Number), false], + ]); + const out = report(fixture("startup-fences.js")); + expect(out).toMatch( + /runs at startup {3}1 site .*\n.*instanceof against this native constructor\n\s+at tests\/coverage-fixtures\/startup-fences\.js:5:13/, + ); + expect(out).toMatch( + /deferred to runtime {3}1 site .*\n.*instanceof against this native constructor\n\s+at tests\/coverage-fixtures\/startup-fences\.js:7:10/, + ); + const envelope = coverageEnvelope(coverage, { compilerVersion: "test", sourceTexts }); + expect(envelope.diagnostics.map((d) => [d.scope, d.line, d.startup])).toEqual([ + ["deferred", 5, true], + ["deferred", 7, undefined], + ]); +}); + test("a failed unreached JavaScript statement is always explained", () => { const { coverage } = analyze(fixture("unreached-js-fence.js")); const failed = diff --git a/tests/harness/differential.test.ts b/tests/harness/differential.test.ts index b70e59c86..905f648dd 100644 --- a/tests/harness/differential.test.ts +++ b/tests/harness/differential.test.ts @@ -23,6 +23,7 @@ import ts5 from "typescript"; import { compile } from "@scriptc/compiler"; import { corpusSelect, shardSuffix } from "./shard.js"; import { oracleEnvKey } from "./oracle-env.js"; +import { tscJsxOracleEntry, wantsTscJsx } from "./jsx-oracle.js"; const execFileAsync = promisify(execFile); const repoRoot = join(import.meta.dirname, "../.."); @@ -122,6 +123,7 @@ function wantsTscDecorators(file: string): boolean { * test cache — a pure function of the program bytes and the typescript * version (both in the oracle cache key). */ function nodeOracleFile(file: string): string { + if (wantsTscJsx(directiveHead(file))) return tscJsxOracleEntry(file, cacheDir); if (!wantsTscDecorators(file)) return file; const src = readFileSync(file, "utf8"); const out = ts5.transpileModule(src, { @@ -241,7 +243,7 @@ const comptimeShim = pathToFileURL(join(import.meta.dirname, "comptime-shim.mjs" function programInputs(file: string): string[] { if (!/\/main\.(ts|js|mjs|cjs)$/.test(file)) return [file]; return [ - ...ENTRY_EXTS.flatMap((ext) => globSync(join(file, `../**/*.${ext}`))), + ...[...ENTRY_EXTS, "tsx", "jsx"].flatMap((ext) => globSync(join(file, `../**/*.${ext}`))), ...globSync(join(file, "../**/tsconfig.json")), ...globSync(join(file, "../**/package.json")), ].sort(); diff --git a/tests/harness/errors.test.ts b/tests/harness/errors.test.ts index 00ee40043..c8b0b51b8 100644 --- a/tests/harness/errors.test.ts +++ b/tests/harness/errors.test.ts @@ -847,14 +847,110 @@ try { expect(r.exitCode).toBe(0); expect(r.stdout).toBe( "Error SC2020 The cipher 'aes-128-ccm' has no native implementation; the supported ciphers are " + - "aes-128-cbc, aes-192-cbc, aes-256-cbc, aes-128-ctr, aes-192-ctr, aes-256-ctr, aes-128-gcm, " + - "aes-192-gcm, aes-256-gcm and chacha20-poly1305\n" + + "aes-128, aes-192 and aes-256 in cbc, ctr, gcm, ecb, cfb, cfb8, cfb1 and ofb modes, des-ede3 in " + + "cbc, ecb, cfb, cfb8, cfb1 and ofb modes, des-ede in cbc, ecb, cfb and ofb modes, chacha20 and " + + "chacha20-poly1305\n" + "Error ERR_CRYPTO_UNKNOWN_CIPHER Unknown cipher\n", ); expect(r.stderr).toBe(""); }); }); +// new tls.TLSSocket reads a runtime options record: the client mode, whose +// handshake Node never starts, and members without a lowering throw the +// fence, as do sockets it cannot take over (a second wrap, and +// tls.connect({ socket }) over a socket that is still dialing). +describe("tls.TLSSocket runtime fences", () => { + test("client mode, unlowered members and sockets that cannot be taken over throw", async () => { + const r = await compileAndRun( + "tls-socket-fences", + `import { readFileSync } from "node:fs"; +import * as net from "node:net"; +import * as tls from "node:tls"; +const certs = "tests/fixtures/server/certs"; +const cert = readFileSync(\`\${certs}/localhost.pem\`); +const key = readFileSync(\`\${certs}/localhost-key.pem\`); +function report(label: string, run: () => void): void { + try { + run(); + console.log(label, "no error"); + } catch (error) { + console.log(label, (error as Error).message); + } +} +const client: Record = { cert, key }; +const session: Record = { isServer: true, cert, key, session: Buffer.alloc(1) }; +const server: Record = { isServer: true, cert, key }; +const listener = net.createServer((raw) => { + report("client mode", () => new tls.TLSSocket(raw, client)); + report("session", () => new tls.TLSSocket(raw, session)); + const wrapped = new tls.TLSSocket(raw, server); + report("twice", () => new tls.TLSSocket(raw, server)); + wrapped.destroy(); + listener.close(); +}); +listener.listen(0, () => { + const c = net.connect((listener.address() as net.AddressInfo).port, "localhost"); + report("dialing", () => tls.connect({ socket: c })); + c.on("error", () => {}); +}); +`, + ); + expect(r.stderr).toBe(""); + expect(r.stdout).toBe( + "dialing tls.connect over a socket that is not connected, or is already wrapped, is not supported yet\n" + + "client mode new tls.TLSSocket option 'isServer' has no static lowering — Node starts no handshake for a client-mode TLSSocket; tls.connect({ socket }) runs the client handshake over an open socket\n" + + "session new tls.TLSSocket option 'session' has no static lowering — isServer, cert, key, secureContext, ALPNProtocols, minVersion and maxVersion are the implemented members\n" + + "twice new tls.TLSSocket over a socket that is not connected, or is already wrapped, is not supported yet\n", + ); + }); +}); + +// With a secureContext, the TLSSocket's own options still apply: a truthy +// requestCert, which asks for client certificates this runtime does not +// verify, and SNICallback throw the fence instead of being dropped. +describe("tls.TLSSocket socket options beside a secureContext", () => { + test("requestCert and SNICallback throw the fence", async () => { + const r = await compileAndRun( + "tls-socket-context-fences", + `import { readFileSync } from "node:fs"; +import * as net from "node:net"; +import * as tls from "node:tls"; +const certs = "tests/fixtures/server/certs"; +const cert = readFileSync(\`\${certs}/localhost.pem\`); +const key = readFileSync(\`\${certs}/localhost-key.pem\`); +const context = tls.createSecureContext({ cert, key }); +function report(label: string, run: () => void): void { + try { + run(); + console.log(label, "no error"); + } catch (error) { + console.log(label, (error as Error).message); + } +} +const listener = net.createServer((raw) => { + report("requestCert", () => new tls.TLSSocket(raw, { isServer: true, secureContext: context, requestCert: true, rejectUnauthorized: true })); + report("SNICallback", () => new tls.TLSSocket(raw, { isServer: true, secureContext: context, SNICallback: (name: string, cb: (err: Error | null, ctx?: tls.SecureContext) => void) => cb(null, context) })); + report("requestCert false", () => new tls.TLSSocket(raw, { isServer: true, secureContext: context, requestCert: false, rejectUnauthorized: true }).destroy()); + listener.close(); +}); +listener.listen(0, () => { + const c = net.connect((listener.address() as net.AddressInfo).port, "localhost"); + c.on("error", () => {}); + c.on("close", () => console.log("client closed")); +}); +`, + ); + expect(r.stderr).toBe(""); + expect(r.stdout).toBe( + "requestCert new tls.TLSSocket option 'requestCert' has no static lowering — client-certificate handshakes are not modeled (SEMANTICS.md divergence 55) — requestCert stays at Node's false default\n" + + "SNICallback new tls.TLSSocket option 'SNICallback' has no static lowering — isServer, cert, key, secureContext, ALPNProtocols, minVersion and maxVersion are the implemented members\n" + + "requestCert false no error\n" + + "client closed\n", + ); + }); +}); + // A curve Node lists in crypto.getCurves() but the runtime does not // implement throws a coded SC2020 error naming the curve when the name is // computed; literal names are refused at compile time (node-types-fenced). @@ -918,6 +1014,53 @@ for (const run of [() => ecdh.getPublicKey.name, () => { ecdh.custom = 1; }]) { }); }); +// DiffieHellman primes past mbedTLS's 8192-bit limit throw a coded SC2020 +// error, as do property reads other than verifyError, property writes, and +// Object.prototype method calls on an untyped DiffieHellman. +describe("DiffieHellman runtime refusals", () => { + test("primes longer than 8192 bits throw SC2020", async () => { + const r = await compileAndRun( + "dh-long-prime", + `import { createDiffieHellman } from "node:crypto"; +const prime = Buffer.alloc(1025, 0xff); +for (const run of [() => createDiffieHellman(prime), () => createDiffieHellman(8193)]) { + try { + run(); + } catch (error) { + const e = error as Error & { code?: string }; + console.log(e.name, e.code, e.message); + } +} +console.log(createDiffieHellman(Buffer.alloc(1024, 0xff)).verifyError); +`, + ); + expect(r.exitCode).toBe(0); + const message = "DiffieHellman primes longer than 8192 bits have no native implementation"; + expect(r.stdout).toBe(`Error SC2020 ${message}\nError SC2020 ${message}\n1\n`); + expect(r.stderr).toBe(""); + }); + + test("DiffieHellman property reads, writes and Object.prototype calls throw SC2020", async () => { + const r = await compileAndRun( + "dh-properties", + `const crypto = require("node:crypto"); +const dh = crypto.getDiffieHellman("modp1"); +console.log(dh.verifyError); +for (const run of [() => dh.getPrime.name, () => { dh.verifyError = 1; }, () => dh.hasOwnProperty("verifyError")]) { + try { + run(); + } catch (e) { + console.log(e.name, e.code); + } +} +`, + "cjs", + ); + expect(r.exitCode).toBe(0); + expect(r.stdout).toBe("0\nError SC2020\nError SC2020\nError SC2020\n"); + }); +}); + // Node formats an unknown output encoding with %s, which prints a // function's source text and calls a toString the encoding's class // defines. The cipher runtime has neither, so both throw a coded SC2020 @@ -1634,14 +1777,15 @@ console.log(typeof promisePrototype, typeof promisePrototype.then, promiseProtot }); }); -// WebCrypto's native slice is crypto.subtle's secret-key methods. -// Algorithms Node recognizes outside that slice reject with a coded SC2020 +// WebCrypto's native slice is crypto.subtle's secret-key methods and its +// ECDSA, Ed25519 and RSA algorithms. Algorithms Node recognizes outside +// that slice reject with a coded SC2020 // Error instead of a Node-shaped success, and a JavaScript call of a // method outside it meets its named fence when it runs. // Node's own rejections (unknown names, bad usages) are corpus-pinned // (6292, 6294); these refusals have no Node twin, so they live here. describe("WebCrypto refusals (JS lane)", () => { - test("recognized algorithms outside the secret-key slice reject with SC2020", async () => { + test("recognized algorithms outside the lowered ones reject with SC2020", async () => { const r = await compileAndRun( "webcrypto-unsupported-algorithms", `const bytes = new TextEncoder().encode("secret"); @@ -1653,48 +1797,109 @@ async function attempt(label, run) { console.log(label, e.name, e.code, e.message); } } -await attempt("rsa", () => - crypto.subtle.importKey("raw", bytes, { name: "RSA-PSS", hash: "SHA-256" }, false, ["verify"]), -); const key = await crypto.subtle.importKey("raw", bytes, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]); -await attempt("ecdsa", () => crypto.subtle.sign({ name: "ECDSA", hash: "SHA-256" }, key, bytes)); +await attempt("ed448", () => crypto.subtle.sign({ name: "Ed448" }, key, bytes)); +await attempt("ecdsa with an hmac key", () => crypto.subtle.sign({ name: "ECDSA", hash: "SHA-256" }, key, bytes)); await attempt("sha3", () => crypto.subtle.digest("SHA3-256", bytes)); await attempt("sha3 hmac", () => crypto.subtle.importKey("raw", bytes, { name: "HMAC", hash: "SHA3-256" }, false, ["sign"]), ); -await attempt("ed25519 key", () => crypto.subtle.generateKey({ name: "Ed25519" }, false, ["sign"])); const aes = await crypto.subtle.generateKey({ name: "AES-GCM", length: 128 }, false, ["encrypt"]); await attempt("aes-ocb", () => crypto.subtle.encrypt({ name: "AES-OCB", iv: new Uint8Array(12) }, aes, bytes)); await attempt("ecdh bits", () => crypto.subtle.deriveBits({ name: "ECDH" }, key, 256)); +await attempt("rsa-oaep wrap", () => crypto.subtle.wrapKey("raw", key, key, { name: "RSA-OAEP" })); +const pair = await crypto.subtle.generateKey({ name: "ECDSA", namedCurve: "P-256" }, true, ["sign", "verify"]); +const kw = await crypto.subtle.generateKey({ name: "AES-KW", length: 128 }, false, ["wrapKey", "unwrapKey"]); +await attempt("wrap an ecdsa key", () => crypto.subtle.wrapKey("spki", pair.publicKey, kw, "AES-KW")); +await attempt("wrap with an ecdsa key", () => crypto.subtle.wrapKey("raw", aes, pair.privateKey, "AES-KW")); +await attempt("unwrap an ecdsa key", () => + crypto.subtle.unwrapKey("spki", new Uint8Array(24), kw, "AES-KW", { name: "ECDSA", namedCurve: "P-256" }, true, [ + "verify", + ]), +); `, "mjs", ); expect(r.exitCode).toBe(0); expect(r.stderr).toBe(""); expect(r.stdout).toBe( - "rsa Error SC2020 crypto.subtle.importKey with the RSA-PSS algorithm is not supported yet\n" + - "ecdsa Error SC2020 crypto.subtle.sign with the ECDSA algorithm is not supported yet\n" + + "ed448 Error SC2020 crypto.subtle.sign with the Ed448 algorithm is not supported yet\n" + + "ecdsa with an hmac key InvalidAccessError 15 Key algorithm mismatch\n" + "sha3 Error SC2020 crypto.subtle.digest with the SHA3-256 algorithm is not supported yet\n" + "sha3 hmac Error SC2020 crypto.subtle.importKey with the SHA3-256 algorithm is not supported yet\n" + - "ed25519 key Error SC2020 crypto.subtle.generateKey with the Ed25519 algorithm is not supported yet\n" + "aes-ocb Error SC2020 crypto.subtle.encrypt with the AES-OCB algorithm is not supported yet\n" + - "ecdh bits Error SC2020 crypto.subtle.deriveBits with the ECDH algorithm is not supported yet\n", + "ecdh bits Error SC2020 crypto.subtle.deriveBits with the ECDH algorithm is not supported yet\n" + + "rsa-oaep wrap Error SC2020 crypto.subtle.wrapKey with the RSA-OAEP algorithm is not supported yet\n" + + "wrap an ecdsa key Error SC2020 crypto.subtle.wrapKey with an ECDSA, Ed25519 or RSA key is not supported yet\n" + + "wrap with an ecdsa key InvalidAccessError 15 Key algorithm mismatch\n" + + "unwrap an ecdsa key Error SC2020 crypto.subtle.unwrapKey with the ECDSA algorithm is not supported yet\n", ); }); test("a method outside the slice is a named fence where it runs", async () => { const r = await compileAndRun( - "webcrypto-wrap-key", + "webcrypto-get-public-key", `console.log("before"); -const key = await crypto.subtle.generateKey({ name: "AES-KW", length: 128 }, true, ["wrapKey"]); -await crypto.subtle.wrapKey("raw", key, key, "AES-KW"); +const key = await crypto.subtle.generateKey({ name: "HMAC", hash: "SHA-256" }, true, ["sign"]); +await crypto.subtle.getPublicKey(key, ["verify"]); `, "mjs", ); expect(r.exitCode).toBe(1); expect(r.stdout).toBe("before\n"); expect(r.stderr).toMatch( - /^Uncaught Error: 'crypto\.subtle\.wrapKey' is part of the standard library types but has no scriptc lowering yet.*\n$/, + /^Uncaught Error: 'crypto\.subtle\.getPublicKey' is part of the standard library types but has no scriptc lowering yet.*\n$/, + ); + }); +}); + +// The Ed25519 arithmetic (scr_ed25519.c) links only into programs whose +// string literals name Ed25519. Without it, an Ed25519 public key still +// imports and exports, while private-key parsing, signing and verification +// reject with a coded SC2020 Error where Node answers; there is no Node +// twin, so this lives here. The corpus (7080, 7081) pins the linked path. +describe("Ed25519 without the Ed25519 unit (JS lane)", () => { + test("a program that never names Ed25519 refuses its arithmetic with SC2020", async () => { + const r = await compileAndRun( + "ed25519-unit-not-linked", + `import * as c from "node:crypto"; +const spki = Buffer.from("302a300506032b6570032100d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a", "hex"); +const pkcs8 = Buffer.from("302e020100300506032b6570042204209d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60", "hex"); +const pub = c.createPublicKey({ key: spki, format: "der", type: "spki" }); +console.log(pub.asymmetricKeyType, pub.export({ format: "jwk" }).x); +async function attempt(label, run) { + try { + await run(); + console.log(label, "resolved"); + } catch (e) { + console.log(label, e.name, e.code, e.message); + } +} +await attempt("pkcs8", () => c.createPrivateKey({ key: pkcs8, format: "der", type: "pkcs8" })); +await attempt("verify", () => c.verify(null, Buffer.from("x"), pub, Buffer.alloc(64))); +// ECDSA links the WebCrypto key unit; the Ed25519 name is built at run time. +const ecdsa = await crypto.subtle.generateKey({ name: "ECDSA", namedCurve: "P-256" }, false, ["sign"]); +console.log(ecdsa.privateKey.type); +const name = ["Ed", "25519"].join(""); +await attempt("generateKey", () => crypto.subtle.generateKey(name, false, ["sign", "verify"])); +await attempt("importKey", () => crypto.subtle.importKey("pkcs8", pkcs8, name, false, ["sign"])); +const raw = await crypto.subtle.importKey("raw", spki.subarray(12), name, false, ["verify"]); +await attempt("subtle verify", () => crypto.subtle.verify(name, raw, new Uint8Array(64), new Uint8Array(1))); +`, + "mjs", + ); + const refusal = + "Error SC2020 Ed25519 keys are supported when a string literal in the program names 'ed25519'"; + expect(r.exitCode).toBe(0); + expect(r.stderr).toBe(""); + expect(r.stdout).toBe( + "ed25519 11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo\n" + + `pkcs8 ${refusal}\n` + + `verify ${refusal}\n` + + "private\n" + + `generateKey ${refusal}\n` + + `importKey ${refusal}\n` + + `subtle verify ${refusal}\n`, ); }); }); diff --git a/tests/harness/executable-cache-shared-output.test.ts b/tests/harness/executable-cache-shared-output.test.ts new file mode 100644 index 000000000..bf9e7353c --- /dev/null +++ b/tests/harness/executable-cache-shared-output.test.ts @@ -0,0 +1,102 @@ +/* Two programs whose entries share a name (main.ts) built into one output + * directory share the kept `main.ll` beside their executables. Each build + * must still compile, link and cache its own translation unit: a later + * build that restores either program from the executable cache, or relinks + * it from the cached translation unit, gets that program back. + * + * The overlap is made deterministic with a linker wrapper that rewrites the + * kept `main.ll` with the other program's text while the first build links, + * which is what a concurrent build of the other program does when it writes + * its kept LLVM in that window. A caller-selected linker never publishes the + * final executable, so every cache hit relinks from the cached translation + * unit. Both builds also run concurrently. */ +import { execFile, execFileSync } from "node:child_process"; +import { chmodSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { promisify } from "node:util"; +import { afterAll, beforeAll, describe, expect, test } from "vitest"; + +const execFileAsync = promisify(execFile); +const repoRoot = join(import.meta.dirname, "../.."); +const cli = join(repoRoot, "packages/cli/dist/main.js"); +const windows = process.platform === "win32"; + +let directory = ""; + +beforeAll(() => { + directory = mkdtempSync(join(windows ? tmpdir() : "/tmp", "scriptc-shared-output-")); + for (const name of ["first", "second"]) { + mkdirSync(join(directory, name)); + writeFileSync(join(directory, name, "main.ts"), `console.log("${name} program");\n`); + } + mkdirSync(join(directory, "out")); +}); + +afterAll(() => { + if (directory !== "") rmSync(directory, { recursive: true, force: true }); +}); + +function outPath(name: string): string { + return join(directory, "out", name + (windows ? ".exe" : "")); +} + +async function build(name: string, env: NodeJS.ProcessEnv = {}): Promise { + await execFileAsync( + process.execPath, + [cli, "build", join(directory, name, "main.ts"), "-o", outPath(name)], + { + cwd: repoRoot, + env: { ...process.env, SCRIPTC_CACHE_DIR: join(directory, "cache"), ...env }, + maxBuffer: 1 << 24, + }, + ); +} + +function run(name: string): string { + return execFileSync(outPath(name), { encoding: "utf8" }); +} + +describe.skipIf(windows)("executables built into one output directory", () => { + test( + "keep their own translation units when their builds overlap", + { timeout: 600_000 }, + async () => { + const keptLlvm = join(directory, "out", "main.ll"); + // The second program's kept LLVM, for the wrapper to write while the + // first program links. + await build("second"); + const secondLlvm = join(directory, "second.ll"); + writeFileSync(secondLlvm, readFileSync(keptLlvm)); + const linker = join(directory, "linker.sh"); + writeFileSync( + linker, + [ + "#!/bin/sh", + `if [ -n "$SCRIPTC_TEST_OVERLAP" ]; then cp "${secondLlvm}" "${keptLlvm}"; fi`, + 'exec clang "$@"', + "", + ].join("\n"), + ); + chmodSync(linker, 0o755); + + await build("first", { SCRIPTC_LINKER: linker, SCRIPTC_TEST_OVERLAP: "1" }); + expect(run("first")).toBe("first program\n"); + expect(readFileSync(keptLlvm, "utf8")).toContain("first program"); + // A cache hit relinks from the translation unit the build cached. + rmSync(outPath("first")); + await build("first", { SCRIPTC_LINKER: linker }); + expect(run("first")).toBe("first program\n"); + expect(readFileSync(keptLlvm, "utf8")).toContain("first program"); + + // Both programs at once, cold and then from the cache. + for (const round of [0, 1]) { + if (round === 1) for (const name of ["first", "second"]) rmSync(outPath(name)); + const env = { SCRIPTC_CACHE_DIR: join(directory, "cache-both"), SCRIPTC_LINKER: linker }; + await Promise.all([build("first", env), build("second", env)]); + expect(run("first")).toBe("first program\n"); + expect(run("second")).toBe("second program\n"); + } + }, + ); +}); diff --git a/tests/harness/jsx-oracle.ts b/tests/harness/jsx-oracle.ts new file mode 100644 index 000000000..ba7a49946 --- /dev/null +++ b/tests/harness/jsx-oracle.ts @@ -0,0 +1,120 @@ +import { createHash } from "node:crypto"; +import { + copyFileSync, + existsSync, + mkdirSync, + readFileSync, + readdirSync, + renameSync, + rmSync, + writeFileSync, +} from "node:fs"; +import { dirname, join, relative } from "node:path"; +import ts5 from "typescript"; + +/** `// @tsc-jsx` in a directory program's main.ts head: Node cannot run + * .tsx or .jsx modules, so the oracle runs TypeScript's own emit of the + * program. Every .ts, .tsx, .js and .jsx file of the directory is transpiled + * (target ES2022, ES modules) with the jsx options of the directory's + * tsconfig.json, .ts, .tsx and .jsx files becoming .js, and package.json import + * and export targets renamed alike; other files are copied. + * The result lives in a directory under the test cache keyed by the program + * bytes and the typescript version. */ +export function wantsTscJsx(head: readonly string[]): boolean { + return head.some((line) => /^\/\/ @tsc-jsx\s*$/.test(line)); +} + +/** The oracle entry (`main.js` of the transpiled program) for `file`. */ +export function tscJsxOracleEntry(file: string, cacheDir: string): string { + const root = dirname(file); + const files = listFiles(root); + const hash = createHash("sha256").update(ts5.version); + for (const path of files) + hash.update("\0").update(relative(root, path)).update("\0").update(readFileSync(path)); + const dir = join(cacheDir, `jsx-oracle-${hash.digest("hex").slice(0, 16)}`); + const entry = join(dir, "main.js"); + if (existsSync(entry)) return entry; + const configPath = join(root, "tsconfig.json"); + const config = existsSync(configPath) + ? ts5.parseJsonConfigFileContent( + ts5.readConfigFile(configPath, ts5.sys.readFile).config ?? {}, + ts5.sys, + root, + ).options + : {}; + const compilerOptions: ts5.CompilerOptions = { + target: ts5.ScriptTarget.ES2022, + module: ts5.ModuleKind.ESNext, + jsx: config.jsx, + jsxFactory: config.jsxFactory, + jsxFragmentFactory: config.jsxFragmentFactory, + jsxImportSource: config.jsxImportSource, + reactNamespace: config.reactNamespace, + }; + // Atomic publish: build beside the final directory, then rename it into + // place; a concurrent suite that wins the rename leaves the same content. + const staging = `${dir}.${process.pid}.tmp`; + rmSync(staging, { recursive: true, force: true }); + for (const path of files) { + const target = join(staging, relative(root, path)); + mkdirSync(dirname(target), { recursive: true }); + if (/\.(?:[cm]?ts|tsx|jsx?|[cm]js)$/.test(path) && !/\.d\.[cm]?ts$/.test(path)) { + const output = ts5.transpileModule(readFileSync(path, "utf8"), { + compilerOptions, + fileName: path, + }).outputText; + writeFileSync( + target + .replace(/\.(?:ts|tsx|jsx)$/, ".js") + .replace(/\.mts$/, ".mjs") + .replace(/\.cts$/, ".cjs"), + output, + ); + } else if (/(^|\/)package\.json$/.test(path)) { + // Import and export targets follow their files: .ts, .tsx and .jsx + // become .js. + const renamed = (value: unknown): unknown => + typeof value === "string" + ? /\.d\.[cm]?ts$/.test(value) + ? value + : value + .replace(/\.(?:ts|tsx|jsx)$/, ".js") + .replace(/\.mts$/, ".mjs") + .replace(/\.cts$/, ".cjs") + : Array.isArray(value) + ? value.map(renamed) + : value !== null && typeof value === "object" + ? Object.fromEntries(Object.entries(value).map(([key, item]) => [key, renamed(item)])) + : value; + writeFileSync( + target, + `${JSON.stringify(renamed(JSON.parse(readFileSync(path, "utf8"))), null, 2)}\n`, + ); + } else { + copyFileSync(path, target); + } + } + if (!existsSync(join(staging, "package.json"))) + writeFileSync(join(staging, "package.json"), '{ "type": "module" }\n'); + try { + renameSync(staging, dir); + } catch { + rmSync(staging, { recursive: true, force: true }); + } + return entry; +} + +function listFiles(root: string): string[] { + const out: string[] = []; + const visit = (dir: string): void => { + for (const entry of readdirSync(dir, { withFileTypes: true }).sort((a, b) => + a.name < b.name ? -1 : a.name > b.name ? 1 : 0, + )) { + const path = join(dir, entry.name); + if (entry.isDirectory()) visit(path); + else if (entry.isFile()) out.push(path); + } + }; + visit(root); + return out; +} diff --git a/tests/harness/library-asyncfree.test.ts b/tests/harness/library-asyncfree.test.ts index 7c4d029cc..d924f26d1 100644 --- a/tests/harness/library-asyncfree.test.ts +++ b/tests/harness/library-asyncfree.test.ts @@ -97,6 +97,15 @@ describe("async_free detection over the IR", () => { ).toContain("child_process"); }); + test("asymmetric keys and signatures refuse", async () => { + expect( + await surfaceOf( + "pki", + `import * as crypto from "node:crypto";\nconst { privateKey } = crypto.generateKeyPairSync("ec", { namedCurve: "P-256" });\nconsole.log(crypto.sign("sha256", Buffer.from("x"), privateKey).length > 0);\n`, + ), + ).toContain("asymmetric KeyObjects"); + }); + test("process signal listeners refuse", async () => { expect( await surfaceOf( diff --git a/tests/harness/library-profile.test.ts b/tests/harness/library-profile.test.ts index 2b28a6cd0..b39daefa1 100644 --- a/tests/harness/library-profile.test.ts +++ b/tests/harness/library-profile.test.ts @@ -8,7 +8,12 @@ import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterAll, describe, expect, test } from "vitest"; -import { loadLibraryProfile, profileRemediation, profileTeaching } from "@scriptc/compiler"; +import { + loadLibraryProfile, + profileRemediation, + profileTeaching, + resolveLibraryFences, +} from "@scriptc/compiler"; const dir = mkdtempSync(join(tmpdir(), "scriptc-lib-profile-")); afterAll(() => rmSync(dir, { recursive: true, force: true })); @@ -475,6 +480,13 @@ describe("library profile fences", () => { }); test("a fence remediation feeds the trap-remediation lookup through covered codes", () => { + // A crypto member that still refuses with SC2020, read from the + // manifest: members leave that set as they gain lowerings. + const builtins = resolveLibraryFences([{ path: "fences[0]", prefix: "node-builtin.crypto." }]); + expect(builtins.ok).toBe(true); + if (!builtins.ok) return; + const unlowered = builtins.fences[0]!.surfaces.find((s) => s.code === "SC2020"); + expect(unlowered).toBeDefined(); const r = loadLibraryProfile( writeProfile({ ...good, @@ -482,7 +494,7 @@ describe("library profile fences", () => { remediations: { SC2012: "the explicit map key wins" }, fences: [ { id: "diagnostic.sc2012", remediation: "request it as an effect" }, - { id: "node-builtin.crypto.createSign", remediation: "signatures come from the host" }, + { id: unlowered!.id, remediation: "the host provides it" }, ], }, }), @@ -492,9 +504,9 @@ describe("library profile fences", () => { // Explicit remediations keys win over fence-supplied text. expect(profileRemediation(r.profile, "SC2012")).toBe("the explicit map key wins"); // A fence's remediation answers for its covered entries' codes - // (crypto.createSign is SC2020) — spec §3's "in a fence entry or a - // remediations map", one lookup either way. - expect(profileRemediation(r.profile, "SC2020")).toBe("signatures come from the host"); + // (SC2020 here) — spec §3's "in a fence entry or a remediations map", + // one lookup either way. + expect(profileRemediation(r.profile, "SC2020")).toBe("the host provides it"); expect(profileRemediation(r.profile, "SC4014")).toBeUndefined(); }); diff --git a/tests/harness/linux-differential.test.ts b/tests/harness/linux-differential.test.ts index 9730623cd..9da5199d3 100644 --- a/tests/harness/linux-differential.test.ts +++ b/tests/harness/linux-differential.test.ts @@ -9,6 +9,7 @@ import { compile } from "@scriptc/compiler"; import { npmCases } from "./npm-cases.js"; import { normalizeNodeTestOutput } from "./node-test-normalize.js"; import { eventLoopCases, type StdinScript } from "./event-loop-cases.js"; +import { tscJsxOracleEntry, wantsTscJsx } from "./jsx-oracle.js"; const execFileAsync = promisify(execFile); const enabled = process.env["SCRIPTC_LINUX"] === "1"; @@ -55,6 +56,7 @@ function wantsTscDecorators(file: string): boolean { * programs) its deterministic ES2022 downlevel — differential.test.ts's * nodeOracleFile, with the identical transform and cache key. */ function nodeOracleFile(file: string): string { + if (wantsTscJsx(directiveHead(file))) return tscJsxOracleEntry(file, cacheDir); if (!wantsTscDecorators(file)) return file; const src = readFileSync(file, "utf8"); const out = ts5.transpileModule(src, { @@ -212,7 +214,7 @@ async function runLinuxNode(file: string): Promise { function programInputs(file: string): string[] { if (!/\/main\.(ts|js|mjs|cjs)$/.test(file)) return [file]; return [ - ...ENTRY_EXTS.flatMap((ext) => globSync(join(file, `../**/*.${ext}`))), + ...[...ENTRY_EXTS, "tsx", "jsx"].flatMap((ext) => globSync(join(file, `../**/*.${ext}`))), ...globSync(join(file, "../**/tsconfig.json")), ...globSync(join(file, "../**/package.json")), ].sort(); diff --git a/tests/harness/llvm-differential.test.ts b/tests/harness/llvm-differential.test.ts index 792fdb1d2..15b7e7c0e 100644 --- a/tests/harness/llvm-differential.test.ts +++ b/tests/harness/llvm-differential.test.ts @@ -11,6 +11,7 @@ import { describe, expect, test } from "vitest"; import ts5 from "typescript"; import { compile } from "@scriptc/compiler"; import { corpusSelect, shardSuffix } from "./shard.js"; +import { tscJsxOracleEntry, wantsTscJsx } from "./jsx-oracle.js"; const execFileAsync = promisify(execFile); const repoRoot = join(import.meta.dirname, "../.."); @@ -120,6 +121,7 @@ function wantsTscDecorators(file: string): boolean { } function nodeOracleFile(file: string): string { + if (wantsTscJsx(directiveHead(file))) return tscJsxOracleEntry(file, cacheDir); if (!wantsTscDecorators(file)) return file; const src = readFileSync(file, "utf8"); const out = ts5.transpileModule(src, { @@ -165,7 +167,7 @@ function nodeOracleArgs(file: string): string[] { function programInputs(file: string): string[] { if (!/\/main\.(ts|js|mjs|cjs)$/.test(file)) return [file]; return [ - ...ENTRY_EXTS.flatMap((ext) => globSync(join(file, `../**/*.${ext}`))), + ...[...ENTRY_EXTS, "tsx", "jsx"].flatMap((ext) => globSync(join(file, `../**/*.${ext}`))), ...globSync(join(file, "../**/tsconfig.json")), ...globSync(join(file, "../**/package.json")), ].sort(); diff --git a/tests/harness/project-config.test.ts b/tests/harness/project-config.test.ts index 8532ad6e9..4676499eb 100644 --- a/tests/harness/project-config.test.ts +++ b/tests/harness/project-config.test.ts @@ -307,6 +307,24 @@ test("node-types: TLSSocket.isSessionReused and keyed ??= timings under @types/n expect(comparableStderr(native.stderr)).toBe(node.stderr); }); +test("node-types: new tls.TLSSocket and tls.connect({ socket }) under @types/node", async () => { + const outDir = outDirFor("tls-socket-wrap"); + const entry = join(nodeTypesDir, "tls-socket-wrap.ts"); + const result = await compile(entry, { + outPath: join(outDir, process.platform === "win32" ? "tls-socket-wrap.exe" : "tls-socket-wrap"), + outDir, + sanitize, + }); + expect(result.ok, !result.ok ? JSON.stringify(result.diagnostics, null, 2) : "").toBe(true); + if (!result.ok) return; + const [native, node] = await Promise.all([ + execFileAsync(result.binaryPath), + execFileAsync(process.execPath, ["--disable-warning=MODULE_TYPELESS_PACKAGE_JSON", entry]), + ]); + expect(native.stdout).toBe(node.stdout); + expect(comparableStderr(native.stderr)).toBe(node.stderr); +}); + test("node-types: net.SocketAddress and net.BlockList under @types/node", async () => { const outDir = outDirFor("net-blocklist"); const entry = join(nodeTypesDir, "net-blocklist.ts"); @@ -1114,6 +1132,52 @@ test("node-types: ECDH matches Node under @types/node", async () => { expect(comparableStderr(native.stderr)).toBe(node.stderr); }); +test("node-types: DiffieHellman matches Node under @types/node", async () => { + const outDir = outDirFor("node-dh"); + const entry = join(nodeTypesDir, "dh.ts"); + const result = await compile(entry, { outPath: join(outDir, "dh"), outDir, sanitize }); + expect(result.ok, !result.ok ? JSON.stringify(result.diagnostics, null, 2) : "").toBe(true); + if (!result.ok) return; + const [native, node] = await Promise.all([ + execFileAsync(result.binaryPath), + execFileAsync(process.execPath, ["--disable-warning=MODULE_TYPELESS_PACKAGE_JSON", entry]), + ]); + expect(native.stdout).toBe(node.stdout); + expect(comparableStderr(native.stderr)).toBe(node.stderr); +}); + +test("node-types: asymmetric keys and signatures match Node under @types/node", async () => { + const outDir = outDirFor("node-keys"); + const entry = join(nodeTypesDir, "keys.ts"); + const result = await compile(entry, { outPath: join(outDir, "keys"), outDir, sanitize }); + expect(result.ok, !result.ok ? JSON.stringify(result.diagnostics, null, 2) : "").toBe(true); + if (!result.ok) return; + const [native, node] = await Promise.all([ + execFileAsync(result.binaryPath), + execFileAsync(process.execPath, ["--disable-warning=MODULE_TYPELESS_PACKAGE_JSON", entry]), + ]); + expect(native.stdout).toBe(node.stdout); + expect(comparableStderr(native.stderr)).toBe(node.stderr); +}); + +test("node-types: WebCrypto ECDSA, Ed25519 and RSA match Node under @types/node", async () => { + const outDir = outDirFor("node-webcrypto-ecdsa"); + const entry = join(nodeTypesDir, "webcrypto-ecdsa.ts"); + const result = await compile(entry, { + outPath: join(outDir, "webcrypto-ecdsa"), + outDir, + sanitize, + }); + expect(result.ok, !result.ok ? JSON.stringify(result.diagnostics, null, 2) : "").toBe(true); + if (!result.ok) return; + const [native, node] = await Promise.all([ + execFileAsync(result.binaryPath), + execFileAsync(process.execPath, ["--disable-warning=MODULE_TYPELESS_PACKAGE_JSON", entry]), + ]); + expect(native.stdout).toBe(node.stdout); + expect(comparableStderr(native.stderr)).toBe(node.stderr); +}); + test("node-types: WebCrypto HMAC matches Node under @types/node", async () => { const outDir = outDirFor("node-webcrypto-hmac"); const entry = join(nodeTypesDir, "webcrypto-hmac.ts"); diff --git a/tests/harness/self-hosting-native-toolchain.test.ts b/tests/harness/self-hosting-native-toolchain.test.ts index c01b52595..c0176eb93 100644 --- a/tests/harness/self-hosting-native-toolchain.test.ts +++ b/tests/harness/self-hosting-native-toolchain.test.ts @@ -48,6 +48,9 @@ const features: NativeLinkFeatures = { tlsCa: false, cipher: false, ecdh: false, + dh: false, + pki: false, + ed25519: false, webcrypto: false, perfLoop: false, }; @@ -105,7 +108,7 @@ for (const backend of ["llvm"] as const) { object_format: target.objectFormat, minimum_os: target.minimumOs, }, - runtime_abi: { version: 25, marker: "scr_runtime_abi_v25" }, + runtime_abi: { version: 27, marker: "scr_runtime_abi_v27" }, compiler: { command: "fixture", identity: "fixture", target: target.llvmTriple }, macros: { executable: [], excluded: [], sanitizer: "external-toolchain-required" }, flavors: { diff --git a/tests/harness/self-hosting-runtime-pack.test.ts b/tests/harness/self-hosting-runtime-pack.test.ts index f4fde795c..57857d642 100644 --- a/tests/harness/self-hosting-runtime-pack.test.ts +++ b/tests/harness/self-hosting-runtime-pack.test.ts @@ -49,6 +49,9 @@ const base: NativeLinkFeatures = { tlsCa: false, cipher: false, ecdh: false, + dh: false, + pki: false, + ed25519: false, webcrypto: false, perfLoop: false, }; diff --git a/tests/harness/stream-entry-kinds.test.ts b/tests/harness/stream-entry-kinds.test.ts index f7333b298..ec89c5914 100644 --- a/tests/harness/stream-entry-kinds.test.ts +++ b/tests/harness/stream-entry-kinds.test.ts @@ -138,7 +138,7 @@ r.once('readable', () => { expect(r.stdout).toBe(`caught ${READ_FENCE_MESSAGE}\n`); }); - test("a dynamic source that mixes Buffers and strings without a decoder is destroyed", async () => { + test("a dynamic source that mixes Buffers and strings yields each as it is, as Node does", async () => { const r = await compileAndRun( "mixed-dynamic-source", `import { Readable } from "node:stream"; @@ -151,8 +151,6 @@ r.on("close", () => console.log("close")); `, ); expect(r.exitCode).toBe(0); - expect(r.stdout).toBe( - "data true b1\nerror a Readable.from source that mixes strings and Buffers is not supported yet\nclose\n", - ); + expect(r.stdout).toBe("data true b1\ndata false s1\nclose\n"); }); }); diff --git a/tests/harness/tls-socket-leaks.test.ts b/tests/harness/tls-socket-leaks.test.ts new file mode 100644 index 000000000..64edfac25 --- /dev/null +++ b/tests/harness/tls-socket-leaks.test.ts @@ -0,0 +1,68 @@ +/* new tls.TLSSocket under LeakSanitizer. A TLS socket's transport holds + * its 'secure' and 'session' listeners, which often capture the socket; + * every socket that closes, including one whose handshake fails, must drop + * them. The sanitized lanes otherwise run with detect_leaks=0, so this runs + * the TLSSocket programs with LeakSanitizer explicitly on. 7094's + * handshakes all fail. Linux only (Apple's ASan has no LeakSanitizer); + * runs in the SCRIPTC_SAN=1 lane. */ +import { execFile } from "node:child_process"; +import { createHash } from "node:crypto"; +import { mkdirSync, readFileSync } from "node:fs"; +import { join } from "node:path"; +import { promisify } from "node:util"; +import { describe, expect, test } from "vitest"; +import { compile } from "@scriptc/compiler"; + +const execFileAsync = promisify(execFile); +const repoRoot = join(import.meta.dirname, "../.."); +const cacheDir = join(repoRoot, "node_modules/.cache/scriptc-tests"); +const sanitize = process.env["SCRIPTC_SAN"] === "1"; + +const programs = [ + "tests/corpus/7087-tls-socket-wrap.ts", + "tests/corpus/7088-tls-socket-connect-tunnel.ts", + "tests/corpus/7094-tls-socket-failed-handshakes.ts", +]; + +describe.skipIf(!sanitize || process.platform !== "linux")( + "tls.TLSSocket under LeakSanitizer", + () => { + test.each(programs)("%s reports no leaks", { timeout: 300_000 }, async (program) => { + const file = join(repoRoot, program); + const key = createHash("sha256") + .update(file) + .update(readFileSync(file)) + .update("san") + .digest("hex") + .slice(0, 16); + const outDir = join(cacheDir, `tls-socket-leaks-${key}`); + mkdirSync(outDir, { recursive: true }); + const result = await compile(file, { + outPath: join(outDir, "program"), + outDir, + sanitize: true, + backend: "llvm", + }); + if (!result.ok) { + throw new Error( + "program failed to compile:\n" + + result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"), + ); + } + const run = await execFileAsync(result.binaryPath, [], { + env: { ...process.env, ASAN_OPTIONS: "detect_leaks=1" }, + maxBuffer: 16 * 1024 * 1024, + }).catch((error: { code?: number; stdout?: string; stderr?: string }) => error); + expect(run.stderr).toBe(""); + expect("code" in run ? run.code : 0).toBe(0); + const expected = await execFileAsync( + process.execPath, + ["--no-warnings", "--experimental-strip-types", file], + { + maxBuffer: 16 * 1024 * 1024, + }, + ); + expect(run.stdout).toBe(expected.stdout); + }); + }, +); diff --git a/tests/harness/windows-differential.test.ts b/tests/harness/windows-differential.test.ts index 366820420..8d25d9b07 100644 --- a/tests/harness/windows-differential.test.ts +++ b/tests/harness/windows-differential.test.ts @@ -47,11 +47,12 @@ import { writeFileSync, } from "node:fs"; import { tmpdir } from "node:os"; -import { basename, join } from "node:path"; +import { basename, dirname, join } from "node:path"; import { promisify } from "node:util"; import { afterAll, beforeAll, describe, expect, test } from "vitest"; import ts5 from "typescript"; import { compile } from "@scriptc/compiler"; +import { tscJsxOracleEntry, wantsTscJsx } from "./jsx-oracle.js"; const execFileAsync = promisify(execFile); const enabled = process.env["SCRIPTC_WIN"] === "1"; @@ -343,11 +344,13 @@ async function scpToLane(paths: string[]): Promise { * type-strips .ts natively (same pinned version as the macOS lanes). */ async function runWindowsNode(file: string): Promise { const name = laneName(file); - const entry = wantsTscDecorators(file) - ? decOracleName(file) - : name === basename(file) - ? name - : `${name}\\${basename(file)}`; + const entry = wantsTscJsx(directiveHead(file)) + ? `${basename(dirname(tscJsxOracleEntry(file, cacheDir)))}\\main.js` + : wantsTscDecorators(file) + ? decOracleName(file) + : name === basename(file) + ? name + : `${name}\\${basename(file)}`; const transform = wantsTransformTypes(file) ? "--experimental-transform-types --disable-warning=ExperimentalWarning " : ""; @@ -366,7 +369,7 @@ async function runWindowsNode(file: string): Promise { function programInputs(file: string): string[] { if (!/\/main\.(ts|js|mjs|cjs)$/.test(file)) return [file]; return [ - ...ENTRY_EXTS.flatMap((ext) => globSync(join(file, `../**/*.${ext}`))), + ...[...ENTRY_EXTS, "tsx", "jsx"].flatMap((ext) => globSync(join(file, `../**/*.${ext}`))), ...globSync(join(file, "../**/tsconfig.json")), ...globSync(join(file, "../**/package.json")), ].sort(); @@ -403,7 +406,11 @@ async function shipProgram(file: string): Promise { const programRoot = /\/main\.(ts|js|mjs|cjs)$/.test(file) ? join(file, "..") : file; // Decorator programs additionally ship the tsc downlevel the oracle // executes (the compiler still eats the real source above). - const oracleExtra = wantsTscDecorators(file) ? [decOraclePath(file)] : []; + const oracleExtra = wantsTscJsx(directiveHead(file)) + ? [dirname(tscJsxOracleEntry(file, cacheDir))] + : wantsTscDecorators(file) + ? [decOraclePath(file)] + : []; await scpToLane([binary, programRoot, ...oracleExtra]); }