diff --git a/.gitignore b/.gitignore index 4922ef6df..7b7bc57f2 100644 --- a/.gitignore +++ b/.gitignore @@ -35,6 +35,7 @@ output/ !tests/fixtures/npm/cases/workspace-copied/node_modules/*/dist/ !tests/fixtures/provenance/node_modules/*/dist/ !tests/fixtures/npm-static/node_modules/*/dist/ +!tests/corpus/*/node_modules/*/dist/ .scriptc/ *.log .DS_Store diff --git a/docs/content/docs/coverage.mdx b/docs/content/docs/coverage.mdx index 805d7f612..b27327b91 100644 --- a/docs/content/docs/coverage.mdx +++ b/docs/content/docs/coverage.mdx @@ -76,7 +76,7 @@ Coverage and `scriptc build` report the same diagnostic codes. Common ones inclu Some code compiles but can behave differently than under Node.js. Coverage reports the cases it can detect as `SC6xxx` warnings, and `scriptc build` prints the same warnings after a successful build. They never fail a build, but `--fail-on=divergences` can fail a CI job on them. - `SC6001`: a record passed to, stored as, asserted as, or stored as an array element of a record type with different properties is copied, and the program then writes through the copy or compares it by identity. -- `SC6002`: an object literal's key order differs from its type's declaration order, and the program observes the order through `JSON.stringify`, `Object.keys`, `Object.values`, `Object.entries`, `for...in`, or console output. +- `SC6002`: values of one record type are built in different key orders, so the type keeps its declaration order, an object literal's key order differs from it, and the program observes the order through `JSON.stringify`, `Object.keys`, `Object.values`, `Object.entries`, `for...in`, or console output. - `SC6003`: `localeCompare` compares a string literal outside the built-in Latin-script collation. - `SC6004`: a call passes a class instance to a function that casts it to an unrelated class. - `SC6005`: a `Date` string literal uses a form the native parser does not accept. diff --git a/docs/content/docs/dependencies.mdx b/docs/content/docs/dependencies.mdx index e6020fe54..b34839c93 100644 --- a/docs/content/docs/dependencies.mdx +++ b/docs/content/docs/dependencies.mdx @@ -73,6 +73,33 @@ $ scriptc build app.ts --npm-static=three -o app >/dev/null Naming a package skips the declaration requirement above, but the package still has to compile. Add a trailing `...` to select a package together with every package it depends on, for example `--npm-static=fs-extra...`. `--npm-static=auto` uses the default automatic selection. The option accepts a comma-separated list and can be repeated. +## Built-in packages + +scriptc implements a few packages itself and compiles that implementation in place of the package's published JavaScript. Which implementation compiles depends on the version your program has installed, so an import resolves the same way it does under Node.js: a package that isn't installed is still a missing package, and a version scriptc doesn't implement compiles from its own files as usual. + +`undici` 5.x and 7.x are built in. The implementation covers `request()`, `Dispatcher` (including subclasses that override `dispatch`), `Client`, `Pool` with its `stats`, `Agent` with per-origin `factory` functions and `connect` options, `buildConnector`, the `errors` classes and their `UND_ERR_*` codes, `setGlobalDispatcher` and `getGlobalDispatcher`, `fetch` and `Headers`, and the request events undici publishes on `node:diagnostics_channel`. Requests use HTTP/1.1 with keep-alive connection reuse and undici's headers, body and idle timeouts, and each version keeps its own behavior where 5.x and 7.x differ, such as the error an aborted request rejects with. The package's other exports aren't declared, so using one is a type error at build time. Known differences: + +- A response body supports the readable-stream members undici's body mixin uses: `on`, `once`, `pause`, `resume`, `destroy`, `text()`, `json()`, `arrayBuffer()`, `bytes()`, `blob()` and `dump()`. It can't be piped or iterated with `for await`. +- Diagnostics channel messages carry the request but not the socket or connector. +- `fetch` with a dispatcher returns a response whose `url` is empty and whose `redirected` is `false`, offers only `gzip, deflate` encodings, and sends string and byte bodies. Without a dispatcher it is the built-in `fetch`. +- Request bodies are strings and bytes; stream, iterable and form-data bodies, `pipelining` above 1, `CONNECT` requests and Unix socket paths aren't supported. + +## Optional dependencies that never load + +Some packages load an optional dependency inside `try`/`catch` and work without it, such as `node-fetch` with `encoding`. To keep such a dependency out of the native build even when it is installed, mark it unloaded in your entry's `package.json`: + +```json:package.json +{ + "scriptc": { + "packages": { + "encoding": "unloaded" + } + } +} +``` + +A `require()` of an unloaded package throws the same `MODULE_NOT_FOUND` error Node.js throws when the package isn't installed, and the package isn't compiled. + ## CommonJS packages CommonJS packages compile with Node.js's module semantics. Each `require()` evaluates its module at the call, once, and later calls return the cached exports. A package can assign `module.exports` and run code before it requires its dependencies. If that code reads a `const` or `let` require binding before its `require` runs, the read throws the same `ReferenceError` as Node.js. A `var` binding that earlier code can reach is reported at build time. A `require()` of a JSON file, such as a package's own `package.json`, returns one object parsed at build time, shared by every module that requires it. A require cycle compiles when each of its modules assigns its exports before requiring anything, as semver's classes and esbuild's per-file output do, and uses the other modules only inside functions that its top-level code can't call. Bundles built by esbuild compile with their bundled CommonJS modules and lazily initialized ES modules, each evaluated once, at its first use, as under Node.js. Other require cycles are reported at build time. The [limitations](/docs/limitations) page lists the supported `require` forms. diff --git a/docs/content/limitations/record-identity-and-property-order.mdx b/docs/content/limitations/record-identity-and-property-order.mdx index 76ff0ae74..a1faa1c34 100644 --- a/docs/content/limitations/record-identity-and-property-order.mdx +++ b/docs/content/limitations/record-identity-and-property-order.mdx @@ -2,7 +2,7 @@ A record converted to a record type with a different set of properties, such as Typed records with declared fields, indexed records, and arrays retain identity and mutations through native checked storage. Class instances retain identity at their exact type or a base class, and at any interface or object type that a class instance is passed, stored, returned, or asserted as when the program could tell a copy from the instance: values of such a type are checked views of the original object, so writes, identity comparisons, Set and Map keys, `util.inspect`, and `JSON.stringify` through them reach the instance, as in Node.js. These types use checked dynamic storage, which is slower than native record fields, for all of their values, including plain objects. A type whose values are only read field by field, from fields the class does not write after construction and does not implement as accessors, stays a native record and holds a copy. Object spread of a class instance is refused. Dense array literals created directly in checked storage preserve identity, and supported native handles pass by reference. -`Object.keys`, `Object.values`, `Object.entries`, and `JSON.stringify` use a record's declaration order rather than per-object insertion order. Their order matches Node.js when objects are built in declaration order. `SC6002` reports object literals whose order differs where the program observes it. +`Object.keys`, `Object.values`, `Object.entries`, `for...in`, `JSON.stringify`, and `util.inspect` enumerate a record's keys in one order per record type rather than per object. scriptc chooses that order from the order in which the program's object literals create the keys, follows object spread, rest destructuring, and record conversions back to the values they copy, and places an optional key that the program assigns after construction after the keys the object was built with. The order matches Node.js when every value of the type is created in the same order. When values of one record type are created in different orders, including values of two types with the same properties and property types, or when a key is deleted and assigned again, the type keeps the declaration order of its first type instead, and `SC6002` reports object literals whose order differs where the program observes it. A record converted from a checked value, such as `const rows: { score: number }[] = JSON.parse(text)`, keeps the properties its type does not declare. `JSON.stringify`, `Object.keys`, `Object.values`, `Object.entries`, `util.inspect`, and untyped views of the record see them, together with the declared fields, in the source object's key order. `Object.values` and `Object.entries` check those values against their static result type and throw a `TypeError` if one does not fit. Copies of such a record made by object spread, `Object.assign`, rest destructuring, or conversion to another record type contain only the declared fields. Index-signature records list declared fields before the other keys. diff --git a/docs/content/limitations/strings-and-sorting.mdx b/docs/content/limitations/strings-and-sorting.mdx index eacca198e..b1616f9f4 100644 --- a/docs/content/limitations/strings-and-sorting.mdx +++ b/docs/content/limitations/strings-and-sorting.mdx @@ -1,4 +1,4 @@ -Strings are stored as UTF-8, with lone surrogates kept as their own code units. Length and string methods use UTF-16 semantics, including for lone surrogates, but relational comparisons such as `<` and `>` use code-point order. `split` with a string separator and `replace` or `replaceAll` with a string pattern don't match a lone surrogate against half of a surrogate pair; regular-expression patterns do. Output, file contents, and other encoded text write each lone surrogate as U+FFFD, as Node does. File system paths and network host names that contain a lone surrogate reach the operating system unconverted. +Strings are stored as UTF-8, with lone surrogates kept as their own code units. Length, string methods and relational comparisons such as `<` and `>` use UTF-16 semantics, including for lone surrogates. `split` with a string separator and `replace` or `replaceAll` with a string pattern don't match a lone surrogate against half of a surrogate pair; regular-expression patterns do. Output, file contents, and other encoded text write each lone surrogate as U+FFFD, as Node does. File system paths and network host names that contain a lone surrogate reach the operating system unconverted. One-argument `localeCompare` follows Node's default root-locale collation for Latin-script text (ASCII, Latin-1 Supplement, Latin Extended-A, combining diacritical marks, and U+2044 FRACTION SLASH): letters order before case and accents, so `"a".localeCompare("B")` is `-1`. Strings containing other characters compare by code point, which can differ from Node's ICU collation; `SC6003` reports such string literals. The locales and options arguments are unsupported. diff --git a/docs/src/generated/node-v24-compatibility-meta.json b/docs/src/generated/node-v24-compatibility-meta.json new file mode 100644 index 000000000..167256bca --- /dev/null +++ b/docs/src/generated/node-v24-compatibility-meta.json @@ -0,0 +1,7 @@ +{ + "schemaVersion": 3, + "nodeVersion": "24.22.0", + "nodeCommit": "f14cd3aa7bf9ca8df2cc5b8bb14beb0f335b1d93", + "artifactVersion": "ee72820d45a79aa154f0", + "rowCount": 3880 +} diff --git a/docs/src/generated/node-v24-compatibility.json b/docs/src/generated/node-v24-compatibility.json index e2e4cdd19..be1a931cc 100644 --- a/docs/src/generated/node-v24-compatibility.json +++ b/docs/src/generated/node-v24-compatibility.json @@ -208,10 +208,10 @@ "entries": 152, "apiEntries": 152, "static": { - "supported": 5, + "supported": 6, "partial": 39, "refused": 21, - "not-implemented": 87, + "not-implemented": 86, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -314,9 +314,9 @@ "apiEntries": 280, "static": { "supported": 7, - "partial": 198, + "partial": 200, "refused": 0, - "not-implemented": 75, + "not-implemented": 73, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -328,10 +328,10 @@ "entries": 92, "apiEntries": 92, "static": { - "supported": 0, - "partial": 12, + "supported": 1, + "partial": 15, "refused": 1, - "not-implemented": 79, + "not-implemented": 75, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -853,10 +853,10 @@ "entries": 156, "apiEntries": 156, "static": { - "supported": 1, - "partial": 10, + "supported": 2, + "partial": 21, "refused": 0, - "not-implemented": 145, + "not-implemented": 133, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -12325,9 +12325,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "supported", + "detail": "Implemented by a dedicated static compiler/runtime path.", + "verification": "test-backed" } }, { @@ -30288,9 +30288,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" } }, { @@ -35256,9 +35256,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" } }, { @@ -35693,9 +35693,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" } }, { @@ -36360,9 +36360,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" } }, { @@ -37165,9 +37165,9 @@ "inherited": false }, "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" } }, { @@ -37303,9 +37303,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "supported", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -80183,9 +80183,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "supported", + "detail": "Implemented by a dedicated static compiler/runtime path.", + "verification": "test-backed" } }, { @@ -80229,9 +80229,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" } }, { @@ -80252,9 +80252,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" } }, { @@ -80275,9 +80275,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" } }, { @@ -80298,9 +80298,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" } }, { @@ -80321,9 +80321,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" } }, { @@ -80344,9 +80344,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" } }, { @@ -80436,9 +80436,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" } }, { @@ -80574,9 +80574,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" } }, { @@ -80735,9 +80735,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" } }, { @@ -80758,9 +80758,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" } }, { @@ -80804,9 +80804,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-backlog.json b/internal/compatibility/generated/node-v24-backlog.json index 0ad30b783..a02446908 100644 --- a/internal/compatibility/generated/node-v24-backlog.json +++ b/internal/compatibility/generated/node-v24-backlog.json @@ -3,21 +3,21 @@ "nodeVersion": "24.22.0", "nodeCommit": "f14cd3aa7bf9ca8df2cc5b8bb14beb0f335b1d93", "summary": { - "taskCount": 3456, - "tierItemCount": 3456, + "taskCount": 3453, + "tierItemCount": 3453, "tiers": { "static": { - "audit-partial": 1163, - "verify-gap": 2165, + "audit-partial": 1179, + "verify-gap": 2146, "replace-refusal": 41, "classify": 2, "implement": 85 } }, "priorities": { - "high": 2289, + "high": 2287, "normal": 694, - "low": 473 + "low": 472 } }, "tasks": [ @@ -7359,31 +7359,6 @@ } } }, - { - "id": "crypto:2ab8394e29a7", - "chapter": "crypto", - "label": "crypto.getRandomValues(typedArray)", - "signature": "`crypto.getRandomValues(typedArray)`", - "apiSymbol": "crypto.getRandomValues", - "docsUrl": "https://nodejs.org/docs/v24.22.0/api/crypto.html#cryptogetrandomvaluestypedarray", - "nodeStability": { - "index": "2", - "level": 2, - "text": "Stable", - "inherited": true - }, - "priority": "high", - "tiers": { - "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:crypto", - "tests": [] - } - } - }, { "id": "crypto:21f22c71fb61", "chapter": "crypto", @@ -27067,12 +27042,15 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:fs", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "surface-manifest:node-builtin.fs.constants", + "tests": [ + "tests/corpus/6207-fs-constants-object.ts", + "tests/corpus/6208-fs-constants-require.cjs" + ] } } }, @@ -32764,12 +32742,15 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:fs", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "surface-manifest:node-builtin.fs.constants", + "tests": [ + "tests/corpus/6207-fs-constants-object.ts", + "tests/corpus/6208-fs-constants-require.cjs" + ] } } }, @@ -33220,12 +33201,18 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:globals", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:globals.CryptoKey", + "tests": [ + "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" + ] } } }, @@ -33951,12 +33938,18 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:globals", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:globals.SubtleCrypto", + "tests": [ + "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" + ] } } }, @@ -34872,12 +34865,16 @@ "priority": "low", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:globals", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:globals.navigator", + "tests": [ + "tests/fixtures/node-types/navigator.ts", + "tests/harness/project-config.test.ts", + "tests/corpus/6331-jose-root-entry/main.ts" + ] } } }, @@ -35006,31 +35003,6 @@ } } }, - { - "id": "globals:fc0a32e8c26f", - "chapter": "globals", - "label": "userAgent", - "signature": "`userAgent` Type: {string}", - "apiSymbol": "userAgent", - "docsUrl": "https://nodejs.org/docs/v24.22.0/api/globals.html#navigator", - "nodeStability": { - "index": "1.1", - "level": 1, - "text": "Active development. Disable this API with the [`--no-experimental-global-navigator`][] CLI flag.", - "inherited": true - }, - "priority": "low", - "tiers": { - "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:globals", - "tests": [] - } - } - }, { "id": "globals:b412bf9a456d", "chapter": "globals", @@ -81027,31 +80999,6 @@ } } }, - { - "id": "webcrypto:dcc24f369f43", - "chapter": "webcrypto", - "label": "crypto.getRandomValues(typedArray)", - "signature": "`crypto.getRandomValues(typedArray)`", - "apiSymbol": "crypto.getRandomValues", - "docsUrl": "https://nodejs.org/docs/v24.22.0/api/webcrypto.html#cryptogetrandomvaluestypedarray", - "nodeStability": { - "index": "2", - "level": 2, - "text": "Stable", - "inherited": true - }, - "priority": "high", - "tiers": { - "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:webcrypto", - "tests": [] - } - } - }, { "id": "webcrypto:8c570ec75666", "chapter": "webcrypto", @@ -81068,12 +81015,18 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:webcrypto", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:webcrypto.subtle", + "tests": [ + "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" + ] } } }, @@ -81093,12 +81046,16 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:webcrypto", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:webcrypto.CryptoKey", + "tests": [ + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs" + ] } } }, @@ -81118,12 +81075,16 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:webcrypto", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:webcrypto.cryptoKey.algorithm", + "tests": [ + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs" + ] } } }, @@ -81143,12 +81104,16 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:webcrypto", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:webcrypto.extractable", + "tests": [ + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs" + ] } } }, @@ -81168,12 +81133,16 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:webcrypto", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:webcrypto.type", + "tests": [ + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs" + ] } } }, @@ -81193,12 +81162,16 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:webcrypto", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:webcrypto.usages", + "tests": [ + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs" + ] } } }, @@ -81293,12 +81266,18 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:webcrypto", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:webcrypto.SubtleCrypto", + "tests": [ + "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" + ] } } }, @@ -81443,12 +81422,19 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:webcrypto", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:webcrypto.subtle.digest", + "tests": [ + "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" + ] } } }, @@ -81618,12 +81604,19 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:webcrypto", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:webcrypto.subtle.importKey", + "tests": [ + "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" + ] } } }, @@ -81643,12 +81636,19 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:webcrypto", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:webcrypto.subtle.sign", + "tests": [ + "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" + ] } } }, @@ -81693,12 +81693,19 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:webcrypto", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:webcrypto.subtle.verify", + "tests": [ + "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" + ] } } }, diff --git a/internal/compatibility/generated/node-v24-internal.json b/internal/compatibility/generated/node-v24-internal.json index 2f0565f84..3690ce077 100644 --- a/internal/compatibility/generated/node-v24-internal.json +++ b/internal/compatibility/generated/node-v24-internal.json @@ -190,10 +190,10 @@ "entries": 179, "apiEntries": 152, "static": { - "supported": 5, + "supported": 6, "partial": 39, "refused": 21, - "not-implemented": 87, + "not-implemented": 86, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -326,9 +326,9 @@ "apiEntries": 280, "static": { "supported": 7, - "partial": 198, + "partial": 200, "refused": 0, - "not-implemented": 75, + "not-implemented": 73, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -340,10 +340,10 @@ "entries": 94, "apiEntries": 92, "static": { - "supported": 0, - "partial": 12, + "supported": 1, + "partial": 15, "refused": 1, - "not-implemented": 79, + "not-implemented": 75, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -910,10 +910,10 @@ "entries": 176, "apiEntries": 156, "static": { - "supported": 1, - "partial": 10, + "supported": 2, + "partial": 21, "refused": 0, - "not-implemented": 145, + "not-implemented": 133, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -19872,6 +19872,8 @@ "tests/corpus/5762-crypto-hash-stream.cjs", "tests/corpus/5768-crypto-hash-unknown.ts", "tests/corpus/5769-crypto-hash-unknown.cjs", + "tests/corpus/6204-crypto-get-random-values.ts", + "tests/corpus/6205-crypto-get-random-values.js", "tests/corpus/node24-crypto-kdf-errors.ts", "tests/corpus/node24-crypto-kdf-options.ts", "tests/corpus/node24-crypto-kdf.ts", @@ -21028,8 +21030,12 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:crypto" + "status": "supported", + "evidence": "compiler-dedicated:crypto.getRandomValues", + "tests": [ + "tests/corpus/6204-crypto-get-random-values.ts", + "tests/corpus/6205-crypto-get-random-values.js" + ] }, "anchorSource": "exact" }, @@ -44138,6 +44144,8 @@ "tests/corpus/5568-fs-cp-errors.js", "tests/corpus/5569-fs-cp-symlinks.ts", "tests/corpus/5570-fs-cp-async.ts", + "tests/corpus/6207-fs-constants-object.ts", + "tests/corpus/6208-fs-constants-require.cjs", "tests/corpus/fs-native-realpath.ts", "tests/corpus/node24-fs-links-callbacks.cjs", "tests/corpus/node24-fs-links-errors.cjs", @@ -45925,8 +45933,12 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:fs" + "status": "partial", + "evidence": "surface-manifest:node-builtin.fs.constants", + "tests": [ + "tests/corpus/6207-fs-constants-object.ts", + "tests/corpus/6208-fs-constants-require.cjs" + ] }, "anchorSource": "ancestor" }, @@ -51797,8 +51809,12 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:fs" + "status": "partial", + "evidence": "surface-manifest:node-builtin.fs.constants", + "tests": [ + "tests/corpus/6207-fs-constants-object.ts", + "tests/corpus/6208-fs-constants-require.cjs" + ] }, "anchorSource": "ancestor" }, @@ -52226,8 +52242,16 @@ "tests/corpus/5288-blob-validation.js", "tests/corpus/5290-blob-dynamic.js", "tests/corpus/5830-timers-promises-signal.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/corpus/6331-jose-root-entry/main.ts", "tests/corpus/text-codec-values/main.ts", - "tests/harness/fetch-conformance.test.ts" + "tests/fixtures/node-types/navigator.ts", + "tests/harness/fetch-conformance.test.ts", + "tests/harness/project-config.test.ts" ] }, "anchorSource": "chapter" @@ -52648,8 +52672,15 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:globals" + "status": "partial", + "evidence": "compiler-feature:globals.CryptoKey", + "tests": [ + "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" + ] }, "anchorSource": "exact" }, @@ -53322,8 +53353,15 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:globals" + "status": "partial", + "evidence": "compiler-feature:globals.SubtleCrypto", + "tests": [ + "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" + ] }, "anchorSource": "exact" }, @@ -54228,8 +54266,13 @@ "inherited": false }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:globals" + "status": "partial", + "evidence": "compiler-feature:globals.navigator", + "tests": [ + "tests/fixtures/node-types/navigator.ts", + "tests/harness/project-config.test.ts", + "tests/corpus/6331-jose-root-entry/main.ts" + ] }, "anchorSource": "exact" }, @@ -54366,8 +54409,12 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:globals" + "status": "supported", + "evidence": "compiler-feature:globals.userAgent", + "tests": [ + "tests/fixtures/node-types/navigator.ts", + "tests/harness/project-config.test.ts" + ] }, "anchorSource": "ancestor" }, @@ -106854,10 +106901,18 @@ "tests/corpus/2890-crypto-static-utility.ts", "tests/corpus/5442-crypto-random-values.ts", "tests/corpus/5443-crypto-random-arguments.js", + "tests/corpus/6204-crypto-get-random-values.ts", + "tests/corpus/6205-crypto-get-random-values.js", + "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/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/webcrypto-hmac.ts", "tests/harness/npm-static.test.ts" ] }, @@ -110338,8 +110393,12 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:webcrypto" + "status": "supported", + "evidence": "compiler-dedicated:crypto.getRandomValues", + "tests": [ + "tests/corpus/6204-crypto-get-random-values.ts", + "tests/corpus/6205-crypto-get-random-values.js" + ] }, "anchorSource": "exact" }, @@ -110389,8 +110448,15 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:webcrypto" + "status": "partial", + "evidence": "compiler-feature:webcrypto.subtle", + "tests": [ + "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" + ] }, "anchorSource": "ancestor" }, @@ -110412,8 +110478,13 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:webcrypto" + "status": "partial", + "evidence": "compiler-feature:webcrypto.CryptoKey", + "tests": [ + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs" + ] }, "anchorSource": "exact" }, @@ -110435,8 +110506,13 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:webcrypto" + "status": "partial", + "evidence": "compiler-feature:webcrypto.cryptoKey.algorithm", + "tests": [ + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs" + ] }, "anchorSource": "exact" }, @@ -110458,8 +110534,13 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:webcrypto" + "status": "partial", + "evidence": "compiler-feature:webcrypto.extractable", + "tests": [ + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs" + ] }, "anchorSource": "ancestor" }, @@ -110481,8 +110562,13 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:webcrypto" + "status": "partial", + "evidence": "compiler-feature:webcrypto.type", + "tests": [ + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs" + ] }, "anchorSource": "ancestor" }, @@ -110504,8 +110590,13 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:webcrypto" + "status": "partial", + "evidence": "compiler-feature:webcrypto.usages", + "tests": [ + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs" + ] }, "anchorSource": "ancestor" }, @@ -110596,8 +110687,15 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:webcrypto" + "status": "partial", + "evidence": "compiler-feature:webcrypto.SubtleCrypto", + "tests": [ + "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" + ] }, "anchorSource": "exact" }, @@ -110734,8 +110832,16 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:webcrypto" + "status": "partial", + "evidence": "compiler-feature:webcrypto.subtle.digest", + "tests": [ + "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" + ] }, "anchorSource": "exact" }, @@ -110895,8 +111001,16 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:webcrypto" + "status": "partial", + "evidence": "compiler-feature:webcrypto.subtle.importKey", + "tests": [ + "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" + ] }, "anchorSource": "exact" }, @@ -110918,8 +111032,16 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:webcrypto" + "status": "partial", + "evidence": "compiler-feature:webcrypto.subtle.sign", + "tests": [ + "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" + ] }, "anchorSource": "exact" }, @@ -110964,8 +111086,16 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:webcrypto" + "status": "partial", + "evidence": "compiler-feature:webcrypto.subtle.verify", + "tests": [ + "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" + ] }, "anchorSource": "exact" }, diff --git a/internal/compatibility/static-support.json b/internal/compatibility/static-support.json index 5807c1ae9..855d065f7 100644 --- a/internal/compatibility/static-support.json +++ b/internal/compatibility/static-support.json @@ -111,6 +111,8 @@ "node-builtin.os.uptime": ["tests/corpus/5140-os-host-facts.ts"], "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.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"], "node-builtin.url.resolve": ["tests/corpus/5226-url-resolve.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"], @@ -177,11 +179,19 @@ "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/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/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.sign", "subtle.verify", "subtle.digest"], "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", "tests/fixtures/node-types/webcrypto-hmac.ts"] }, + { "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/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"] }, + { "chapter": "globals", "signatures": ["`navigator`"], "status": "partial", "evidence": ["tests/fixtures/node-types/navigator.ts", "tests/harness/project-config.test.ts", "tests/corpus/6331-jose-root-entry/main.ts"] }, + { "chapter": "globals", "signatures": ["`userAgent` Type: {string}"], "status": "supported", "evidence": ["tests/fixtures/node-types/navigator.ts", "tests/harness/project-config.test.ts"] }, + { "chapter": "globals", "signatures": ["Class: `CryptoKey`", "Class: `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": "errors", "parent": "Class: `SystemError`", "symbols": ["code", "errno", "syscall", "path", "dest"], "status": "partial", "evidence": ["tests/corpus/caught-system-error-fields.ts", "tests/fixtures/node-types/terminal-error-fields.ts", "tests/harness/project-config.test.ts"] }, {"chapter": "events", "symbols": ["EventTarget", "eventTarget.addEventListener", "eventTarget.removeEventListener", "eventTarget.dispatchEvent"], "status": "partial", "evidence": ["tests/corpus/5130-event-target-dispatch.js", "tests/corpus/5131-event-target-listener-errors.js", "tests/corpus/5132-event-target-typed.ts", "tests/corpus/5133-abort-signal-event-target.ts", "tests/corpus/5135-event-target-warnings.js", "tests/corpus/5240-event-target-subclass.ts", "tests/corpus/5242-event-target-subclass-hierarchy.ts", "tests/corpus/5243-event-target-subclass-dynamic.ts", "tests/corpus/5244-event-target-subclass.js", "tests/corpus/5245-event-target-subclass-signals.ts", "tests/corpus/5248-event-target-subclass-warnings.ts"]}, {"chapter": "events", "symbols": ["Event"], "status": "partial", "evidence": ["tests/corpus/5130-event-target-dispatch.js", "tests/corpus/5132-event-target-typed.ts", "tests/corpus/5241-event-subclass.ts", "tests/corpus/5242-event-target-subclass-hierarchy.ts", "tests/corpus/5244-event-target-subclass.js", "tests/corpus/5247-event-target-subclass-lifetimes.ts"]}, @@ -391,6 +401,7 @@ { "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.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"] }, { "symbols": ["crypto.hkdf", "crypto.hkdfSync", "crypto.scrypt", "crypto.scryptSync"], "status": "partial", "evidence": ["tests/corpus/node24-crypto-kdf.ts", "tests/corpus/node24-crypto-kdf-errors.ts", "tests/corpus/node24-crypto-kdf-options.ts", "tests/corpus/5438-crypto-kdf-digests.ts"] }, diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts index 0a0837389..a8f1f8a8b 100644 --- a/packages/compiler/ambient/scriptc-node-fallback.d.ts +++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts @@ -511,14 +511,119 @@ declare var DOMException: { readonly prototype: DOMException; }; -/* Capability-gated globals harness code probes behind feature guards - * (globalThis.crypto under a hasCrypto check, the SQLite-backed storage - * pair): typed `unknown` — no lowering exists, the guard is expected to - * be false in a compiled binary, and any reached use fences per site. */ -declare var crypto: unknown; +/* WebCrypto's HMAC slice (scr_webcrypto.c): crypto.subtle's importKey for + * raw HMAC keys, HMAC sign/verify and the SHA-1/SHA-2 digests, typed as + * exactly that surface (the declarations mirror @types/node's webcrypto + * shapes). 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. */ declare var Crypto: unknown; -declare var CryptoKey: unknown; +declare var CryptoKey: { + prototype: CryptoKey; + new (): CryptoKey; +}; declare var SubtleCrypto: unknown; +type KeyUsage = + | "encrypt" + | "decrypt" + | "sign" + | "verify" + | "deriveKey" + | "deriveBits" + | "encapsulateBits" + | "decapsulateBits" + | "encapsulateKey" + | "decapsulateKey" + | "wrapKey" + | "unwrapKey"; +type KeyType = "private" | "public" | "secret"; +interface Algorithm { + name: string; +} +type AlgorithmIdentifier = Algorithm | string; +type HashAlgorithmIdentifier = AlgorithmIdentifier; +interface HmacImportParams extends Algorithm { + hash: HashAlgorithmIdentifier; + length?: number; +} +interface KeyAlgorithm { + name: string; +} +interface HmacKeyAlgorithm extends KeyAlgorithm { + hash: KeyAlgorithm; + length: number; +} +interface CryptoKey { + readonly algorithm: KeyAlgorithm; + readonly extractable: boolean; + readonly type: KeyType; + readonly usages: KeyUsage[]; +} +type KeyFormat = + | "jwk" + | "pkcs8" + | "raw" + | "raw-private" + | "raw-public" + | "raw-secret" + | "raw-seed" + | "spki"; +interface JsonWebKey { + alg?: string; + crv?: string; + d?: string; + dp?: string; + dq?: string; + e?: string; + ext?: boolean; + k?: string; + key_ops?: string[]; + kty?: string; + n?: string; + p?: string; + q?: string; + qi?: string; + use?: string; + x?: string; + y?: string; +} +/* The 'jwk' form type-checks like @types/node's; the compiler refuses it + * (the JsonWebKey import is not implemented). */ +interface SubtleCrypto { + digest(algorithm: AlgorithmIdentifier, data: BufferSource): Promise; + importKey( + format: "jwk", + keyData: JsonWebKey, + algorithm: AlgorithmIdentifier | HmacImportParams, + extractable: boolean, + keyUsages: readonly KeyUsage[], + ): Promise; + importKey( + format: Exclude, + keyData: BufferSource, + algorithm: AlgorithmIdentifier | HmacImportParams, + extractable: boolean, + keyUsages: readonly KeyUsage[], + ): Promise; + sign(algorithm: AlgorithmIdentifier, key: CryptoKey, data: BufferSource): Promise; + verify( + algorithm: AlgorithmIdentifier, + key: CryptoKey, + signature: BufferSource, + data: BufferSource, + ): Promise; +} +interface Crypto { + readonly subtle: SubtleCrypto; +} + +/* Capability-gated globals harness code probes behind feature guards + * (the SQLite-backed storage pair): typed `unknown` — no lowering exists, + * the guard is expected to be false in a compiled binary, and any reached + * use fences per site. */ declare var localStorage: unknown; declare var sessionStorage: unknown; @@ -1493,6 +1598,51 @@ declare module "node:fs" { readonly COPYFILE_EXCL: number; readonly COPYFILE_FICLONE: number; readonly COPYFILE_FICLONE_FORCE: number; + readonly UV_FS_SYMLINK_DIR: number; + readonly UV_FS_SYMLINK_JUNCTION: number; + readonly UV_DIRENT_UNKNOWN: number; + readonly UV_DIRENT_FILE: number; + readonly UV_DIRENT_DIR: number; + readonly UV_DIRENT_LINK: number; + readonly UV_DIRENT_FIFO: number; + readonly UV_DIRENT_SOCKET: number; + readonly UV_DIRENT_CHAR: number; + readonly UV_DIRENT_BLOCK: number; + readonly UV_FS_O_FILEMAP: number; + readonly UV_FS_O_TEMPORARY: number; + readonly UV_FS_O_SHORT_LIVED: number; + readonly UV_FS_O_SEQUENTIAL: number; + readonly UV_FS_O_RANDOM: number; + readonly UV_FS_COPYFILE_EXCL: number; + readonly UV_FS_COPYFILE_FICLONE: number; + readonly UV_FS_COPYFILE_FICLONE_FORCE: number; + readonly S_IFMT: number; + readonly S_IFREG: number; + readonly S_IFDIR: number; + readonly S_IFCHR: number; + readonly S_IFBLK: number; + readonly S_IFIFO: number; + readonly S_IFLNK: number; + readonly S_IFSOCK: number; + readonly O_NOCTTY: number; + readonly O_DIRECTORY: number; + readonly O_NOATIME: number; + readonly O_SYNC: number; + readonly O_DSYNC: number; + readonly O_SYMLINK: number; + readonly O_DIRECT: number; + readonly S_IRWXU: number; + readonly S_IRUSR: number; + readonly S_IWUSR: number; + readonly S_IXUSR: number; + readonly S_IRWXG: number; + readonly S_IRGRP: number; + readonly S_IWGRP: number; + readonly S_IXGRP: number; + readonly S_IRWXO: number; + readonly S_IROTH: number; + readonly S_IWOTH: number; + readonly S_IXOTH: number; }; /* A stat(2) snapshot (statSync follows symlinks, lstatSync does not — * Node's split) — immutable; the supported surface is exactly these @@ -2557,6 +2707,23 @@ declare module "crypto" { a: ArrayBufferView | ArrayBufferLike, b: ArrayBufferView | ArrayBufferLike, ): boolean; + /* WebCrypto's getRandomValues (also the webcrypto object's): integer + * typed arrays, filled in place and returned. The crypto GLOBAL stays + * unknown in fallback builds. */ + type RandomValuesArray = + | Int8Array + | Uint8Array + | Uint8ClampedArray + | Int16Array + | Uint16Array + | Int32Array + | Uint32Array + | BigInt64Array + | BigUint64Array; + export function getRandomValues(typedArray: T): T; + export const webcrypto: { + getRandomValues(typedArray: T): T; + }; export function randomFillSync( buffer: T, offset?: number, diff --git a/packages/compiler/package.json b/packages/compiler/package.json index 6e5442dc6..e4f276b17 100644 --- a/packages/compiler/package.json +++ b/packages/compiler/package.json @@ -29,6 +29,7 @@ "files": [ "dist", "ambient", + "shims", "surface-manifest.json" ], "publishConfig": { diff --git a/packages/compiler/shims/undici/core/agent.ts b/packages/compiler/shims/undici/core/agent.ts new file mode 100644 index 000000000..c59f3cd1d --- /dev/null +++ b/packages/compiler/shims/undici/core/agent.ts @@ -0,0 +1,163 @@ +// undici's Agent (lib/dispatcher/agent.js): one dispatcher per origin, +// created by `factory` (default: a Pool, or a Client when connections is 1). +import { Client } from "./client.ts"; +import { Dispatcher, DispatcherBase } from "./dispatcher.ts"; +import { InvalidArgumentError } from "./errors.ts"; +import { Pool } from "./pool.ts"; +import type { + AgentOptions, + DispatchHandlers, + DispatchOptions, + PoolOptions, + Profile, +} from "./types.ts"; + +interface AgentEntry { + dispatcher: Dispatcher; + count: number; +} + +export class Agent extends DispatcherBase { + __options: PoolOptions; + __factory: ((origin: string | URL, opts: object) => Dispatcher) | null; + __clients: Map; + __onDrain: (origin: URL, targets: Dispatcher[]) => void; + __onConnect: (origin: URL, targets: Dispatcher[]) => void; + __onDisconnect: (origin: URL, targets: Dispatcher[], err: Error) => void; + __onConnectionError: (origin: URL, targets: Dispatcher[], err: Error) => void; + + constructor(options: AgentOptions | undefined, profile: Profile) { + super(profile); + const opts: AgentOptions = options ?? {}; + const interceptors = opts.interceptors; + if (profile.major < 7 && interceptors !== undefined && interceptors.Agent !== undefined) { + this.__interceptors = interceptors.Agent; + } + this.__options = { + keepAliveTimeout: opts.keepAliveTimeout, + keepAliveMaxTimeout: opts.keepAliveMaxTimeout, + keepAliveTimeoutThreshold: opts.keepAliveTimeoutThreshold, + pipelining: opts.pipelining, + maxHeaderSize: opts.maxHeaderSize, + bodyTimeout: opts.bodyTimeout, + headersTimeout: opts.headersTimeout, + strictContentLength: opts.strictContentLength, + maxRequestsPerClient: opts.maxRequestsPerClient, + maxResponseSize: opts.maxResponseSize, + connectTimeout: opts.connectTimeout, + localAddress: opts.localAddress, + connections: opts.connections, + connect: opts.connect, + interceptors: + interceptors !== undefined + ? { Pool: interceptors.Pool, Client: interceptors.Client } + : undefined, + }; + const factory = opts.factory; + this.__factory = factory !== undefined ? factory : null; + this.__clients = new Map(); + this.__onDrain = (origin: URL, targets: Dispatcher[]): void => { + this.emit("drain", origin, [this as Dispatcher, ...targets]); + }; + this.__onConnect = (origin: URL, targets: Dispatcher[]): void => { + this.emit("connect", origin, [this as Dispatcher, ...targets]); + }; + this.__onDisconnect = (origin: URL, targets: Dispatcher[], err: Error): void => { + this.emit("disconnect", origin, [this as Dispatcher, ...targets], err); + }; + this.__onConnectionError = (origin: URL, targets: Dispatcher[], err: Error): void => { + this.emit("connectionError", origin, [this as Dispatcher, ...targets], err); + }; + } + + __runningCount(): number { + let ret = 0; + for (const entry of this.__clients.values()) ret += entry.dispatcher.__runningCount(); + return ret; + } + + __dispatch(opts: DispatchOptions, handler: DispatchHandlers): boolean { + const origin = opts.origin; + if (origin === undefined || origin === "") { + throw new InvalidArgumentError("opts.origin must be a non-empty string or URL."); + } + const key = typeof origin === "string" ? origin : origin.href; + const existing = this.__clients.get(key); + let dispatcher: Dispatcher; + if (existing !== undefined) { + dispatcher = existing.dispatcher; + } else { + const factory = this.__factory; + dispatcher = + factory !== null + ? factory(origin, this.__options) + : this.__defaultFactory(origin, this.__options); + const entry: AgentEntry = { dispatcher, count: 0 }; + if (this.__profile.major >= 7) { + // 7.x closes and forgets an origin's dispatcher once all of its + // connections are gone. + const closeIfUnused = (connected: boolean): void => { + const current = this.__clients.get(key); + if (current !== undefined) { + if (connected) current.count -= 1; + if (current.count <= 0) { + this.__clients.delete(key); + if (!current.dispatcher.__isDestroyed()) current.dispatcher.close(); + } + } + }; + dispatcher.on("drain", this.__onDrain); + dispatcher.on("connect", (o: URL, targets: Dispatcher[]) => { + const current = this.__clients.get(key); + if (current !== undefined) current.count += 1; + this.__onConnect(o, targets); + }); + dispatcher.on("disconnect", (o: URL, targets: Dispatcher[], err: Error) => { + closeIfUnused(true); + this.__onDisconnect(o, targets, err); + }); + dispatcher.on("connectionError", (o: URL, targets: Dispatcher[], err: Error) => { + closeIfUnused(false); + this.__onConnectionError(o, targets, err); + }); + } else { + dispatcher.on("drain", this.__onDrain); + dispatcher.on("connect", this.__onConnect); + dispatcher.on("disconnect", this.__onDisconnect); + dispatcher.on("connectionError", this.__onConnectionError); + } + this.__clients.set(key, entry); + } + return dispatcher.dispatch(opts, handler); + } + + /** The default factory: a Client when `connections` is 1, else a Pool, of + * the same undici version. */ + __defaultFactory(origin: string | URL, opts: PoolOptions): Dispatcher { + return opts.connections === 1 + ? this.__createClient(origin, opts) + : this.__createPool(origin, opts); + } + + __createClient(origin: string | URL, opts: PoolOptions): Dispatcher { + return new Client(origin, opts, this.__profile); + } + + __createPool(origin: string | URL, opts: PoolOptions): Dispatcher { + return new Pool(origin, opts, this.__profile); + } + + __close(): Promise { + const closing: Promise[] = []; + for (const entry of this.__clients.values()) closing.push(entry.dispatcher.close()); + if (this.__profile.major >= 7) this.__clients.clear(); + return Promise.all(closing).then(() => {}); + } + + __destroy(err: Error): Promise { + const destroying: Promise[] = []; + for (const entry of this.__clients.values()) destroying.push(entry.dispatcher.destroy(err)); + if (this.__profile.major >= 7) this.__clients.clear(); + return Promise.all(destroying).then(() => {}); + } +} diff --git a/packages/compiler/shims/undici/core/api.ts b/packages/compiler/shims/undici/core/api.ts new file mode 100644 index 000000000..b94d354ce --- /dev/null +++ b/packages/compiler/shims/undici/core/api.ts @@ -0,0 +1,93 @@ +// undici's top-level request() (index.js makeDispatcher over +// lib/api/api-request.js) and the global dispatcher +// (lib/global.js: one process-wide slot shared by every undici copy). +import { requestThrough } from "./dispatcher.ts"; +import type { Dispatcher, RequestOptions, ResponseData } from "./dispatcher.ts"; +import { parseOrigin, parseURL } from "./util.ts"; +import type { IncomingHttpHeaders, Profile, RequestBody, UndiciHeaders } from "./types.ts"; + +/** request(url, options)'s options: Dispatcher.RequestOptions without + * origin/path, an optional method, and the dispatcher to use. */ +export interface TopLevelRequestOptions { + dispatcher?: Dispatcher; + method?: string; + path?: string; + body?: RequestBody; + headers?: UndiciHeaders; + idempotent?: boolean; + blocking?: boolean; + headersTimeout?: number | null; + bodyTimeout?: number | null; + reset?: boolean; + throwOnError?: boolean; + opaque?: unknown; + signal?: AbortSignal | null; + onInfo?: (info: { statusCode: number; headers: IncomingHttpHeaders }) => void; + highWaterMark?: number; +} + +let globalDispatcher: Dispatcher | null = null; +let globalDispatcherSet = false; + +export function setGlobalDispatcher(dispatcher: Dispatcher): void { + globalDispatcher = dispatcher; + globalDispatcherSet = true; +} + +/** The global dispatcher, created on first use by the asking version's + * Agent (undici stores whichever copy asked first). */ +export function getGlobalDispatcher(create: () => Dispatcher): Dispatcher { + let dispatcher = globalDispatcher; + if (dispatcher === null) { + dispatcher = create(); + globalDispatcher = dispatcher; + } + return dispatcher; +} + +/** True once a program installed its own global dispatcher. */ +export function hasCustomGlobalDispatcher(): boolean { + return globalDispatcherSet; +} + +export function request( + url: string | URL, + options: TopLevelRequestOptions | undefined, + createGlobal: () => Dispatcher, + profile: Profile, +): Promise { + // makeDispatcher validates synchronously: an invalid URL throws at the + // call rather than rejecting. + { + const opts: TopLevelRequestOptions = options ?? {}; + let target: URL; + const path = opts.path; + if (path !== undefined) { + const prefixed = path.startsWith("/") ? path : `/${path}`; + target = new URL(parseOrigin(url, profile).origin + prefixed); + } else { + target = parseURL(url, profile); + } + const dispatcher = opts.dispatcher ?? getGlobalDispatcher(createGlobal); + const method = + opts.method !== undefined && opts.method !== "" ? opts.method : opts.body ? "PUT" : "GET"; + const dispatchOptions: RequestOptions = { + origin: target.origin, + path: target.search ? `${target.pathname}${target.search}` : target.pathname, + method, + body: opts.body, + headers: opts.headers, + idempotent: opts.idempotent, + blocking: opts.blocking, + headersTimeout: opts.headersTimeout, + bodyTimeout: opts.bodyTimeout, + reset: opts.reset, + throwOnError: opts.throwOnError, + opaque: opts.opaque, + signal: opts.signal, + onInfo: opts.onInfo, + highWaterMark: opts.highWaterMark, + }; + return requestThrough(dispatcher, dispatchOptions, profile); + } +} diff --git a/packages/compiler/shims/undici/core/client.ts b/packages/compiler/shims/undici/core/client.ts new file mode 100644 index 000000000..24ebe54d0 --- /dev/null +++ b/packages/compiler/shims/undici/core/client.ts @@ -0,0 +1,1075 @@ +// undici's Client (lib/dispatcher/client.js + client-h1.js): HTTP/1.1 over +// one TCP/TLS connection at a time, with keep-alive reuse, a request queue, +// headers/body/idle timeouts and the same request lifecycle callbacks. +import type { Socket } from "node:net"; +import { buildConnector } from "./connector.ts"; +import { channels } from "./diagnostics.ts"; +import { DispatcherBase } from "./dispatcher.ts"; +import type { Dispatcher } from "./dispatcher.ts"; +import { + BodyTimeoutError, + HeadersOverflowError, + HeadersTimeoutError, + HTTPParserError, + InformationalError, + InvalidArgumentError, + NotSupportedError, + RequestAbortedError, + RequestContentLengthMismatchError, + ResponseContentLengthMismatchError, + ResponseExceededMaxSizeError, + SocketError, + UndiciError, +} from "./errors.ts"; +import type { SocketInfo } from "./errors.ts"; +import { Request } from "./request.ts"; +import { FastTimer } from "./timers.ts"; +import { parseKeepAliveTimeout, parseOrigin } from "./util.ts"; +import type { + ClientOptions, + Connector, + ConnectorOptions, + DispatchHandlers, + DispatchOptions, + Profile, +} from "./types.ts"; + +const TIMEOUT_NONE = 0; +const TIMEOUT_HEADERS = 1; +const TIMEOUT_BODY = 2; +const TIMEOUT_IDLE = 3; + +export class Client extends DispatcherBase { + __url: URL; + __connector: Connector; + __socket: Socket | null; + __connecting: boolean; + __queue: (Request | null)[]; + __runningIdx: number; + __pendingIdx: number; + __pipelining: number; + __hostHeader: string; + __maxHeaderSize: number; + __keepAliveDefaultTimeout: number; + __keepAliveMaxTimeout: number; + __keepAliveTimeoutThreshold: number; + __keepAliveTimeoutValue: number; + __headersTimeout: number; + __bodyTimeout: number; + __strictContentLength: boolean; + __maxRequests: number; + __maxResponseSize: number; + __resuming: number; + __needDrain: number; + __closedResolve: (() => void) | null; + __serverName: string | null; + __localAddress: string | null; + // Per-connection state (one socket at a time). + __sockNoRef: boolean; + __sockReset: boolean; + __sockBlocking: boolean; + __sockCounter: number; + __parser: ResponseParser | null; + __sockError: Error | null; + // The parser timer, held as closures over its handle (a nullable timer + // field does not compile under @types/node). + __timerCancel: (() => void) | null; + __timerRefresh: (() => void) | null; + __timerType: number; + + constructor(url: string | URL, options: ClientOptions | undefined, profile: Profile) { + super(profile); + const opts: ClientOptions = options ?? {}; + const pipelining = opts.pipelining ?? 1; + if (!Number.isInteger(pipelining) || pipelining < 0) { + throw new InvalidArgumentError("invalid pipelining"); + } + if (pipelining > 1) { + throw new NotSupportedError( + "scriptc's undici client does not pipeline more than one request yet", + ); + } + const maxHeaderSize = opts.maxHeaderSize ?? null; + if (maxHeaderSize !== null && !Number.isFinite(maxHeaderSize)) { + throw new InvalidArgumentError("invalid maxHeaderSize"); + } + const connectTimeout = opts.connectTimeout ?? null; + if (connectTimeout !== null && (!Number.isFinite(connectTimeout) || connectTimeout < 0)) { + throw new InvalidArgumentError("invalid connectTimeout"); + } + const keepAliveTimeout = opts.keepAliveTimeout ?? null; + if ( + keepAliveTimeout !== null && + (!Number.isFinite(keepAliveTimeout) || keepAliveTimeout <= 0) + ) { + throw new InvalidArgumentError("invalid keepAliveTimeout"); + } + const keepAliveMaxTimeout = opts.keepAliveMaxTimeout ?? null; + if ( + keepAliveMaxTimeout !== null && + (!Number.isFinite(keepAliveMaxTimeout) || keepAliveMaxTimeout <= 0) + ) { + throw new InvalidArgumentError("invalid keepAliveMaxTimeout"); + } + const keepAliveTimeoutThreshold = opts.keepAliveTimeoutThreshold ?? null; + if (keepAliveTimeoutThreshold !== null && !Number.isFinite(keepAliveTimeoutThreshold)) { + throw new InvalidArgumentError("invalid keepAliveTimeoutThreshold"); + } + const headersTimeout = opts.headersTimeout ?? null; + if (headersTimeout !== null && (!Number.isInteger(headersTimeout) || headersTimeout < 0)) { + throw new InvalidArgumentError("headersTimeout must be a positive integer or zero"); + } + const bodyTimeout = opts.bodyTimeout ?? null; + if (bodyTimeout !== null && (!Number.isInteger(bodyTimeout) || bodyTimeout < 0)) { + throw new InvalidArgumentError("bodyTimeout must be a positive integer or zero"); + } + const maxRequests = opts.maxRequestsPerClient ?? 0; + if (!Number.isInteger(maxRequests) || maxRequests < 0) { + throw new InvalidArgumentError("maxRequestsPerClient must be a positive number"); + } + const maxResponseSize = opts.maxResponseSize ?? -1; + if (!Number.isInteger(maxResponseSize) || maxResponseSize < -1) { + throw new InvalidArgumentError("maxResponseSize must be a positive number"); + } + + const connect = opts.connect ?? null; + if (typeof connect === "function") { + this.__connector = connect; + } else { + this.__connector = buildConnector( + connect !== null + ? { + ...connect, + timeout: connect.timeout !== undefined ? connect.timeout : connectTimeout, + } + : { timeout: connectTimeout }, + ); + } + // 5.x composes `interceptors.Client` around dispatch; 7.x dropped the + // option and ignores it. + const interceptors = opts.interceptors; + if (profile.major < 7 && interceptors !== undefined && interceptors.Client !== undefined) { + this.__interceptors = interceptors.Client; + } + + this.__url = parseOrigin(url, profile); + this.__socket = null; + this.__connecting = false; + this.__queue = []; + this.__runningIdx = 0; + this.__pendingIdx = 0; + this.__pipelining = pipelining; + this.__hostHeader = `host: ${this.__url.hostname}${this.__url.port ? `:${this.__url.port}` : ""}\r\n`; + this.__maxHeaderSize = maxHeaderSize !== null && maxHeaderSize !== 0 ? maxHeaderSize : 16384; + this.__keepAliveDefaultTimeout = keepAliveTimeout ?? 4e3; + this.__keepAliveMaxTimeout = keepAliveMaxTimeout ?? 600e3; + this.__keepAliveTimeoutThreshold = + keepAliveTimeoutThreshold ?? (profile.major >= 7 ? 2e3 : 1e3); + this.__keepAliveTimeoutValue = this.__keepAliveDefaultTimeout; + const defaultTimeout = profile.major >= 7 ? 300e3 : 30e3; + this.__headersTimeout = headersTimeout ?? defaultTimeout; + this.__bodyTimeout = bodyTimeout ?? defaultTimeout; + this.__strictContentLength = opts.strictContentLength ?? true; + this.__maxRequests = maxRequests; + this.__maxResponseSize = maxResponseSize; + this.__resuming = 0; + this.__needDrain = 0; + this.__closedResolve = null; + this.__serverName = null; + this.__localAddress = opts.localAddress ?? null; + this.__sockNoRef = false; + this.__sockReset = false; + this.__sockBlocking = false; + this.__sockCounter = 0; + this.__parser = null; + this.__sockError = null; + this.__timerCancel = null; + this.__timerRefresh = null; + this.__timerType = TIMEOUT_NONE; + } + + get pipelining(): number { + return this.__pipelining; + } + + __pendingCount(): number { + return this.__queue.length - this.__pendingIdx; + } + + __runningCount(): number { + return this.__pendingIdx - this.__runningIdx; + } + + __sizeCount(): number { + return this.__queue.length - this.__runningIdx; + } + + __connectedFlag(): boolean { + const socket = this.__socket; + return socket !== null && !this.__connecting && !socket.destroyed; + } + + __busy(): boolean { + return ( + (this.__socket !== null && (this.__sockReset || this.__sockBlocking)) || + this.__sizeCount() >= (this.__pipelining || 1) || + this.__pendingCount() > 0 + ); + } + + __dispatch(opts: DispatchOptions, handler: DispatchHandlers): boolean { + const originValue = opts.origin; + const origin = + originValue === undefined || originValue === "" + ? this.__url.origin + : typeof originValue === "string" + ? originValue + : originValue.origin; + const request = new Request(origin, opts, handler, this.__profile); + this.__queue.push(request); + if (this.__resuming === 0) this.__resume(true); + if (this.__resuming !== 0 && this.__needDrain !== 2 && this.__busy()) this.__needDrain = 2; + return this.__needDrain < 2; + } + + __close(): Promise { + return new Promise((resolve) => { + if (this.__sizeCount() === 0) { + this.destroy().then(() => resolve()); + } else { + this.__closedResolve = resolve; + } + }); + } + + __destroy(err: Error): Promise { + return new Promise((resolve) => { + const requests = this.__queue.splice(this.__pendingIdx); + for (const request of requests) if (request !== null) this.__errorRequest(request, err); + const done = (): void => { + const closed = this.__closedResolve; + if (closed !== null) { + this.__closedResolve = null; + closed(); + } + resolve(); + }; + const socket = this.__socket; + if (socket === null) { + queueMicrotask(done); + } else { + socket.on("close", () => done()); + this.__destroySocket(socket, err); + } + this.__resume(false); + }); + } + + /** util.destroy(socket, err): remember the first error for 'close'. */ + __destroySocket(socket: Socket, err: Error): void { + if (socket === this.__socket && this.__sockError === null) this.__sockError = err; + socket.destroy(); + } + + __errorRequest(request: Request, err: Error): void { + try { + request.onError(err); + } catch (e) { + this.emit("error", e as Error); + } + } + + __socketInfo(): SocketInfo { + const socket = this.__socket; + if (socket === null) return {}; + return { + localAddress: socket.localAddress, + localPort: socket.localPort, + remoteAddress: socket.remoteAddress, + remotePort: socket.remotePort, + remoteFamily: socket.remoteFamily, + timeout: undefined, + bytesWritten: socket.bytesWritten, + bytesRead: socket.bytesRead, + }; + } + + /** The parser timer: 5.x uses native timers throughout; 7.x drives the + * headers and body timeouts from its fast timers. */ + __setTimeout(delay: number, type: number): void { + this.__clearTimer(); + this.__timerType = type; + if (delay > 0) { + if (this.__profile.major >= 7 && (type === TIMEOUT_HEADERS || type === TIMEOUT_BODY)) { + const fast = new FastTimer(() => this.__onTimeout(), delay); + this.__timerCancel = () => fast.clear(); + this.__timerRefresh = () => fast.refresh(); + } else { + const timer = setTimeout(() => this.__onTimeout(), delay); + timer.unref(); + this.__timerCancel = () => clearTimeout(timer); + this.__timerRefresh = () => { + timer.refresh(); + }; + } + } + } + + __clearTimer(): void { + const cancel = this.__timerCancel; + this.__timerCancel = null; + this.__timerRefresh = null; + if (cancel !== null) cancel(); + } + + __refreshTimer(): void { + const refresh = this.__timerRefresh; + if (refresh !== null) refresh(); + } + + __onTimeout(): void { + this.__timerCancel = null; + this.__timerRefresh = null; + const socket = this.__socket; + const parser = this.__parser; + if (socket === null || parser === null) return; + if (this.__timerType === TIMEOUT_HEADERS) { + this.__destroySocket(socket, new HeadersTimeoutError()); + } else if (this.__timerType === TIMEOUT_BODY) { + if (!parser.paused) this.__destroySocket(socket, new BodyTimeoutError()); + } else if (this.__timerType === TIMEOUT_IDLE) { + this.__destroySocket(socket, new InformationalError("socket idle timeout")); + } + } + + __connect(): void { + const url = this.__url; + let hostname = url.hostname; + if (hostname.startsWith("[")) { + const idx = hostname.indexOf("]"); + hostname = hostname.slice(1, idx); + } + this.__connecting = true; + const params: ConnectorOptions = { + host: url.host, + hostname, + protocol: url.protocol, + port: url.port, + servername: this.__serverName ?? undefined, + localAddress: this.__localAddress, + }; + const connectParams = { + host: url.host, + hostname, + protocol: url.protocol, + port: url.port, + servername: this.__serverName, + localAddress: this.__localAddress, + }; + if (channels.beforeConnect.hasSubscribers) { + channels.beforeConnect.publish({ connectParams }); + } + let settled = false; + const onConnected = (err: Error | null, socket: Socket | null): void => { + if (settled) return; + settled = true; + // undici awaits the connector's promise: continue on a later tick. + queueMicrotask(() => { + if (err !== null || socket === null) + this.__onConnectError(err ?? new SocketError("connect failed"), connectParams); + else this.__onConnected(socket, connectParams); + }); + }; + try { + this.__connector(params, onConnected); + } catch (err) { + onConnected(err as Error, null); + } + } + + __onConnected( + socket: Socket, + connectParams: { + host: string; + hostname: string; + protocol: string; + port: string; + servername: string | null; + localAddress: string | null; + }, + ): void { + this.__connecting = false; + if (this.destroyed) { + socket.destroy(); + this.__resume(false); + return; + } + this.__socket = socket; + this.__sockNoRef = false; + this.__sockReset = false; + this.__sockBlocking = false; + this.__sockCounter = 0; + this.__sockError = null; + const parser = new ResponseParser(this, socket); + this.__parser = parser; + socket.on("data", (chunk: Buffer) => { + if (this.__socket === socket) parser.execute(chunk); + }); + socket.on("error", (err: Error) => { + if (this.__socket === socket) this.__onSocketError(socket, err); + }); + socket.on("end", () => { + if (this.__socket === socket) this.__onSocketEnd(socket); + }); + socket.on("close", () => { + if (this.__socket === socket) this.__onSocketClose(socket); + }); + if (channels.connected.hasSubscribers) { + channels.connected.publish({ connectParams }); + } + this.emit("connect", this.__url, [this as Dispatcher]); + this.__resume(false); + } + + __onConnectError( + err: Error, + connectParams: { + host: string; + hostname: string; + protocol: string; + port: string; + servername: string | null; + localAddress: string | null; + }, + ): void { + this.__connecting = false; + if (channels.connectError.hasSubscribers) { + channels.connectError.publish({ connectParams, error: err }); + } + this.__onError(err); + this.emit("connectionError", this.__url, [this as Dispatcher], err); + this.__resume(false); + } + + /** client.js onError: with nothing running, a connection-level failure + * fails every queued request. */ + __onError(err: Error): void { + const code = errorCode(err); + if (this.__runningCount() === 0 && code !== "UND_ERR_INFO" && code !== "UND_ERR_SOCKET") { + const requests = this.__queue.splice(this.__runningIdx); + for (const request of requests) if (request !== null) this.__errorRequest(request, err); + } + } + + __onSocketError(socket: Socket, err: Error): void { + const parser = this.__parser; + if ( + errorCode(err) === "ECONNRESET" && + parser !== null && + parser.statusCode !== 0 && + !parser.shouldKeepAlive + ) { + parser.finishOnEof(); + return; + } + this.__destroySocket(socket, err); + this.__onError(err); + } + + __onSocketEnd(socket: Socket): void { + const parser = this.__parser; + if (parser !== null && parser.statusCode !== 0 && !parser.shouldKeepAlive) { + parser.finishOnEof(); + return; + } + this.__destroySocket(socket, new SocketError("other side closed", this.__socketInfo())); + } + + __onSocketClose(_socket: Socket): void { + const parser = this.__parser; + const recorded = this.__sockError; + this.__sockError = null; + if ( + recorded === null && + parser !== null && + parser.statusCode !== 0 && + !parser.shouldKeepAlive + ) { + parser.finishOnEof(); + } + const err = recorded ?? new SocketError("closed", this.__socketInfo()); + this.__parser = null; + this.__socket = null; + this.__clearTimer(); + this.__timerType = TIMEOUT_NONE; + if (this.destroyed) { + const requests = this.__queue.splice(this.__runningIdx); + for (const request of requests) if (request !== null) this.__errorRequest(request, err); + } else if (this.__runningCount() > 0 && errorCode(err) !== "UND_ERR_INFO") { + const request = this.__queue[this.__runningIdx]; + this.__queue[this.__runningIdx++] = null; + if (request !== null && request !== undefined) this.__errorRequest(request, err); + } + this.__pendingIdx = this.__runningIdx; + this.emit("disconnect", this.__url, [this as Dispatcher], err); + this.__resume(false); + } + + __emitDrain(): void { + this.__needDrain = 0; + this.emit("drain", this.__url, [this as Dispatcher]); + } + + __resume(sync: boolean): void { + if (this.__resuming === 2) return; + this.__resuming = 2; + this.__resumeLoop(sync); + this.__resuming = 0; + if (this.__runningIdx > 256) { + this.__queue.splice(0, this.__runningIdx); + this.__pendingIdx -= this.__runningIdx; + this.__runningIdx = 0; + } + } + + __resumeLoop(sync: boolean): void { + for (;;) { + if (this.destroyed) return; + if (this.closed && this.__sizeCount() === 0) { + this.destroy(); + return; + } + const socket = this.__socket; + const parser = this.__parser; + if (socket !== null && parser !== null) { + if (this.__sizeCount() === 0) { + if (!this.__sockNoRef) { + socket.unref(); + this.__sockNoRef = true; + } + } else if (this.__sockNoRef) { + socket.ref(); + this.__sockNoRef = false; + } + if (this.__sizeCount() === 0) { + if (this.__timerType !== TIMEOUT_IDLE) + this.__setTimeout(this.__keepAliveTimeoutValue, TIMEOUT_IDLE); + } else if (this.__runningCount() > 0 && parser.statusCode < 200) { + if (this.__timerType !== TIMEOUT_HEADERS) { + const request = this.__queue[this.__runningIdx]; + const headersTimeout = + request !== null && request !== undefined && request.headersTimeout !== null + ? request.headersTimeout + : this.__headersTimeout; + this.__setTimeout(headersTimeout, TIMEOUT_HEADERS); + } + } + } + + if (this.__busy()) { + this.__needDrain = 2; + } else if (this.__needDrain === 2) { + if (sync) { + this.__needDrain = 1; + process.nextTick(() => this.__emitDrain()); + } else { + this.__emitDrain(); + } + continue; + } + + if (this.__pendingCount() === 0) return; + if (this.__runningCount() >= (this.__pipelining || 1)) return; + + const request = this.__queue[this.__pendingIdx]!; + if (this.__url.protocol === "https:" && this.__serverName !== request.servername) { + if (this.__runningCount() > 0) return; + this.__serverName = request.servername; + if (socket !== null) { + this.__destroySocket(socket, new InformationalError("servername changed")); + return; + } + } + if (this.__connecting) return; + if (socket === null) { + this.__connect(); + continue; + } + if (socket.destroyed || this.__sockReset || this.__sockBlocking) return; + if (this.__runningCount() > 0 && !request.idempotent) return; + + if (!request.aborted && this.__write(request, socket)) { + this.__pendingIdx++; + } else { + this.__queue.splice(this.__pendingIdx, 1); + } + } + } + + __write(request: Request, socket: Socket): boolean { + const method = request.method; + const body = request.body; + const expectsPayload = method === "PUT" || method === "POST" || method === "PATCH"; + // util.bodyLength: a missing body has length 0. + let contentLength: number | null = body !== null ? body.length : 0; + if (contentLength === 0 && !expectsPayload) contentLength = null; + const declared = request.contentLength; + const sendsLength = + method !== "GET" && method !== "HEAD" && method !== "OPTIONS" && method !== "TRACE"; + const mismatch = + declared !== null && + declared !== contentLength && + (this.__profile.major < 7 || (sendsLength && contentLength !== null && contentLength > 0)); + if (mismatch) { + if (this.__strictContentLength) { + this.__errorRequest(request, new RequestContentLengthMismatchError()); + return false; + } + process.emitWarning(new RequestContentLengthMismatchError()); + } + + const abort = (err?: Error): void => { + if (request.aborted || request.completed) return; + this.__errorRequest(request, err ?? new RequestAbortedError()); + this.__destroySocket(socket, new InformationalError("aborted")); + }; + try { + request.onConnect(abort); + } catch (err) { + this.__errorRequest(request, err as Error); + } + if (request.aborted) return false; + + if (method === "HEAD") this.__sockReset = true; + if (request.reset !== null) this.__sockReset = request.reset; + if (this.__maxRequests > 0 && this.__sockCounter++ >= this.__maxRequests) + this.__sockReset = true; + if (request.blocking) this.__sockBlocking = true; + + let header = `${method} ${request.path} HTTP/1.1\r\n`; + header += request.host !== null ? `host: ${request.host}\r\n` : this.__hostHeader; + if (this.__pipelining !== 0 && !(this.__profile.major >= 7 && this.__sockReset)) { + header += "connection: keep-alive\r\n"; + } else { + header += "connection: close\r\n"; + } + header += request.headers; + if (channels.sendHeaders.hasSubscribers) { + channels.sendHeaders.publish({ request: request.diagnosticView(), headers: header }); + } + if (body === null) { + if (contentLength === 0) socket.write(`${header}content-length: 0\r\n\r\n`); + else socket.write(`${header}\r\n`); + } else { + socket.write(`${header}content-length: ${contentLength}\r\n\r\n`); + socket.write(body); + request.onBodySent(body); + if (!expectsPayload && (this.__profile.major < 7 || request.reset !== false)) + this.__sockReset = true; + } + request.onRequestSent(); + return true; + } + + // ResponseParser callbacks. + + __onHeadersComplete(parser: ResponseParser, socket: Socket): number { + const request = this.__queue[this.__runningIdx]; + if (request === null || request === undefined || socket.destroyed) return -1; + const statusCode = parser.statusCode; + if (statusCode === 100) { + this.__destroySocket(socket, new SocketError("bad response", this.__socketInfo())); + return -1; + } + if (statusCode >= 200) { + const bodyTimeout = request.bodyTimeout !== null ? request.bodyTimeout : this.__bodyTimeout; + this.__setTimeout(bodyTimeout, TIMEOUT_BODY); + } + if (parser.shouldKeepAlive && this.__pipelining !== 0) { + const hint = + parser.keepAliveHeader !== "" ? parseKeepAliveTimeout(parser.keepAliveHeader) : null; + if (hint !== null) { + const timeout = Math.min( + hint - this.__keepAliveTimeoutThreshold, + this.__keepAliveMaxTimeout, + ); + if (timeout <= 0) this.__sockReset = true; + else this.__keepAliveTimeoutValue = timeout; + } else { + this.__keepAliveTimeoutValue = this.__keepAliveDefaultTimeout; + } + } else { + this.__sockReset = true; + } + let pause = false; + try { + pause = + request.onHeaders(statusCode, parser.headers, () => parser.resume(), parser.statusText) === + false; + } catch (err) { + this.__destroySocket(socket, err as Error); + return -1; + } + if (request.method === "HEAD") return 1; + if (statusCode < 200) return 1; + if (this.__sockBlocking) { + this.__sockBlocking = false; + this.__resume(false); + } + return pause ? 2 : 0; + } + + __onBody(parser: ResponseParser, socket: Socket, chunk: Buffer): boolean { + const request = this.__queue[this.__runningIdx]; + if (request === null || request === undefined || socket.destroyed) return false; + if (this.__timerType === TIMEOUT_BODY) this.__refreshTimer(); + if (this.__maxResponseSize > -1 && parser.bytesRead + chunk.length > this.__maxResponseSize) { + this.__destroySocket(socket, new ResponseExceededMaxSizeError()); + return false; + } + parser.bytesRead += chunk.length; + try { + return request.onData(chunk) !== false; + } catch (err) { + this.__destroySocket(socket, err as Error); + return false; + } + } + + /** Returns false when the socket was torn down (stop parsing). */ + __onMessageComplete(parser: ResponseParser, socket: Socket, trailers: Buffer[]): boolean { + if (socket.destroyed && (parser.statusCode === 0 || parser.shouldKeepAlive)) return false; + const request = this.__queue[this.__runningIdx]; + if (request === null || request === undefined) return false; + const statusCode = parser.statusCode; + const shouldKeepAlive = parser.shouldKeepAlive; + const contentLength = parser.contentLength; + const bytesRead = parser.bytesRead; + parser.reset(); + if (statusCode < 200) return true; + if (request.method !== "HEAD" && contentLength >= 0 && bytesRead !== contentLength) { + this.__destroySocket(socket, new ResponseContentLengthMismatchError()); + return false; + } + try { + request.onComplete(trailers); + } catch (err) { + this.__errorRequest(request, err as Error); + } + this.__queue[this.__runningIdx++] = null; + if (!shouldKeepAlive) { + this.__destroySocket(socket, new InformationalError("reset")); + return false; + } + if (this.__sockReset && this.__runningCount() === 0) { + this.__destroySocket(socket, new InformationalError("reset")); + return false; + } + if (this.__pipelining === 1) setImmediate(() => this.__resume(false)); + else this.__resume(false); + return true; + } + + __onParseError(socket: Socket, err: Error): void { + this.__destroySocket(socket, err); + } +} + +function errorCode(err: Error): string { + if (err instanceof UndiciError) return err.code; + return (err as NodeJS.ErrnoException).code ?? ""; +} + +const CRLF = Buffer.from("\r\n"); + +/** An incremental HTTP/1.1 response parser (the subset of llhttp undici + * relies on): status line, raw header pairs, Content-Length, chunked and + * read-until-close bodies, trailers, and pause/resume back-pressure. */ +class ResponseParser { + client: Client; + socket: Socket; + buf: Buffer; + state: number; + paused: boolean; + statusCode: number; + statusText: string; + headers: Buffer[]; + headersSize: number; + versionMinor: number; + contentLength: number; + chunked: boolean; + connectionHeader: string; + keepAliveHeader: string; + shouldKeepAlive: boolean; + remaining: number; + bytesRead: number; + trailers: Buffer[]; + skipBody: boolean; + + constructor(client: Client, socket: Socket) { + this.client = client; + this.socket = socket; + this.buf = Buffer.alloc(0); + this.state = 0; + this.paused = false; + this.statusCode = 0; + this.statusText = ""; + this.headers = []; + this.headersSize = 0; + this.versionMinor = 1; + this.contentLength = -1; + this.chunked = false; + this.connectionHeader = ""; + this.keepAliveHeader = ""; + this.shouldKeepAlive = true; + this.remaining = 0; + this.bytesRead = 0; + this.trailers = []; + this.skipBody = false; + } + + reset(): void { + this.state = 0; + this.statusCode = 0; + this.statusText = ""; + this.headers = []; + this.headersSize = 0; + this.contentLength = -1; + this.chunked = false; + this.connectionHeader = ""; + this.keepAliveHeader = ""; + this.remaining = 0; + this.bytesRead = 0; + this.trailers = []; + this.skipBody = false; + } + + execute(data: Buffer): void { + this.buf = this.buf.length === 0 ? data : Buffer.concat([this.buf, data]); + if (!this.paused) this.run(); + } + + resume(): void { + if (!this.paused) return; + this.paused = false; + this.socket.resume(); + this.run(); + } + + pause(): void { + this.paused = true; + this.socket.pause(); + } + + /** EOF while reading a close-delimited body completes the message. */ + finishOnEof(): void { + if (this.state === 6) { + this.client.__onMessageComplete(this, this.socket, []); + } + } + + fail(message: string, code: string): void { + this.client.__onParseError(this.socket, new HTTPParserError(message, code, "")); + this.state = 99; + } + + line(): Buffer | null { + const idx = this.buf.indexOf(CRLF); + if (idx === -1) return null; + const out = this.buf.subarray(0, idx); + this.buf = this.buf.subarray(idx + 2); + return out; + } + + run(): void { + while (!this.paused && this.state !== 99) { + if (this.state === 0) { + // Status line. + const line = this.line(); + if (line === null) { + if (this.buf.length > this.client.__maxHeaderSize) { + this.client.__onParseError(this.socket, new HeadersOverflowError()); + this.state = 99; + } + return; + } + if (line.length === 0) continue; + const text = line.toString("latin1"); + const m = /^HTTP\/1\.([01]) (\d{3})(?: (.*))?$/.exec(text); + if (m === null) { + this.fail("Response does not match the HTTP/1.1 protocol", "INVALID_CONSTANT"); + return; + } + this.versionMinor = Number(m[1]); + this.statusCode = Number(m[2]); + this.statusText = m[3] ?? ""; + this.headersSize = line.length; + this.state = 1; + } else if (this.state === 1 || this.state === 5) { + // Header (1) or trailer (5) lines. + const line = this.line(); + if (line === null) { + if (this.headersSize + this.buf.length > this.client.__maxHeaderSize) { + this.client.__onParseError(this.socket, new HeadersOverflowError()); + this.state = 99; + } + return; + } + this.headersSize += line.length; + if (this.headersSize > this.client.__maxHeaderSize) { + this.client.__onParseError(this.socket, new HeadersOverflowError()); + this.state = 99; + return; + } + if (line.length > 0) { + const colon = line.indexOf(0x3a); + if (colon <= 0) { + this.fail("Invalid header token", "INVALID_HEADER_TOKEN"); + return; + } + const name = line.subarray(0, colon); + let start = colon + 1; + let end = line.length; + while (start < end && (line[start] === 0x20 || line[start] === 0x09)) start++; + while (end > start && (line[end - 1] === 0x20 || line[end - 1] === 0x09)) end--; + const value = line.subarray(start, end); + if (this.state === 5) { + this.trailers.push(Buffer.from(name), Buffer.from(value)); + continue; + } + this.headers.push(Buffer.from(name), Buffer.from(value)); + const lower = name.toString("latin1").toLowerCase(); + if (lower === "content-length") { + const n = Number(value.toString("latin1")); + if (!Number.isInteger(n) || n < 0) { + this.fail("Invalid character in Content-Length", "INVALID_CONTENT_LENGTH"); + return; + } + this.contentLength = n; + } else if (lower === "transfer-encoding") { + this.chunked = /(?:^|,)\s*chunked\s*$/i.test(value.toString("latin1")); + } else if (lower === "connection") { + this.connectionHeader = value.toString("latin1").toLowerCase(); + } else if (lower === "keep-alive") { + this.keepAliveHeader = value.toString("latin1"); + } + continue; + } + if (this.state === 5) { + if (!this.complete()) return; + continue; + } + // End of headers. + const noBody = this.statusCode < 200 || this.statusCode === 204 || this.statusCode === 304; + const closeRequested = /(?:^|,)\s*close\s*(?:,|$)/.test(this.connectionHeader); + const keepAliveRequested = /(?:^|,)\s*keep-alive\s*(?:,|$)/.test(this.connectionHeader); + this.shouldKeepAlive = this.versionMinor === 1 ? !closeRequested : keepAliveRequested; + if (!noBody && !this.chunked && this.contentLength < 0) this.shouldKeepAlive = false; + const ret = this.client.__onHeadersComplete(this, this.socket); + if (ret === -1) { + this.state = 99; + return; + } + this.skipBody = ret === 1 || noBody; + if (this.skipBody) { + if (!this.complete()) return; + continue; + } + if (this.chunked) { + this.state = 3; + } else if (this.contentLength >= 0) { + this.remaining = this.contentLength; + this.state = 2; + if (this.remaining === 0) { + if (ret === 2) this.pause(); + if (!this.complete()) return; + continue; + } + } else { + this.state = 6; + } + if (ret === 2) this.pause(); + } else if (this.state === 2) { + // Content-Length body. + if (this.buf.length === 0) return; + const n = Math.min(this.remaining, this.buf.length); + const chunk = this.buf.subarray(0, n); + this.buf = this.buf.subarray(n); + this.remaining -= n; + const more = this.client.__onBody(this, this.socket, Buffer.from(chunk)); + if (this.socket.destroyed) { + this.state = 99; + return; + } + if (!more) this.pause(); + if (this.remaining === 0) { + if (!this.complete()) return; + } + } else if (this.state === 3) { + // Chunk size line. + const line = this.line(); + if (line === null) return; + const sizeText = line.toString("latin1").split(";")[0]!.trim(); + if (!/^[0-9a-fA-F]+$/.test(sizeText)) { + this.fail("Invalid character in chunk size", "INVALID_CHUNK_SIZE"); + return; + } + const size = parseInt(sizeText, 16); + if (size === 0) { + this.state = 5; + } else { + this.remaining = size; + this.state = 4; + } + } else if (this.state === 4) { + // Chunk data, then its CRLF. + if (this.remaining > 0) { + if (this.buf.length === 0) return; + const n = Math.min(this.remaining, this.buf.length); + const chunk = this.buf.subarray(0, n); + this.buf = this.buf.subarray(n); + this.remaining -= n; + const more = this.client.__onBody(this, this.socket, Buffer.from(chunk)); + if (this.socket.destroyed) { + this.state = 99; + return; + } + if (!more) this.pause(); + continue; + } + if (this.buf.length < 2) return; + if (this.buf[0] !== 0x0d || this.buf[1] !== 0x0a) { + this.fail("Expected LF after chunk data", "STRICT"); + return; + } + this.buf = this.buf.subarray(2); + this.state = 3; + } else if (this.state === 6) { + // Read until the connection closes. + if (this.buf.length === 0) return; + const chunk = this.buf; + this.buf = Buffer.alloc(0); + const more = this.client.__onBody(this, this.socket, Buffer.from(chunk)); + if (this.socket.destroyed) { + this.state = 99; + return; + } + if (!more) this.pause(); + } else { + return; + } + } + } + + /** Message complete: hand the trailers over and get ready for the next + * response on this connection. Returns false when parsing must stop. */ + complete(): boolean { + const trailers = this.trailers; + const ok = this.client.__onMessageComplete(this, this.socket, trailers); + if (!ok) { + this.state = 99; + return false; + } + return true; + } +} diff --git a/packages/compiler/shims/undici/core/connector.ts b/packages/compiler/shims/undici/core/connector.ts new file mode 100644 index 000000000..f264912ea --- /dev/null +++ b/packages/compiler/shims/undici/core/connector.ts @@ -0,0 +1,157 @@ +// undici's buildConnector (lib/core/connect.js) over node:net and node:tls. +import * as net from "node:net"; +import * as tls from "node:tls"; +import { ConnectTimeoutError, InvalidArgumentError, NotSupportedError } from "./errors.ts"; +import type { + Connector, + ConnectorBuildOptions, + ConnectorCallback, + ConnectorOptions, + LookupAddress, +} from "./types.ts"; + +export function buildConnector(options: ConnectorBuildOptions): Connector { + const maxCachedSessions = options.maxCachedSessions; + if ( + maxCachedSessions !== undefined && + maxCachedSessions !== null && + (!Number.isInteger(maxCachedSessions) || maxCachedSessions < 0) + ) { + throw new InvalidArgumentError("maxCachedSessions must be a positive integer or zero"); + } + if (options.socketPath !== undefined && options.socketPath !== null) { + throw new NotSupportedError("scriptc's undici connector does not dial socketPath yet"); + } + const timeout = + options.timeout === undefined || options.timeout === null ? 10e3 : options.timeout; + const lookup = options.lookup; + // The plain-TCP dial, with the caller's resolver when one is configured. + let dialTcp: (port: number, host: string) => net.Socket; + if (lookup !== undefined) { + // net.connect's lowered resolver answers every address at once; adapt a + // resolver that answers a single address string. + const userLookup: ( + hostname: string, + options: unknown, + callback: ( + err: NodeJS.ErrnoException | null, + address: string | LookupAddress[], + family?: number, + ) => void, + ) => void = lookup; + const resolver = ( + hostname: string, + lookupOptions: unknown, + callback: (err: NodeJS.ErrnoException | null, addresses: LookupAddress[]) => void, + ): void => { + userLookup(hostname, lookupOptions, (err, address, family) => { + if (err !== null) callback(err, []); + else if (typeof address === "string") { + callback(null, [{ address, family: family ?? (net.isIPv6(address) ? 6 : 4) }]); + } else callback(null, address); + }); + }; + dialTcp = (port: number, host: string): net.Socket => + net.connect({ port, host, autoSelectFamily: true, lookup: resolver }); + } else { + dialTcp = (port: number, host: string): net.Socket => net.connect({ port, host }); + } + const rejectUnauthorized = options.rejectUnauthorized; + const ca = options.ca; + const defaultServername = options.servername; + + return (params: ConnectorOptions, callback: ConnectorCallback): net.Socket => { + let pending: ConnectorCallback | null = callback; + const https = params.protocol === "https:"; + const port = params.port !== "" ? Number(params.port) : https ? 443 : 80; + let socket: net.Socket; + if (https) { + if (lookup !== undefined) { + throw new NotSupportedError( + "scriptc's undici connector does not apply a custom lookup to TLS connections yet", + ); + } + const servername = + params.servername !== undefined && params.servername !== "" + ? params.servername + : defaultServername !== undefined + ? defaultServername + : serverNameOf(params.host ?? params.hostname); + socket = tls.connect({ + port, + host: params.hostname, + servername, + rejectUnauthorized: rejectUnauthorized ?? true, + ...(ca !== undefined ? { ca } : {}), + }); + } else { + socket = dialTcp(port, params.hostname); + } + + let cancelTimer: (() => void) | null = null; + if (timeout !== 0) { + const timer = setTimeout(() => { + cancelTimer = null; + setImmediate(() => { + const err = new ConnectTimeoutError(); + socket.destroy(); + if (pending !== null) { + const cb = pending; + pending = null; + cb(err, null); + } + }); + }, timeout); + cancelTimer = () => clearTimeout(timer); + } + const cancelTimeout = (): void => { + const cancel = cancelTimer; + cancelTimer = null; + if (cancel !== null) cancel(); + }; + + socket.setNoDelay(true); + const onConnected = (): void => { + cancelTimeout(); + if (pending !== null) { + const cb = pending; + pending = null; + cb(null, socket); + } + }; + if (https) socket.once("secureConnect", onConnected); + else socket.once("connect", onConnected); + socket.on("error", (err: Error) => { + cancelTimeout(); + if (pending !== null) { + const cb = pending; + pending = null; + cb(err, null); + } + }); + return socket; + }; +} + +/** undici's util.getServerName: the host without its port, or null for an + * IP literal (no SNI). */ +function serverNameOf(host: string): string | undefined { + if (host === "") return undefined; + let servername = host; + if (servername.startsWith("[")) { + const idx = servername.indexOf("]"); + servername = idx === -1 ? servername : servername.slice(1, idx); + } else { + const idx = servername.indexOf(":"); + if (idx !== -1) servername = servername.slice(0, idx); + } + return net.isIP(servername) !== 0 ? undefined : servername; +} + +/** The `buildConnector.*` type namespace of undici's types/connector.d.ts. */ +export declare namespace buildConnector { + type BuildOptions = ConnectorBuildOptions; + type Options = ConnectorOptions; + type Callback = ConnectorCallback; + type connector = Connector; +} diff --git a/packages/compiler/shims/undici/core/diagnostics-types.ts b/packages/compiler/shims/undici/core/diagnostics-types.ts new file mode 100644 index 000000000..3b84915ec --- /dev/null +++ b/packages/compiler/shims/undici/core/diagnostics-types.ts @@ -0,0 +1,68 @@ +// undici's DiagnosticsChannel message types (types/diagnostics-channel.d.ts). +import type { Socket } from "node:net"; +import type { Connector } from "./types.ts"; + +export declare namespace DiagnosticsChannel { + interface Request { + origin?: string | URL; + completed: boolean; + method?: string; + path: string; + headers: string; + addHeader(key: string, value: string): Request; + } + interface Response { + statusCode: number; + statusText: string; + headers: Array; + } + type Error = unknown; + interface ConnectParams { + host: string; + hostname: string; + protocol: string; + port: string; + servername: string | null; + } + type Connector = import("./types.ts").Connector; + interface RequestCreateMessage { + request: Request; + } + interface RequestBodySentMessage { + request: Request; + } + interface RequestHeadersMessage { + request: Request; + response: Response; + } + interface RequestTrailersMessage { + request: Request; + trailers: Array; + } + interface RequestErrorMessage { + request: Request; + error: Error; + } + interface ClientSendHeadersMessage { + request: Request; + headers: string; + socket: Socket; + } + interface ClientBeforeConnectMessage { + connectParams: ConnectParams; + connector: Connector; + } + interface ClientConnectedMessage { + socket: Socket; + connectParams: ConnectParams; + connector: Connector; + } + interface ClientConnectErrorMessage { + error: Error; + socket: Socket; + connectParams: ConnectParams; + connector: Connector; + } +} + +export type { Connector }; diff --git a/packages/compiler/shims/undici/core/diagnostics.ts b/packages/compiler/shims/undici/core/diagnostics.ts new file mode 100644 index 000000000..4efd897f9 --- /dev/null +++ b/packages/compiler/shims/undici/core/diagnostics.ts @@ -0,0 +1,16 @@ +// undici's diagnostics_channel publications (lib/core/diagnostics.js). The +// channel names and message shapes are undici's; subscribers see the same +// request objects across one request's lifetime. +import { channel } from "node:diagnostics_channel"; + +export const channels = { + create: channel("undici:request:create"), + bodySent: channel("undici:request:bodySent"), + headers: channel("undici:request:headers"), + trailers: channel("undici:request:trailers"), + error: channel("undici:request:error"), + beforeConnect: channel("undici:client:beforeConnect"), + connected: channel("undici:client:connected"), + connectError: channel("undici:client:connectError"), + sendHeaders: channel("undici:client:sendHeaders"), +}; diff --git a/packages/compiler/shims/undici/core/dispatcher.ts b/packages/compiler/shims/undici/core/dispatcher.ts new file mode 100644 index 000000000..cd195b5da --- /dev/null +++ b/packages/compiler/shims/undici/core/dispatcher.ts @@ -0,0 +1,568 @@ +// undici's Dispatcher (lib/dispatcher/dispatcher.js), DispatcherBase +// (dispatcher-base.js) and the request() API's handler (lib/api/api-request.js). +import { EventEmitter } from "node:events"; +import { BodyReadable } from "./readable.ts"; +import { + ClientClosedError, + ClientDestroyedError, + InvalidArgumentError, + RequestAbortedError, + ResponseStatusCodeError, +} from "./errors.ts"; +import { parseHeaders, rawHeaderStrings } from "./util.ts"; +import type { + DispatchController, + DispatchFn, + DispatchHandlers, + DispatchInterceptor, + DispatchOptions, + IncomingHttpHeaders, + Profile, +} from "./types.ts"; + +/** Dispatcher.RequestOptions: the dispatch options (which carry the + * request members, see DispatchOptions). */ +export type RequestOptions = DispatchOptions; + +export interface ResponseData { + statusCode: number; + /** 7.x only. */ + statusText?: string; + headers: IncomingHttpHeaders; + body: BodyReadable; + trailers: Record; + opaque: unknown; + context: object | undefined; +} + +const defaultProfile: Profile = { major: 7 }; + +export class Dispatcher extends EventEmitter { + dispatch(_options: DispatchOptions, _handler: DispatchHandlers): boolean { + throw new Error("not implemented"); + } + + close(): Promise { + return Promise.reject(new Error("not implemented")); + } + + destroy(_err?: Error | null): Promise { + return Promise.reject(new Error("not implemented")); + } + + /** Dispatcher.prototype.request: dispatches through this dispatcher and + * resolves with the response once its headers arrive. */ + request(options: RequestOptions): Promise { + return requestThrough(this, options, this.__undiciProfile()); + } + + __undiciProfile(): Profile { + return defaultProfile; + } + + // Pool statistics hooks (undici's kConnected/kPending/kRunning/kSize/ + // kBusy); dispatchers that are not clients or pools count nothing. + __connectedFlag(): boolean { + return false; + } + + __isDestroyed(): boolean { + return false; + } + + __connectedCount(): number { + return 0; + } + + __freeCount(): number { + return 0; + } + + __pendingCount(): number { + return 0; + } + + __queuedCount(): number { + return 0; + } + + __runningCount(): number { + return 0; + } + + __sizeCount(): number { + return 0; + } +} + +export class DispatcherBase extends Dispatcher { + __profile: Profile; + __destroyed: boolean; + __closed: boolean; + __onDestroyed: (() => void)[] | null; + __onClosed: (() => void)[] | null; + __interceptors: readonly DispatchInterceptor[]; + __interceptedDispatch: DispatchFn | null; + + constructor(profile: Profile) { + super(); + this.__profile = profile; + this.__destroyed = false; + this.__closed = false; + this.__onDestroyed = []; + this.__onClosed = []; + this.__interceptors = []; + this.__interceptedDispatch = null; + } + + __undiciProfile(): Profile { + return this.__profile; + } + + get destroyed(): boolean { + return this.__destroyed; + } + + __isDestroyed(): boolean { + return this.__destroyed; + } + + get closed(): boolean { + return this.__closed; + } + + close(): Promise { + return new Promise((resolve, reject) => { + if (this.__destroyed) { + queueMicrotask(() => reject(new ClientDestroyedError())); + return; + } + if (this.__closed) { + const waiting = this.__onClosed; + if (waiting !== null) waiting.push(() => resolve()); + else queueMicrotask(() => resolve()); + return; + } + this.__closed = true; + this.__onClosed!.push(() => resolve()); + this.__close() + .then(() => this.destroy()) + .then(() => { + queueMicrotask(() => { + const callbacks = this.__onClosed; + this.__onClosed = null; + if (callbacks !== null) for (const cb of callbacks) cb(); + }); + }); + }); + } + + destroy(err?: Error | null): Promise { + return new Promise((resolve) => { + if (this.__destroyed) { + const waiting = this.__onDestroyed; + if (waiting !== null) waiting.push(() => resolve()); + else queueMicrotask(() => resolve()); + return; + } + const error = err ?? new ClientDestroyedError(); + this.__destroyed = true; + this.__onDestroyed!.push(() => resolve()); + this.__destroy(error).then(() => { + queueMicrotask(() => { + const callbacks = this.__onDestroyed; + this.__onDestroyed = null; + if (callbacks !== null) for (const cb of callbacks) cb(); + }); + }); + }); + } + + dispatch(options: DispatchOptions, handler: DispatchHandlers): boolean { + const target = this.__profile.major >= 7 ? unwrapHandler(handler, this.__profile) : handler; + try { + if (this.__destroyed) throw new ClientDestroyedError(); + if (this.__closed) throw new ClientClosedError(); + return this.__dispatchIntercepted(options, target); + } catch (err) { + if (target.onError === undefined) { + if (this.__profile.major >= 7) throw err; + throw new InvalidArgumentError("invalid onError method"); + } + target.onError!(err as Error); + return false; + } + } + + __dispatchIntercepted(options: DispatchOptions, handler: DispatchHandlers): boolean { + let dispatch = this.__interceptedDispatch; + if (dispatch === null) { + dispatch = (o: DispatchOptions, h: DispatchHandlers): boolean => this.__dispatch(o, h); + for (let i = this.__interceptors.length - 1; i >= 0; i--) { + dispatch = this.__interceptors[i]!(dispatch); + } + this.__interceptedDispatch = dispatch; + } + return dispatch(options, handler); + } + + __dispatch(_options: DispatchOptions, _handler: DispatchHandlers): boolean { + throw new Error("not implemented"); + } + + __close(): Promise { + return Promise.resolve(); + } + + __destroy(_err: Error): Promise { + return Promise.resolve(); + } +} + +/** The request() API over a dispatcher (api-request.js's request with the + * promise callback). */ +export function requestThrough( + dispatcher: Dispatcher, + options: RequestOptions, + profile: Profile, +): Promise { + return new Promise((resolve, reject) => { + const callback = (err: Error | null, data: ResponseData | null): void => { + if (err !== null) reject(err); + else resolve(data!); + }; + try { + const handler = createRequestHandler(options, profile, callback); + dispatcher.dispatch(options, handler); + } catch (err) { + queueMicrotask(() => callback(err as Error, null)); + } + }); +} + +/** api-request.js's RequestHandler, as a handler record over closure state. */ +function createRequestHandler( + opts: RequestOptions, + profile: Profile, + done: (err: Error | null, data: ResponseData | null) => void, +): DispatchHandlers { + if (opts.method === "CONNECT") throw new InvalidArgumentError("invalid method"); + if ( + opts.highWaterMark !== undefined && + (!Number.isFinite(opts.highWaterMark) || opts.highWaterMark < 0) + ) { + throw new InvalidArgumentError("invalid highWaterMark"); + } + const v7 = profile.major >= 7; + const opaque: unknown = opts.opaque ?? null; + const onInfo = opts.onInfo ?? null; + const throwOnError = opts.throwOnError === true; + const trailers: Record = {}; + let callback: ((err: Error | null, data: ResponseData | null) => void) | null = done; + let res: BodyReadable | null = null; + let abortFn: ((err?: Error) => void) | null = null; + let reason: Error | null = null; + let removeAbortListener: (() => void) | null = null; + + const removeSignal = (): void => { + const remove = removeAbortListener; + removeAbortListener = null; + if (remove !== null) remove(); + }; + + const onError = (err: Error): void => { + removeSignal(); + const cb = callback; + if (cb !== null) { + callback = null; + queueMicrotask(() => cb(err, null)); + } + const body = res; + if (body !== null) { + res = null; + queueMicrotask(() => { + body.on("error", () => {}); + body.destroy(err); + }); + } + }; + + // 5.x: abort() before the request reached a socket fails it at once. + const abortV5 = (): void => { + const abort = abortFn; + if (abort !== null) abort(); + else onError(new RequestAbortedError()); + }; + + const signal = opts.signal ?? null; + if (signal !== null) { + if (v7) { + if (signal.aborted) { + reason = abortReason(signal); + } else { + const listener = (): void => { + const why = abortReason(signal); + reason = why; + const body = res; + const abort = abortFn; + if (body !== null) { + body.on("error", () => {}); + body.destroy(why); + } else if (abort !== null) { + abort(why); + } + }; + signal.addEventListener("abort", listener); + removeAbortListener = () => signal.removeEventListener("abort", listener); + } + } else if (signal.aborted) { + abortV5(); + } else { + const listener = (): void => abortV5(); + signal.addEventListener("abort", listener); + removeAbortListener = () => signal.removeEventListener("abort", listener); + } + } + + return { + onConnect: (abort: (err?: Error) => void): void => { + if (v7) { + if (reason !== null) { + abort(reason); + return; + } + } else if (callback === null) { + throw new RequestAbortedError(); + } + abortFn = abort; + }, + onHeaders: ( + statusCode: number, + rawHeaders: Buffer[], + resume: () => void, + statusText: string, + ): boolean => { + const headers = parseHeaders(rawHeaderStrings(rawHeaders, profile), profile); + if (statusCode < 200) { + if (onInfo !== null) onInfo({ statusCode, headers }); + return true; + } + const contentTypeValue = headers["content-type"]; + const contentType = typeof contentTypeValue === "string" ? contentTypeValue : ""; + const abort = abortFn; + const body = new BodyReadable( + resume, + (err: Error) => { + if (abort !== null) { + if (v7) abort(err); + else abort(); + } + }, + contentType, + profile, + ); + if (v7 && removeAbortListener !== null) body.on("close", () => removeSignal()); + const cb = callback; + callback = null; + res = body; + if (cb !== null) { + if (!v7 && throwOnError && statusCode >= 400) { + resolveErrorBody(cb, body, contentType, statusCode, statusText, headers); + return true; + } + const data: ResponseData = v7 + ? { statusCode, statusText, headers, trailers, opaque, body, context: undefined } + : { statusCode, headers, trailers, opaque, body, context: undefined }; + cb(null, data); + } + return true; + }, + onData: (chunk: Buffer): boolean => { + const body = res; + return body !== null ? body.push(chunk) : true; + }, + onComplete: (rawTrailers: string[] | null): void => { + if (!v7) removeSignal(); + if (rawTrailers !== null) { + const parsed = parseHeaders(rawTrailers, profile); + for (const key of Object.keys(parsed)) { + const value = parsed[key]; + if (typeof value === "string") trailers[key] = value; + else if (value !== undefined) trailers[key] = value.join(", "); + } + } + const body = res; + if (body !== null) body.push(null); + }, + onError, + }; +} + +function abortReason(signal: AbortSignal): Error { + const reason: unknown = signal.reason; + return reason instanceof Error ? reason : new RequestAbortedError(); +} + +/** 5.x throwOnError: reject with ResponseStatusCodeError carrying the parsed + * JSON or text body. */ +function resolveErrorBody( + callback: (err: Error | null, data: ResponseData | null) => void, + body: BodyReadable, + contentType: string, + statusCode: number, + statusMessage: string, + headers: IncomingHttpHeaders, +): void { + const message = `Response status code ${statusCode}${statusMessage ? `: ${statusMessage}` : ""}`; + const record: Record = {}; + for (const key of Object.keys(headers)) { + const value = headers[key]; + if (value !== undefined) record[key] = value; + } + if (statusCode === 204 || contentType === "") { + body.dump(); + process.nextTick(() => + callback(new ResponseStatusCodeError(message, statusCode, record), null), + ); + return; + } + const finish = (payload: unknown): void => { + process.nextTick(() => + callback(new ResponseStatusCodeError(message, statusCode, record, payload), null), + ); + }; + const fallback = (): void => { + body.dump(); + process.nextTick(() => + callback(new ResponseStatusCodeError(message, statusCode, record), null), + ); + }; + if (contentType.startsWith("application/json")) { + body.json().then((payload: unknown) => finish(payload), fallback); + } else if (contentType.startsWith("text/")) { + body.text().then((payload: string) => finish(payload), fallback); + } else { + fallback(); + } +} + +/** 7.x's UnwrapHandler: a controller-API handler (onRequestStart, ...) + * driven through the legacy callbacks the clients invoke. */ +function unwrapHandler(handler: DispatchHandlers, profile: Profile): DispatchHandlers { + if (handler.onRequestStart === undefined) return handler; + let controller: UnwrapController | null = null; + const controllerOf = (): UnwrapController => { + let c = controller; + if (c === null) { + c = new UnwrapController(() => {}); + controller = c; + } + return c; + }; + return { + onConnect: (abort: (err?: Error) => void): void => { + const c = new UnwrapController(abort); + controller = c; + if (handler.onRequestStart !== undefined) handler.onRequestStart!(c, undefined); + }, + onHeaders: ( + statusCode: number, + rawHeaders: Buffer[], + resume: () => void, + statusText: string, + ): boolean => { + const c = controllerOf(); + c.__resume = resume; + if (handler.onResponseStart !== undefined) { + handler.onResponseStart!( + c, + statusCode, + parseHeaders(rawHeaderStrings(rawHeaders, profile), profile), + statusText, + ); + } + return !c.paused; + }, + onData: (chunk: Buffer): boolean => { + const c = controllerOf(); + if (handler.onResponseData !== undefined) handler.onResponseData!(c, chunk); + return !c.paused; + }, + onComplete: (trailers: string[] | null): void => { + if (handler.onResponseEnd !== undefined) { + handler.onResponseEnd!(controllerOf(), parseHeaders(trailers ?? [], profile)); + } + }, + onError: (err: Error): void => { + if (handler.onResponseError === undefined) + throw new InvalidArgumentError("invalid onError method"); + handler.onResponseError!(controllerOf(), err); + }, + }; +} + +/** 7.x's UnwrapController. */ +class UnwrapController implements DispatchController { + __paused: boolean; + __reason: Error | null; + __aborted: boolean; + __abort: (err?: Error) => void; + __resume: (() => void) | null; + + constructor(abort: (err?: Error) => void) { + this.__paused = false; + this.__reason = null; + this.__aborted = false; + this.__abort = abort; + this.__resume = null; + } + + pause(): void { + this.__paused = true; + } + + resume(): void { + if (this.__paused) { + this.__paused = false; + const resume = this.__resume; + if (resume !== null) resume(); + } + } + + abort(reason: Error): void { + if (!this.__aborted) { + this.__aborted = true; + this.__reason = reason; + this.__abort(reason); + } + } + + get aborted(): boolean { + return this.__aborted; + } + + get reason(): Error | null { + return this.__reason; + } + + get paused(): boolean { + return this.__paused; + } +} + +/** The `Dispatcher.*` type namespace of undici's types/dispatcher.d.ts + * (5.x and 7.x spellings). */ +export declare namespace Dispatcher { + type DispatchOptions = import("./types.ts").DispatchOptions; + type DispatchHandlers = import("./types.ts").DispatchHandlers; + type DispatchHandler = import("./types.ts").DispatchHandlers; + type DispatchController = import("./types.ts").DispatchController; + type DispatchInterceptor = import("./types.ts").DispatchInterceptor; + type Dispatch = import("./types.ts").DispatchFn; + type HttpMethod = string; + type UndiciHeaders = import("./types.ts").UndiciHeaders; + // The 7.x spellings take an opaque type parameter; the opaque value is + // `unknown` here. + type RequestOptions<_TOpaque = null> = import("./dispatcher.ts").RequestOptions; + type ResponseData<_TOpaque = null> = import("./dispatcher.ts").ResponseData; +} diff --git a/packages/compiler/shims/undici/core/errors-v5.ts b/packages/compiler/shims/undici/core/errors-v5.ts new file mode 100644 index 000000000..781dc7ce1 --- /dev/null +++ b/packages/compiler/shims/undici/core/errors-v5.ts @@ -0,0 +1,22 @@ +// undici 5.x's `errors` export (lib/core/errors.js module.exports). +export { + HTTPParserError, + UndiciError, + HeadersTimeoutError, + HeadersOverflowError, + BodyTimeoutError, + RequestContentLengthMismatchError, + ConnectTimeoutError, + ResponseStatusCodeError, + InvalidArgumentError, + InvalidReturnValueError, + RequestAbortedError, + ClientDestroyedError, + ClientClosedError, + InformationalError, + SocketError, + NotSupportedError, + ResponseContentLengthMismatchError, + BalancedPoolMissingUpstreamError, + ResponseExceededMaxSizeError, +} from "./errors.ts"; diff --git a/packages/compiler/shims/undici/core/errors-v7.ts b/packages/compiler/shims/undici/core/errors-v7.ts new file mode 100644 index 000000000..644a00490 --- /dev/null +++ b/packages/compiler/shims/undici/core/errors-v7.ts @@ -0,0 +1,25 @@ +// undici 7.x's `errors` export (the implemented classes of +// lib/core/errors.js module.exports). +export { + AbortError, + HTTPParserError, + UndiciError, + HeadersTimeoutError, + HeadersOverflowError, + BodyTimeoutError, + RequestContentLengthMismatchError, + ConnectTimeoutError, + InvalidArgumentError, + InvalidReturnValueError, + RequestAbortedError, + ClientDestroyedError, + ClientClosedError, + InformationalError, + SocketError, + NotSupportedError, + ResponseContentLengthMismatchError, + BalancedPoolMissingUpstreamError, + ResponseExceededMaxSizeError, + RequestRetryError, + ResponseError, +} from "./errors.ts"; diff --git a/packages/compiler/shims/undici/core/errors.ts b/packages/compiler/shims/undici/core/errors.ts new file mode 100644 index 000000000..0af5835da --- /dev/null +++ b/packages/compiler/shims/undici/core/errors.ts @@ -0,0 +1,255 @@ +// undici's error classes (lib/core/errors.js), shared by the 5.x and 7.x +// entries. Names, messages and codes are undici's; the 7.x-only classes are +// exported by the 7.x entry alone. + +export interface SocketInfo { + localAddress?: string; + localPort?: number; + remoteAddress?: string; + remotePort?: number; + remoteFamily?: string; + timeout?: number; + bytesWritten?: number; + bytesRead?: number; +} + +export class UndiciError extends Error { + code: string; + constructor(message?: string) { + super(message); + this.name = "UndiciError"; + this.code = "UND_ERR"; + } +} + +export class ConnectTimeoutError extends UndiciError { + constructor(message?: string) { + super(message); + this.name = "ConnectTimeoutError"; + this.message = message || "Connect Timeout Error"; + this.code = "UND_ERR_CONNECT_TIMEOUT"; + } +} + +export class HeadersTimeoutError extends UndiciError { + constructor(message?: string) { + super(message); + this.name = "HeadersTimeoutError"; + this.message = message || "Headers Timeout Error"; + this.code = "UND_ERR_HEADERS_TIMEOUT"; + } +} + +export class HeadersOverflowError extends UndiciError { + constructor(message?: string) { + super(message); + this.name = "HeadersOverflowError"; + this.message = message || "Headers Overflow Error"; + this.code = "UND_ERR_HEADERS_OVERFLOW"; + } +} + +export class BodyTimeoutError extends UndiciError { + constructor(message?: string) { + super(message); + this.name = "BodyTimeoutError"; + this.message = message || "Body Timeout Error"; + this.code = "UND_ERR_BODY_TIMEOUT"; + } +} + +export class InvalidArgumentError extends UndiciError { + constructor(message?: string) { + super(message); + this.name = "InvalidArgumentError"; + this.message = message || "Invalid Argument Error"; + this.code = "UND_ERR_INVALID_ARG"; + } +} + +export class InvalidReturnValueError extends UndiciError { + constructor(message?: string) { + super(message); + this.name = "InvalidReturnValueError"; + this.message = message || "Invalid Return Value Error"; + this.code = "UND_ERR_INVALID_RETURN_VALUE"; + } +} + +/** 7.x's base of RequestAbortedError (5.x has no AbortError export; its + * RequestAbortedError extends UndiciError directly, which no 5.x program can + * observe through the exported classes). */ +export class AbortError extends UndiciError { + constructor(message?: string) { + super(message); + this.name = "AbortError"; + this.message = message || "The operation was aborted"; + this.code = "UND_ERR_ABORT"; + } +} + +export class RequestAbortedError extends AbortError { + constructor(message?: string) { + super(message); + this.name = "AbortError"; + this.message = message || "Request aborted"; + this.code = "UND_ERR_ABORTED"; + } +} + +export class InformationalError extends UndiciError { + constructor(message?: string) { + super(message); + this.name = "InformationalError"; + this.message = message || "Request information"; + this.code = "UND_ERR_INFO"; + } +} + +export class RequestContentLengthMismatchError extends UndiciError { + constructor(message?: string) { + super(message); + this.name = "RequestContentLengthMismatchError"; + this.message = message || "Request body length does not match content-length header"; + this.code = "UND_ERR_REQ_CONTENT_LENGTH_MISMATCH"; + } +} + +export class ResponseContentLengthMismatchError extends UndiciError { + constructor(message?: string) { + super(message); + this.name = "ResponseContentLengthMismatchError"; + this.message = message || "Response body length does not match content-length header"; + this.code = "UND_ERR_RES_CONTENT_LENGTH_MISMATCH"; + } +} + +export class ClientDestroyedError extends UndiciError { + constructor(message?: string) { + super(message); + this.name = "ClientDestroyedError"; + this.message = message || "The client is destroyed"; + this.code = "UND_ERR_DESTROYED"; + } +} + +export class ClientClosedError extends UndiciError { + constructor(message?: string) { + super(message); + this.name = "ClientClosedError"; + this.message = message || "The client is closed"; + this.code = "UND_ERR_CLOSED"; + } +} + +export class SocketError extends UndiciError { + socket: SocketInfo | null; + constructor(message?: string, socket?: SocketInfo | null) { + super(message); + this.name = "SocketError"; + this.message = message || "Socket error"; + this.code = "UND_ERR_SOCKET"; + this.socket = socket ?? null; + } +} + +export class NotSupportedError extends UndiciError { + constructor(message?: string) { + super(message); + this.name = "NotSupportedError"; + this.message = message || "Not supported error"; + this.code = "UND_ERR_NOT_SUPPORTED"; + } +} + +export class BalancedPoolMissingUpstreamError extends UndiciError { + constructor(message?: string) { + super(message); + this.name = "MissingUpstreamError"; + this.message = message || "No upstream has been added to the BalancedPool"; + this.code = "UND_ERR_BPL_MISSING_UPSTREAM"; + } +} + +export class HTTPParserError extends Error { + code: string | undefined; + data: string | undefined; + constructor(message?: string, code?: string, data?: string) { + super(message); + this.name = "HTTPParserError"; + this.code = code ? `HPE_${code}` : undefined; + this.data = data ? data : undefined; + } +} + +export class ResponseExceededMaxSizeError extends UndiciError { + constructor(message?: string) { + super(message); + this.name = "ResponseExceededMaxSizeError"; + this.message = message || "Response content exceeded max size"; + this.code = "UND_ERR_RES_EXCEEDED_MAX_SIZE"; + } +} + +/** 5.x only (throwOnError). */ +export class ResponseStatusCodeError extends UndiciError { + body: unknown; + status: number; + statusCode: number; + headers: Record | null; + constructor( + message?: string, + statusCode?: number, + headers?: Record | null, + body?: unknown, + ) { + super(message); + this.name = "ResponseStatusCodeError"; + this.message = message || "Response Status Code Error"; + this.code = "UND_ERR_RESPONSE_STATUS_CODE"; + this.body = body; + this.status = statusCode ?? 0; + this.statusCode = statusCode ?? 0; + this.headers = headers ?? null; + } +} + +/** 7.x only. */ +export class RequestRetryError extends UndiciError { + statusCode: number; + data: { count: number }; + headers: Record; + constructor( + message: string, + code: number, + options: { headers?: Record; data?: { count: number } }, + ) { + super(message); + this.name = "RequestRetryError"; + this.message = message || "Request retry error"; + this.code = "UND_ERR_REQ_RETRY"; + this.statusCode = code; + this.data = options.data ?? { count: 0 }; + this.headers = options.headers ?? {}; + } +} + +/** 7.x only. */ +export class ResponseError extends UndiciError { + statusCode: number; + body: unknown; + headers: Record | null; + constructor( + message: string, + code: number, + options: { headers?: Record | null; body?: unknown }, + ) { + super(message); + this.name = "ResponseError"; + this.message = message || "Response error"; + this.code = "UND_ERR_RESPONSE"; + this.statusCode = code; + this.body = options.body; + this.headers = options.headers ?? null; + } +} diff --git a/packages/compiler/shims/undici/core/fetch.ts b/packages/compiler/shims/undici/core/fetch.ts new file mode 100644 index 000000000..9b287a7bd --- /dev/null +++ b/packages/compiler/shims/undici/core/fetch.ts @@ -0,0 +1,413 @@ +// undici's fetch(). Without a dispatcher (no init.dispatcher and no +// program-installed global dispatcher) it is the runtime's own fetch, which +// implements the same WHATWG algorithm. With one, the request goes through +// that dispatcher (its connector, lookup and pool), following undici's +// fetch for request headers, redirects and errors; the response body is +// buffered into a native Response. +// +// Known gaps: a Response built here reports `url` as "" and `redirected` as +// false (the runtime's Response has no settable URL yet), and https requests +// offer `gzip, deflate` rather than undici's `br, gzip, deflate` (the runtime +// has no Brotli decoder). +import { gunzipSync, inflateSync } from "node:zlib"; +import type { Dispatcher } from "./dispatcher.ts"; +import { getGlobalDispatcher, hasCustomGlobalDispatcher } from "./api.ts"; +import type { DispatchHandlers, Profile } from "./types.ts"; + +/** undici's RequestInit, restricted to the implemented members: string and + * byte bodies, and the init members the dispatcher path honors. */ +export interface UndiciRequestInit { + method?: string; + headers?: Headers | [string, string][] | Record; + body?: string | Uint8Array | null; + signal?: AbortSignal | null; + redirect?: "follow" | "manual" | "error"; + dispatcher?: Dispatcher; +} + +/** The fetch standard's blocked ports (undici lib/web/fetch/constants.js). */ +const badPorts = new Set([ + "1", + "7", + "9", + "11", + "13", + "15", + "17", + "19", + "20", + "21", + "22", + "23", + "25", + "37", + "42", + "43", + "53", + "69", + "77", + "79", + "87", + "95", + "101", + "102", + "103", + "104", + "109", + "110", + "111", + "113", + "115", + "117", + "119", + "123", + "135", + "137", + "139", + "143", + "161", + "179", + "389", + "427", + "465", + "512", + "513", + "514", + "515", + "526", + "530", + "531", + "532", + "540", + "548", + "554", + "556", + "563", + "587", + "601", + "636", + "989", + "990", + "993", + "995", + "1719", + "1720", + "1723", + "2049", + "3659", + "4045", + "4190", + "5060", + "5061", + "6000", + "6566", + "6665", + "6666", + "6667", + "6668", + "6669", + "6679", + "6697", + "10080", +]); + +interface RawResponse { + status: number; + statusText: string; + headers: string[]; + body: Buffer; +} + +const redirectStatuses = [301, 302, 303, 307, 308]; +const nullBodyStatuses = [101, 204, 205, 304]; + +export function undiciFetch( + input: string | URL, + init: UndiciRequestInit | undefined, + createGlobal: () => Dispatcher, + _profile: Profile, +): Promise { + const explicit = init !== undefined ? init.dispatcher : undefined; + if (explicit === undefined && !hasCustomGlobalDispatcher()) { + if (init === undefined) return fetch(input); + return fetch(input, { + method: init.method, + headers: init.headers, + body: init.body, + signal: init.signal ?? undefined, + redirect: init.redirect, + }); + } + const dispatcher = explicit ?? getGlobalDispatcher(createGlobal); + return fetchThrough(dispatcher, input, init ?? {}); +} + +function normalizeMethod(method: string): string { + const upper = method.toUpperCase(); + return ["DELETE", "GET", "HEAD", "OPTIONS", "POST", "PUT", "PATCH"].includes(upper) + ? upper + : method; +} + +async function fetchThrough( + dispatcher: Dispatcher, + input: string | URL, + init: UndiciRequestInit, +): Promise { + let url = typeof input === "string" ? new URL(input) : new URL(input.href); + let method = normalizeMethod(init.method ?? "GET"); + const signal = init.signal ?? null; + const redirectMode = init.redirect ?? "follow"; + + // Request headers: the caller's (as a Headers list: lower-cased, sorted, + // combined), then undici fetch's defaults. + const headers = headerListOf(init.headers); + let body: Buffer | null = null; + const rawBody = init.body ?? null; + if (rawBody !== null) { + if (typeof rawBody === "string") { + body = Buffer.from(rawBody); + if (!headers.has("content-type")) headers.set("content-type", "text/plain;charset=UTF-8"); + } else { + body = Buffer.from(rawBody.buffer, rawBody.byteOffset, rawBody.byteLength); + } + } + if (!headers.has("accept")) headers.set("accept", "*/*"); + if (!headers.has("accept-language")) headers.set("accept-language", "*"); + + for (let redirects = 0; ; redirects++) { + if (signal !== null && signal.aborted) throw abortReason(signal); + if (badPorts.has(url.port)) { + throw new TypeError("fetch failed", { cause: new Error("bad port") }); + } + const requestHeaders = new HeaderList(headers); + if (body !== null) { + if (!requestHeaders.has("content-length")) + requestHeaders.set("content-length", String(body.length)); + } else if ((method === "POST" || method === "PUT") && !requestHeaders.has("content-length")) { + requestHeaders.set("content-length", "0"); + } + requestHeaders.set("sec-fetch-mode", "cors"); + if (!requestHeaders.has("user-agent")) requestHeaders.set("user-agent", "undici"); + // undici also offers br over https; the native runtime has no Brotli + // decoder, so only the codings it can decode are offered. + if (!requestHeaders.has("accept-encoding")) + requestHeaders.set("accept-encoding", "gzip, deflate"); + const pairs = requestHeaders.pairs(); + + let raw: RawResponse; + try { + raw = await dispatchOnce(dispatcher, url, method, pairs, body, signal); + } catch (err) { + if (signal !== null && signal.aborted) throw abortReason(signal); + throw new TypeError("fetch failed", { cause: err }); + } + + const responseHeaders = new Headers(); + for (let i = 0; i + 1 < raw.headers.length; i += 2) { + responseHeaders.append(raw.headers[i]!, raw.headers[i + 1]!); + } + const location = responseHeaders.get("location"); + if (redirectStatuses.includes(raw.status) && location !== null && redirectMode !== "manual") { + if (redirectMode === "error") { + throw new TypeError("fetch failed", { cause: new Error("unexpected redirect") }); + } + if (redirects >= 20) { + throw new TypeError("fetch failed", { cause: new Error("redirect count exceeded") }); + } + const next = new URL(location, url); + if ( + raw.status === 303 && method !== "GET" && method !== "HEAD" + ? true + : (raw.status === 301 || raw.status === 302) && method === "POST" + ) { + method = "GET"; + body = null; + for (const name of [ + "content-encoding", + "content-language", + "content-location", + "content-type", + "content-length", + ]) { + headers.delete(name); + } + } + if (next.origin !== url.origin) { + headers.delete("authorization"); + headers.delete("proxy-authorization"); + headers.delete("cookie"); + headers.delete("host"); + } + url = next; + continue; + } + + const nullBody = nullBodyStatuses.includes(raw.status) || method === "HEAD"; + return new Response( + nullBody ? null : decode(raw.body, responseHeaders.get("content-encoding")), + { + status: raw.status, + statusText: raw.statusText, + headers: responseHeaders, + }, + ); + } +} + +/** A Fetch header list: lower-cased names, values combined with ", " in + * insertion order, serialized sorted by name (set-cookie values stay + * separate) — what a Headers object yields when iterated. */ +class HeaderList { + names: string[]; + values: Map; + + constructor(from: HeaderList | null) { + this.names = from !== null ? from.names.slice() : []; + this.values = new Map(); + if (from !== null) + for (const name of from.names) this.values.set(name, from.values.get(name)!.slice()); + } + + has(name: string): boolean { + return this.values.has(name.toLowerCase()); + } + + append(name: string, value: string): void { + const key = name.toLowerCase(); + const normalized = value.replace(/^[\t\n\r ]+|[\t\n\r ]+$/g, ""); + const existing = this.values.get(key); + if (existing !== undefined) existing.push(normalized); + else { + this.names.push(key); + this.values.set(key, [normalized]); + } + } + + set(name: string, value: string): void { + this.delete(name); + this.append(name, value); + } + + delete(name: string): void { + const key = name.toLowerCase(); + if (!this.values.has(key)) return; + this.values.delete(key); + this.names = this.names.filter((n) => n !== key); + } + + pairs(): string[] { + const out: string[] = []; + for (const name of this.names.slice().sort()) { + const list = this.values.get(name)!; + if (name === "set-cookie") for (const value of list) out.push(name, value); + else out.push(name, list.join(", ")); + } + return out; + } +} + +/** The caller's headers init as a header list. Records and pair arrays read + * directly; a Headers object is read through forEach. */ +function headerListOf( + init: Headers | [string, string][] | Record | undefined, +): HeaderList { + const list = new HeaderList(null); + if (init === undefined) return list; + if (init instanceof Headers) { + init.forEach((value: string, name: string) => list.append(name, value)); + } else if (Array.isArray(init)) { + for (const pair of init) list.append(pair[0], pair[1]); + } else { + for (const name of Object.keys(init)) list.append(name, init[name]!); + } + return list; +} + +function decode(body: Buffer, encoding: string | null): Buffer { + if (encoding === null || body.length === 0) return body; + let out = body; + const codings = encoding + .toLowerCase() + .split(",") + .map((c) => c.trim()) + .reverse(); + for (const coding of codings) { + if (coding === "gzip" || coding === "x-gzip") out = gunzipSync(out); + else if (coding === "deflate") out = inflateSync(out); + else return body; + } + return out; +} + +function abortReason(signal: AbortSignal): Error { + const reason: unknown = signal.reason; + return reason instanceof Error + ? reason + : new DOMException("This operation was aborted", "AbortError"); +} + +function dispatchOnce( + dispatcher: Dispatcher, + url: URL, + method: string, + headers: string[], + body: Buffer | null, + signal: AbortSignal | null, +): Promise { + return new Promise((resolve, reject) => { + let status = 0; + let statusText = ""; + let rawHeaders: string[] = []; + const chunks: Buffer[] = []; + let settled = false; + let abortFn: ((err?: Error) => void) | null = null; + const onAbort = (): void => { + const abort = abortFn; + if (abort !== null && signal !== null) abort(abortReason(signal)); + }; + if (signal !== null) signal.addEventListener("abort", onAbort); + const finish = (err: Error | null): void => { + if (settled) return; + settled = true; + if (signal !== null) signal.removeEventListener("abort", onAbort); + if (err !== null) reject(err); + else resolve({ status, statusText, headers: rawHeaders, body: Buffer.concat(chunks) }); + }; + const handler: DispatchHandlers = { + onConnect: (abort: (err?: Error) => void): void => { + abortFn = abort; + if (signal !== null && signal.aborted) abort(abortReason(signal)); + }, + onHeaders: ( + statusCode: number, + raw: Buffer[], + _resume: () => void, + text: string, + ): boolean => { + if (statusCode < 200) return true; + status = statusCode; + statusText = text; + rawHeaders = []; + for (const b of raw) rawHeaders.push(b.toString("latin1")); + return true; + }, + onData: (chunk: Buffer): boolean => { + chunks.push(chunk); + return true; + }, + onComplete: (_trailers: string[] | null): void => finish(null), + onError: (err: Error): void => finish(err), + }; + dispatcher.dispatch( + { + origin: url.origin, + path: url.pathname + url.search, + method, + headers, + body, + }, + handler, + ); + }); +} diff --git a/packages/compiler/shims/undici/core/pool.ts b/packages/compiler/shims/undici/core/pool.ts new file mode 100644 index 000000000..101d5947b --- /dev/null +++ b/packages/compiler/shims/undici/core/pool.ts @@ -0,0 +1,280 @@ +// undici's Pool (lib/dispatcher/pool.js, pool-base.js, pool-stats.js): a +// set of Clients for one origin, created on demand up to `connections`, with +// a queue for requests no client can take. +import { buildConnector } from "./connector.ts"; +import { Client } from "./client.ts"; +import { Dispatcher, DispatcherBase } from "./dispatcher.ts"; +import { InvalidArgumentError } from "./errors.ts"; +import { parseOrigin } from "./util.ts"; +import type { + ClientOptions, + DispatchHandlers, + DispatchOptions, + PoolOptions, + Profile, +} from "./types.ts"; + +interface QueuedDispatch { + opts: DispatchOptions; + handler: DispatchHandlers; +} + +/** pool-stats.js: live counters over a pool's clients. */ +export class PoolStats { + __pool: PoolBase; + + constructor(pool: PoolBase) { + this.__pool = pool; + } + + get connected(): number { + return this.__pool.__connectedCount(); + } + + get free(): number { + return this.__pool.__freeCount(); + } + + get pending(): number { + return this.__pool.__pendingCount(); + } + + get queued(): number { + return this.__pool.__queuedCount(); + } + + get running(): number { + return this.__pool.__runningCount(); + } + + get size(): number { + return this.__pool.__sizeCount(); + } +} + +export class PoolBase extends DispatcherBase { + __clients: Dispatcher[]; + __clientNeedDrain: Map; + __queue: QueuedDispatch[]; + __queued: number; + __needDrain: boolean; + __closedResolve: (() => void) | null; + __stats: PoolStats; + __onDrain: (origin: URL, targets: Dispatcher[]) => void; + __onConnect: (origin: URL, targets: Dispatcher[]) => void; + __onDisconnect: (origin: URL, targets: Dispatcher[], err: Error) => void; + __onConnectionError: (origin: URL, targets: Dispatcher[], err: Error) => void; + + constructor(profile: Profile) { + super(profile); + this.__clients = []; + this.__clientNeedDrain = new Map(); + this.__queue = []; + this.__queued = 0; + this.__needDrain = false; + this.__closedResolve = null; + this.__stats = new PoolStats(this); + this.__onDrain = (origin: URL, targets: Dispatcher[]): void => { + const client = targets[0]!; + let needDrain = false; + while (!needDrain) { + const item = this.__queue.shift(); + if (item === undefined) break; + this.__queued--; + needDrain = !client.dispatch(item.opts, item.handler); + } + this.__clientNeedDrain.set(client, needDrain); + if (!needDrain && this.__needDrain) { + this.__needDrain = false; + this.emit("drain", origin, [this as Dispatcher, ...targets]); + } + const closed = this.__closedResolve; + if (closed !== null && this.__queue.length === 0) { + Promise.all(this.__clients.map((c) => c.close())).then(() => closed()); + } + }; + this.__onConnect = (origin: URL, targets: Dispatcher[]): void => { + this.emit("connect", origin, [this as Dispatcher, ...targets]); + }; + this.__onDisconnect = (origin: URL, targets: Dispatcher[], err: Error): void => { + this.emit("disconnect", origin, [this as Dispatcher, ...targets], err); + }; + this.__onConnectionError = (origin: URL, targets: Dispatcher[], err: Error): void => { + this.emit("connectionError", origin, [this as Dispatcher, ...targets], err); + }; + } + + get stats(): PoolStats { + return this.__stats; + } + + __busyFlag(): boolean { + return this.__needDrain; + } + + __connectedCount(): number { + return this.__clients.filter((c) => c.__connectedFlag()).length; + } + + __freeCount(): number { + return this.__clients.filter((c) => c.__connectedFlag() && !this.__clientNeedDrain.get(c)) + .length; + } + + __pendingCount(): number { + let ret = this.__queued; + for (const c of this.__clients) ret += c.__pendingCount(); + return ret; + } + + __queuedCount(): number { + return this.__queued; + } + + __runningCount(): number { + let ret = 0; + for (const c of this.__clients) ret += c.__runningCount(); + return ret; + } + + __sizeCount(): number { + let ret = this.__queued; + for (const c of this.__clients) ret += c.__sizeCount(); + return ret; + } + + __close(): Promise { + if (this.__queue.length === 0) { + return Promise.all(this.__clients.map((c) => c.close())).then(() => {}); + } + return new Promise((resolve) => { + this.__closedResolve = resolve; + }); + } + + __destroy(err: Error): Promise { + for (;;) { + const item = this.__queue.shift(); + if (item === undefined) break; + if (item.handler.onError !== undefined) item.handler.onError!(err); + } + return Promise.all(this.__clients.map((c) => c.destroy(err))).then(() => {}); + } + + __dispatch(opts: DispatchOptions, handler: DispatchHandlers): boolean { + const dispatcher = this.__getDispatcher(); + if (dispatcher === null) { + this.__needDrain = true; + this.__queue.push({ opts, handler }); + this.__queued++; + } else if (!dispatcher.dispatch(opts, handler)) { + this.__clientNeedDrain.set(dispatcher, true); + this.__needDrain = this.__getDispatcher() === null; + } + return !this.__needDrain; + } + + __getDispatcher(): Dispatcher | null { + return null; + } + + __addClient(client: Dispatcher): void { + client.on("drain", this.__onDrain); + client.on("connect", this.__onConnect); + client.on("disconnect", this.__onDisconnect); + client.on("connectionError", this.__onConnectionError); + this.__clients.push(client); + this.__clientNeedDrain.set(client, false); + if (this.__needDrain) { + process.nextTick(() => { + if (this.__needDrain) this.__onDrain(clientUrl(client), [client, this as Dispatcher]); + }); + } + } +} + +function clientUrl(client: Dispatcher): URL { + return client instanceof Client ? client.__url : new URL("http://localhost"); +} + +export class Pool extends PoolBase { + __connections: number | null; + __url: URL; + __options: ClientOptions; + __factory: ((origin: URL, opts: object) => Dispatcher) | null; + + constructor(origin: string | URL, options: PoolOptions | undefined, profile: Profile) { + super(profile); + const opts: PoolOptions = options ?? {}; + const connections = opts.connections ?? null; + if (connections !== null && (!Number.isFinite(connections) || connections < 0)) { + throw new InvalidArgumentError("invalid connections"); + } + const connect = opts.connect ?? null; + const connector = + typeof connect === "function" + ? connect + : buildConnector( + connect !== null + ? { + ...connect, + timeout: + connect.timeout !== undefined ? connect.timeout : (opts.connectTimeout ?? 10e3), + } + : { timeout: opts.connectTimeout ?? 10e3 }, + ); + const interceptors = opts.interceptors; + if (profile.major < 7 && interceptors !== undefined && interceptors.Pool !== undefined) { + this.__interceptors = interceptors.Pool; + } + this.__connections = connections !== null && connections !== 0 ? connections : null; + this.__url = parseOrigin(origin, profile); + this.__options = { + keepAliveTimeout: opts.keepAliveTimeout, + keepAliveMaxTimeout: opts.keepAliveMaxTimeout, + keepAliveTimeoutThreshold: opts.keepAliveTimeoutThreshold, + pipelining: opts.pipelining, + maxHeaderSize: opts.maxHeaderSize, + bodyTimeout: opts.bodyTimeout, + headersTimeout: opts.headersTimeout, + strictContentLength: opts.strictContentLength, + maxRequestsPerClient: opts.maxRequestsPerClient, + maxResponseSize: opts.maxResponseSize, + localAddress: opts.localAddress, + interceptors: interceptors !== undefined ? { Client: interceptors.Client } : undefined, + connect: connector, + }; + const factory = opts.factory; + this.__factory = factory !== undefined ? factory : null; + if (profile.major >= 7) { + // 7.x drops a client whose connection attempt failed. + this.on("connectionError", (_origin: URL, targets: Dispatcher[], _err: Error) => { + for (const target of targets) { + const idx = this.__clients.indexOf(target); + if (idx !== -1) this.__clients.splice(idx, 1); + } + }); + } + } + + __getDispatcher(): Dispatcher | null { + for (const client of this.__clients) { + if (!this.__clientNeedDrain.get(client)) return client; + } + if (this.__connections === null || this.__clients.length < this.__connections) { + const factory = this.__factory; + const client = + factory !== null + ? factory(this.__url, this.__options) + : this.__createClient(this.__url, this.__options); + this.__addClient(client); + return client; + } + return null; + } + + /** The default factory: a Client of the same undici version. */ + __createClient(origin: URL, options: ClientOptions): Dispatcher { + return new Client(origin, options, this.__profile); + } +} diff --git a/packages/compiler/shims/undici/core/readable.ts b/packages/compiler/shims/undici/core/readable.ts new file mode 100644 index 000000000..53a7cc14e --- /dev/null +++ b/packages/compiler/shims/undici/core/readable.ts @@ -0,0 +1,268 @@ +// undici's response body (lib/api/readable.js): a readable byte stream fed +// by the dispatch handler, with the fetch body mixin (text/json/ +// arrayBuffer/blob/bytes) and dump(). +// +// It follows node:stream's Readable contract for the members it declares +// (flowing on a 'data' listener or resume(), pause(), 'data'/'end'/'close'/ +// 'error' events and their ordering, destroy(err), back-pressure through +// push()'s return value), built on EventEmitter: scriptc compiles program +// classes extending EventEmitter under both its own Node declarations and +// @types/node, which a node:stream subclass does not get. +import { EventEmitter } from "node:events"; +import { NotSupportedError, RequestAbortedError } from "./errors.ts"; +import type { Profile } from "./types.ts"; + +const HIGH_WATER_MARK = 64 * 1024; + +export class BodyReadable extends EventEmitter { + __resumeFn: () => void; + __abortFn: (err: Error) => void; + __contentType: string; + __consuming: boolean; + __profile: Profile; + __buffer: Buffer[]; + __length: number; + __flowing: boolean; + __ended: boolean; + __endEmitted: boolean; + __destroyed: boolean; + __flowScheduled: boolean; + __needsResume: boolean; + + constructor( + resume: () => void, + abort: (err: Error) => void, + contentType: string, + profile: Profile, + ) { + super(); + this.__resumeFn = resume; + this.__abortFn = abort; + this.__contentType = contentType; + this.__consuming = false; + this.__profile = profile; + this.__buffer = []; + this.__length = 0; + this.__flowing = false; + this.__ended = false; + this.__endEmitted = false; + this.__destroyed = false; + this.__flowScheduled = false; + this.__needsResume = false; + // Readable.on('data') switches the stream to flowing mode. + this.on("newListener", (event: string) => { + if (event === "data" && !this.__flowing) this.resume(); + }); + } + + get destroyed(): boolean { + return this.__destroyed; + } + + get readableEnded(): boolean { + return this.__endEmitted; + } + + get readableLength(): number { + return this.__length; + } + + get readableFlowing(): boolean { + return this.__flowing; + } + + /** Readable.push: queue a chunk (null ends the body). Returns false once + * the buffer reaches the high-water mark; the dispatcher then pauses the + * connection until the consumer drains it. */ + push(chunk: Buffer | null): boolean { + if (this.__destroyed || this.__ended) return false; + if (chunk === null) { + this.__ended = true; + if (this.__length === 0) this.__scheduleEnd(); + else this.__scheduleFlow(); + return false; + } + if (this.__flowing && this.__length === 0 && this.listenerCount("data") > 0) { + this.emit("data", chunk); + return true; + } + this.__buffer.push(chunk); + this.__length += chunk.length; + if (this.__flowing) this.__scheduleFlow(); + const below = this.__length < HIGH_WATER_MARK; + if (!below) this.__needsResume = true; + return below; + } + + resume(): this { + if (!this.__flowing) { + this.__flowing = true; + this.__scheduleFlow(); + } + return this; + } + + pause(): this { + this.__flowing = false; + return this; + } + + destroy(err?: Error): this { + if (this.__destroyed) return this; + this.__destroyed = true; + let error: Error | null = err ?? null; + if (error === null && !this.__endEmitted) error = new RequestAbortedError(); + if (error !== null) this.__abortFn(error); + this.__buffer = []; + this.__length = 0; + const reported = error; + process.nextTick(() => { + if (reported !== null) this.emit("error", reported); + this.emit("close"); + }); + return this; + } + + __scheduleFlow(): void { + if (this.__flowScheduled) return; + this.__flowScheduled = true; + process.nextTick(() => { + this.__flowScheduled = false; + this.__flow(); + }); + } + + __flow(): void { + while (this.__flowing && !this.__destroyed && this.__buffer.length > 0) { + const chunk = this.__buffer.shift()!; + this.__length -= chunk.length; + this.emit("data", chunk); + } + if (this.__destroyed) return; + if (this.__needsResume && this.__length < HIGH_WATER_MARK) { + this.__needsResume = false; + this.__resumeFn(); + } + if (this.__ended && this.__length === 0 && this.__flowing) this.__scheduleEnd(); + } + + __scheduleEnd(): void { + process.nextTick(() => { + if (this.__endEmitted || this.__destroyed || !this.__flowing || this.__length > 0) return; + this.__endEmitted = true; + this.emit("end"); + // autoDestroy: a fully read body closes. + process.nextTick(() => { + if (this.__destroyed) return; + this.__destroyed = true; + this.emit("close"); + }); + }); + } + + /** True once text()/json()/arrayBuffer()/bytes()/blob()/dump() started + * consuming the body. */ + get bodyUsed(): boolean { + return this.__consuming; + } + + text(): Promise { + return this.__consume().then((bytes) => decodeText(bytes, this.__profile)); + } + + // undici declares json() as Promise: callers assign the parsed value + // to their own annotated types without a cast, exactly as with undici. + // oxlint-disable-next-line typescript/no-explicit-any + json(): Promise { + return this.__consume().then((bytes) => JSON.parse(decodeText(bytes, this.__profile))); + } + + arrayBuffer(): Promise { + return this.__consume().then((bytes) => { + const out = new ArrayBuffer(bytes.length); + new Uint8Array(out).set(bytes); + return out; + }); + } + + bytes(): Promise { + return this.__consume().then((bytes) => new Uint8Array(bytes)); + } + + blob(): Promise { + return this.__consume().then((bytes) => new Blob([bytes], { type: this.__contentType })); + } + + formData(): Promise { + return Promise.reject(new NotSupportedError()); + } + + /** Reads and discards up to `limit` bytes (default 256 KiB on 5.x, 128 KiB + * on 7.x), then resolves; read errors are swallowed like undici's. */ + dump(opts?: { limit: number }): Promise { + let limit = + opts !== undefined && Number.isFinite(opts.limit) + ? opts.limit + : this.__profile.major >= 7 + ? 131072 + : 262144; + this.__consuming = true; + return new Promise((resolve) => { + if (this.__endEmitted || this.__destroyed) { + resolve(); + return; + } + this.on("data", (chunk: Buffer) => { + limit -= chunk.length; + if (limit < 0) { + this.destroy(); + resolve(); + } + }); + this.on("end", () => resolve()); + this.on("error", () => resolve()); + this.on("close", () => resolve()); + }); + } + + __consume(): Promise { + if (this.bodyUsed) return Promise.reject(new TypeError("unusable")); + this.__consuming = true; + return new Promise((resolve, reject) => { + const chunks: Buffer[] = []; + let done = false; + this.on("data", (chunk: Buffer) => { + chunks.push(chunk); + }); + this.on("end", () => { + if (done) return; + done = true; + resolve(Buffer.concat(chunks)); + }); + this.on("error", (err: Error) => { + if (done) return; + done = true; + reject(err); + }); + this.on("close", () => { + if (done) return; + done = true; + reject(new RequestAbortedError()); + }); + }); + } +} + +/** 5.x decodes UTF-8 as-is; 7.x strips a leading byte-order mark first. */ +function decodeText(bytes: Buffer, profile: Profile): string { + if ( + profile.major >= 7 && + bytes.length >= 3 && + bytes[0] === 0xef && + bytes[1] === 0xbb && + bytes[2] === 0xbf + ) { + return bytes.subarray(3).toString("utf8"); + } + return bytes.toString("utf8"); +} diff --git a/packages/compiler/shims/undici/core/request.ts b/packages/compiler/shims/undici/core/request.ts new file mode 100644 index 000000000..a876e42f0 --- /dev/null +++ b/packages/compiler/shims/undici/core/request.ts @@ -0,0 +1,282 @@ +// undici's per-dispatch request record (lib/core/request.js): option +// validation, header processing, and the handler callbacks with their +// diagnostics_channel publications. +import { channels } from "./diagnostics.ts"; +import { InvalidArgumentError, NotSupportedError } from "./errors.ts"; +import type { DispatchHandlers, DispatchOptions, Profile } from "./types.ts"; + +const tokenRegExp = /^[\^_`a-zA-Z\-0-9!#$%&'*+.|~]+$/; +const headerCharRegex = /[^\t\x20-\x7e\x80-\xff]/; +const invalidPathRegex = /[^!-ÿ]/; + +export class Request { + origin: string; + method: string; + path: string; + body: Buffer | null; + /** Header name/value pairs in insertion order (host and content-length + * are consumed into their own fields, like undici). */ + headerPairs: string[]; + host: string | null; + contentLength: number | null; + contentType: string | null; + completed: boolean; + aborted: boolean; + idempotent: boolean; + blocking: boolean; + reset: boolean | null; + headersTimeout: number | null; + bodyTimeout: number | null; + throwOnError: boolean; + servername: string | null; + error: Error | null; + abortFn: ((err?: Error) => void) | null; + handler: DispatchHandlers; + /** The request as diagnostics_channel subscribers see it: one boxed + * value per request, so its identity is stable across publications. */ + diagnostic: unknown; + + constructor(origin: string, opts: DispatchOptions, handler: DispatchHandlers, profile: Profile) { + const path = opts.path; + const method = opts.method; + if (typeof path !== "string") { + throw new InvalidArgumentError("path must be a string"); + } else if ( + path[0] !== "/" && + !(path.startsWith("http://") || path.startsWith("https://")) && + method !== "CONNECT" + ) { + throw new InvalidArgumentError("path must be an absolute URL or start with a slash"); + } else if (invalidPathRegex.test(path)) { + throw new InvalidArgumentError("invalid request path"); + } + if (typeof method !== "string") { + throw new InvalidArgumentError("method must be a string"); + } else if (!tokenRegExp.test(method)) { + throw new InvalidArgumentError("invalid request method"); + } + if (method === "CONNECT") { + throw new NotSupportedError( + "scriptc's undici client does not implement CONNECT requests yet", + ); + } + const headersTimeout = opts.headersTimeout ?? null; + if (headersTimeout !== null && (!Number.isFinite(headersTimeout) || headersTimeout < 0)) { + throw new InvalidArgumentError("invalid headersTimeout"); + } + const bodyTimeout = opts.bodyTimeout ?? null; + if (bodyTimeout !== null && (!Number.isFinite(bodyTimeout) || bodyTimeout < 0)) { + throw new InvalidArgumentError("invalid bodyTimeout"); + } + + this.origin = origin; + this.method = method; + this.path = path; + this.headersTimeout = headersTimeout; + this.bodyTimeout = bodyTimeout; + // 7.x dropped throwOnError (the responseError interceptor replaced it). + if (profile.major >= 7 && opts.throwOnError !== undefined) { + throw new InvalidArgumentError("invalid throwOnError"); + } + this.throwOnError = opts.throwOnError === true; + + const body = opts.body ?? null; + if (body === null) { + this.body = null; + } else if (typeof body === "string") { + this.body = body.length > 0 ? Buffer.from(body) : null; + } else { + this.body = + body.byteLength > 0 ? Buffer.from(body.buffer, body.byteOffset, body.byteLength) : null; + } + + this.completed = false; + this.aborted = false; + this.idempotent = opts.idempotent ?? (method === "HEAD" || method === "GET"); + this.blocking = opts.blocking ?? (profile.major >= 7 ? method !== "HEAD" : false); + this.reset = opts.reset ?? null; + this.host = null; + this.contentLength = null; + this.contentType = null; + this.headerPairs = []; + this.error = null; + this.abortFn = null; + + const headers = opts.headers ?? null; + if (headers !== null) { + if (Array.isArray(headers)) { + if (headers.length % 2 !== 0) throw new InvalidArgumentError("headers array must be even"); + for (let i = 0; i < headers.length; i += 2) { + processHeader(this, headers[i]!, headers[i + 1]!, profile); + } + } else { + for (const key of Object.keys(headers)) { + const value = headers[key]; + if (value === undefined) continue; + if (typeof value === "string") { + processHeader(this, key, value, profile); + } else if (profile.major >= 7) { + for (const item of value) processHeader(this, key, item, profile); + } else { + throw new InvalidArgumentError(`invalid ${key} header`); + } + } + } + } + + this.servername = serverName(this.host); + this.handler = handler; + this.diagnostic = null; + if (channels.create.hasSubscribers) channels.create.publish({ request: this.diagnosticView() }); + } + + diagnosticView(): unknown { + let view = this.diagnostic; + if (view === null) { + view = { + origin: this.origin, + completed: this.completed, + method: this.method, + path: this.path, + headers: this.headers, + }; + this.diagnostic = view; + } + return view; + } + + /** The DiagnosticsChannel.Request view's raw header block. */ + get headers(): string { + let out = ""; + for (let i = 0; i < this.headerPairs.length; i += 2) { + out += `${this.headerPairs[i]}: ${this.headerPairs[i + 1]}\r\n`; + } + return out; + } + + addHeader(key: string, value: string): Request { + this.headerPairs.push(key, value); + return this; + } + + onBodySent(chunk: Buffer): void { + const cb = this.handler.onBodySent; + if (cb !== undefined) { + try { + this.handler.onBodySent!(chunk.length, chunk.length); + } catch (err) { + this.onError(err as Error); + } + } + } + + onRequestSent(): void { + if (channels.bodySent.hasSubscribers) + channels.bodySent.publish({ request: this.diagnosticView() }); + } + + onConnect(abort: (err?: Error) => void): void { + if (this.error !== null) { + abort(this.error); + return; + } + this.abortFn = abort; + if (this.handler.onConnect !== undefined) this.handler.onConnect!(abort); + } + + onHeaders( + statusCode: number, + headers: Buffer[], + resume: () => void, + statusText: string, + ): boolean { + if (channels.headers.hasSubscribers) { + channels.headers.publish({ + request: this.diagnosticView(), + response: { statusCode, headers, statusText }, + }); + } + if (this.handler.onHeaders === undefined) return true; + return this.handler.onHeaders!(statusCode, headers, resume, statusText); + } + + onData(chunk: Buffer): boolean { + if (this.handler.onData === undefined) return true; + return this.handler.onData!(chunk); + } + + onComplete(trailers: Buffer[]): void { + this.completed = true; + if (channels.trailers.hasSubscribers) + channels.trailers.publish({ request: this.diagnosticView(), trailers }); + if (this.handler.onComplete !== undefined) { + const raw: string[] = []; + for (const t of trailers) raw.push(t.toString("latin1")); + this.handler.onComplete!(raw); + } + } + + onError(error: Error): void { + if (channels.error.hasSubscribers) + channels.error.publish({ request: this.diagnosticView(), error }); + if (this.aborted) return; + this.aborted = true; + if (this.handler.onError !== undefined) this.handler.onError!(error); + } +} + +function processHeader(request: Request, key: string, val: string, profile: Profile): void { + const lower = key.toLowerCase(); + if (lower === "host") { + if (request.host === null) { + request.host = val; + return; + } + if (profile.major >= 7) throw new InvalidArgumentError("duplicate host header"); + } else if (lower === "content-length") { + if (request.contentLength === null) { + const n = parseInt(val, 10); + if (!Number.isFinite(n) || (profile.major >= 7 && !/^\d+$/.test(val))) { + throw new InvalidArgumentError("invalid content-length header"); + } + request.contentLength = n; + return; + } + if (profile.major >= 7) throw new InvalidArgumentError("duplicate content-length header"); + } + if (lower === "content-type" && request.contentType === null && !headerCharRegex.test(val)) { + request.contentType = val; + request.headerPairs.push(key, val); + return; + } + if (lower === "transfer-encoding" || lower === "keep-alive" || lower === "upgrade") { + throw new InvalidArgumentError(`invalid ${lower} header`); + } + if (lower === "connection") { + if (profile.major < 7) throw new InvalidArgumentError("invalid connection header"); + for (const token of val.toLowerCase().split(",")) { + const trimmed = token.trim(); + if (!tokenRegExp.test(trimmed)) throw new InvalidArgumentError("invalid connection header"); + if (trimmed === "close") request.reset = true; + } + return; + } + if (lower === "expect") throw new NotSupportedError("expect header not supported"); + if (!tokenRegExp.test(key)) throw new InvalidArgumentError("invalid header key"); + if (headerCharRegex.test(val)) throw new InvalidArgumentError(`invalid ${key} header`); + request.headerPairs.push(key, val); +} + +/** util.getServerName: the Host header without its port; null for IPs. */ +function serverName(host: string | null): string | null { + if (host === null || host === "") return null; + let name = host; + if (name.startsWith("[")) { + const idx = name.indexOf("]"); + if (idx !== -1) name = name.slice(1, idx); + return null; + } + const idx = name.indexOf(":"); + if (idx !== -1) name = name.slice(0, idx); + return /^\d+\.\d+\.\d+\.\d+$/.test(name) ? null : name; +} diff --git a/packages/compiler/shims/undici/core/timers.ts b/packages/compiler/shims/undici/core/timers.ts new file mode 100644 index 000000000..d5028b6eb --- /dev/null +++ b/packages/compiler/shims/undici/core/timers.ts @@ -0,0 +1,78 @@ +// undici 7.x's fast timers (lib/util/timers.js): one unref'd native timer +// ticking every 499 ms drives every headers/body timeout, so those fire on +// the first tick at least `delay` after the timer (re)started. +const RESOLUTION_MS = 1e3; +const TICK_MS = (RESOLUTION_MS >> 1) - 1; +const NOT_IN_LIST = -2; +const TO_BE_CLEARED = -1; +const PENDING = 0; +const ACTIVE = 1; + +let fastNow = 0; +// The ticking native timer, held as its refresh closure. +let refreshTick: (() => void) | null = null; +const fastTimers: FastTimer[] = []; + +function onTick(): void { + fastNow += TICK_MS; + let idx = 0; + let len = fastTimers.length; + while (idx < len) { + const timer = fastTimers[idx]!; + if (timer.state === PENDING) { + timer.idleStart = fastNow - TICK_MS; + timer.state = ACTIVE; + } else if (timer.state === ACTIVE && fastNow >= timer.idleStart + timer.idleTimeout) { + timer.state = TO_BE_CLEARED; + timer.idleStart = -1; + timer.onTimeout(); + } + if (timer.state === TO_BE_CLEARED) { + timer.state = NOT_IN_LIST; + if (--len !== 0) fastTimers[idx] = fastTimers[len]!; + } else { + ++idx; + } + } + fastTimers.length = len; + if (fastTimers.length !== 0) refreshTimeout(); +} + +function refreshTimeout(): void { + const refresh = refreshTick; + if (refresh !== null) { + refresh(); + } else { + const timeout = setTimeout(onTick, TICK_MS); + timeout.unref(); + refreshTick = () => { + timeout.refresh(); + }; + } +} + +export class FastTimer { + state: number; + idleTimeout: number; + idleStart: number; + onTimeout: () => void; + + constructor(callback: () => void, delay: number) { + this.state = NOT_IN_LIST; + this.idleTimeout = delay; + this.idleStart = -1; + this.onTimeout = callback; + this.refresh(); + } + + refresh(): void { + if (this.state === NOT_IN_LIST) fastTimers.push(this); + if (refreshTick === null || fastTimers.length === 1) refreshTimeout(); + this.state = PENDING; + } + + clear(): void { + this.state = TO_BE_CLEARED; + this.idleStart = -1; + } +} diff --git a/packages/compiler/shims/undici/core/types.ts b/packages/compiler/shims/undici/core/types.ts new file mode 100644 index 000000000..652918afa --- /dev/null +++ b/packages/compiler/shims/undici/core/types.ts @@ -0,0 +1,167 @@ +// Types shared by the undici entries. They restate the subset of undici's +// own declarations (types/dispatcher.d.ts, connector.d.ts, ...) that the +// shim implements; test/package-shims/undici-types.test.ts checks them +// against the installed 5.x and 7.x declarations. +import type { Socket } from "node:net"; +import type { Dispatcher } from "./dispatcher.ts"; + +/** Response and request header records (undici's IncomingHttpHeaders). */ +export type IncomingHttpHeaders = Record; + +/** What a request's `headers` option accepts here: a record or a flat + * [name, value, ...] array. */ +export type UndiciHeaders = Record | string[] | null; + +/** The payloads the shim writes. Streams, iterables, Blob and FormData + * bodies are outside the implemented subset. */ +export type RequestBody = string | Buffer | Uint8Array | null; + +export interface DispatchOptions { + origin?: string | URL; + path: string; + method: string; + body?: RequestBody; + headers?: UndiciHeaders; + idempotent?: boolean; + blocking?: boolean; + headersTimeout?: number | null; + bodyTimeout?: number | null; + reset?: boolean; + throwOnError?: boolean; + // Dispatcher.RequestOptions' members live here too: a record passed to + // dispatch() is copied to this shape, and interceptors read them back + // through a `params as Dispatcher.RequestOptions` cast, as with undici. + opaque?: unknown; + signal?: AbortSignal | null; + onInfo?: (info: { statusCode: number; headers: IncomingHttpHeaders }) => void; + highWaterMark?: number; +} + +/** undici's dispatch-controller (7.x handler API). */ +export interface DispatchController { + readonly aborted: boolean; + readonly paused: boolean; + readonly reason: Error | null; + abort(reason: Error): void; + pause(): void; + resume(): void; +} + +/** Dispatch handlers: the 5.x callbacks, plus 7.x's controller callbacks. */ +export interface DispatchHandlers { + onConnect?(abort: (err?: Error) => void): void; + onError?(err: Error): void; + onHeaders?( + statusCode: number, + headers: Buffer[], + resume: () => void, + statusText: string, + ): boolean; + onData?(chunk: Buffer): boolean; + onComplete?(trailers: string[] | null): void; + onBodySent?(chunkSize: number, totalBytesSent: number): void; + onRequestStart?(controller: DispatchController, context: unknown): void; + onResponseStart?( + controller: DispatchController, + statusCode: number, + headers: IncomingHttpHeaders, + statusMessage?: string, + ): void; + onResponseData?(controller: DispatchController, chunk: Buffer): void; + onResponseEnd?(controller: DispatchController, trailers: IncomingHttpHeaders): void; + onResponseError?(controller: DispatchController, error: Error): void; +} + +export type DispatchFn = (options: DispatchOptions, handler: DispatchHandlers) => boolean; + +export type DispatchInterceptor = (dispatch: DispatchFn) => DispatchFn; + +/** buildConnector's per-connection parameters. */ +export interface ConnectorOptions { + hostname: string; + host?: string; + protocol: string; + port: string; + servername?: string; + localAddress?: string | null; +} + +export type ConnectorCallback = (err: Error | null, socket: Socket | null) => void; + +/** A connector: dials, reports through the callback, and returns the socket + * (undici 5 and 7 both return it although their types declare void). */ +export type Connector = (options: ConnectorOptions, callback: ConnectorCallback) => Socket; + +/** net/tls connect options a connector forwards (the implemented subset). */ +export interface ConnectorBuildOptions { + timeout?: number | null; + maxCachedSessions?: number | null; + socketPath?: string | null; + port?: number; + rejectUnauthorized?: boolean; + ca?: string | Buffer | (string | Buffer)[]; + servername?: string; + /** net.connect's resolver hook: @types/node's LookupFunction is + * assignable (method syntax; the callback takes one address or all). */ + lookup?( + hostname: string, + options: unknown, + callback: ( + err: NodeJS.ErrnoException | null, + address: string | LookupAddress[], + family?: number, + ) => void, + ): void; + keepAlive?: boolean; + keepAliveInitialDelay?: number; + noDelay?: boolean; + autoSelectFamily?: boolean; + autoSelectFamilyAttemptTimeout?: number; +} + +export interface LookupAddress { + address: string; + family: number; +} + +export interface ClientOptions { + keepAliveTimeout?: number | null; + keepAliveMaxTimeout?: number | null; + keepAliveTimeoutThreshold?: number | null; + pipelining?: number | null; + connect?: ConnectorBuildOptions | Connector | null; + maxHeaderSize?: number | null; + bodyTimeout?: number | null; + headersTimeout?: number | null; + strictContentLength?: boolean; + maxRequestsPerClient?: number; + maxResponseSize?: number | null; + connectTimeout?: number | null; + localAddress?: string; + /** 5.x applies `Client` interceptors; 7.x ignores the option. */ + interceptors?: { Client?: readonly DispatchInterceptor[] | undefined }; +} + +export interface PoolOptions extends ClientOptions { + factory?(origin: URL, opts: object): Dispatcher; + connections?: number | null; + interceptors?: { + Pool?: readonly DispatchInterceptor[] | undefined; + Client?: readonly DispatchInterceptor[] | undefined; + }; +} + +export interface AgentOptions extends PoolOptions { + factory?(origin: string | URL, opts: object): Dispatcher; + interceptors?: { + Agent?: readonly DispatchInterceptor[] | undefined; + Pool?: readonly DispatchInterceptor[] | undefined; + Client?: readonly DispatchInterceptor[] | undefined; + }; +} + +/** Version-dependent behavior, selected by the importing package's installed + * undici (the 5.x or 7.x entry). */ +export interface Profile { + major: number; +} diff --git a/packages/compiler/shims/undici/core/util.ts b/packages/compiler/shims/undici/core/util.ts new file mode 100644 index 000000000..2fc9ffbf6 --- /dev/null +++ b/packages/compiler/shims/undici/core/util.ts @@ -0,0 +1,67 @@ +// Helpers from undici's lib/core/util.js. +import { InvalidArgumentError } from "./errors.ts"; +import type { IncomingHttpHeaders, Profile } from "./types.ts"; + +/** util.parseHeaders over raw [name, value, ...] pairs: lower-cased names, + * repeated names collected into arrays. 5.x decodes values as UTF-8 and + * treats an empty first value as absent; 7.x decodes latin1. */ +export function parseHeaders(raw: readonly string[], profile: Profile): IncomingHttpHeaders { + const obj: IncomingHttpHeaders = {}; + for (let i = 0; i + 1 < raw.length; i += 2) { + const key = raw[i]!.toLowerCase(); + const value = raw[i + 1]!; + const existing = obj[key]; + const absent = profile.major >= 7 ? existing === undefined : !existing; + if (absent) { + obj[key] = value; + } else if (typeof existing === "string") { + obj[key] = [existing, value]; + } else if (existing !== undefined) { + existing.push(value); + } + } + return obj; +} + +/** Raw header Buffers as strings (latin1 on 7.x, UTF-8 on 5.x). */ +export function rawHeaderStrings(raw: readonly Buffer[], profile: Profile): string[] { + const out: string[] = []; + for (let i = 0; i < raw.length; i++) { + // Names are ASCII tokens; values follow the version's decoding. + out.push( + i % 2 === 0 || profile.major >= 7 ? raw[i]!.toString("latin1") : raw[i]!.toString("utf8"), + ); + } + return out; +} + +/** util.parseOrigin: an http(s) origin URL. */ +export function parseOrigin(url: string | URL, profile: Profile): URL { + const parsed = parseURL(url, profile); + if (parsed.pathname !== "/" || parsed.search !== "" || parsed.hash !== "") { + throw new InvalidArgumentError("invalid url"); + } + return parsed; +} + +/** util.parseURL for the string/URL forms. */ +export function parseURL(url: string | URL, profile: Profile): URL { + const parsed = typeof url === "string" ? new URL(url) : url; + if (parsed.protocol !== "http:" && parsed.protocol !== "https:") { + // 7.x checks only string inputs; 5.x checks every input. + if (profile.major < 7) throw new InvalidArgumentError("invalid protocol"); + if (typeof url === "string") { + throw new InvalidArgumentError( + "Invalid URL protocol: the URL must start with `http:` or `https:`.", + ); + } + } + return parsed; +} + +/** util.parseKeepAliveTimeout: the server's Keep-Alive `timeout=` hint in + * milliseconds, or null. */ +export function parseKeepAliveTimeout(value: string): number | null { + const m = /timeout=(\d+)/.exec(value); + return m !== null ? parseInt(m[1]!, 10) * 1000 : null; +} diff --git a/packages/compiler/shims/undici/v5.ts b/packages/compiler/shims/undici/v5.ts new file mode 100644 index 000000000..b97c65dfa --- /dev/null +++ b/packages/compiler/shims/undici/v5.ts @@ -0,0 +1,119 @@ +// scriptc's built-in implementation of the `undici` package, 5.x API. +// +// Programs whose installed undici is 5.x compile this module in its place +// (see packages/compiler/src/frontend/package-shims.ts). It implements the +// HTTP/1.1 client subset over node:net / node:tls: Dispatcher, Client, Pool, +// Agent, request(), buildConnector, errors, the global dispatcher, fetch and +// the diagnostics_channel publications. Anything else undici exports is not +// declared here, so using it is a compile-time error. +import { Agent as CoreAgent } from "./core/agent.ts"; +import { + getGlobalDispatcher as coreGetGlobalDispatcher, + request as coreRequest, + setGlobalDispatcher as coreSetGlobalDispatcher, +} from "./core/api.ts"; +import type { TopLevelRequestOptions } from "./core/api.ts"; +import { Client as CoreClient } from "./core/client.ts"; +import { buildConnector as coreBuildConnector } from "./core/connector.ts"; +import { Dispatcher } from "./core/dispatcher.ts"; +import type { ResponseData } from "./core/dispatcher.ts"; +import { undiciFetch } from "./core/fetch.ts"; +import type { UndiciRequestInit } from "./core/fetch.ts"; +import { Pool as CorePool, PoolStats as CorePoolStats } from "./core/pool.ts"; +import type { + AgentOptions, + ClientOptions, + Connector, + ConnectorBuildOptions, + ConnectorCallback, + ConnectorOptions, + DispatchOptions, + PoolOptions, + Profile, +} from "./core/types.ts"; +import type { SocketInfo as CoreSocketInfo } from "./core/errors.ts"; + +const profile: Profile = { major: 5 }; + +export { Dispatcher }; +export type { DiagnosticsChannel } from "./core/diagnostics-types.ts"; +export * as errors from "./core/errors-v5.ts"; + +export class Client extends CoreClient { + constructor(url: string | URL, options?: Client.Options) { + super(url, options, profile); + } +} + +export declare namespace Client { + type Options = ClientOptions; + type SocketInfo = CoreSocketInfo; +} + +export class Pool extends CorePool { + constructor(url: string | URL, options?: Pool.Options) { + super(url, options, profile); + } + + __createClient(origin: URL, options: ClientOptions): Dispatcher { + return new Client(origin, options); + } +} + +export declare namespace Pool { + type Options = PoolOptions; + type PoolStats = CorePoolStats; +} + +export class Agent extends CoreAgent { + constructor(options?: Agent.Options) { + super(options, profile); + } + + __createClient(origin: string | URL, options: PoolOptions): Dispatcher { + return new Client(origin, options); + } + + __createPool(origin: string | URL, options: PoolOptions): Dispatcher { + return new Pool(origin, options); + } +} + +export declare namespace Agent { + type Options = AgentOptions; + type DispatchOptions = import("./core/types.ts").DispatchOptions; +} + +export function buildConnector(options?: buildConnector.BuildOptions): buildConnector.connector { + return coreBuildConnector(options ?? {}); +} + +export declare namespace buildConnector { + type BuildOptions = ConnectorBuildOptions; + type Options = ConnectorOptions; + type Callback = ConnectorCallback; + type connector = Connector; +} + +export function setGlobalDispatcher(dispatcher: Dispatcher): void { + coreSetGlobalDispatcher(dispatcher); +} + +export function getGlobalDispatcher(): Dispatcher { + return coreGetGlobalDispatcher(() => new Agent()); +} + +export function request( + url: string | URL, + options?: TopLevelRequestOptions, +): Promise { + return coreRequest(url, options, () => new Agent(), profile); +} + +export function fetch(input: string | URL, init?: UndiciRequestInit): Promise { + return undiciFetch(input, init, () => new Agent(), profile); +} + +// undici's Headers is the WHATWG class the runtime implements natively. +export import Headers = globalThis.Headers; +export type { DispatchOptions }; diff --git a/packages/compiler/shims/undici/v7.ts b/packages/compiler/shims/undici/v7.ts new file mode 100644 index 000000000..63b1b1f64 --- /dev/null +++ b/packages/compiler/shims/undici/v7.ts @@ -0,0 +1,119 @@ +// scriptc's built-in implementation of the `undici` package, 7.x API. +// +// Programs whose installed undici is 7.x compile this module in its place +// (see packages/compiler/src/frontend/package-shims.ts). It implements the +// HTTP/1.1 client subset over node:net / node:tls: Dispatcher, Client, Pool, +// Agent, request(), buildConnector, errors, the global dispatcher, fetch and +// the diagnostics_channel publications. Anything else undici exports is not +// declared here, so using it is a compile-time error. +import { Agent as CoreAgent } from "./core/agent.ts"; +import { + getGlobalDispatcher as coreGetGlobalDispatcher, + request as coreRequest, + setGlobalDispatcher as coreSetGlobalDispatcher, +} from "./core/api.ts"; +import type { TopLevelRequestOptions } from "./core/api.ts"; +import { Client as CoreClient } from "./core/client.ts"; +import { buildConnector as coreBuildConnector } from "./core/connector.ts"; +import { Dispatcher } from "./core/dispatcher.ts"; +import type { ResponseData } from "./core/dispatcher.ts"; +import { undiciFetch } from "./core/fetch.ts"; +import type { UndiciRequestInit } from "./core/fetch.ts"; +import { Pool as CorePool, PoolStats as CorePoolStats } from "./core/pool.ts"; +import type { + AgentOptions, + ClientOptions, + Connector, + ConnectorBuildOptions, + ConnectorCallback, + ConnectorOptions, + DispatchOptions, + PoolOptions, + Profile, +} from "./core/types.ts"; +import type { SocketInfo as CoreSocketInfo } from "./core/errors.ts"; + +const profile: Profile = { major: 7 }; + +export { Dispatcher }; +export type { DiagnosticsChannel } from "./core/diagnostics-types.ts"; +export * as errors from "./core/errors-v7.ts"; + +export class Client extends CoreClient { + constructor(url: string | URL, options?: Client.Options) { + super(url, options, profile); + } +} + +export declare namespace Client { + type Options = ClientOptions; + type SocketInfo = CoreSocketInfo; +} + +export class Pool extends CorePool { + constructor(url: string | URL, options?: Pool.Options) { + super(url, options, profile); + } + + __createClient(origin: URL, options: ClientOptions): Dispatcher { + return new Client(origin, options); + } +} + +export declare namespace Pool { + type Options = PoolOptions; + type PoolStats = CorePoolStats; +} + +export class Agent extends CoreAgent { + constructor(options?: Agent.Options) { + super(options, profile); + } + + __createClient(origin: string | URL, options: PoolOptions): Dispatcher { + return new Client(origin, options); + } + + __createPool(origin: string | URL, options: PoolOptions): Dispatcher { + return new Pool(origin, options); + } +} + +export declare namespace Agent { + type Options = AgentOptions; + type DispatchOptions = import("./core/types.ts").DispatchOptions; +} + +export function buildConnector(options?: buildConnector.BuildOptions): buildConnector.connector { + return coreBuildConnector(options ?? {}); +} + +export declare namespace buildConnector { + type BuildOptions = ConnectorBuildOptions; + type Options = ConnectorOptions; + type Callback = ConnectorCallback; + type connector = Connector; +} + +export function setGlobalDispatcher(dispatcher: Dispatcher): void { + coreSetGlobalDispatcher(dispatcher); +} + +export function getGlobalDispatcher(): Dispatcher { + return coreGetGlobalDispatcher(() => new Agent()); +} + +export function request( + url: string | URL, + options?: TopLevelRequestOptions, +): Promise { + return coreRequest(url, options, () => new Agent(), profile); +} + +export function fetch(input: string | URL, init?: UndiciRequestInit): Promise { + return undiciFetch(input, init, () => new Agent(), profile); +} + +// undici's Headers is the WHATWG class the runtime implements natively. +export import Headers = globalThis.Headers; +export type { DispatchOptions }; diff --git a/packages/compiler/src/backend/executable-features.ts b/packages/compiler/src/backend/executable-features.ts index a565965f1..e9629b83c 100644 --- a/packages/compiler/src/backend/executable-features.ts +++ b/packages/compiler/src/backend/executable-features.ts @@ -16,6 +16,8 @@ export function executableLinkFeatures(mod: IrModule): NativeLinkFeatures { fileHandle: features.fileHandle, fetch: features.fetch, ...(features.eventTarget ? { eventTarget: true } : {}), + ...(features.warningReplace ? { warningReplace: true } : {}), + ...(features.stdioExtended ? { stdioExtended: true } : {}), zlib: features.zlib, assert: features.assert, inspect: features.inspect, @@ -40,5 +42,6 @@ export function executableLinkFeatures(mod: IrModule): NativeLinkFeatures { tls: features.tls, tlsCa: features.tlsCa, cipher: features.cipher, + webcrypto: features.webcrypto, }; } diff --git a/packages/compiler/src/backend/library-pack.ts b/packages/compiler/src/backend/library-pack.ts index 833ef93e7..e1fd9784b 100644 --- a/packages/compiler/src/backend/library-pack.ts +++ b/packages/compiler/src/backend/library-pack.ts @@ -52,6 +52,7 @@ export async function compilePackedLibrary( nodeTest: false, tls: false, cipher: false, + webcrypto: false, tlsCa: false, }; const selection = await loadRuntimePack({ diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index f20c63b1d..138c49a34 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -3671,13 +3671,30 @@ export class LlEmitter { } defs.push(...helper.define(this.sizeType, FN_ATTRS, scanInline), ``); } - // Ordering also decides inline when the first bytes differ: byte order - // is the plain comparison's order, and the UTF-16 order whenever both - // bytes are ASCII. Callers only test the sign of the result. - for (const fn of ["scr_str_cmp", "scr_str_cmp_u16"]) { - if (!this.decls.has(`declare i32 @${fn}(ptr, ptr)`)) continue; + // Ordering decides inline from the first differing bytes. The bytes + // before them are equal, so both strings are at the same position of a + // character: either both bytes continue characters with the same lead + // byte, or both start a character. Byte order is the UTF-16 order for + // two continuation bytes, and for two lead bytes when one starts a + // character below U+D000, whose UTF-16 unit is below every unit of a + // character with a lead byte of at least 0xED. Between lead bytes of at + // least 0xED, a supplementary character (0xF0..0xF4, a high surrogate + // unit) orders below U+E000..U+FFFF (0xEE, 0xEF), and a lead byte of + // 0xED (U+D000..U+D7FF or a lone surrogate) takes the runtime. + // Differing words without a byte of at least 0xED decide in byte + // order; other differing words decide by their first differing byte, + // and strings shorter than a word by their first byte. Otherwise a + // proven-ASCII string on either side orders by bytes (scr_str_cmp), and + // other pairs call scr_str_cmp_u16_bytes, which only reads the two + // strings, so LLVM can merge repeated comparisons of one pair. Callers + // only test the sign of the result. + if (this.decls.has(`declare i32 @scr_str_cmp_u16(ptr, ptr)`)) { + const fn = "scr_str_cmp_u16"; this.declare("declare i64 @llvm.bswap.i64(i64)"); - const utf16 = fn === "scr_str_cmp_u16"; + this.declare("declare i64 @llvm.cttz.i64(i64, i1 immarg)"); + this.declare("declare i8 @llvm.umin.i8(i8, i8)"); + this.declare("declare i32 @scr_str_cmp(ptr, ptr)"); + this.declare("declare i32 @scr_str_cmp_u16_bytes(ptr, ptr)"); defs.push( `define internal i32 @${fn.replace(/^scr_/, "sc_")}(ptr %a, ptr %b) ${FN_ATTRS} {`, `entry:`, @@ -3692,8 +3709,7 @@ export class LlEmitter { ` %bempty = icmp eq ${this.sizeType} %blen, 0`, ` %empty = or i1 %aempty, %bempty`, ` br i1 %empty, label %order, label %words`, - // Both at least 8 bytes: the first differing byte of one word pair. - // Byte-swapped little-endian words compare in byte order. + // Both at least 8 bytes: the first word of each. `words:`, ` %along = icmp uge ${this.sizeType} %alen, 8`, ` %blong = icmp uge ${this.sizeType} %blen, 8`, @@ -3705,22 +3721,7 @@ export class LlEmitter { ` %aw = load i64, ptr %awp, align 1`, ` %bw = load i64, ptr %bwp, align 1`, ` %wdiffer = icmp ne i64 %aw, %bw`, - ` br i1 %wdiffer, label %wdiff, label %wsame`, - `wdiff:`, - ...(utf16 - ? [ - ` %wbits = or i64 %aw, %bw`, - ` %whigh = and i64 %wbits, -9187201950435737472`, - ` %wascii = icmp eq i64 %whigh, 0`, - ` br i1 %wascii, label %wordorder, label %order`, - ] - : [` br label %wordorder`]), - `wordorder:`, - ` %asw = call i64 @llvm.bswap.i64(i64 %aw)`, - ` %bsw = call i64 @llvm.bswap.i64(i64 %bw)`, - ` %wbelow = icmp ult i64 %asw, %bsw`, - ` %wsign = select i1 %wbelow, i32 -1, i32 1`, - ` ret i32 %wsign`, + ` br i1 %wdiffer, label %differ, label %wsame`, // Equal first words and one length of at most 16 bytes: the word // ending at that length covers the rest, and any difference lies // past the equal first word. @@ -3736,42 +3737,89 @@ export class LlEmitter { ` %at = load i64, ptr %atp, align 1`, ` %bt = load i64, ptr %btp, align 1`, ` %tdiffer = icmp ne i64 %at, %bt`, - ` br i1 %tdiffer, label %tdiff, label %equal`, - `tdiff:`, - ...(utf16 - ? [ - ` %tbits = or i64 %at, %bt`, - ` %thigh = and i64 %tbits, -9187201950435737472`, - ` %tascii = icmp eq i64 %thigh, 0`, - ` br i1 %tascii, label %tailorder, label %order`, - ] - : [` br label %tailorder`]), - `tailorder:`, - ` %ats = call i64 @llvm.bswap.i64(i64 %at)`, - ` %bts = call i64 @llvm.bswap.i64(i64 %bt)`, - ` %tbelow = icmp ult i64 %ats, %bts`, - ` %tsign = select i1 %tbelow, i32 -1, i32 1`, - ` ret i32 %tsign`, + ` br i1 %tdiffer, label %differ, label %equal`, `first:`, ` %adata = getelementptr inbounds %ScrStr, ptr %a, i32 1`, ` %bdata = getelementptr inbounds %ScrStr, ptr %b, i32 1`, ` %x = load i8, ptr %adata`, ` %y = load i8, ptr %bdata`, - ` %differ = icmp ne i8 %x, %y`, - ...(utf16 - ? [ - ` %bits = or i8 %x, %y`, - ` %ascii = icmp sge i8 %bits, 0`, - ` %decide = and i1 %differ, %ascii`, - ] - : [` %decide = and i1 %differ, true`]), - ` br i1 %decide, label %byte, label %order`, + ` %fdiffer = icmp ne i8 %x, %y`, + ` br i1 %fdiffer, label %bytes, label %order`, + // Two differing words. When neither has a byte of at least 0xED + // (a byte b is when its high bit and bit 7 of (b & 0x7f) + 0x13 are + // set), byte-swapped little-endian words compare in byte order. + // Otherwise the lowest set byte of their xor is the first differing + // byte. + `differ:`, + ` %dx = phi i64 [ %aw, %word ], [ %at, %tail ]`, + ` %dy = phi i64 [ %bw, %word ], [ %bt, %tail ]`, + ` %xl = and i64 %dx, 9187201950435737471`, + ` %yl = and i64 %dy, 9187201950435737471`, + ` %xa = add i64 %xl, 1374463283923456787`, + ` %ya = add i64 %yl, 1374463283923456787`, + ` %xh = and i64 %dx, %xa`, + ` %yh = and i64 %dy, %ya`, + ` %hh = or i64 %xh, %yh`, + ` %whigh = and i64 %hh, -9187201950435737472`, + ` %dlow = icmp eq i64 %whigh, 0`, + ` br i1 %dlow, label %wordorder, label %locate`, + `wordorder:`, + ` %xsw = call i64 @llvm.bswap.i64(i64 %dx)`, + ` %ysw = call i64 @llvm.bswap.i64(i64 %dy)`, + ` %wbelow = icmp ult i64 %xsw, %ysw`, + ` %wsign = select i1 %wbelow, i32 -1, i32 1`, + ` ret i32 %wsign`, + `locate:`, + ` %dxor = xor i64 %dx, %dy`, + ` %dtz = call i64 @llvm.cttz.i64(i64 %dxor, i1 true)`, + ` %dshift = and i64 %dtz, 56`, + ` %dxs = lshr i64 %dx, %dshift`, + ` %dys = lshr i64 %dy, %dshift`, + ` %dxb = trunc i64 %dxs to i8`, + ` %dyb = trunc i64 %dys to i8`, + ` br label %bytes`, + // The first differing bytes. + `bytes:`, + ` %bx = phi i8 [ %x, %first ], [ %dxb, %locate ]`, + ` %by = phi i8 [ %y, %first ], [ %dyb, %locate ]`, + ` %bmin = call i8 @llvm.umin.i8(i8 %bx, i8 %by)`, + ` %blow = icmp ult i8 %bmin, -19`, + ` br i1 %blow, label %byte, label %high`, + // Both at least 0xED. A lone surrogate (0xED) takes the runtime; + // supplementary lead bytes (0xF0..0xF4) order below 0xEE and 0xEF + // once 0x10 is subtracted from them. + `high:`, + ` %xed = icmp eq i8 %bx, -19`, + ` %yed = icmp eq i8 %by, -19`, + ` %ed = or i1 %xed, %yed`, + ` br i1 %ed, label %order, label %planes`, + `planes:`, + ` %xsupp = icmp uge i8 %bx, -16`, + ` %ysupp = icmp uge i8 %by, -16`, + ` %xlow = add i8 %bx, -16`, + ` %ylow = add i8 %by, -16`, + ` %xk = select i1 %xsupp, i8 %xlow, i8 %bx`, + ` %yk = select i1 %ysupp, i8 %ylow, i8 %by`, + ` %kbelow = icmp ult i8 %xk, %yk`, + ` %ksign = select i1 %kbelow, i32 -1, i32 1`, + ` ret i32 %ksign`, `byte:`, - ` %below = icmp ult i8 %x, %y`, + ` %below = icmp ult i8 %bx, %by`, ` %sign = select i1 %below, i32 -1, i32 1`, ` ret i32 %sign`, `order:`, - ` %c = call i32 @${fn}(ptr %a, ptr %b)`, + ` %acw.p = getelementptr inbounds %ScrStr, ptr %a, i32 0, i32 2`, + ` %bcw.p = getelementptr inbounds %ScrStr, ptr %b, i32 0, i32 2`, + ` %acw = load i32, ptr %acw.p`, + ` %bcw = load i32, ptr %bcw.p`, + ` %cws = or i32 %acw, %bcw`, + ` %anyascii = icmp slt i32 %cws, 0`, + ` br i1 %anyascii, label %memorder, label %units`, + `memorder:`, + ` %m = call i32 @scr_str_cmp(ptr %a, ptr %b)`, + ` ret i32 %m`, + `units:`, + ` %c = call i32 @scr_str_cmp_u16_bytes(ptr %a, ptr %b) nounwind willreturn memory(argmem: read)`, ` ret i32 %c`, `equal:`, ` ret i32 0`, @@ -5881,6 +5929,8 @@ export class LlEmitter { // Process streams use a numeric descriptor internally, including zero // for stdin. They remain JavaScript objects in boolean contexts. case "procStream": + // A module namespace object is always present. + case "moduleNs": return "true"; // A module namespace (a required builtin module object) is an object. case "moduleNs": diff --git a/packages/compiler/src/backend/llvm/expr-primitives.ts b/packages/compiler/src/backend/llvm/expr-primitives.ts index 4e7d418f8..35e759110 100644 --- a/packages/compiler/src/backend/llvm/expr-primitives.ts +++ b/packages/compiler/src/backend/llvm/expr-primitives.ts @@ -379,7 +379,29 @@ export function emitOperatorExpr( const fn = libm[e.op]; if (fn === undefined) throw new LlvmUnsupportedError(`bin:${e.op}`, e.loc); host.declare(`declare double @${fn}(double, double)`); - B.line(`${t} = call double @${fn}(double ${l.name}, double ${r.name})`); + // libm's pow answers 1 for pow(±1, ±Infinity) and pow(1, NaN), where + // `**` answers NaN (and LLVM folds pow(1.0, x) to 1.0): an inline + // guard selects NaN for a base of ±1 with a non-finite exponent and + // keeps the direct pow call. A finite literal exponent, or a literal + // base other than ±1, cannot meet those cases and needs no guard. + if (fn === "pow" && !libmPowAgrees(e.left, e.right)) { + host.declare(`declare double @llvm.fabs.f64(double)`); + const power = B.tmp(); + B.line(`${power} = call double @pow(double ${l.name}, double ${r.name})`); + const base = B.tmp(); + const unit = B.tmp(); + const magnitude = B.tmp(); + const finite = B.tmp(); + const reject = B.tmp(); + B.line(`${base} = call double @llvm.fabs.f64(double ${l.name})`); + B.line(`${unit} = fcmp oeq double ${base}, 1.0`); + B.line(`${magnitude} = call double @llvm.fabs.f64(double ${r.name})`); + B.line(`${finite} = fcmp olt double ${magnitude}, 0x7FF0000000000000`); + B.line(`${reject} = select i1 ${finite}, i1 false, i1 ${unit}`); + B.line(`${t} = select i1 ${reject}, double 0x7FF8000000000000, double ${power}`); + } else { + B.line(`${t} = call double @${fn}(double ${l.name}, double ${r.name})`); + } } return { name: t, type: e.type }; } @@ -602,7 +624,7 @@ export function emitStringExpr( } case "strCmp": { const [l, r] = emitStringInputs(host, [e.left, e.right]); - const fn = e.utf16 === true ? "scr_str_cmp_u16" : "scr_str_cmp"; + const fn = "scr_str_cmp_u16"; host.declare(`declare i32 @${fn}(ptr, ptr)`); const c = B.tmp(); const t = B.tmp(); @@ -1106,3 +1128,19 @@ export function emitRecordLitFields( host.initField(ptr, type, inline, v); } } + +/** Whether libm pow matches `**` for these operands: every case except a + * base of ±1 with a NaN or infinite exponent agrees. */ +function libmPowAgrees(base: IrExpr, exponent: IrExpr): boolean { + const power = numericLiteral(exponent); + if (power !== null && Number.isFinite(power)) return true; + const value = numericLiteral(base); + return value !== null && !Number.isNaN(value) && Math.abs(value) !== 1; +} + +function numericLiteral(expr: IrExpr): number | null { + if (expr.kind === "numLit") return expr.value; + if (expr.kind === "unary" && expr.op === "-" && expr.operand.kind === "numLit") + return -expr.operand.value; + return null; +} diff --git a/packages/compiler/src/backend/llvm/lib-dispatch.ts b/packages/compiler/src/backend/llvm/lib-dispatch.ts index daa8e08f3..3a0e007db 100644 --- a/packages/compiler/src/backend/llvm/lib-dispatch.ts +++ b/packages/compiler/src/backend/llvm/lib-dispatch.ts @@ -832,6 +832,7 @@ export function emitLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue { case "util": case "bigint": case "crypto": + case "webcrypto": case "buffer": case "bytes": case "tlsca": diff --git a/packages/compiler/src/backend/llvm/lib-process.ts b/packages/compiler/src/backend/llvm/lib-process.ts index e6d8acdbe..eaf7f31cd 100644 --- a/packages/compiler/src/backend/llvm/lib-process.ts +++ b/packages/compiler/src/backend/llvm/lib-process.ts @@ -546,15 +546,24 @@ export function emitProcessLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll B.line(`call void @scr_stdin_on_end(ptr ${cb.name}, i1 ${once.name})`); return { name: "", type: e.type }; } + // A string listener takes the setEncoding-decoded chunk. + const stringData = e.fn === "stdin.onData" && cbT.params[0]?.kind === "string"; const adapter = e.fn === "stdin.onData" ? cbT.params.length === 0 ? "scr_stdin_data_thunk0" - : "scr_stdin_data_thunk_bytes" + : stringData + ? "scr_stdin_data_thunk_str" + : "scr_stdin_data_thunk_bytes" : cbT.params.length === 0 ? "scr_child_err_thunk0" : "scr_child_err_thunk_error"; - const sym = e.fn === "stdin.onData" ? "scr_stdin_on_data" : "scr_stdin_on_error"; + const sym = + e.fn === "stdin.onData" + ? stringData + ? "scr_stdin_on_data_str" + : "scr_stdin_on_data" + : "scr_stdin_on_error"; host.declare(`declare void @${adapter}(ptr, ptr)`); host.declare(`declare void @${sym}(ptr, ptr, i1 zeroext)`); B.line(`call void @${sym}(ptr ${cb.name}, ptr @${adapter}, i1 ${once.name})`); diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index be67e1b67..2d91386b3 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -94,6 +94,8 @@ export const LIB_FN_SYMS: Record = { "num.parseInt": "scr_parse_int", "num.parseFloat": "scr_parse_float", "str.localeCompare": "scr_str_locale_compare", + "str.substr": "scr_str_substr", + "str.localeCase": "scr_str_locale_case", "str.replaceLiteral": "scr_str_replace_literal", "str.replaceChars": "scr_str_replace_chars", "num.fromString": "scr_string_to_number", @@ -143,6 +145,8 @@ export const LIB_FN_SYMS: Record = { "num.toExponential": "scr_num_to_exponential", "num.toFixed0": "scr_num_to_fixed0", "num.toFixed": "scr_num_to_fixed", + "num.toPrecision": "scr_num_to_precision", + "num.toExponentialDigits": "scr_num_to_exponential_digits", "num.toStringRadix": "scr_num_to_string_radix", "num.toStringRadixF64": "scr_num_to_string_radix_f64", "num.sameValue": "scr_num_same_value", @@ -195,6 +199,7 @@ export const LIB_FN_SYMS: Record = { "os.cpusModel": "scr_os_cpus_model", "os.cpusField": "scr_os_cpus_field", "os.constants": "scr_os_constants", + "fs.constants": "scr_fs_constants_object", "constants.object": "scr_node_constants", "os.release": "scr_os_release", "os.userName": "scr_os_user_name", @@ -408,6 +413,7 @@ export const LIB_FN_SYMS: Record = { "buffer.fromStr": "scr_bytes_from_str", "buffer.fromStrChecked": "scr_bytes_from_str_checked", "buffer.brand": "scr_bytes_as_buffer", + "buffer.isBranded": "scr_bytes_is_buffer", "buffer.fromDyn": "scr_buffer_from_dyn", "buffer.concat": "scr_buffer_concat", "buffer.concatLen": "scr_buffer_concat_len", @@ -574,12 +580,19 @@ export const LIB_FN_SYMS: Record = { "crypto.randomIntMax": "scr_crypto_random_int_max", "crypto.randomFillView": "scr_crypto_random_fill", "crypto.randomFillViewRest": "scr_crypto_random_fill_rest", + "crypto.getRandomValues": "scr_crypto_get_random_values", + "crypto.getRandomValuesDyn": "scr_crypto_get_random_values_dyn", "crypto.randomFillSyncDyn": "scr_crypto_random_fill_sync_dyn", "crypto.randomUUIDOpts": "scr_crypto_random_uuid_opts", "crypto.createSecretKey": "scr_crypto_create_secret_key", "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.sign": "scr_webcrypto_sign", + "webcrypto.verify": "scr_webcrypto_verify", + "webcrypto.digest": "scr_webcrypto_digest", + "webcrypto.isCryptoKey": "scr_webcrypto_is_crypto_key", "crypto.randomUUIDv7": "scr_crypto_random_uuid_v7", "crypto.pbkdf2": "scr_crypto_pbkdf2", "crypto.hkdf": "scr_crypto_hkdf", @@ -1328,6 +1341,7 @@ export const LIB_FN_SYMS: Record = { "process.emitWarningText": "scr_process_emit_warning_text", "process.deprecationFlag": "scr_process_deprecation_flag", "process.setDeprecationFlag": "scr_process_set_deprecation_flag", + "process.setEmitWarning": "scr_process_set_emit_warning", "process.onUncaughtException": "scr_process_on_uncaught_exception", "process.offUncaughtException": "scr_process_off_uncaught_exception", "process.onUnhandledRejection": "scr_process_on_unhandled_rejection", @@ -1463,6 +1477,10 @@ export const USES_TIMERS_LIB_FNS = new Set([ "process.emitWarning", // Decipheriv.setAuthTag reports DEP0182 on the next tick. "crypto.cipherNew", + // WebCrypto results settle through the deferred crypto queue. + "webcrypto.sign", + "webcrypto.verify", + "webcrypto.digest", "process.emitWarningText", // The legacy url entries queue DEP0169/DEP0170 on the next tick. "url.legacyParse", diff --git a/packages/compiler/src/backend/llvm/string-lifetimes.test.ts b/packages/compiler/src/backend/llvm/string-lifetimes.test.ts index bf140d2e6..8898b86d3 100644 --- a/packages/compiler/src/backend/llvm/string-lifetimes.test.ts +++ b/packages/compiler/src/backend/llvm/string-lifetimes.test.ts @@ -432,7 +432,6 @@ test("borrowed literals keep UTF-16 string comparison without temporary owners", op: "<", left: str("a"), right: str("b"), - utf16: true, type: BOOL, loc, }); @@ -443,16 +442,14 @@ test("borrowed literals keep UTF-16 string comparison without temporary owners", }); test("string comparisons decide identity, lengths and leading bytes before the runtime", () => { - const order = (utf16: boolean) => - fn(`order${utf16 ? 16 : 8}`, [], { - kind: "strCmp", - op: "<", - left: str("apple"), - right: str("apricot"), - utf16, - type: BOOL, - loc, - }); + const order = fn("order", [], { + kind: "strCmp", + op: "<", + left: str("apple"), + right: str("apricot"), + type: BOOL, + loc, + }); const same = fn("same", ["a", "b"], { kind: "strEq", left: { kind: "varRef", localId: "a", type: STRING, loc }, @@ -462,23 +459,28 @@ test("string comparisons decide identity, lengths and leading bytes before the r loc, }); for (const bits of [32, 64] as const) { - const ir = emitLlvmModule(mod(order(false), order(true), same), { pointerBits: bits }); + const ir = emitLlvmModule(mod(order, same), { pointerBits: bits }); const helper = (name: string) => new RegExp(`^define internal [^\\n]*@${name}(?:\\.i32)?\\([^]*?^}`, "m").exec(ir)![0]; const eq = helper("sc_str_eq"); expect(eq).toContain("icmp eq ptr %a, %b"); expect(eq).toMatch(/icmp eq i(32|64) %alen, %blen/); expect(eq).toContain("call zeroext i1 @scr_str_eq(ptr %a, ptr %b)"); - const bytes = helper("sc_str_cmp"); - expect(bytes).toContain("@llvm.bswap.i64"); - expect(bytes).not.toContain("%ascii"); - expect(bytes).toContain("call i32 @scr_str_cmp(ptr %a, ptr %b)"); const units = helper("sc_str_cmp_u16"); - expect(units).toContain("%wascii = icmp eq i64 %whigh, 0"); - expect(units).toContain("%tascii = icmp eq i64 %thigh, 0"); - expect(bytes).toContain("%tailable = and i1 %samelen, %short"); - expect(units).toContain("%ascii = icmp sge i8 %bits, 0"); - expect(units).toContain("call i32 @scr_str_cmp_u16(ptr %a, ptr %b)"); + expect(units).toContain("%dx = phi i64 [ %aw, %word ], [ %at, %tail ]"); + expect(units).toContain("%dlow = icmp eq i64 %whigh, 0"); + expect(units).toContain("@llvm.bswap.i64"); + expect(units).toContain("%dtz = call i64 @llvm.cttz.i64(i64 %dxor, i1 true)"); + expect(units).toContain("%bx = phi i8 [ %x, %first ], [ %dxb, %locate ]"); + expect(units).toContain("%blow = icmp ult i8 %bmin, -19"); + expect(units).toContain("%kbelow = icmp ult i8 %xk, %yk"); + expect(units).toContain("%below = icmp ult i8 %bx, %by"); + expect(units).toContain("%tailable = and i1 %samelen, %short"); + expect(units).toContain("%anyascii = icmp slt i32 %cws, 0"); + expect(units).toContain("call i32 @scr_str_cmp(ptr %a, ptr %b)"); + expect(units).toContain( + "call i32 @scr_str_cmp_u16_bytes(ptr %a, ptr %b) nounwind willreturn memory(argmem: read)", + ); } }); diff --git a/packages/compiler/src/backend/native-link-info-core.ts b/packages/compiler/src/backend/native-link-info-core.ts index 3beb84a20..07bcc4887 100644 --- a/packages/compiler/src/backend/native-link-info-core.ts +++ b/packages/compiler/src/backend/native-link-info-core.ts @@ -25,6 +25,12 @@ export interface NativeLinkFeatures { fileHandle: boolean; fetch: boolean; eventTarget?: boolean; + /** The program assigns process.emitWarning: links the + * SCR_WARNING_REPLACE variant of the dyn-async unit. */ + warningReplace?: boolean; + /** The program uses process.stdin.setEncoding, string 'data' listeners + * or stdio 'resize' events: links the SCR_STDIO_EXTENDED events unit. */ + stdioExtended?: boolean; zlib: boolean; assert: boolean; inspect: boolean; @@ -51,6 +57,9 @@ export interface NativeLinkFeatures { * archive. */ cipher: boolean; tlsCa: boolean; + /** crypto.subtle importKey/sign/verify/digest and CryptoKey values: + * scr_webcrypto.c. */ + webcrypto: boolean; } export interface NativeLinkInfo { diff --git a/packages/compiler/src/backend/native/contracts.ts b/packages/compiler/src/backend/native/contracts.ts index 875a723bc..ff301f456 100644 --- a/packages/compiler/src/backend/native/contracts.ts +++ b/packages/compiler/src/backend/native/contracts.ts @@ -114,6 +114,10 @@ export interface CcOptions { * native fetch AbortSignal built on the same engine: compiles * scr_event_target.c. */ eventTarget?: boolean; + /** SCR_WARNING_REPLACE: process.emitWarning replacement support. */ + warningReplace?: boolean; + /** SCR_STDIO_EXTENDED: stdin encoding, string data listeners, resize. */ + stdioExtended?: boolean; /** The program uses the process-events surface (signal/exit listeners, * stdin events, for-await over stdin — moduleUsesProcessEvents on the * IR): compiles scr_events.c into the binary. Event-free binaries keep @@ -199,6 +203,8 @@ export interface CcOptions { /** crypto.createCipheriv/createDecipheriv: compiles scr_cipher.c and links * the mbedTLS archive (shared with tls). */ cipher?: boolean; + /** crypto.subtle importKey/sign/verify/digest: compiles scr_webcrypto.c. */ + webcrypto?: boolean; /** Internal compiler hook: called only after a strict native artifact hit * or a successful stable build has installed `outPath`. The executable * frontend cache uses it to publish its stamp after native dependencies diff --git a/packages/compiler/src/backend/native/executable.ts b/packages/compiler/src/backend/native/executable.ts index 6c8673fe4..818b057a9 100644 --- a/packages/compiler/src/backend/native/executable.ts +++ b/packages/compiler/src/backend/native/executable.ts @@ -445,6 +445,8 @@ export async function compileCInternal( : [optimization === "dev" ? "-O0" : "-O2"]), ...executableSectionFlags.compile, ...(opts.textDecoderLegacy ? ["-DSCR_TEXT_DECODER_LEGACY"] : []), + ...(opts.warningReplace ? ["-DSCR_WARNING_REPLACE"] : []), + ...(opts.stdioExtended ? ["-DSCR_STDIO_EXTENDED"] : []), ...(opts.youngNursery ? ["-DSCR_YIN"] : []), "-fno-math-errno", // The runtime object model uses type-punned C: a hierarchy @@ -578,6 +580,7 @@ export async function compileCInternal( ...(opts.watch ? [rt(join(rtDir, "scr_watch.c"))] : []), ...(opts.foreignFfi ? [rt(join(rtDir, "scr_ffi_queue.c"))] : []), ...(opts.nodeTest ? [rt(join(rtDir, "scr_test.c"))] : []), + ...(opts.webcrypto ? [rt(join(rtDir, "scr_webcrypto.c"))] : []), // The CA-store unit rides its own gate OR the tls one: scr_tls.c // references its default-set override unconditionally. ...(tlsCa ? [rt(join(rtDir, "scr_tls_ca.c"))] : []), @@ -644,6 +647,8 @@ export async function compileCInternal( : [optimization === "dev" ? "-O0" : "-O2"]), ...executableSectionFlags.compile, ...(opts.textDecoderLegacy ? ["-DSCR_TEXT_DECODER_LEGACY"] : []), + ...(opts.warningReplace ? ["-DSCR_WARNING_REPLACE"] : []), + ...(opts.stdioExtended ? ["-DSCR_STDIO_EXTENDED"] : []), ...(opts.youngNursery ? ["-DSCR_YIN"] : []), "-fno-math-errno", "-fno-strict-aliasing", // the emitted object model type-puns — see buildArgs diff --git a/packages/compiler/src/backend/runtime-pack-core.ts b/packages/compiler/src/backend/runtime-pack-core.ts index 16fbc2a36..3fc2a1bbb 100644 --- a/packages/compiler/src/backend/runtime-pack-core.ts +++ b/packages/compiler/src/backend/runtime-pack-core.ts @@ -82,6 +82,8 @@ export interface RuntimeFeatureSet extends NativeLinkFeatures { workers: boolean; youngNursery: boolean; eventTarget: boolean; + warningReplace: boolean; + stdioExtended: boolean; nativeFetch: boolean; netEffective: boolean; httpEffective: boolean; @@ -269,6 +271,8 @@ export function effectiveRuntimeFeatures( workers: features.workers === true, youngNursery: features.youngNursery === true, eventTarget: features.eventTarget === true, + warningReplace: features.warningReplace === true, + stdioExtended: features.stdioExtended === true, nativeFetch, netEffective, httpEffective, diff --git a/packages/compiler/src/backend/runtime-pack-targets.test.ts b/packages/compiler/src/backend/runtime-pack-targets.test.ts index faa335e33..fd57cb49d 100644 --- a/packages/compiler/src/backend/runtime-pack-targets.test.ts +++ b/packages/compiler/src/backend/runtime-pack-targets.test.ts @@ -119,6 +119,7 @@ const features = { tls: false, tlsCa: false, cipher: false, + webcrypto: false, }; test.each([ diff --git a/packages/compiler/src/backend/runtime-pack.test.ts b/packages/compiler/src/backend/runtime-pack.test.ts index ffe581366..61d1dba5e 100644 --- a/packages/compiler/src/backend/runtime-pack.test.ts +++ b/packages/compiler/src/backend/runtime-pack.test.ts @@ -56,6 +56,7 @@ const BASE: NativeLinkFeatures = { tls: false, tlsCa: false, cipher: false, + webcrypto: false, }; async function fixture(target: NativeTargetSpec = MACOS_ARM64_TARGET) { diff --git a/packages/compiler/src/backend/warm-cache.ts b/packages/compiler/src/backend/warm-cache.ts index 67e627c91..2cd111060 100644 --- a/packages/compiler/src/backend/warm-cache.ts +++ b/packages/compiler/src/backend/warm-cache.ts @@ -61,6 +61,7 @@ export async function warmNativeCaches( nodeTest: false, tls: false, cipher: false, + webcrypto: false, tlsCa: false, }, }); diff --git a/packages/compiler/src/coverage/verdict-cache.test.ts b/packages/compiler/src/coverage/verdict-cache.test.ts index 1c3e9bc3d..0380e8b8d 100644 --- a/packages/compiler/src/coverage/verdict-cache.test.ts +++ b/packages/compiler/src/coverage/verdict-cache.test.ts @@ -11,7 +11,8 @@ afterAll(() => rmSync(dir, { recursive: true, force: true })); test("unchanged and comment-only reruns replay the verdict; code edits relower", () => { const entry = join(dir, "main.ts"); - const program = `const pairs: [number, number][] = [[1, 2]];\nconst [[a, b]] = pairs;\nconsole.log(a + b);\nconst { length } = [1, 2] as number[];\nconsole.log(length);\n`; + // The blocker: an object pattern over a union source (arrays alone compile). + const program = `const pairs: [number, number][] = [[1, 2]];\nconst [[a, b]] = pairs;\nconsole.log(a + b);\nconst { length } = (a > 9 ? [1, 2] : "ab") as number[] | string;\nconsole.log(length);\n`; writeFileSync(entry, program); const entries = new Map(); const store: VerdictStore = { diff --git a/packages/compiler/src/executable/executable-cache.test.ts b/packages/compiler/src/executable/executable-cache.test.ts index e5887e855..1b42c3eca 100644 --- a/packages/compiler/src/executable/executable-cache.test.ts +++ b/packages/compiler/src/executable/executable-cache.test.ts @@ -57,6 +57,7 @@ const native: EarlyExecutableNativeFeatures = { tls: false, tlsCa: false, cipher: false, + webcrypto: false, }; async function fixture(): Promise<{ diff --git a/packages/compiler/src/executable/executable-cache.ts b/packages/compiler/src/executable/executable-cache.ts index ee9d84fcf..789df1563 100644 --- a/packages/compiler/src/executable/executable-cache.ts +++ b/packages/compiler/src/executable/executable-cache.ts @@ -56,6 +56,8 @@ export interface EarlyExecutableNativeFeatures { fileHandle: boolean; fetch: boolean; eventTarget?: boolean; + warningReplace?: boolean; + stdioExtended?: boolean; zlib: boolean; assert: boolean; inspect: boolean; @@ -80,6 +82,7 @@ export interface EarlyExecutableNativeFeatures { tls: boolean; cipher: boolean; tlsCa: boolean; + webcrypto: boolean; } interface EarlyExecutableCacheStamp { @@ -193,6 +196,7 @@ const BOOLEAN_NATIVE_KEYS = [ "tls", "tlsCa", "cipher", + "webcrypto", ] as const satisfies readonly (keyof EarlyExecutableNativeFeatures)[]; function validNativeFeatures(value: unknown): value is EarlyExecutableNativeFeatures { diff --git a/packages/compiler/src/frontend/contract-types.ts b/packages/compiler/src/frontend/contract-types.ts index 9ba041d2c..c30788fa1 100644 --- a/packages/compiler/src/frontend/contract-types.ts +++ b/packages/compiler/src/frontend/contract-types.ts @@ -109,7 +109,7 @@ function markerNamed( if (!MARKERS.has(written)) return null; let member = written; const symbol = checker.getSymbolAtLocation(ident); - const decl = symbol ? checker.declarationsOf(symbol)[0] : undefined; + const decl = symbol ? symbol.declarations[0] : undefined; if (decl === undefined) return null; let importDecl: ts.Node | undefined; if (ts.isImportSpecifier(decl) && !qualified) { diff --git a/packages/compiler/src/frontend/dts-paths.ts b/packages/compiler/src/frontend/dts-paths.ts index 064cb9b85..3b2f32413 100644 --- a/packages/compiler/src/frontend/dts-paths.ts +++ b/packages/compiler/src/frontend/dts-paths.ts @@ -1,5 +1,5 @@ import { createRequire } from "node:module"; -import { join } from "node:path"; +import { dirname, join } from "node:path"; import { npmPackageNameOf } from "./workspace-registry.js"; import { tsgoPath } from "./ts7/session-path.js"; @@ -25,6 +25,15 @@ function declarationPath(name: string): string { ); } +/** Directory of the shipped package shims (packages/compiler/shims): the + * implementation modules package-shims.ts maps npm packages to. A native + * distribution ships them beside its declarations. */ +export function packageShimRoot(): string { + return installedDeclarationRoot === null + ? join(dirname(require.resolve("@scriptc/compiler/scriptc.d.ts")), "..", "shims") + : join(installedDeclarationRoot, "shims"); +} + /** Path of the shipped ambient declarations — the always-shipped CORE * (comptime, setTimeout). Part of EVERY program scriptc * builds, the project-world preflight program included. */ diff --git a/packages/compiler/src/frontend/experimental-import.ts b/packages/compiler/src/frontend/experimental-import.ts index 86aa10933..8fc2238bf 100644 --- a/packages/compiler/src/frontend/experimental-import.ts +++ b/packages/compiler/src/frontend/experimental-import.ts @@ -18,7 +18,7 @@ export function experimentalImportOf( ): ExperimentalMember | null { const checker = program.getTypeChecker(); const symbol = checker.getSymbolAtLocation(ident); - const decl = symbol ? checker.declarationsOf(symbol)[0] : undefined; + const decl = symbol ? symbol.declarations[0] : undefined; if (decl === undefined || !ts.isImportSpecifier(decl)) return null; const importDecl = decl.parent?.parent?.parent; if ( @@ -67,7 +67,7 @@ export function lifetimeRegionMemberOf( ): "run" | "release" | null { const checker = program.getTypeChecker(); const symbol = checker.getSymbolAtLocation(access.name); - const decl = symbol ? checker.declarationsOf(symbol)[0] : undefined; + const decl = symbol ? symbol.declarations[0] : undefined; if (decl === undefined || !ts.isMethodDeclaration(decl)) return null; const owner = decl.parent; if ( @@ -91,7 +91,7 @@ export function internTableMemberOf( ): InternTableMember | null { const checker = program.getTypeChecker(); const symbol = checker.getSymbolAtLocation(access.name); - const decl = symbol ? checker.declarationsOf(symbol)[0] : undefined; + const decl = symbol ? symbol.declarations[0] : undefined; if ( decl === undefined || !( @@ -129,7 +129,7 @@ export function threadSlotsMemberOf( ): ThreadSlotsMember | null { const checker = program.getTypeChecker(); const symbol = checker.getSymbolAtLocation(access.name); - const decl = symbol ? checker.declarationsOf(symbol)[0] : undefined; + const decl = symbol ? symbol.declarations[0] : undefined; if (decl === undefined || !ts.isMethodDeclaration(decl)) return null; const owner = decl.parent; if ( diff --git a/packages/compiler/src/frontend/fork-target.ts b/packages/compiler/src/frontend/fork-target.ts index a7248c674..592bbb2d6 100644 --- a/packages/compiler/src/frontend/fork-target.ts +++ b/packages/compiler/src/frontend/fork-target.ts @@ -8,7 +8,7 @@ import { moduleSourceCandidates } from "./module-source-candidates.js"; export function isNodeGlobal(program: ts.Program, expression: ts.Identifier): boolean { const checker = program.getTypeChecker(); const symbol = checker.getSymbolAtLocation(expression); - const declarations = symbol ? checker.declarationsOf(symbol) : []; + const declarations = symbol ? symbol.declarations : []; return ( declarations.length > 0 && declarations.every((declaration) => { @@ -45,7 +45,7 @@ export function isBuiltinMemberImport( ): boolean { const checker = program.getTypeChecker(); const symbol = checker.getSymbolAtLocation(ident); - const decl = symbol ? checker.declarationsOf(symbol)[0] : undefined; + const decl = symbol ? symbol.declarations[0] : undefined; if (decl !== undefined && ts.isImportSpecifier(decl)) { const importDecl = decl.parent?.parent?.parent; if (ts.isImportDeclaration(importDecl) && ts.isStringLiteral(importDecl.moduleSpecifier)) { @@ -60,7 +60,7 @@ export function isBuiltinMemberImport( } if (symbol !== undefined && (symbol.flags & ts.SymbolFlags.Alias) !== 0) { const target = checker.getAliasedSymbol(symbol); - const targetDecl = checker.declarationsOf(target)[0]; + const targetDecl = target.declarations[0]; if (targetDecl !== undefined && targetDecl.getSourceFile().isDeclarationFile) { for ( let parent: ts.Node | undefined = targetDecl.parent; @@ -88,7 +88,7 @@ export function isBuiltinNamespaceImport( ): boolean { const checker = program.getTypeChecker(); const symbol = checker.getSymbolAtLocation(ident); - const decl = symbol ? checker.declarationsOf(symbol)[0] : undefined; + const decl = symbol ? symbol.declarations[0] : undefined; let importDecl: ts.ImportDeclaration | undefined; if (decl !== undefined && ts.isNamespaceImport(decl)) { const candidate = decl.parent?.parent; @@ -108,12 +108,9 @@ function constInitializer(program: ts.Program, expr: ts.Expression): ts.Expressi if (!ts.isIdentifier(current)) return null; const symbol = program.getTypeChecker().getSymbolAtLocation(current); const decl = symbol - ? program - .getTypeChecker() - .declarationsOf(symbol) - .find((candidate): candidate is ts.VariableDeclaration => - ts.isVariableDeclaration(candidate), - ) + ? symbol.declarations.find((candidate): candidate is ts.VariableDeclaration => + ts.isVariableDeclaration(candidate), + ) : undefined; if ( decl === undefined || diff --git a/packages/compiler/src/frontend/inferred-js-diagnostics.ts b/packages/compiler/src/frontend/inferred-js-diagnostics.ts index a4ad48892..7095cea5d 100644 --- a/packages/compiler/src/frontend/inferred-js-diagnostics.ts +++ b/packages/compiler/src/frontend/inferred-js-diagnostics.ts @@ -25,15 +25,13 @@ export function inferredJsDiagnosticSuppressed( const symbol = checker.getSymbolAtLocation(node.name); if ( symbol && - checker - .declarationsOf(symbol) - .some( - (decl) => - ts.isPropertyDeclaration(decl) && - !decl.type && - !decl.initializer && - !/@type\b/.test(file.text.slice(decl.pos, decl.getStart(file))), - ) + symbol.declarations.some( + (decl) => + ts.isPropertyDeclaration(decl) && + !decl.type && + !decl.initializer && + !/@type\b/.test(file.text.slice(decl.pos, decl.getStart(file))), + ) ) { // Bare JS fields exist with value undefined before the first write; // native storage includes that arm even when inference omits it. @@ -43,7 +41,7 @@ export function inferredJsDiagnosticSuppressed( } if (diagnostic.code === 2554 && ts.isCallExpression(node)) { const signature = checker.getResolvedSignature(node); - const declaration = signature && checker.signatureDeclaration(signature); + const declaration = signature && signature.declaration; if ( declaration && isJsSourceFileName(declaration.getSourceFile().fileName) && @@ -83,9 +81,7 @@ export function inferredJsDiagnosticSuppressed( checker.getTypeOfSymbolAtLocation(throwMethod, node.expression), ); const declaration = - throwSignatures.length === 1 - ? checker.signatureDeclaration(throwSignatures[0]!) - : undefined; + throwSignatures.length === 1 ? throwSignatures[0]!.declaration : undefined; // A sole throw cannot return an invalid IteratorResult. Avoid inferring // termination through arbitrary control flow or explicit return types. if ( diff --git a/packages/compiler/src/frontend/lowering/assert-instances.ts b/packages/compiler/src/frontend/lowering/assert-instances.ts index feeb23019..ee8a34ea2 100644 --- a/packages/compiler/src/frontend/lowering/assert-instances.ts +++ b/packages/compiler/src/frontend/lowering/assert-instances.ts @@ -176,7 +176,7 @@ export function assertInstanceOf( ): AssertInstanceOptions | null { if (!ts.isIdentifier(expr)) return null; const symbol = lowerer.checker.getSymbolAtLocation(expr); - const decl = symbol ? lowerer.checker.declarationsOf(symbol)[0] : undefined; + const decl = symbol ? symbol.declarations[0] : undefined; if (decl === undefined || !ts.isVariableDeclaration(decl)) return null; return staticAssertInstanceDecl(lowerer, decl.name, decl.initializer); } diff --git a/packages/compiler/src/frontend/lowering/assertion-identity.ts b/packages/compiler/src/frontend/lowering/assertion-identity.ts new file mode 100644 index 000000000..6a00b7afe --- /dev/null +++ b/packages/compiler/src/frontend/lowering/assertion-identity.ts @@ -0,0 +1,78 @@ +/* Type assertions of the global object. + * + * `globalThis as { [KEY]?: State }` is the process-global state idiom: a + * module asserts the global object to a record type and reads and writes + * its members, and every other module that asserts it sees the same + * members. Node evaluates the assertion to the global object itself. + * + * The global object is a checked-dynamic value, and a checked-dynamic value + * asserted to a record or array type takes the checked extraction + * (dynCheck), which builds a new native value. For the global object that + * copy is always wrong: a write through it never reached the global object, + * and a view bound before another module's write never saw it. So an + * assertion of the global object keeps the checked-dynamic value itself: a + * view whose member reads and writes go to the global object, and bindings + * of it hold that object. + * + * Other checked-dynamic operands keep the checked extraction: a native value + * held as `unknown` unwraps by identity there, and a checked-dynamic tree is + * copied at the documented checked-value boundary. + */ +import * as ts from "../ts7/adapter.js"; +import type { IrType } from "../../ir/ir.js"; +import type { Lowerer } from "./lowerer.js"; +import { stdlibGlobalNameOf } from "./surfaces.js"; + +/** A target whose checked extraction builds a new mutable value. */ +export function extractionCopiesReference(lowerer: Lowerer, type: IrType): boolean { + const seen = new Set(); + const copies = (t: IrType): boolean => { + if (t.kind === "record" || t.kind === "array") return true; + if (t.kind !== "union" || seen.has(t.unionId)) return false; + seen.add(t.unionId); + return lowerer.unions.get(t.unionId)?.arms.some(copies) ?? false; + }; + return copies(type); +} + +/** The asserted expression without parentheses, `!` and nested + * assertions (`globalThis as unknown as T`). */ +function assertedSource(expr: ts.Expression): ts.Expression { + let e = expr; + while ( + ts.isParenthesizedExpression(e) || + ts.isNonNullExpression(e) || + ts.isAsExpression(e) || + ts.isTypeAssertion(e) || + ts.isSatisfiesExpression(e) + ) + e = e.expression; + return e; +} + +/** `expr` asserts the global object (`globalThis`, `global`). */ +export function assertsGlobalObject( + lowerer: Lowerer, + expr: ts.AsExpression | ts.TypeAssertion, +): boolean { + const name = stdlibGlobalNameOf(lowerer, assertedSource(expr.expression)); + return name === "globalThis" || name === "global"; +} + +/** The assertion a binding initializer is, when it keeps the global object: + * bindings of it hold that object rather than a native value of the + * asserted type. A target with no native layout keeps the object as well + * (the assertion lowers to the checked-dynamic value either way). */ +export function keptIdentityAssertion( + lowerer: Lowerer, + initializer: ts.Expression, +): ts.AsExpression | ts.TypeAssertion | undefined { + let e = initializer; + while (ts.isParenthesizedExpression(e)) e = e.expression; + if (!ts.isAsExpression(e) && !ts.isTypeAssertion(e)) return undefined; + if (!assertsGlobalObject(lowerer, e)) return undefined; + const target = lowerer.mapTypeOf(lowerer.checker.getTypeFromTypeNode(e.type)); + return !target || target.kind === "dyn" || extractionCopiesReference(lowerer, target) + ? e + : undefined; +} diff --git a/packages/compiler/src/frontend/lowering/binding-analysis.ts b/packages/compiler/src/frontend/lowering/binding-analysis.ts index 816294e08..88b0f91dc 100644 --- a/packages/compiler/src/frontend/lowering/binding-analysis.ts +++ b/packages/compiler/src/frontend/lowering/binding-analysis.ts @@ -114,7 +114,7 @@ export function nullishValueUnitOf( const cached = lowerer.nullishBindings.get(sym); if (cached !== undefined) return cached; lowerer.nullishBindings.set(sym, null); // cycle guard: self-referential probes answer non-qualifying - const decl = lowerer.checker.valueDeclarationOf(sym); + const decl = sym.valueDeclaration; // Statement-position declarators only: a for-loop head (`for (let x = // null as any; ...)`) declares a LOCAL with per-iteration semantics — // lowerVarDeclList's contract requires a lowered statement for it, so diff --git a/packages/compiler/src/frontend/lowering/builtin-function-fences.ts b/packages/compiler/src/frontend/lowering/builtin-function-fences.ts new file mode 100644 index 000000000..e6221384b --- /dev/null +++ b/packages/compiler/src/frontend/lowering/builtin-function-fences.ts @@ -0,0 +1,52 @@ +/** A builtin module function used as a JavaScript value when it has no + * value adapter (lowerNativeCallableValue). Node hands the program a real + * function; the compiled value is one function per builtin that throws the + * named fence when called. Identity flows (`fn === fs.readFileSync`, Set + * membership) still hold, `typeof` answers "function" and inspection + * prints `[Function: name]`, where a string token would answer "string" + * and turn a call into an ordinary "is not a function" TypeError. */ +import * as ts from "../ts7/adapter.js"; +import { DYN, type IrExpr, type SrcLoc, funcOf } from "../../ir/ir.js"; +import { unsupportedDiag } from "../../diagnostics/diagnostic.js"; +import type { Lowerer } from "./lowerer.js"; + +export function builtinFunctionFenceValue( + lowerer: Lowerer, + bi: { module: string; member: string }, + node: ts.Identifier, + loc: SrcLoc, +): IrExpr { + const display = `${bi.module}.${bi.member}`; + const name = `%builtin.fence.${display}`; + if (!lowerer.liftedFns.some((fn) => fn.name === name)) { + const diag = unsupportedDiag( + "SC1090", + loc, + `library functions as values ('${display}' has no function-value lowering; call it directly)`, + ); + lowerer.runtimeFences.push(diag); + const line = ts.getLineAndCharacterOfPosition(node.getSourceFile(), loc.start).line + 1; + lowerer.liftedFns.push({ + name, + params: [], + locals: [], + returnType: DYN, + body: [ + { + kind: "runtimeFence", + code: diag.code, + message: `${diag.message} [${diag.code} at ${loc.file}:${line}]`, + loc, + }, + ], + loc, + }); + } + return { + kind: "dynFrom", + value: { kind: "closure", fnName: name, captures: [], type: funcOf([], DYN), loc }, + fnName: bi.member, + type: DYN, + loc, + }; +} diff --git a/packages/compiler/src/frontend/lowering/builtins/async-context.ts b/packages/compiler/src/frontend/lowering/builtins/async-context.ts index 708284be1..e6de99b09 100644 --- a/packages/compiler/src/frontend/lowering/builtins/async-context.ts +++ b/packages/compiler/src/frontend/lowering/builtins/async-context.ts @@ -34,7 +34,7 @@ export function lowerDiagnosticsSubscriber(lowerer: Lowerer, node: ts.Expression // has no other first-class story. if (ts.isIdentifier(node) && node.text === "setImmediate") { const sym = lowerer.checker.getSymbolAtLocation(node); - const decls = sym ? lowerer.checker.declarationsOf(sym) : []; + const decls = sym ? sym.declarations : []; if (decls.length > 0 && decls.every((d) => lowerer.isStdlibFile(d.getSourceFile()))) { return { kind: "libCall", diff --git a/packages/compiler/src/frontend/lowering/builtins/class-values.ts b/packages/compiler/src/frontend/lowering/builtins/class-values.ts index 5e4db921c..416217716 100644 --- a/packages/compiler/src/frontend/lowering/builtins/class-values.ts +++ b/packages/compiler/src/frontend/lowering/builtins/class-values.ts @@ -93,7 +93,7 @@ export function builtinClassAliasOwnsNoSlot(lowerer: Lowerer, symbol: ts.Symbol) if (builtinClassAliasOf(lowerer, symbol) === null) return false; const binding = symbol.flags & ts.SymbolFlags.Alias ? lowerer.checker.getAliasedSymbol(symbol) : symbol; - const decl = lowerer.checker.valueDeclarationOf(binding); + const decl = binding.valueDeclaration; if (decl === undefined || !ts.isVariableDeclaration(decl)) return false; if (builtinMemberRequireDecl(decl.name, decl.initializer)) return false; if (streamClassAliasDecl(lowerer, decl.name, decl.initializer)) return false; @@ -189,7 +189,7 @@ export function sameBuiltinClass(a: BuiltinClassRef, b: BuiltinClassRef): boolea * can name a class (an identifier or a property access), or null. */ function constInitializerOf(lowerer: Lowerer, symbol: ts.Symbol): ts.Expression | null { if ((symbol.flags & ts.SymbolFlags.BlockScopedVariable) === 0) return null; - const decl = lowerer.checker.valueDeclarationOf(symbol); + const decl = symbol.valueDeclaration; if ( decl === undefined || !ts.isVariableDeclaration(decl) || diff --git a/packages/compiler/src/frontend/lowering/builtins/compression.ts b/packages/compiler/src/frontend/lowering/builtins/compression.ts index 0cd25cde6..e69011522 100644 --- a/packages/compiler/src/frontend/lowering/builtins/compression.ts +++ b/packages/compiler/src/frontend/lowering/builtins/compression.ts @@ -138,9 +138,7 @@ export function zlibStreamModeOf( symbol.flags & ts.SymbolFlags.Alias ? lowerer.checker.getAliasedSymbol(symbol) : symbol; const mode = ZLIB_STREAM_MODES.get(target.name); if (mode === undefined) return null; - return lowerer.checker - .declarationsOf(target) - .some((d) => ts.isClassDeclaration(d) && isZlibFallbackDecl(lowerer, d)) + return target.declarations.some((d) => ts.isClassDeclaration(d) && isZlibFallbackDecl(lowerer, d)) ? mode : null; } @@ -161,15 +159,13 @@ function isZlibFallbackDecl(lowerer: Lowerer, d: ts.Node): boolean { export function isZlibStreamMember(lowerer: Lowerer, access: ts.PropertyAccessExpression): boolean { const symbol = lowerer.checker.getSymbolAtLocation(access.name); if (!symbol) return false; - return lowerer.checker - .declarationsOf(symbol) - .some( - (d) => - d.parent !== undefined && - ts.isClassDeclaration(d.parent) && - d.parent.name?.text === "ZlibBase" && - isZlibFallbackDecl(lowerer, d.parent), - ); + return symbol.declarations.some( + (d) => + d.parent !== undefined && + ts.isClassDeclaration(d.parent) && + d.parent.name?.text === "ZlibBase" && + isZlibFallbackDecl(lowerer, d.parent), + ); } /** `stream.bytesWritten` on a zlib stream. */ diff --git a/packages/compiler/src/frontend/lowering/builtins/constants.ts b/packages/compiler/src/frontend/lowering/builtins/constants.ts index dd9fcf987..611805d94 100644 --- a/packages/compiler/src/frontend/lowering/builtins/constants.ts +++ b/packages/compiler/src/frontend/lowering/builtins/constants.ts @@ -1,11 +1,11 @@ import * as ts from "../../ts7/adapter.js"; import { type Lowerer, own } from "../lowerer.js"; -import { canonicalBuiltinModule, locOf, requireSpecOf } from "../../program.js"; +import { canonicalBuiltinModule, isJsSourceFile, locOf, requireSpecOf } from "../../program.js"; import { builtinConstLit } from "../surfaces.js"; import { HTTP2_CONSTANTS } from "../http2-constants.js"; import { CRYPTO_CONSTANTS } from "../crypto-tables.js"; import { ZLIB_CONSTANTS } from "../zlib-constants.js"; -import { F64, type IrExpr } from "../../../ir/ir.js"; +import { DYN, F64, type IrExpr, STRING, type SrcLoc } from "../../../ir/ir.js"; import { builtinImportOf } from "./module-bindings.js"; /** The builtin modules whose `constants` object bakes as literals at @@ -106,7 +106,7 @@ export function builtinConstantsDestructureDecl( * its baked literal value. Null for every other binding. */ export function builtinConstantBindingOf(lowerer: Lowerer, ident: ts.Identifier): IrExpr | null { const symbol = lowerer.checker.getSymbolAtLocation(ident); - const decl = symbol ? lowerer.checker.declarationsOf(symbol)[0] : undefined; + const decl = symbol ? symbol.declarations[0] : undefined; if (!decl || !ts.isBindingElement(decl) || decl.dotDotDotToken || decl.initializer) return null; if (!ts.isObjectBindingPattern(decl.parent)) return null; const varDecl = decl.parent.parent; @@ -147,12 +147,30 @@ export function lowerFsConstantsProperty( COPYFILE_FICLONE_FORCE: 4, }; const value = own(MODES, expr.name.text); - if (value === undefined) { - lowerer.noLowering( - `fs.constants.${expr.name.text}`, - expr, - "F_OK, R_OK, W_OK, X_OK, and COPYFILE_* are the lowered constants", - ); - } + if (value === undefined) return fsConstantMemberRead(expr); return { kind: "numLit", value, type: F64, loc: locOf(expr) }; } + +/** fs.constants as a value: the target's null-prototype constants object, + * built by the runtime from its own libc's macros. */ +export function fsConstantsObject(loc: SrcLoc): IrExpr { + return { kind: "libCall", fn: "fs.constants", args: [], type: DYN, loc }; +} + +/** `fs.constants.O_RDONLY` (any member the access-mode literals do not + * cover): a read of the runtime object, so a constant the target lacks + * reads as undefined in JavaScript and fails the number check in + * TypeScript, whose declarations promise every name. */ +export function fsConstantMemberRead(expr: ts.PropertyAccessExpression): IrExpr { + const loc = locOf(expr); + const read: IrExpr = { + kind: "dynKeyGet", + value: fsConstantsObject(loc), + key: { kind: "strLit", value: expr.name.text, type: STRING, loc }, + type: DYN, + loc, + }; + return isJsSourceFile(expr.getSourceFile()) + ? read + : { kind: "dynCheck", value: read, type: F64, loc }; +} diff --git a/packages/compiler/src/frontend/lowering/builtins/crypto.ts b/packages/compiler/src/frontend/lowering/builtins/crypto.ts index 575748ef6..cd490bc87 100644 --- a/packages/compiler/src/frontend/lowering/builtins/crypto.ts +++ b/packages/compiler/src/frontend/lowering/builtins/crypto.ts @@ -263,6 +263,7 @@ export function lowerCryptoModuleCall( if (args.some(ts.isSpreadElement)) { lowerer.noLowering(`crypto.${bi.member} with spread arguments`, expr); } + if (bi.member === "getRandomValues") return lowerCryptoGetRandomValues(lowerer, expr); if (bi.member === "randomBytes") { if (args.length === 1) return null; if (args.length !== 2) { @@ -878,7 +879,7 @@ export function isCipherType(lowerer: Lowerer, type: ts.Type): boolean { return ( !!symbol && CIPHER_TYPE_NAMES.has(symbol.name) && - lowerer.checker.declarationsOf(symbol).some((d) => lowerer.isStdlibFile(d.getSourceFile())) + symbol.declarations.some((d) => lowerer.isStdlibFile(d.getSourceFile())) ); } @@ -1284,3 +1285,49 @@ export function lowerPromisifiedCryptoCall( "KeyObject", ); } + +/** `crypto.getRandomValues(view)` — the WebCrypto global, node:crypto's + * export and `webcrypto.getRandomValues`: a typed view fills in place and + * returns itself at its own type (float views, DataViews and views over + * 65536 bytes throw Node's DOMExceptions at run time); any other value + * takes the checked form. Null for other arities (the member fence keeps + * them). */ +export function lowerCryptoGetRandomValues( + lowerer: Lowerer, + call: ts.CallExpression, +): IrExpr | null { + if (call.arguments.nodes.length !== 1 || ts.isSpreadElement(call.arguments.nodes[0]!)) + return null; + const loc = locOf(call); + const value = lowerer.lowerExpr(call.arguments.nodes[0]!); + if (value.type.kind === "bytes") + return { kind: "libCall", fn: "crypto.getRandomValues", args: [value], type: value.type, loc }; + return { + kind: "libCall", + fn: "crypto.getRandomValuesDyn", + args: [lowerer.coerceInto(call.arguments.nodes[0]!, value, DYN)], + type: DYN, + loc, + }; +} + +/** Whether `receiver` names a WebCrypto Crypto object: the `crypto` + * global (also through globalThis) or node:crypto's `webcrypto`. */ +export function isWebCryptoReceiver(lowerer: Lowerer, receiver: ts.Expression): boolean { + if (ts.isIdentifier(receiver)) { + if (receiver.text === "crypto" && lowerer.isStdlibGlobal(receiver, "crypto")) return true; + const imported = lowerer.builtinImportOf(receiver); + return imported?.module === "crypto" && imported.member === "webcrypto"; + } + if (ts.isPropertyAccessExpression(receiver) && !receiver.questionDotToken) { + if ( + receiver.name.text === "crypto" && + ts.isIdentifier(receiver.expression) && + lowerer.isStdlibGlobal(receiver.expression, "globalThis") + ) + return true; + const member = lowerer.builtinMemberOf(receiver); + return member?.module === "crypto" && member.member === "webcrypto"; + } + return false; +} diff --git a/packages/compiler/src/frontend/lowering/builtins/errors.ts b/packages/compiler/src/frontend/lowering/builtins/errors.ts index 1572aa6ab..0313cdd10 100644 --- a/packages/compiler/src/frontend/lowering/builtins/errors.ts +++ b/packages/compiler/src/frontend/lowering/builtins/errors.ts @@ -19,9 +19,9 @@ export function isExecExceptionType(lowerer: Lowerer, t: ts.Type): boolean { const sym = t.getSymbol(); return ( sym?.name === "ExecException" && - lowerer.checker - .declarationsOf(sym) - .some((d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(d.getSourceFile())) + sym.declarations.some( + (d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(d.getSourceFile()), + ) ); } diff --git a/packages/compiler/src/frontend/lowering/builtins/fs-streams.ts b/packages/compiler/src/frontend/lowering/builtins/fs-streams.ts index 58ad82358..22e524690 100644 --- a/packages/compiler/src/frontend/lowering/builtins/fs-streams.ts +++ b/packages/compiler/src/frontend/lowering/builtins/fs-streams.ts @@ -34,15 +34,13 @@ function isFsFallbackDecl(lowerer: Lowerer, d: ts.Node): boolean { export function isFsStreamMember(lowerer: Lowerer, access: ts.PropertyAccessExpression): boolean { const symbol = lowerer.checker.getSymbolAtLocation(access.name); if (!symbol) return false; - return lowerer.checker - .declarationsOf(symbol) - .some( - (d) => - d.parent !== undefined && - ts.isClassDeclaration(d.parent) && - (d.parent.name?.text === "ReadStream" || d.parent.name?.text === "WriteStream") && - isFsFallbackDecl(lowerer, d.parent), - ); + return symbol.declarations.some( + (d) => + d.parent !== undefined && + ts.isClassDeclaration(d.parent) && + (d.parent.name?.text === "ReadStream" || d.parent.name?.text === "WriteStream") && + isFsFallbackDecl(lowerer, d.parent), + ); } /** bytesRead / bytesWritten / pending / path reads. Null for other @@ -156,9 +154,9 @@ export function fsStreamClassOf( symbol: ts.Symbol | null | undefined, ): "ReadStream" | "WriteStream" | null { if (!symbol || (symbol.name !== "ReadStream" && symbol.name !== "WriteStream")) return null; - const declared = lowerer.checker - .declarationsOf(symbol) - .some((d) => ts.isClassDeclaration(d) && isFsFallbackDecl(lowerer, d)); + const declared = symbol.declarations.some( + (d) => ts.isClassDeclaration(d) && isFsFallbackDecl(lowerer, d), + ); return declared ? symbol.name : null; } diff --git a/packages/compiler/src/frontend/lowering/builtins/json.ts b/packages/compiler/src/frontend/lowering/builtins/json.ts index b89e25f39..b48030ba5 100644 --- a/packages/compiler/src/frontend/lowering/builtins/json.ts +++ b/packages/compiler/src/frontend/lowering/builtins/json.ts @@ -504,9 +504,7 @@ function jsonNullishArgument(lowerer: Lowerer, node: ts.Expression): boolean { const symbol = lowerer.checker.getSymbolAtLocation(node); return ( symbol !== undefined && - lowerer.checker - .declarationsOf(symbol) - .every((decl) => lowerer.isStdlibFile(decl.getSourceFile())) + symbol.declarations.every((decl) => lowerer.isStdlibFile(decl.getSourceFile())) ); } diff --git a/packages/compiler/src/frontend/lowering/builtins/module-bindings.ts b/packages/compiler/src/frontend/lowering/builtins/module-bindings.ts index 3a178b126..86d93de2c 100644 --- a/packages/compiler/src/frontend/lowering/builtins/module-bindings.ts +++ b/packages/compiler/src/frontend/lowering/builtins/module-bindings.ts @@ -28,7 +28,7 @@ export function builtinImportOf( ts.isShorthandPropertyAssignment(ident.parent) && ident.parent.name === ident ? lowerer.checker.getShorthandAssignmentValueSymbol(ident.parent) : lowerer.checker.getSymbolAtLocation(ident); - const decl = symbol ? lowerer.checker.declarationsOf(symbol)[0] : undefined; + const decl = symbol ? symbol.declarations[0] : undefined; if (!decl) return null; // The CommonJS twin of the named import: a destructured require // binding (`const { readFileSync } = require("fs")`, renames via @@ -123,7 +123,7 @@ export function builtinImportOf( // keys the same tables under the builtin's own member name. if (symbol !== undefined && (symbol.flags & ts.SymbolFlags.Alias) !== 0) { const target = lowerer.checker.getAliasedSymbol(symbol); - const tdecl = lowerer.checker.declarationsOf(target)[0]; + const tdecl = target.declarations[0]; if (tdecl !== undefined && tdecl.getSourceFile().isDeclarationFile) { for ( let p: ts.Node | undefined = tdecl.parent; @@ -153,7 +153,7 @@ export function esmImportBindingKind( ident: ts.Identifier, ): "namespace" | "named" | null { const symbol = lowerer.checker.getSymbolAtLocation(ident); - const decl = symbol ? lowerer.checker.declarationsOf(symbol)[0] : undefined; + const decl = symbol ? symbol.declarations[0] : undefined; if (decl === undefined) return null; if (ts.isNamespaceImport(decl)) return "namespace"; return ts.isImportSpecifier(decl) ? "named" : null; @@ -290,7 +290,7 @@ export function createRequireCalleeFileOf( } if (!ts.isIdentifier(e)) return null; const symbol = lowerer.checker.getSymbolAtLocation(e); - const decl = symbol ? lowerer.checker.declarationsOf(symbol)[0] : undefined; + const decl = symbol ? symbol.declarations[0] : undefined; if (!decl || !ts.isVariableDeclaration(decl) || decl.initializer === undefined) return null; return createRequireBindingDecl(lowerer, decl.name, decl.initializer) ? decl.getSourceFile() @@ -391,7 +391,7 @@ export function createRequireProgramModuleDecl( * binding poison with the same module-naming diagnostic. */ export function fencedBuiltinImportOf(lowerer: Lowerer, ident: ts.Identifier): string | null { const symbol = lowerer.checker.getSymbolAtLocation(ident); - const decl = symbol ? lowerer.checker.declarationsOf(symbol)[0] : undefined; + const decl = symbol ? symbol.declarations[0] : undefined; if (!decl) return null; // The CommonJS twins: `const x = require("net")` and // `const { createServer } = require("net")` — the require statement diff --git a/packages/compiler/src/frontend/lowering/builtins/navigator.ts b/packages/compiler/src/frontend/lowering/builtins/navigator.ts new file mode 100644 index 000000000..637edaf3c --- /dev/null +++ b/packages/compiler/src/frontend/lowering/builtins/navigator.ts @@ -0,0 +1,60 @@ +/* Node 24's `navigator` global (lib/internal/navigator.js), the part + * libraries read to tell runtimes apart: `typeof navigator` is "object" + * and `navigator.userAgent` is `Node.js/`. jose reads exactly + * that at module load (jwks/remote.js). Both fold to string literals, so + * nothing links. The other members (hardwareConcurrency, language, + * languages, platform, locks) and the object as a value keep a named + * refusal. */ +import * as ts from "../../ts7/adapter.js"; +import { strLit } from "../../../ir/build.js"; +import type { IrExpr } from "../../../ir/ir.js"; +import { isJsSourceFile, locOf } from "../../program.js"; +import type { Lowerer } from "../lowerer.js"; + +/** True when `node` names Node's global `navigator`: the @types/node + * declaration, or, in a JavaScript file, the free name no declaration + * binds (Node defines it on the global object). */ +export function isNavigatorGlobal(lowerer: Lowerer, node: ts.Expression): boolean { + let expr = node; + while (ts.isParenthesizedExpression(expr)) expr = expr.expression; + if (!ts.isIdentifier(expr) || expr.text !== "navigator") return false; + if (lowerer.isStdlibGlobal(expr, "navigator")) return true; + return ( + isJsSourceFile(expr.getSourceFile()) && lowerer.checker.getSymbolAtLocation(expr) === undefined + ); +} + +/** `navigator.userAgent` (`Node.js/` of `nodeVersion`, the pinned + * release builtins/process.ts names), or the named refusal for another + * member. Null when the receiver is not the global. */ +export function lowerNavigatorMember( + lowerer: Lowerer, + access: ts.PropertyAccessExpression, + nodeVersion: string, +): IrExpr | null { + if (!isNavigatorGlobal(lowerer, access.expression)) return null; + if (access.name.text === "userAgent") { + return strLit(`Node.js/${nodeVersion.split(".")[0]}`, locOf(access)); + } + return refuseNavigator( + lowerer, + access, + `navigator.${access.name.text}`, + lowerer.checker.getSymbolAtLocation(access.name), + ); +} + +/** The refusal for the `navigator` object itself or an unlowered member. */ +export function refuseNavigator( + lowerer: Lowerer, + node: ts.Node, + what: string, + symbol?: ts.Symbol, +): never { + return lowerer.noLowering( + what, + node, + "of Node's navigator global only typeof navigator and navigator.userAgent compile", + symbol ?? null, + ); +} diff --git a/packages/compiler/src/frontend/lowering/builtins/process.ts b/packages/compiler/src/frontend/lowering/builtins/process.ts index e4e5004c1..555eaf84f 100644 --- a/packages/compiler/src/frontend/lowering/builtins/process.ts +++ b/packages/compiler/src/frontend/lowering/builtins/process.ts @@ -26,6 +26,7 @@ import { funcOf, } from "../../../ir/ir.js"; import { lowerBuiltinLoaderValue } from "../lower-builtin-values.js"; +import { canonicalBuiltinModule } from "../../builtin-modules.js"; import { lowerProcessIpcSend, ipcMessageListener, ipcDisconnectListener } from "./ipc.js"; import { lowerDiagnosticsSubscriber, lowerTracingArguments } from "./async-context.js"; @@ -148,6 +149,18 @@ export function lowerProcessStreamProperty( loc: locOf(expr), }; } + // `if (process.stdin.setRawMode)`: Node's stdin has the method only + // when it is a TTY. The runtime's stream object answers the same + // presence (the method or undefined); calls keep their direct lowering. + if (direct === "stdin" && member === "setRawMode") { + return { + kind: "dynKeyGet", + value: lowerer.lowerExpr(expr.expression), + key: strLit(member, locOf(expr.name)), + type: DYN, + loc: locOf(expr), + }; + } if (member !== "isTTY" && member !== "columns" && member !== "rows") return null; let recv: ts.Expression = expr.expression; while (ts.isParenthesizedExpression(recv) || ts.isAsExpression(recv) || ts.isTypeAssertion(recv)) @@ -364,6 +377,11 @@ export function lowerProcessProperty( if (member === "versions") { return { kind: "libCall", fn: "process.versions", args: [], type: DYN, loc }; } + // process.features as a value (`"sea" in process.features`): an object + // holding none of the flags, consistent with the member reads above. + if (member === "features") { + return { kind: "dynObjLit", type: DYN, loc }; + } // process.noDeprecation / process.throwDeprecation: the values the // program assigned (undefined before that — no Node flags reach a // compiled binary). @@ -627,6 +645,73 @@ export function lowerProcessMethodCall( } return { kind: "libCall", fn: "process.stdinSetRawMode", args: [mode], type: VOID, loc }; } + // process.stdout.on("resize", cb) (and once/off/removeListener/ + // addListener, stderr too): the terminal resize event, registered on the + // runtime's process stream object (a SIGWINCH listener that emits when a + // TTY's size changed; it never keeps the loop alive). Statement position: + // Node returns the stream. + if ( + ["on", "once", "off", "removeListener", "addListener"].includes(access.name.text) && + ts.isPropertyAccessExpression(access.expression) && + call.arguments.nodes.length === 2 && + !call.arguments.nodes.some(ts.isSpreadElement) && + ts.isExpressionStatement(call.parent) + ) { + const stream = lowerer.stdlibGlobalMember(access.expression, "process"); + const evT = lowerer.typeOf(call.arguments.nodes[0]!); + if ( + (stream === "stdout" || stream === "stderr") && + evT.isStringLiteralType() && + evT.value === "resize" + ) { + const loc = locOf(call); + const receiverLocal = lowerer.declareHiddenLocal("%stdioReceiver", DYN); + const receiver = varRef(receiverLocal.id, DYN, loc); + const method = + access.name.text === "off" + ? "removeListener" + : access.name.text === "addListener" + ? "on" + : access.name.text; + return { + kind: "seqExpr", + stmts: [ + { + kind: "varDecl", + localId: receiverLocal.id, + init: { + kind: "libCall", + fn: "process.stdio", + args: [numLit(stream === "stdout" ? 1 : 2, loc)], + type: DYN, + loc, + }, + loc, + }, + ], + result: { + kind: "dynCall", + callee: { + kind: "dynKeyGet", + value: receiver, + key: strLit(method, locOf(access.name)), + type: DYN, + loc, + }, + receiver, + calleeName: access.getText(), + args: [ + lowerer.lowerExprExpecting(call.arguments.nodes[0]!, DYN), + lowerer.lowerExprExpecting(call.arguments.nodes[1]!, DYN), + ], + type: DYN, + loc, + }, + type: DYN, + loc, + }; + } + } // process.stdin.on/once("data" | "end" | "error", cb): the piped-stdin // event slice. A 'data' listener keeps the event loop alive until EOF // (Node's flowing stdin); 'end'/'error' listeners alone do not. `once` @@ -678,11 +763,18 @@ export function lowerProcessMethodCall( const param = cb.type.params[0]; const onceArg: IrExpr = { kind: "boolLit", value: once, type: BOOL, loc }; if (event === "data") { - if (param !== undefined && !(param.kind === "bytes" && param.elem === "u8")) { + // A string parameter receives the chunks process.stdin.setEncoding + // decodes (pi-tui's terminal input); the runtime refuses a delivery + // while no encoding is set, where Node would pass a Buffer. + if ( + param !== undefined && + !(param.kind === "bytes" && param.elem === "u8") && + param.kind !== "string" + ) { lowerer.unsupported( "SC1090", call.arguments.nodes[1]!, - `data listeners whose parameter is not 'Uint8Array' (got '${lowerer.fmt(param)}')`, + `data listeners whose parameter is not 'Uint8Array' or 'string' (got '${lowerer.fmt(param)}')`, ); } return { kind: "libCall", fn: "stdin.onData", args: [cb, onceArg], type: VOID, loc }; @@ -1537,3 +1629,36 @@ function processCredentialCall(member: string, loc: SrcLoc): IrExpr { loc, }; } + +/** `process.getBuiltinModule("node:os")`: a string-literal id naming a + * supported builtin whose call the checker types as that module's + * namespace (@types/node's BuiltInModule map) answers the namespace value + * `import * as ns` binds, so its members key the builtin tables at their + * use sites. The receiver may be cast (`(process as P)`) and the call + * optional: Node 24's process always has the member, and the cast changes + * no runtime value. Other ids keep the dynamic loader (undefined for + * unknown ids, Node's argument errors). */ +export function lowerProcessBuiltinModuleNamespace( + lowerer: Lowerer, + call: ts.CallExpression, +): IrExpr | null { + const callee = call.expression; + if (!ts.isPropertyAccessExpression(callee) || callee.name.text !== "getBuiltinModule") + return null; + const id = call.arguments.nodes[0]; + if (call.arguments.nodes.length !== 1 || !id || !ts.isStringLiteralLike(id)) return null; + let receiver: ts.Expression = callee.expression; + while ( + ts.isParenthesizedExpression(receiver) || + ts.isAsExpression(receiver) || + ts.isSatisfiesExpression(receiver) || + ts.isNonNullExpression(receiver) + ) + receiver = receiver.expression; + if (!lowerer.isStdlibGlobal(receiver, "process")) return null; + const module = canonicalBuiltinModule(id.text); + if (module === null) return null; + const type = lowerer.mapTypeOf(lowerer.checker.getNonNullableType(lowerer.typeOf(call))); + if (type?.kind !== "moduleNs" || type.moduleId !== `builtin:${module}`) return null; + return { kind: "moduleNsRef", moduleId: type.moduleId, type, loc: locOf(call) }; +} diff --git a/packages/compiler/src/frontend/lowering/builtins/readline.ts b/packages/compiler/src/frontend/lowering/builtins/readline.ts index 3a0e7ccf0..6baeb65d1 100644 --- a/packages/compiler/src/frontend/lowering/builtins/readline.ts +++ b/packages/compiler/src/frontend/lowering/builtins/readline.ts @@ -46,7 +46,7 @@ export function isReadlineTyped(lowerer: Lowerer, node: ts.Expression): boolean const t = lowerer.checker.getTypeAtLocation(node); const sym = t.getAliasSymbol() ?? t.getSymbol(); if (sym?.name !== "Interface") return false; - return lowerer.checker.declarationsOf(sym).some((d) => { + return sym.declarations.some((d) => { if (!ts.isClassDeclaration(d) && !ts.isInterfaceDeclaration(d)) return false; if (!lowerer.isStdlibFile(d.getSourceFile())) return false; return isReadlineModule(ambientModuleOf(d)); @@ -107,7 +107,7 @@ function isReadlineUiTyped(lowerer: Lowerer, node: ts.Expression): boolean { const t = lowerer.checker.getTypeAtLocation(node); const sym = t.getAliasSymbol() ?? t.getSymbol(); if (sym?.name !== "Readline") return false; - return lowerer.checker.declarationsOf(sym).some((d) => { + return sym.declarations.some((d) => { if (!ts.isClassDeclaration(d) && !ts.isInterfaceDeclaration(d)) return false; if (!lowerer.isStdlibFile(d.getSourceFile())) return false; const spec = ambientModuleOf(d); diff --git a/packages/compiler/src/frontend/lowering/builtins/receiver-types.ts b/packages/compiler/src/frontend/lowering/builtins/receiver-types.ts index b90eb4844..fc507e2bd 100644 --- a/packages/compiler/src/frontend/lowering/builtins/receiver-types.ts +++ b/packages/compiler/src/frontend/lowering/builtins/receiver-types.ts @@ -11,11 +11,9 @@ export function isBuiltinClassInstance( const type = lowerer.checker.getTypeAtLocation(node); const symbol = type.getAliasSymbol() ?? type.getSymbol(); if (symbol?.name !== name) return false; - return lowerer.checker - .declarationsOf(symbol) - .some( - (declaration) => - (ts.isClassDeclaration(declaration) || ts.isInterfaceDeclaration(declaration)) && - lowerer.isStdlibFile(declaration.getSourceFile()), - ); + return symbol.declarations.some( + (declaration) => + (ts.isClassDeclaration(declaration) || ts.isInterfaceDeclaration(declaration)) && + lowerer.isStdlibFile(declaration.getSourceFile()), + ); } diff --git a/packages/compiler/src/frontend/lowering/builtins/strings.ts b/packages/compiler/src/frontend/lowering/builtins/strings.ts index 013cd3fda..54ab22c05 100644 --- a/packages/compiler/src/frontend/lowering/builtins/strings.ts +++ b/packages/compiler/src/frontend/lowering/builtins/strings.ts @@ -4,7 +4,19 @@ import { type Lowerer } from "../lowerer.js"; import { locOf } from "../../program.js"; import { lowerOptionalArgument, lowerStringSearchArgument } from "../optional-arguments.js"; import { lowerToNumberArgument } from "../lower-exprs.js"; -import { F64, type IrExpr, STRING, arrayOf, isUnitType, typeEquals } from "../../../ir/ir.js"; +import { + BOOL, + DYN, + F64, + type IrExpr, + type IrStmt, + NULL_T, + STRING, + arrayOf, + bytesOf, + isUnitType, + typeEquals, +} from "../../../ir/ir.js"; /** `String.fromCharCode/fromCodePoint(...codes)` on THE String global: every argument * lowers as a number and packs into ONE f64[] array-literal argument @@ -17,6 +29,8 @@ export function lowerStringStaticCall( access: ts.PropertyAccessExpression, ): IrExpr | null { if (call.questionDotToken) return null; + const applied = lowerFromCharCodeApply(lowerer, call, access); + if (applied) return applied; const member = lowerer.stdlibGlobalMember(access, "String"); if (member !== "fromCharCode" && member !== "fromCodePoint" && member !== "raw") return null; const loc = locOf(call); @@ -108,6 +122,99 @@ export function lowerStringStaticCall( return { kind: "libCall", fn: `string.${member}`, args: [packed], type: STRING, loc }; } +/** `String.fromCharCode.apply(thisArg, codes)`, the btoa chunking idiom + * (`String.fromCharCode.apply(null, bytes.subarray(i, j))`): the receiver + * is ignored, and a typed array or number array forwards to the packed + * fromCharCode like a spread. An untyped list reads a Uint8Array the same + * way at run time; any other value keeps Function.prototype.apply's + * generic array-like call. Null for every other call. */ +function lowerFromCharCodeApply( + lowerer: Lowerer, + call: ts.CallExpression, + access: ts.PropertyAccessExpression, +): IrExpr | null { + if (access.name.text !== "apply" || access.questionDotToken) return null; + const target = access.expression; + if ( + !ts.isPropertyAccessExpression(target) || + lowerer.stdlibGlobalMember(target, "String") !== "fromCharCode" + ) { + return null; + } + if (call.arguments.nodes.length !== 2 || call.arguments.nodes.some(ts.isSpreadElement)) + return null; + const listNode = call.arguments.nodes[1]!; + const checkerT = lowerer.typeOf(listNode); + const untyped = (checkerT.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0; + const listT = untyped ? DYN : lowerer.mapTypeOf(checkerT); + if ( + listT === null || + (listT.kind !== "bytes" && listT.kind !== "dyn" && !typeEquals(listT, arrayOf(F64))) + ) { + return null; + } + const loc = locOf(call); + const thisArg = lowerer.lowerExprExpecting(call.arguments.nodes[0]!, DYN); + const stmts: IrStmt[] = [{ kind: "exprStmt", expr: thisArg, loc: thisArg.loc }]; + if (listT.kind !== "dyn") { + const list = lowerer.lowerExprExpecting(listNode, listT); + const result: IrExpr = { + kind: "libCall", + fn: "string.fromCharCode", + args: [list], + type: STRING, + loc, + }; + return { kind: "seqExpr", stmts, result, type: STRING, loc }; + } + const listLocal = lowerer.declareHiddenLocal("%charCodes", DYN); + stmts.push({ + kind: "varDecl", + localId: listLocal.id, + init: lowerer.lowerExprExpecting(listNode, DYN), + loc, + }); + const list = (): IrExpr => ({ kind: "varRef", localId: listLocal.id, type: DYN, loc }); + const bytes = bytesOf("u8"); + const generic: IrExpr = { + kind: "dynCheck", + value: { + kind: "dynInvoke", + recv: lowerer.lowerExprExpecting(target, DYN), + method: "apply", + calleeName: "String.fromCharCode.apply", + args: [ + { + kind: "dynFrom", + value: { kind: "unitLit", unit: "null", type: NULL_T, loc }, + type: DYN, + loc, + }, + list(), + ], + type: DYN, + loc, + }, + type: STRING, + loc, + }; + const result: IrExpr = { + kind: "ternary", + cond: { kind: "dynTest", test: "bytes", bytesElem: "u8", value: list(), type: BOOL, loc }, + then: { + kind: "libCall", + fn: "string.fromCharCode", + args: [{ kind: "dynCheck", value: list(), type: bytes, loc }], + type: STRING, + loc, + }, + else_: generic, + type: STRING, + loc, + }; + return { kind: "seqExpr", stmts, result, type: STRING, loc }; +} + /** `s.lastIndexOf(searchValue?, position?)` on string receivers, using UTF-16 * indices. An omitted search value searches for "undefined". Omitted or undefined positions clamp to the string's end; * MAX_SAFE_INTEGER has the same effect for every representable string. diff --git a/packages/compiler/src/frontend/lowering/builtins/text-codecs.ts b/packages/compiler/src/frontend/lowering/builtins/text-codecs.ts index a5f34f44e..e89b1f07e 100644 --- a/packages/compiler/src/frontend/lowering/builtins/text-codecs.ts +++ b/packages/compiler/src/frontend/lowering/builtins/text-codecs.ts @@ -361,13 +361,11 @@ export function storedTextCodecClassOf( ): TextCodecCtor["cls"] | null { const sym = lowerer.typeOf(expr).getSymbol(); if (!sym || (sym.name !== "TextEncoder" && sym.name !== "TextDecoder")) return null; - return lowerer.checker - .declarationsOf(sym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - lowerer.isStdlibFile(d.getSourceFile()), - ) + return sym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + lowerer.isStdlibFile(d.getSourceFile()), + ) ? sym.name : null; } diff --git a/packages/compiler/src/frontend/lowering/builtins/timers.ts b/packages/compiler/src/frontend/lowering/builtins/timers.ts index dabfb1d2a..8c6606782 100644 --- a/packages/compiler/src/frontend/lowering/builtins/timers.ts +++ b/packages/compiler/src/frontend/lowering/builtins/timers.ts @@ -155,9 +155,13 @@ function isTimerHandleTyped( const t = lowerer.checker.getTypeAtLocation(node); const sym = t.getAliasSymbol() ?? t.getSymbol(); if (sym?.name !== name) return false; - return lowerer.checker - .declarationsOf(sym) - .some((d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(d.getSourceFile())); + return sym.declarations.some( + (d) => + // An interface in the fallback and @types/node 22+, a class in + // @types/node 20 and older. + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + lowerer.isStdlibFile(d.getSourceFile()), + ); } function isTimeoutTyped(lowerer: Lowerer, node: ts.Expression): boolean { return isTimerHandleTyped(lowerer, node, "Timeout"); diff --git a/packages/compiler/src/frontend/lowering/builtins/webcrypto.ts b/packages/compiler/src/frontend/lowering/builtins/webcrypto.ts new file mode 100644 index 000000000..2a9ff9eef --- /dev/null +++ b/packages/compiler/src/frontend/lowering/builtins/webcrypto.ts @@ -0,0 +1,130 @@ +import * as ts from "../../ts7/adapter.js"; +import { type Lowerer } from "../lowerer.js"; +import { isJsSourceFile, locOf } from "../../program.js"; +import { BOOL, DYN, type IrExpr, type IrLibFn } from "../../../ir/ir.js"; +import { lowerDynObjectLiteral } from "../expressions/object-literals.js"; +import { stdlibGlobalNameOf } from "../surfaces.js"; +import { WEBCRYPTO_DYN_TYPES } from "../../type-mapper.js"; + +/* WebCrypto's HMAC slice (scr_webcrypto.c): `crypto.subtle.importKey`, + * `sign`, `verify` and `digest` called on the crypto global (also through + * globalThis) lower to runtime entry points that take the argument vector + * as one dyn array and answer a promise, so Web IDL conversion, algorithm + * normalization and every error stay Node-shaped at run time. CryptoKey + * values are native handles; their type maps to dyn and their members + * are handle reads. Other SubtleCrypto methods are refused by name. */ + +const SUBTLE_LOWERED: ReadonlyMap = new Map([ + ["importKey", "webcrypto.importKey"], + ["sign", "webcrypto.sign"], + ["verify", "webcrypto.verify"], + ["digest", "webcrypto.digest"], +]); + +const SUBTLE_HINT = + "the native WebCrypto surface is crypto.subtle.importKey('raw', ...) for HMAC keys, sign and verify with HMAC, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle"; + +/** Whether `node` is `crypto.subtle` on the WebCrypto global. */ +function isSubtleReceiver(lowerer: Lowerer, node: ts.Expression): boolean { + while (ts.isParenthesizedExpression(node)) node = node.expression; + return ( + ts.isPropertyAccessExpression(node) && + !node.questionDotToken && + node.name.text === "subtle" && + stdlibGlobalNameOf(lowerer, node.expression) === "crypto" + ); +} + +/** `crypto.subtle.(...args)`: the lowered methods become their + * libCall over the argument vector; every other method is refused. */ +export function lowerWebCryptoCall(lowerer: Lowerer, call: ts.CallExpression): IrExpr | null { + const callee = call.expression; + if (!ts.isPropertyAccessExpression(callee) || !isSubtleReceiver(lowerer, callee.expression)) { + return null; + } + const member = callee.name.text; + const fn = SUBTLE_LOWERED.get(member); + if (fn === undefined) lowerer.noLowering(`crypto.subtle.${member}`, callee, SUBTLE_HINT); + if (callee.questionDotToken || call.questionDotToken) { + lowerer.noLowering(`optional calls of crypto.subtle.${member}`, call, SUBTLE_HINT); + } + if (call.arguments.nodes.some(ts.isSpreadElement)) { + lowerer.noLowering(`crypto.subtle.${member} with spread arguments`, call, SUBTLE_HINT); + } + // JsonWebKey imports are not implemented: a literal 'jwk' format is + // refused here (the runtime refuses a computed one). + const format = call.arguments.nodes[0]; + if ( + member === "importKey" && + format !== undefined && + ts.isStringLiteralLike(format) && + format.text === "jwk" + ) { + lowerer.noLowering("crypto.subtle.importKey with the 'jwk' key format", format, SUBTLE_HINT); + } + const loc = locOf(call); + const args = call.arguments.nodes.map((arg) => + ts.isObjectLiteralExpression(arg) + ? lowerDynObjectLiteral(lowerer, arg) + : lowerer.lowerExprExpecting(arg, DYN), + ); + return { + kind: "libCall", + fn, + args: [{ kind: "dynArrLit", elems: args, type: DYN, loc }], + type: { kind: "promise", inner: member === "verify" ? BOOL : DYN }, + loc, + }; +} + +/** `v instanceof CryptoKey` against the global. */ +export function lowerCryptoKeyInstanceof( + lowerer: Lowerer, + expr: ts.BinaryExpression, +): IrExpr | null { + if (stdlibGlobalNameOf(lowerer, expr.right) !== "CryptoKey") return null; + const value = lowerer.coerceInto(expr.left, lowerer.lowerExpr(expr.left), DYN); + return { + kind: "libCall", + fn: "webcrypto.isCryptoKey", + args: [value], + type: BOOL, + loc: locOf(expr), + }; +} + +/** TypeScript reads of `crypto.subtle` and its members outside a direct + * call: the global's dynamic object does not carry the lowered methods, + * so a detached read is refused rather than answering a value without + * them. JavaScript keeps the dynamic object's reads. */ +export function fenceWebCryptoMember( + lowerer: Lowerer, + access: ts.PropertyAccessExpression | ts.ElementAccessExpression, + use: "read" | "call", +): IrExpr | null { + if (use !== "read" || !ts.isPropertyAccessExpression(access)) return null; + if (isJsSourceFile(access.getSourceFile())) return null; + if (isSubtleReceiver(lowerer, access)) { + lowerer.noLowering("crypto.subtle as a value", access, SUBTLE_HINT); + } + if (isSubtleReceiver(lowerer, access.expression)) { + lowerer.noLowering(`crypto.subtle.${access.name.text} as a value`, access, SUBTLE_HINT); + } + return null; +} + +/** Whether a checker type is one of the WebCrypto interfaces reached + * through the crypto global's members (SubtleCrypto, CryptoKey and the + * algorithm dictionaries). While `crypto` was typed `unknown` those member + * reads were untyped, so JavaScript destructurings of them keep the + * module-global path untyped bindings take. `Crypto` itself is excluded: + * the global was `unknown`, which already mapped to dyn. */ +export function isWebCryptoMemberType(lowerer: Lowerer, type: ts.Type): boolean { + const symbol = type.getAliasSymbol() ?? type.getSymbol(); + return ( + symbol !== undefined && + symbol.name !== "Crypto" && + WEBCRYPTO_DYN_TYPES.has(symbol.name) && + lowerer.isStdlibSymbol(symbol) + ); +} diff --git a/packages/compiler/src/frontend/lowering/builtins/workers.ts b/packages/compiler/src/frontend/lowering/builtins/workers.ts index aa826cc53..335fdb969 100644 --- a/packages/compiler/src/frontend/lowering/builtins/workers.ts +++ b/packages/compiler/src/frontend/lowering/builtins/workers.ts @@ -175,7 +175,7 @@ function isWorkerThreadsClass(lowerer: Lowerer, node: ts.Expression, name: strin const t = lowerer.checker.getTypeAtLocation(node); const sym = t.getSymbol(); if (sym?.name !== name) return false; - return lowerer.checker.declarationsOf(sym).some((d) => { + return sym.declarations.some((d) => { if (!ts.isClassDeclaration(d) && !ts.isInterfaceDeclaration(d)) return false; if (!lowerer.isStdlibFile(d.getSourceFile())) return false; let up: ts.Node | undefined = d.parent; diff --git a/packages/compiler/src/frontend/lowering/call-signatures.ts b/packages/compiler/src/frontend/lowering/call-signatures.ts index 9de7cd026..d1eee8937 100644 --- a/packages/compiler/src/frontend/lowering/call-signatures.ts +++ b/packages/compiler/src/frontend/lowering/call-signatures.ts @@ -408,7 +408,15 @@ export function paramShapes( return { type: mapped, mode: "rest" }; } if (declParam.initializer) return defaultParameterShape(lowerer, declParam, mapped); - if (declParam.questionToken && !lowerer.bareUndefinedArmedUnion(mapped)) { + // A checked-value slot holds undefined directly, so an omitted + // argument needs no union arm (paramShape's declared-signature rule; + // an erased type parameter and a class-implemented interface view + // both map here). + if ( + declParam.questionToken && + !lowerer.bareUndefinedArmedUnion(mapped) && + mapped.kind !== "dyn" + ) { lowerer.unsupported( "SC1090", declParam, @@ -903,8 +911,7 @@ export function lambdaSignature( const ctxSigs = ctxType ? lowerer.checker.getCallSignatures(lowerer.checker.getNonNullableType(ctxType)) : []; - const ctxDecl = - ctxSigs.length === 1 ? lowerer.checker.signatureDeclaration(ctxSigs[0]!) : undefined; + const ctxDecl = ctxSigs.length === 1 ? ctxSigs[0]!.declaration : undefined; const ctxRetRaw = ctxDecl && !ctxDecl.getSourceFile().isDeclarationFile ? lowerer.mapTypeOf(lowerer.checker.getReturnTypeOfSignature(ctxSigs[0]!)) diff --git a/packages/compiler/src/frontend/lowering/cjs-exports-object.ts b/packages/compiler/src/frontend/lowering/cjs-exports-object.ts index 0fb5aadd3..5ea4001fc 100644 --- a/packages/compiler/src/frontend/lowering/cjs-exports-object.ts +++ b/packages/compiler/src/frontend/lowering/cjs-exports-object.ts @@ -37,7 +37,7 @@ export function isCjsExportsIdentifier(lowerer: Lowerer, node: ts.Expression): b if (!ts.isIdentifier(node) || node.text !== "exports") return false; if (!isCjsJsFile(node.getSourceFile(), lowerer.program)) return false; const symbol = lowerer.checker.getSymbolAtLocation(node); - const decls = symbol ? lowerer.checker.declarationsOf(symbol) : []; + const decls = symbol ? symbol.declarations : []; return !decls.some( (d) => !d.getSourceFile().isDeclarationFile && @@ -349,7 +349,7 @@ function functionUsesThis(fn: ts.FunctionLikeDeclaration): boolean { /** A function declaration, or a variable initialized with a function or * arrow expression, that never reads `this`. */ function symbolIsThisFreeFunction(lowerer: Lowerer, sym: ts.Symbol | undefined): boolean { - const decls = sym ? lowerer.checker.declarationsOf(sym) : []; + const decls = sym ? sym.declarations : []; const fn = decls.find(ts.isFunctionDeclaration); if (fn !== undefined) return !functionUsesThis(fn); const v = decls.find(ts.isVariableDeclaration); diff --git a/packages/compiler/src/frontend/lowering/cjs-inline-require.ts b/packages/compiler/src/frontend/lowering/cjs-inline-require.ts index 9b41f0012..174f9dc7c 100644 --- a/packages/compiler/src/frontend/lowering/cjs-inline-require.ts +++ b/packages/compiler/src/frontend/lowering/cjs-inline-require.ts @@ -353,7 +353,7 @@ export function cjsExportValueByName( if (sym.flags & ts.SymbolFlags.Alias) sym = lowerer.checker.getAliasedSymbol(sym); const direct = lowerer.globalsBySymbol.get(sym); if (direct) return { kind: "varRef", localId: direct.id, type: direct.type, loc: locOf(blame) }; - for (const d of lowerer.checker.declarationsOf(sym)) { + for (const d of sym.declarations) { const byDecl = lowerer.globalsByDeclNode.get(d); if (byDecl) return { kind: "varRef", localId: byDecl.id, type: byDecl.type, loc: locOf(blame) }; // Alias plumbing: a shorthand/identifier table entry or a member diff --git a/packages/compiler/src/frontend/lowering/cjs-tsc-interop.ts b/packages/compiler/src/frontend/lowering/cjs-tsc-interop.ts index 93a1e3260..cd83c8709 100644 --- a/packages/compiler/src/frontend/lowering/cjs-tsc-interop.ts +++ b/packages/compiler/src/frontend/lowering/cjs-tsc-interop.ts @@ -132,7 +132,7 @@ export function tscInteropBindingOf( ): TscInteropBinding | null { if (!ts.isIdentifier(expr)) return null; const sym = lowerer.checker.getSymbolAtLocation(expr); - const decl = sym ? lowerer.checker.declarationsOf(sym).find(ts.isVariableDeclaration) : undefined; + const decl = sym ? sym.declarations.find(ts.isVariableDeclaration) : undefined; return decl ? tscInteropBindingOfDecl(lowerer, decl) : null; } diff --git a/packages/compiler/src/frontend/lowering/class-construction.ts b/packages/compiler/src/frontend/lowering/class-construction.ts index 4dae7ecd0..7f4a003c9 100644 --- a/packages/compiler/src/frontend/lowering/class-construction.ts +++ b/packages/compiler/src/frontend/lowering/class-construction.ts @@ -1,4 +1,5 @@ import { dynUndefinedExpr, varRef } from "../../ir/build.js"; +import { constructedValue } from "./constructor-returns.js"; import { BOOL, DYN, @@ -389,13 +390,13 @@ export class ClassConstructionDispatch { if (!args) continue; const type: IrType = { kind: "classval", className: name }; const callee = varRef("p.0", DYN, loc); - const value: IrExpr = { + const value = constructedValue(lowerer, info, { kind: "newValue", callee: { kind: "dynCheck", value: callee, type, loc }, args, type: { kind: "object", className: name }, loc, - }; + }); const body: IrStmt[] = [ { kind: "return", value: lowerer.coerceToExpected(value, DYN), loc }, ]; diff --git a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts index a0ed496e1..6b515c1ff 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 { dynUndefinedExpr, helperLoc, nodeThrowExpr, varRef } from "../../ir/build.js"; +import { constructedValue } from "./constructor-returns.js"; import { eventTargetApiMembers, eventTargetInheritedMember } from "./lower-event-target.js"; import { BOOL, @@ -2883,7 +2884,7 @@ export class ClassDynamicDispatch { if (info.symbolMethods) for (const [symbol, name] of info.symbolMethods) symbolEntries.push([symbol, name]); for (const [symbol, name] of symbolEntries) { - const declaration = lowerer.checker.valueDeclarationOf(symbol); + const declaration = symbol.valueDeclaration; if ( declaration && ts.isVariableDeclaration(declaration) && @@ -3164,13 +3165,13 @@ export class ClassDynamicDispatch { } if (!supported || !info.decl) continue; const cls = classValueRef(lowerer, info, info.decl); - const result: IrExpr = { + const result = constructedValue(lowerer, info, { kind: "new", className: name, args, type: { kind: "object", className: name }, loc, - }; + }); this.constructDispatch.body.unshift({ kind: "if", cond: { diff --git a/packages/compiler/src/frontend/lowering/class-method-mutations.ts b/packages/compiler/src/frontend/lowering/class-method-mutations.ts index 2a0e3a7eb..c07ec9136 100644 --- a/packages/compiler/src/frontend/lowering/class-method-mutations.ts +++ b/packages/compiler/src/frontend/lowering/class-method-mutations.ts @@ -93,7 +93,7 @@ export function collectClassMethodMutations( value = unwrap(value); if (ts.isIdentifier(value)) { const symbol = lowerer.checker.getSymbolAtLocation(value); - const decl = symbol && lowerer.checker.declarationsOf(symbol)[0]; + const decl = symbol && symbol.declarations[0]; if ( !decl || !ts.isVariableDeclaration(decl) || @@ -162,7 +162,7 @@ export function collectClassMethodMutations( const literalLoopKeys = (key: ts.Expression): (string | number)[] | null => { if (!ts.isIdentifier(key)) return null; const symbol = lowerer.checker.getSymbolAtLocation(key); - const decl = symbol && lowerer.checker.declarationsOf(symbol)[0]; + const decl = symbol && symbol.declarations[0]; if (!decl || !ts.isVariableDeclaration(decl) || !ts.isIdentifier(decl.name)) return null; const list = decl.parent; if (!ts.isVariableDeclarationList(list) || !(list.flags & ts.NodeFlags.Const)) return null; @@ -413,7 +413,7 @@ export function collectClassMethodMutations( const symbol = ts.isPropertyAccessExpression(access) ? lowerer.checker.getSymbolAtLocation(access.name) : lowerer.checker.getPropertyOfType(lowerer.typeOf(access.expression), name); - if (symbol && lowerer.checker.declarationsOf(symbol).some(ts.isMethodDeclaration)) + if (symbol && symbol.declarations.some(ts.isMethodDeclaration)) lowerer.prototypeMethodAccesses.set(name, access); } lowerer.checker.prefetchSymbolRoots([...globals, ...reflection]); @@ -480,8 +480,7 @@ export function collectClassMethodMutations( } const type = lowerer.checker.getDeclaredTypeOfSymbol(symbol); const property = lowerer.checker.getPropertyOfType(type, name); - if (!property || !lowerer.checker.declarationsOf(property).some(ts.isMethodDeclaration)) - continue; + if (!property || !property.declarations.some(ts.isMethodDeclaration)) continue; if (cls.typeParameters !== undefined && cls.typeParameters.nodes.length > 0) { owners = null; break; diff --git a/packages/compiler/src/frontend/lowering/class-protocol-dispatch.ts b/packages/compiler/src/frontend/lowering/class-protocol-dispatch.ts index c8e56f3cc..0c9a24f24 100644 --- a/packages/compiler/src/frontend/lowering/class-protocol-dispatch.ts +++ b/packages/compiler/src/frontend/lowering/class-protocol-dispatch.ts @@ -99,7 +99,7 @@ function ownedBinding(lowerer: Lowerer, id: ts.Identifier): boolean { if (!symbol) return false; const cached = lowerer.protocolOwnedBindings.get(symbol); if (cached !== undefined) return cached; - const decl = lowerer.checker.valueDeclarationOf(symbol); + const decl = symbol.valueDeclaration; let owned = decl !== undefined && decl.getSourceFile() === id.getSourceFile() && @@ -191,7 +191,7 @@ function protocolSignature( const recvTs = lowerer.typeOf(access.expression); const property = lowerer.checker.getPropertyOfType(recvTs, access.name.text); if (!property) return null; - const declarations = lowerer.checker.declarationsOf(property); + const declarations = property.declarations; if ( declarations.length === 0 || !declarations.every( @@ -200,7 +200,7 @@ function protocolSignature( ) return null; const signature = lowerer.checker.getResolvedSignature(call); - const declaration = signature && lowerer.checker.signatureDeclaration(signature); + const declaration = signature && signature.declaration; if ( !signature || !declaration || @@ -490,7 +490,7 @@ export function lowerProtocolPropertyRead( if (!lowerer.classImplementedProtocol(receiverType)) return null; const name = access.name.text; const property = lowerer.checker.getPropertyOfType(receiverType, name); - const declarations = property ? lowerer.checker.declarationsOf(property) : []; + const declarations = property ? property.declarations : []; if (declarations.length === 0 || !declarations.every((decl) => ts.isPropertySignature(decl))) return null; const result = lowerer.mapTypeOf(lowerer.typeOf(access)); diff --git a/packages/compiler/src/frontend/lowering/class-protocols.ts b/packages/compiler/src/frontend/lowering/class-protocols.ts index 9e69d08b7..5a27f0815 100644 --- a/packages/compiler/src/frontend/lowering/class-protocols.ts +++ b/packages/compiler/src/frontend/lowering/class-protocols.ts @@ -37,15 +37,13 @@ export class ClassProtocols { const instance = symbol ? this.checker.getDeclaredTypeOfSymbol(symbol) : undefined; for (const property of instance ? this.checker.getPropertiesOfType(instance) : []) { if ( - !this.checker - .declarationsOf(property) - .some( - (member) => - ts.isMethodDeclaration(member) && - !member.modifiers?.nodes.some( - (modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword, - ), - ) + !property.declarations.some( + (member) => + ts.isMethodDeclaration(member) && + !member.modifiers?.nodes.some( + (modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword, + ), + ) ) continue; const entries = this.methods.get(property.name) ?? []; @@ -289,7 +287,7 @@ export class ClassProtocols { for (const part of parts) { if ((part.flags & ts.TypeFlags.TypeParameter) === 0) continue; const symbol = part.getSymbol(); - for (const declaration of symbol ? this.checker.declarationsOf(symbol) : []) + for (const declaration of symbol ? symbol.declarations : []) if (ts.isTypeParameterDeclaration(declaration) && declaration.parent !== undefined) facts.genericObservers.add(declaration.parent); } @@ -501,7 +499,7 @@ export class ClassProtocols { if (context !== undefined && this.acceptsAnyValue(context)) return true; if (facts.genericObservers.size === 0) return false; const signature = this.checker.getResolvedSignature(arg.call); - const declaration = signature && this.checker.signatureDeclaration(signature); + const declaration = signature && signature.declaration; return ( declaration !== undefined && (facts.genericObservers.has(declaration) || @@ -520,13 +518,10 @@ export class ClassProtocols { .some( (property) => names.has(property.name) && - this.checker - .declarationsOf(property) - .some( - (declaration) => - ts.isGetAccessorDeclaration(declaration) || - ts.isSetAccessorDeclaration(declaration), - ), + property.declarations.some( + (declaration) => + ts.isGetAccessorDeclaration(declaration) || ts.isSetAccessorDeclaration(declaration), + ), ); } @@ -580,9 +575,7 @@ export class ClassProtocols { return ( symbol !== undefined && (symbol.flags & ts.SymbolFlags.Class) !== 0 && - this.checker - .declarationsOf(symbol) - .some((declaration) => !declaration.getSourceFile().isDeclarationFile) && + symbol.declarations.some((declaration) => !declaration.getSourceFile().isDeclarationFile) && // `typeof C` (the class value) shares the class symbol. this.checker.getConstructSignatures(type).length === 0 ); @@ -603,13 +596,10 @@ export class ClassProtocols { // A program interface a class matches by its methods (protocol below). return ( this.methods.size > 0 && - this.checker - .declarationsOf(symbol) - .some( - (declaration) => - ts.isInterfaceDeclaration(declaration) && - !declaration.getSourceFile().isDeclarationFile, - ) && + symbol.declarations.some( + (declaration) => + ts.isInterfaceDeclaration(declaration) && !declaration.getSourceFile().isDeclarationFile, + ) && this.protocol(type) ); } @@ -624,7 +614,7 @@ export class ClassProtocols { if (this.checker.getConstructSignatures(type).length > 0) return false; const symbol = type.getSymbol(); if (symbol === undefined || (symbol.flags & ts.SymbolFlags.Class) !== 0) return false; - const declarations = this.checker.declarationsOf(symbol); + const declarations = symbol.declarations; return ( declarations.length > 0 && declarations.every((declaration) => !declaration.getSourceFile().isDeclarationFile) @@ -712,13 +702,10 @@ export class ClassProtocols { if (symbol && protocols.has(symbol)) return true; if ( !symbol || - !this.checker - .declarationsOf(symbol) - .some( - (declaration) => - ts.isInterfaceDeclaration(declaration) && - !declaration.getSourceFile().isDeclarationFile, - ) + !symbol.declarations.some( + (declaration) => + ts.isInterfaceDeclaration(declaration) && !declaration.getSourceFile().isDeclarationFile, + ) ) return false; for (const property of properties) { diff --git a/packages/compiler/src/frontend/lowering/class-prototypes.ts b/packages/compiler/src/frontend/lowering/class-prototypes.ts index 403a4a3b4..2787c9021 100644 --- a/packages/compiler/src/frontend/lowering/class-prototypes.ts +++ b/packages/compiler/src/frontend/lowering/class-prototypes.ts @@ -555,7 +555,7 @@ function prototypeProtectedKeys( } else if (name.startsWith("%symbol:")) { const entries: [ts.Symbol, string][] = owner.symbolMethods ? [...owner.symbolMethods] : []; const entry = entries.find(([, field]) => field === name); - const declaration = entry ? lowerer.checker.valueDeclarationOf(entry[0]) : undefined; + const declaration = entry ? entry[0].valueDeclaration : undefined; if ( !declaration || !ts.isVariableDeclaration(declaration) || diff --git a/packages/compiler/src/frontend/lowering/class-to-primitive.ts b/packages/compiler/src/frontend/lowering/class-to-primitive.ts index 516a62f3c..f3a6b23fa 100644 --- a/packages/compiler/src/frontend/lowering/class-to-primitive.ts +++ b/packages/compiler/src/frontend/lowering/class-to-primitive.ts @@ -270,8 +270,8 @@ function refuse(lowerer: Lowerer, node: ts.Node, info: ClassInfo, what: string): } /** ToString(ToPrimitive(value, hint)) of a union whose arms are program - * class instances and primitives (`Box | undefined`): each arm converts as - * it would alone. Null for other unions. */ + * class instances, URLs and primitives (`Box | undefined`, `string | URL`): + * each arm converts as it would alone. Null for other unions. */ export function classUnionToString( lowerer: Lowerer, value: IrExpr, @@ -281,9 +281,11 @@ export function classUnionToString( if (value.type.kind !== "union") return null; const unionId = value.type.unionId; const arms = lowerer.unions.get(unionId)?.arms; - if (!arms || !arms.some((arm) => arm.kind === "object")) return null; + if (!arms || !arms.some((arm) => arm.kind === "object" || arm.kind === "url")) return null; const convertible = (arm: IrType): boolean => { if (PRIMITIVE_KINDS.has(arm.kind)) return true; + // A URL arm converts through its href, like String(url). + if (arm.kind === "url") return true; if (arm.kind !== "object") return false; const info = lowerer.classes.get(arm.className); return info !== undefined && plainProgramClass(info); diff --git a/packages/compiler/src/frontend/lowering/class-unions.ts b/packages/compiler/src/frontend/lowering/class-unions.ts index c172b40cf..f52026ba4 100644 --- a/packages/compiler/src/frontend/lowering/class-unions.ts +++ b/packages/compiler/src/frontend/lowering/class-unions.ts @@ -1,8 +1,115 @@ -import { BOOL, type IrExpr, type IrStmt, type IrType } from "../../ir/ir.js"; +import * as ts from "../ts7/adapter.js"; +import { BOOL, typeKey, type IrExpr, type IrStmt, type IrType } from "../../ir/ir.js"; import { nodeThrowExpr, varRef } from "../../ir/build.js"; import { checkedClassAssertion } from "./class-assertions.js"; import type { Lowerer } from "./lowerer.js"; +/** The classes a narrowed read of a `staticClass` value can hold when the + * checker typed it as the subclass `narrowed`, as a class union, or null + * when `narrowed` is the only one. + * + * TypeScript reduces a union of type-parameter intersections by structural + * subtyping. With ``, `node instanceof Identifier || node + * instanceof PrivateIdentifier` narrows `node` to `T & PrivateIdentifier` + * when Identifier has every member PrivateIdentifier has, so the narrowed + * read can hold an Identifier. Class identity is nominal in compiled code: + * such a read holds the narrowed class or any class under `staticClass` + * that is assignable to it without extending it. A narrowing outside a + * type-parameter intersection keeps every class it names. */ +export function structuralNarrowing( + lowerer: Lowerer, + node: ts.Node, + staticClass: string, + narrowed: IrType & { kind: "object" }, +): (IrType & { kind: "union" }) | null { + if (!intersectsTypeParameter(lowerer.checker.getTypeAtLocation(node))) return null; + const key = `${staticClass}:${narrowed.className}`; + let peers = lowerer.structuralClassPeers.get(key); + if (peers === undefined) { + peers = assignableClassPeers(lowerer, staticClass, narrowed.className); + lowerer.structuralClassPeers.set(key, peers); + } + if (peers.length === 0) return null; + const arms: IrType[] = [ + narrowed, + ...peers.map((className) => ({ kind: "object" as const, className })), + ]; + arms.sort((a, b) => (typeKey(a) < typeKey(b) ? -1 : 1)); + return { kind: "union", unionId: lowerer.unions.intern(arms) }; +} + +/** The type a member access dispatches on when its receiver `node` is a + * narrowed read of a type-parameter intersection, or null to use the + * checker's type: the class union structuralNarrowing reads, or the + * receiver's own class when this instantiation's class is unrelated to the + * narrowed one but assignable to it (with `T = Identifier`, the reduced + * `T & PrivateIdentifier` still holds an Identifier). */ +export function structuralReadType(lowerer: Lowerer, node: ts.Expression): IrType | null { + if (!ts.isIdentifier(node) && !ts.isPropertyAccessExpression(node)) return null; + if (!intersectsTypeParameter(lowerer.checker.getTypeAtLocation(node))) return null; + const narrowed = lowerer.mapTypeOf(lowerer.typeOf(node)); + if (narrowed?.kind !== "object") return null; + const stored = ts.isIdentifier(node) ? lowerer.peekLocal(node)?.type : undefined; + const symbol = lowerer.checker.getSymbolAtLocation(ts.isIdentifier(node) ? node : node.name); + const declared = + stored ?? (symbol ? lowerer.mapTypeOf(lowerer.checker.getTypeOfSymbol(symbol)) : null); + if (declared?.kind !== "object" || declared.className === narrowed.className) return null; + if (lowerer.isSubclassOf(narrowed.className, declared.className)) + return structuralNarrowing(lowerer, node, declared.className, narrowed); + if (lowerer.isSubclassOf(declared.className, narrowed.className)) return null; + const declaredType = classInstanceType(lowerer, declared.className); + const narrowedType = classInstanceType(lowerer, narrowed.className); + return declaredType !== null && + narrowedType !== null && + lowerer.checker.isTypeAssignableTo(declaredType, narrowedType) + ? declared + : null; +} + +function classInstanceType(lowerer: Lowerer, className: string): ts.Type | null { + const decl = lowerer.classes.get(className)?.decl; + if (!decl?.name || decl.typeParameters?.nodes.length) return null; + const symbol = lowerer.checker.getSymbolAtLocation(decl.name); + return symbol ? lowerer.checker.getDeclaredTypeOfSymbol(symbol) : null; +} + +function intersectsTypeParameter(type: ts.Type): boolean { + try { + if (type.isUnionType()) return ts.constituentTypes(type).some(intersectsTypeParameter); + return ( + (type.flags & ts.TypeFlags.Intersection) !== 0 && + ts.constituentTypes(type).some((part) => (part.flags & ts.TypeFlags.TypeParameter) !== 0) + ); + } catch (e) { + if (ts.isUnreadableConstituentsPanic(e)) return false; + throw e; + } +} + +/** The outermost classes under `staticClass`, other than `target` and its + * subclasses, whose instance type is assignable to `target`'s. */ +function assignableClassPeers(lowerer: Lowerer, staticClass: string, target: string): string[] { + const targetType = classInstanceType(lowerer, target); + if (targetType === null) return []; + const assignable: string[] = []; + for (const className of lowerer.classes.keys()) { + if ( + className === target || + !lowerer.isSubclassOf(className, staticClass) || + lowerer.isSubclassOf(className, target) + ) + continue; + const type = classInstanceType(lowerer, className); + if (type !== null && lowerer.checker.isTypeAssignableTo(type, targetType)) + assignable.push(className); + } + // A subclass of an assignable class is one of its instances already. + return assignable.filter( + (className) => + !assignable.some((other) => other !== className && lowerer.isSubclassOf(className, other)), + ); +} + /** A base-class slot stays one pointer after a guard selects several * subclasses. Recover the tagged view from runtime membership without * copying the object or guessing which of the remaining layouts it has. */ diff --git a/packages/compiler/src/frontend/lowering/coercions/structural-plans.ts b/packages/compiler/src/frontend/lowering/coercions/structural-plans.ts index 78c7cb73d..2bc991c14 100644 --- a/packages/compiler/src/frontend/lowering/coercions/structural-plans.ts +++ b/packages/compiler/src/frontend/lowering/coercions/structural-plans.ts @@ -1,4 +1,4 @@ -import { planRecordUnionWrap } from "../union-retag.js"; +import { planRecordUnionLift, planRecordUnionWrap } from "../union-retag.js"; import type { WidthLift } from "../width-lift.js"; import type { IrType } from "../../../ir/ir.js"; import { isUnitType, typeEquals, UNDEFINED_T } from "../../../ir/ir.js"; @@ -78,6 +78,24 @@ export function widthLiftPlan(lowerer: Lowerer, src: IrType, dst: IrType): Width }); if (absentOnly.length === 1) selected = absentOnly; } + // A record that fits several arms of a discriminated union selects one + // by its discriminant at run time. Without this, an arm that a nested + // field could reach only ambiguously dropped out of the candidates, and + // an arm reached by dropping fields could win alone. + if (selected.length !== 1 && src.kind === "record") { + const source = lowerer.shapes.get(src.shapeId); + if ( + source && + planRecordUnionLift( + source, + def, + (id) => lowerer.shapes.get(id), + (from, to) => lowerer.widthLiftPlan(from, to), + ) + ) { + return { how: "discriminantRoute" }; + } + } if (selected.length !== 1) return null; return { how: "liftWrap", tag: selected[0]!.tag, arm: selected[0]!.arm }; } diff --git a/packages/compiler/src/frontend/lowering/coercions/structural.ts b/packages/compiler/src/frontend/lowering/coercions/structural.ts index 925f59d9b..c85b9868d 100644 --- a/packages/compiler/src/frontend/lowering/coercions/structural.ts +++ b/packages/compiler/src/frontend/lowering/coercions/structural.ts @@ -1,3 +1,4 @@ +import { constructedValue } from "../constructor-returns.js"; import { dynUndefinedExpr, varRef } from "../../../ir/build.js"; import type { FieldLift } from "./structural-plans.js"; import { buildArrayConversion } from "./builders.js"; @@ -106,6 +107,16 @@ export function applyWidthLift( ); return { kind: "call", callee: helper, args: [value], type: dst, loc }; } + case "discriminantRoute": { + if (dst.kind !== "union" || value.type.kind !== "record") + throw new InternalCompilerError("lowerer bug: record discriminant route shape"); + const helper = lowerer.recordUnionRouteHelper(value.type, dst.unionId, loc); + if (!helper) + throw new InternalCompilerError( + "lowerer bug: planned record discriminant route failed to intern", + ); + return { kind: "call", callee: helper, args: [value], type: dst, loc }; + } case "retag": { if (dst.kind !== "union" || value.type.kind !== "union") throw new InternalCompilerError("lowerer bug: retag lift shape"); @@ -524,7 +535,13 @@ export function recordClassWidthHelper( params: [{ localId: "r.0", name: "r", type: fromT }], returnType: toT, locals: [{ id: "r.0", name: "r", type: fromT, mutable: true }], - body: [{ kind: "return", value: { kind: "new", className, args, type: toT, loc }, loc }], + body: [ + { + kind: "return", + value: constructedValue(lowerer, info, { kind: "new", className, args, type: toT, loc }), + loc, + }, + ], loc, }); return name; @@ -603,6 +620,10 @@ function buildRecordProjection( const localId = parameter + ".0"; const receiver = varRef(localId, source, loc); const result: IrType = { kind: "record", shapeId: target.id }; + // A record source's copy keeps the source's key order; a class + // instance's fields keep today's table. + const copyOf = (node: IrExpr): IrExpr => + source.kind === "record" ? lowerer.keyOrder.copy(node, source.shapeId) : node; return { name, params: [{ localId, name: parameter, type: source }], @@ -611,7 +632,7 @@ function buildRecordProjection( body: [ { kind: "return", - value: { + value: copyOf({ kind: "recordLit", fields: target.fields.map((field) => { const conversion = plan.get(field.name)!; @@ -661,7 +682,7 @@ function buildRecordProjection( }), type: result, loc, - }, + }), loc, }, ], diff --git a/packages/compiler/src/frontend/lowering/coercions/unions.ts b/packages/compiler/src/frontend/lowering/coercions/unions.ts index 0356b714e..06c0af9c6 100644 --- a/packages/compiler/src/frontend/lowering/coercions/unions.ts +++ b/packages/compiler/src/frontend/lowering/coercions/unions.ts @@ -4,6 +4,7 @@ import { nodeThrowExpr } from "../../../ir/build.js"; import { planUnionRetag, buildUnionRetag, + planRecordUnionLift, planRecordUnionWrap, buildRecordUnionWrap, } from "../union-retag.js"; @@ -284,6 +285,38 @@ export function recordUnionWrapHelper( return name; } +/** Interned `%record.route.(r)`: a record that fits several arms of a + * discriminated union, converted into the arm its discriminant names + * (planRecordUnionLift). The width plan's `discriminantRoute` lift. */ +export function recordUnionRouteHelper( + lowerer: Lowerer, + source: IrType & { kind: "record" }, + toId: string, + loc: SrcLoc, +): string | null { + const shape = lowerer.shapes.get(source.shapeId); + const to = lowerer.unions.get(toId); + if (!shape || !to) return null; + const key = `recordRoute:${source.shapeId}:${toId}`; + const existing = lowerer.coercions.retags.get(key); + if (existing) return existing; + const plan = planRecordUnionLift( + shape, + to, + (id) => lowerer.shapes.get(id), + (from, dst) => lowerer.widthLiftPlan(from, dst), + ); + if (!plan) return null; + const name = `%record.route.${lowerer.coercions.retags.size}`; + lowerer.coercions.retags.set(key, name); + lowerer.liftedFns.push( + buildRecordUnionWrap(name, source, to, plan, loc, (lift, value, dst) => + lowerer.applyWidthLift(lift, value, dst, loc), + ), + ); + return name; +} + export function unionRetagHelper( lowerer: Lowerer, fromId: string, diff --git a/packages/compiler/src/frontend/lowering/collection-narrowing.ts b/packages/compiler/src/frontend/lowering/collection-narrowing.ts index 57b75daf5..29b358e99 100644 --- a/packages/compiler/src/frontend/lowering/collection-narrowing.ts +++ b/packages/compiler/src/frontend/lowering/collection-narrowing.ts @@ -13,12 +13,7 @@ export function concreteCollectionNarrow( const symbol = narrowed.getSymbol(); const name = symbol?.name; if (name !== "Map" && name !== "Set") return null; - if ( - !symbol || - !lowerer.checker - .declarationsOf(symbol) - .some((decl) => lowerer.isStdlibFile(decl.getSourceFile())) - ) + if (!symbol || !symbol.declarations.some((decl) => lowerer.isStdlibFile(decl.getSourceFile()))) return null; const args = lowerer.checker.getTypeArguments(narrowed as ts.TypeReference); if (args.length === 0 || !args.every((arg) => (arg.flags & ts.TypeFlags.Any) !== 0)) return null; diff --git a/packages/compiler/src/frontend/lowering/constructor-returns.ts b/packages/compiler/src/frontend/lowering/constructor-returns.ts new file mode 100644 index 000000000..bee2e7567 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/constructor-returns.ts @@ -0,0 +1,522 @@ +/** Constructors that `return` a value. + * + * JavaScript's `new C()` answers the constructor's returned value when it + * is an object (functions included) and the constructed `this` otherwise. + * A primitive or bare return therefore keeps the ordinary lowering, which + * evaluates the operand and drops it. + * + * A return whose value is an instance of the constructor's own class keeps + * the class representation (`if (Single.instance) return + * Single.instance;`). The return stores that instance in a per-class + * result slot and every direct `new C()` takes it from there. Any other + * object has no representation as a `C` instance and refuses at the return + * statement, deferred to run time in JavaScript like other statement + * fences. A JavaScript value that is an object only at run time (`any`) + * is tested when it returns: a primitive answers the constructed + * instance, an instance of the class takes the result slot, and any other + * object refuses. + * + * `super()` from a subclass refuses: Node would rebind the subclass's + * `this` to the returned instance. In JavaScript the refusal is a run-time + * test after the call: an instance other than the subclass's own `this` + * refuses, and the constructed instance itself (`return this.set(r, g, + * b)`, where `set` answers `this`) continues. */ +import * as ts from "../ts7/adapter.js"; +import { varRef } from "../../ir/build.js"; +import { + BOOL, + DYN, + type IrExpr, + type IrGlobal, + type IrStmt, + type IrType, + STRING, + typeKey, + VOID, +} from "../../ir/ir.js"; +import { unsupportedDiag } from "../../diagnostics/diagnostic.js"; +import { isJsSourceFile, locOf } from "../program.js"; +import type { ClassInfo } from "./lower-classes.js"; +import type { Lowerer } from "./lowerer.js"; + +type ReturnKind = "instance" | "object" | "mixed"; + +export interface ConstructorReturnPlan { + /** Returns whose value may be an object, by kind. Primitive and `this` + * returns are absent: they keep the ordinary lowering. */ + returns: Map; + /** The result slot (`C | undefined`), present when some return yields an + * instance of the class itself. */ + slot: IrGlobal | null; +} + +const PRIMITIVE_FLAGS = + ts.TypeFlags.Primitive | ts.TypeFlags.EnumLike | ts.TypeFlags.Void | ts.TypeFlags.Never; + +/** Types whose values may be primitives or objects: open types, type + * variables and intersections (a branded `string & { tag }` is a string). */ +const OPEN_FLAGS = + ts.TypeFlags.AnyOrUnknown | ts.TypeFlags.InstantiableNonPrimitive | ts.TypeFlags.Intersection; + +function unparenthesized(node: ts.Expression): ts.Expression { + let e = node; + while (ts.isParenthesizedExpression(e)) e = e.expression; + return e; +} + +/** Whether a value of this checker type can be an object: "never", + * "always", or "maybe" (open types and primitive/object unions). */ +function objectness(type: ts.Type): "never" | "always" | "maybe" { + const parts = type.isUnionType() ? ts.constituentTypes(type) : [type]; + let primitive = false; + let object = false; + for (const part of parts) { + if ((part.flags & OPEN_FLAGS) !== 0) return "maybe"; + if ((part.flags & PRIMITIVE_FLAGS) !== 0) primitive = true; + else object = true; + } + if (!object) return "never"; + return primitive ? "maybe" : "always"; +} + +/** The constructor's own `return ` statements: nested functions, + * arrows and classes have their own returns. */ +function ownValueReturns(ctor: ts.ConstructorDeclaration): ts.ReturnStatement[] { + const out: ts.ReturnStatement[] = []; + const visit = (node: ts.Node): void => { + if (ts.isFunctionLike(node) || ts.isClassLikeDeclaration(node)) return; + if (ts.isReturnStatement(node) && node.expression !== undefined) out.push(node); + node.forEachChild(visit); + }; + ctor.body?.forEachChild(visit); + return out; +} + +/** A return inside a `try` with a `finally` block: the finally block runs + * after the result is stored and may throw past it. */ +function insideFinallyTry(node: ts.Node, ctor: ts.ConstructorDeclaration): boolean { + for (let p = node.parent; p !== undefined && p !== ctor; p = p.parent) { + if (ts.isTryStatement(p) && p.finallyBlock !== undefined && !nodeWithin(node, p.finallyBlock)) + return true; + } + return false; +} + +function nodeWithin(node: ts.Node, ancestor: ts.Node): boolean { + return node.pos >= ancestor.pos && node.end <= ancestor.end; +} + +function instanceType(info: ClassInfo): IrType { + return { kind: "object", className: info.def.name }; +} + +function isInstanceOf(lowerer: Lowerer, info: ClassInfo, type: IrType | null): boolean { + if (type === null) return false; + if (type.kind === "object") return type.className === info.def.name; + if (type.kind !== "union") return false; + const arms = lowerer.unions.get(type.unionId)?.arms ?? []; + return ( + arms.length === 2 && + arms.some((arm) => arm.kind === "object" && arm.className === info.def.name) && + arms.some((arm) => arm.kind === "undefinedT") + ); +} + +/** Whether an expression always yields the receiver itself: `this`, or a + * call `this.m(...)` / `super.m(...)` whose every implementation the + * receiver can reach (the statically resolved method and, for `this` + * calls, each subclass override) returns only `this`, a primitive, or + * nothing (`return this.set(r, g, b)` with a chainable setter). Such a + * return answers the constructed instance. Methods already being checked + * count as returning `this` (their other returns decide). */ +function returnsThis( + lowerer: Lowerer, + info: ClassInfo, + node: ts.Expression, + visiting: Set = new Set(), +): boolean { + let e = node; + while (ts.isParenthesizedExpression(e) || ts.isAsExpression(e) || ts.isNonNullExpression(e)) + e = e.expression; + if (e.kind === ts.SyntaxKind.ThisKeyword) return true; + if (!ts.isCallExpression(e)) return false; + const callee = unparenthesized(e.expression); + if ( + !ts.isPropertyAccessExpression(callee) || + callee.questionDotToken !== undefined || + !ts.isIdentifier(callee.name) + ) + return false; + const receiver = unparenthesized(callee.expression).kind; + if (receiver !== ts.SyntaxKind.ThisKeyword && receiver !== ts.SyntaxKind.SuperKeyword) + return false; + const name = callee.name.text; + const symbol = lowerer.checker.getSymbolAtLocation(callee.name); + if (symbol === undefined) return false; + const methods: ts.Node[] = [...symbol.declarations]; + if (receiver === ts.SyntaxKind.ThisKeyword) { + const pending = [...info.subclasses]; + while (pending.length > 0) { + const sub = pending.pop()!; + pending.push(...sub.subclasses); + for (const member of sub.decl?.members.nodes ?? []) { + if (member.name === undefined || !ts.isIdentifier(member.name) || member.name.text !== name) + continue; + if (!ts.isMethodDeclaration(member)) return false; + methods.push(member); + } + } + } + if (methods.length === 0) return false; + for (const method of methods) { + if (!ts.isMethodDeclaration(method) || method.body === undefined) return false; + if (visiting.has(method)) continue; + visiting.add(method); + for (const ret of ownReturns(method.body)) { + if (ret.expression === undefined) continue; + if (objectness(lowerer.typeOf(ret.expression)) === "never") continue; + if (!returnsThis(lowerer, info, ret.expression, visiting)) return false; + } + } + return true; +} + +/** A function body's own return statements (nested functions, arrows and + * classes have their own). */ +function ownReturns(body: ts.Node): ts.ReturnStatement[] { + const out: ts.ReturnStatement[] = []; + const visit = (node: ts.Node): void => { + if (ts.isFunctionLike(node) || ts.isClassLikeDeclaration(node)) return; + if (ts.isReturnStatement(node)) out.push(node); + node.forEachChild(visit); + }; + body.forEachChild(visit); + return out; +} + +export function constructorReturnPlan( + lowerer: Lowerer, + info: ClassInfo, +): ConstructorReturnPlan | null { + const byClass = lowerer.constructorReturnPlans; + const known = byClass.get(info.def.name); + if (known !== undefined) return known; + const ctor = info.ctor; + let plan: ConstructorReturnPlan | null = null; + if (ctor?.body !== undefined && info.decl !== null) { + const returns = new Map(); + for (const stmt of ownValueReturns(ctor)) { + const value = unparenthesized(stmt.expression!); + if (returnsThis(lowerer, info, value)) continue; + const type = lowerer.typeOf(value); + const shape = objectness(type); + if (shape === "never") continue; + // Local classes construct through their own environment, which has + // no result slot. + const instance = + info.localClass === undefined && + !insideFinallyTry(stmt, ctor) && + isInstanceOf(lowerer, info, lowerer.mapTypeOf(type)); + returns.set(stmt, instance ? "instance" : shape === "always" ? "object" : "mixed"); + } + if (returns.size > 0) { + let slot: IrGlobal | null = null; + // A JavaScript value typed only at run time may hold an instance of + // the class too (`if (C.instance) return C.instance;`). + const runtimeInstance = + info.localClass === undefined && + isJsSourceFile(ctor.getSourceFile()) && + [...returns].some(([stmt, kind]) => kind === "mixed" && !insideFinallyTry(stmt, ctor)); + if ([...returns.values()].includes("instance") || runtimeInstance) { + slot = { + id: `%g.${info.def.name}.%constructorResult`, + name: `${info.def.name}.%constructorResult`, + type: lowerer.withUndefinedArm(instanceType(info)), + mutable: true, + }; + lowerer.globalsList.push(slot); + } + plan = { returns, slot }; + } + } + byClass.set(info.def.name, plan); + return plan; +} + +function foreignObjectFeature(info: ClassInfo): string { + const display = info.def.name.replace(/^%|^%m\d+\./, ""); + return `returning an object other than an instance of the class from a constructor (Node's 'new ${display}()' answers the returned object, which has no representation as a '${display}' instance)`; +} + +/** `return ` inside a class constructor whose value may be an + * object. Null when the statement keeps the ordinary lowering. */ +export function lowerConstructorReturn(lowerer: Lowerer, stmt: ts.ReturnStatement): IrStmt | null { + const info = lowerer.currentClass; + if (info === null || info.ctor === null || stmt.expression === undefined) return null; + let fn: ts.Node | undefined = stmt.parent; + while (fn !== undefined && !ts.isFunctionLike(fn) && !ts.isClassLikeDeclaration(fn)) + fn = fn.parent; + if (fn !== info.ctor) return null; + const plan = constructorReturnPlan(lowerer, info); + const kind = plan?.returns.get(stmt); + if (plan === null || kind === undefined) return null; + const loc = locOf(stmt); + if (kind === "instance") { + const value = lowerer.coerceInto( + stmt.expression, + lowerer.lowerExpr(stmt.expression), + plan.slot!.type, + ); + return { + kind: "block", + body: [ + { kind: "assign", localId: plan.slot!.id, value, loc }, + { kind: "return", value: null, loc }, + ], + loc, + }; + } + const feature = foreignObjectFeature(info); + if (kind === "object" || !isJsSourceFile(stmt.getSourceFile())) + lowerer.unsupported("SC1090", stmt, feature); + // A JavaScript value that is an object only sometimes: a primitive + // still answers the constructed instance, an object refuses. + const value = lowerer.coerceToExpected(lowerer.lowerExpr(stmt.expression), DYN); + if (value.type.kind !== "dyn") lowerer.unsupported("SC1090", stmt, feature); + const held = lowerer.declareHiddenLocal("%returned", DYN); + const read = varRef(held.id, DYN, loc); + const typeOf: IrExpr = { kind: "libCall", fn: "dyn.typeof", args: [read], type: STRING, loc }; + const typeIs = (name: string): IrExpr => ({ + kind: "strEq", + negated: false, + left: typeOf, + right: { kind: "strLit", value: name, type: STRING, loc }, + type: BOOL, + loc, + }); + const diag = unsupportedDiag("SC1090", loc, feature); + lowerer.runtimeFences.push(diag); + const sf = stmt.getSourceFile(); + const line = ts.getLineAndCharacterOfPosition(sf, loc.start).line + 1; + return { + kind: "block", + body: [ + { kind: "varDecl", localId: held.id, init: value, loc }, + { + kind: "if", + // null answers "object" and is falsy; every object is truthy. + cond: { + kind: "logical", + op: "||", + left: typeIs("function"), + right: { + kind: "logical", + op: "&&", + left: typeIs("object"), + right: lowerer.ensureBool(read, stmt.expression), + type: BOOL, + loc, + }, + type: BOOL, + loc, + }, + then: [ + ...(plan.slot !== null && !insideFinallyTry(stmt, info.ctor) + ? [ + { + kind: "if" as const, + cond: { + kind: "libCall" as const, + fn: "dyn.typedRefIs" as const, + args: [ + read, + { + kind: "strLit" as const, + value: typeKey(instanceType(info)), + type: STRING, + loc, + }, + ], + type: BOOL, + loc, + }, + then: [ + { + kind: "assign" as const, + localId: plan.slot.id, + value: lowerer.coerceToExpected( + lowerer.coerceToExpected(read, instanceType(info)), + plan.slot.type, + ), + loc, + }, + { kind: "return" as const, value: null, loc }, + ], + else_: null, + loc, + }, + ] + : []), + { + kind: "runtimeFence", + code: diag.code, + message: `${diag.message} [${diag.code} at ${loc.file}:${line}]`, + loc, + }, + ], + else_: null, + loc, + }, + { kind: "return", value: null, loc }, + ], + loc, + }; +} + +/** The value of a construction of exactly class `C`: the constructed + * instance, or the instance the constructor returned. Unchanged for + * classes without a result slot. The slot is read and cleared by a + * per-class helper, so every construction site (direct `new`, class + * values, the checked dynamic dispatchers) shares one protocol. */ +export function constructedValue(lowerer: Lowerer, info: ClassInfo, constructed: IrExpr): IrExpr { + const slot = constructorReturnPlan(lowerer, info)?.slot; + if (slot === null || slot === undefined) return constructed; + const loc = constructed.loc; + const type = instanceType(info); + const name = `%${info.def.name}.%constructed`; + if (!lowerer.liftedFns.some((fn) => fn.name === name)) { + const instance = { id: "instance.0", name: "instance", type, mutable: false }; + const returned = { id: "returned.0", name: "returned", type: slot.type, mutable: false }; + lowerer.liftedFns.push({ + name, + params: [{ localId: instance.id, name: instance.name, type }], + returnType: type, + locals: [instance, returned], + body: [ + { kind: "varDecl", localId: returned.id, init: varRef(slot.id, slot.type, loc), loc }, + { kind: "assign", localId: slot.id, value: lowerer.wrappedUndefined(slot.type, loc)!, loc }, + { + kind: "return", + value: { + kind: "orDefault", + left: varRef(returned.id, slot.type, loc), + right: varRef(instance.id, type, loc), + type, + loc, + }, + loc, + }, + ], + loc, + }); + } + return { kind: "call", callee: name, args: [constructed], type, loc }; +} + +/** A construction through a class value whose static class is `info`: the + * value holds `info` itself or a descendant. Descendants of a class with a + * result slot refuse at their `super()`, so only `info`'s own slot can + * apply; a descendant with its own slot has no static home here. */ +export function constructedFromValue( + lowerer: Lowerer, + info: ClassInfo, + constructed: IrExpr, + blame: ts.Node, +): IrExpr { + const pending = [...info.subclasses]; + while (pending.length > 0) { + const sub = pending.pop()!; + pending.push(...sub.subclasses); + const plan = constructorReturnPlan(lowerer, sub); + if (plan === null || plan.slot === null) continue; + const display = sub.def.name.replace(/^%|^%m\d+\./, ""); + lowerer.unsupported( + "SC1090", + blame, + `constructing through a class value that may hold '${display}', whose constructor can return an existing instance`, + ); + } + return constructedValue(lowerer, info, constructed); +} + +/** `super()` into a class with a result slot, from the constructor of + * class `sub`: `call` runs the base constructor over `self` (the + * subclass's `this`, upcast to the base). Node rebinds the subclass's + * `this` to an instance the base constructor returns, which has no + * representation. A TypeScript subclass refuses. In JavaScript the base + * stores the instance it returns in its slot, and a per-subclass helper + * reads and clears the slot after the call: an instance other than `self` + * refuses at run time. Unchanged for bases without a result slot. */ +export function superIntoResultSlot( + lowerer: Lowerer, + base: ClassInfo, + sub: ClassInfo, + call: IrStmt, + self: IrExpr, + blame: ts.Node, +): IrStmt { + const plan = constructorReturnPlan(lowerer, base); + if (plan === null || plan.slot === null) return call; + const slot = plan.slot; + const display = base.def.name.replace(/^%|^%m\d+\./, ""); + const feature = `extending a class whose constructor can return an existing instance ('super()' would rebind 'this' to the instance '${display}' returns)`; + if (!isJsSourceFile(blame.getSourceFile())) { + lowerer.unsupported("SC1090", blame, feature); + return call; + } + const loc = locOf(blame); + const type = instanceType(base); + const name = `%${sub.def.name}.%superResult`; + if (!lowerer.liftedFns.some((fn) => fn.name === name)) { + const diag = unsupportedDiag("SC1090", loc, feature); + lowerer.runtimeFences.push(diag); + const line = ts.getLineAndCharacterOfPosition(blame.getSourceFile(), loc.start).line + 1; + const instance = { id: "instance.0", name: "instance", type, mutable: false }; + const returned = { id: "returned.0", name: "returned", type: slot.type, mutable: false }; + lowerer.liftedFns.push({ + name, + params: [{ localId: instance.id, name: instance.name, type }], + returnType: VOID, + locals: [instance, returned], + body: [ + { kind: "varDecl", localId: returned.id, init: varRef(slot.id, slot.type, loc), loc }, + { kind: "assign", localId: slot.id, value: lowerer.wrappedUndefined(slot.type, loc)!, loc }, + { + kind: "if", + cond: { + kind: "bin", + op: "!==", + left: { + kind: "orDefault", + left: varRef(returned.id, slot.type, loc), + right: varRef(instance.id, type, loc), + type, + loc, + }, + right: varRef(instance.id, type, loc), + type: BOOL, + loc, + }, + then: [ + { + kind: "runtimeFence", + code: diag.code, + message: `${diag.message} [${diag.code} at ${loc.file}:${line}]`, + loc, + }, + ], + else_: null, + loc, + }, + { kind: "return", value: null, loc }, + ], + loc, + }); + } + const check: IrStmt = { + kind: "exprStmt", + expr: { kind: "call", callee: name, args: [self], type: VOID, loc }, + loc, + }; + return { kind: "block", body: [call, check], loc }; +} diff --git a/packages/compiler/src/frontend/lowering/containers/bytes.ts b/packages/compiler/src/frontend/lowering/containers/bytes.ts index 7f1f758ca..c5a41cf32 100644 --- a/packages/compiler/src/frontend/lowering/containers/bytes.ts +++ b/packages/compiler/src/frontend/lowering/containers/bytes.ts @@ -20,6 +20,7 @@ import { arrayOf, bytesOf, funcOf, + isUnitType, typeEquals, } from "../../../ir/ir.js"; import { isJsSourceFile, locOf } from "../../program.js"; @@ -554,9 +555,9 @@ export function lowerBytesMethodCall( const nameSym = lowerer.checker.getSymbolAtLocation(access.name); return ( nameSym !== undefined && - lowerer.checker - .declarationsOf(nameSym) - .some((d) => ts.isInterfaceDeclaration(d.parent) && d.parent.name.text === "Buffer") + nameSym.declarations.some( + (d) => ts.isInterfaceDeclaration(d.parent) && d.parent.name.text === "Buffer", + ) ); })(); const method = name === "subarray" || declaredOnBuffer ? "subarray" : "slice"; @@ -580,13 +581,11 @@ export function lowerBytesMethodCall( const symbol = lowerer.checker.getSymbolAtLocation(access.name); return ( symbol !== undefined && - lowerer.checker - .declarationsOf(symbol) - .some( - (declaration) => - ts.isInterfaceDeclaration(declaration.parent) && - declaration.parent.name.text === "Buffer", - ) + symbol.declarations.some( + (declaration) => + ts.isInterfaceDeclaration(declaration.parent) && + declaration.parent.name.text === "Buffer", + ) ); })() ) { @@ -646,9 +645,9 @@ export function lowerBytesMethodCall( const nameSym = lowerer.checker.getSymbolAtLocation(access.name); return ( nameSym !== undefined && - lowerer.checker - .declarationsOf(nameSym) - .some((d) => ts.isInterfaceDeclaration(d.parent) && d.parent.name.text === "Buffer") + nameSym.declarations.some( + (d) => ts.isInterfaceDeclaration(d.parent) && d.parent.name.text === "Buffer", + ) ); })(); if (receiverIr.elem !== "u8" || !declaredOnBuffer) { @@ -724,9 +723,9 @@ export function lowerBytesMethodCall( const nameSym = lowerer.checker.getSymbolAtLocation(access.name); return ( nameSym !== undefined && - lowerer.checker - .declarationsOf(nameSym) - .some((d) => ts.isInterfaceDeclaration(d.parent) && d.parent.name.text === "Buffer") + nameSym.declarations.some( + (d) => ts.isInterfaceDeclaration(d.parent) && d.parent.name.text === "Buffer", + ) ); })(); if (declOnBuffer && receiverIr.elem === "u8") { @@ -1273,16 +1272,14 @@ function knownBufferProducer( const symbol = lowerer.checker.getSymbolAtLocation(node); if (!symbol || seen.has(symbol)) return false; seen.add(symbol); - return lowerer.checker - .declarationsOf(symbol) - .some( - (decl) => - ts.isVariableDeclaration(decl) && - ts.isVariableDeclarationList(decl.parent) && - (decl.parent.flags & ts.NodeFlags.Const) !== 0 && - decl.initializer !== undefined && - knownBufferProducer(lowerer, decl.initializer, seen), - ); + return symbol.declarations.some( + (decl) => + ts.isVariableDeclaration(decl) && + ts.isVariableDeclarationList(decl.parent) && + (decl.parent.flags & ts.NodeFlags.Const) !== 0 && + decl.initializer !== undefined && + knownBufferProducer(lowerer, decl.initializer, seen), + ); } if (!ts.isCallExpression(node)) return false; const callee = node.expression; @@ -1429,7 +1426,14 @@ function lowerBufferStaticValue( } return { kind: "libCall", fn, args: [s, enc], type: BYTES_U8, loc }; } - if (args.length === 1 && srcIr?.kind === "bytes" && srcIr.elem === "u8") { + // A checker type with no mapping (pako's conditional `O extends + // { toText: true } ? string : Uint8Array` result) whose lowered + // value is a u8 view copies like the typed form. + if ( + args.length === 1 && + ((srcIr?.kind === "bytes" && srcIr.elem === "u8") || + (srcIr === null && value.type.kind === "bytes" && value.type.elem === "u8")) + ) { return { kind: "libCall", fn: "buffer.poolFromBytes", @@ -1447,6 +1451,34 @@ function lowerBufferStaticValue( loc, }; } + // A union of strings, u8 views and undefined/null (a JS-inferred + // npm result such as pako's deflate): the checked form dispatches + // on the runtime kind exactly as for an untyped JS value, Node's + // TypeError for the unit arms included. + if ( + args.length === 1 && + value.type.kind === "union" && + lowerer.dynConvertible(value.type) && + lowerer.unions + .get(value.type.unionId) + ?.arms.every( + (arm) => + arm.kind === "string" || + (arm.kind === "bytes" && arm.elem === "u8") || + isUnitType(arm), + ) === true + ) { + return { + kind: "libCall", + fn: "buffer.poolFromDyn", + args: [ + lowerer.coerceToExpected(value, DYN), + { kind: "strLit", value: "utf8", type: STRING, loc }, + ], + type: BYTES_U8, + loc, + }; + } } } lowerer.noLowering( @@ -1659,6 +1691,12 @@ function lowerBufferStaticValue( }; } } + // A typed view of unknown provenance (a Uint8Array parameter, + // safe-buffer's Buffer.from result): Buffer is a brand of the view, + // which the runtime keeps, so the test reads it. + if (v.type.kind === "bytes") { + return { kind: "libCall", fn: "buffer.isBranded", args: [v], type: BOOL, loc }; + } lowerer.noLowering( `Buffer.isBuffer of '${lowerer.fmt(v.type)}' values`, args[0]!, diff --git a/packages/compiler/src/frontend/lowering/containers/for-of.ts b/packages/compiler/src/frontend/lowering/containers/for-of.ts index 79f970eb1..2331ee25a 100644 --- a/packages/compiler/src/frontend/lowering/containers/for-of.ts +++ b/packages/compiler/src/frontend/lowering/containers/for-of.ts @@ -153,7 +153,7 @@ export function lowerForOfSearchParams( } : singleRead(); if (ts.isIdentifier(decl.name)) { - const varTarget = forOfVarTarget(lowerer, decl); + const varTarget = forOfVarTarget(lowerer, decl, elemT!); if (varTarget) { // `for (var x of ...)`: one function-scoped binding, assigned // per pass — closures made in the loop share it, and the value @@ -357,7 +357,7 @@ export function lowerForOfArrayIter( } : keyRead(); if (ts.isIdentifier(decl.name)) { - const varTarget = forOfVarTarget(lowerer, decl); + const varTarget = forOfVarTarget(lowerer, decl, declT!); if (varTarget) { // `for (var x of ...)`: one function-scoped binding, assigned // per pass — closures made in the loop share it, and the value @@ -650,7 +650,7 @@ function lowerForOfMapOrSet( } : singleRead(); if (ts.isIdentifier(decl!.name)) { - const varTarget = forOfVarTarget(lowerer, decl!); + const varTarget = forOfVarTarget(lowerer, decl!, elemT!); if (varTarget) { // `for (var x of ...)`: one function-scoped binding, assigned // per pass — closures made in the loop share it, and the value diff --git a/packages/compiler/src/frontend/lowering/containers/indexed-objects.ts b/packages/compiler/src/frontend/lowering/containers/indexed-objects.ts index 38df368ac..450e64142 100644 --- a/packages/compiler/src/frontend/lowering/containers/indexed-objects.ts +++ b/packages/compiler/src/frontend/lowering/containers/indexed-objects.ts @@ -810,6 +810,7 @@ export function lowerRecordOvfCaptureHelper( // Every source declared field must flow into the keyed-write slot — // in DECLARATION order (insertion order below; declaredOrder omits // internal '%'-fields, which never enter the copy). + lowerer.keyOrder.observed.add(from.id); const orderedFields = from.declaredOrder ? from.declaredOrder.flatMap((n) => { const f = from.fields.find((x) => x.name === n); @@ -1097,6 +1098,7 @@ export function lowerObjectAssignIndexShape( const from = lowerer.shapes.get(srcIr.shapeId); if (!from || from.tuple) return null; if (from.fields.some((f) => f.name.startsWith("%"))) return null; + lowerer.keyOrder.observed.add(from.id); const orderedFields = from.declaredOrder ? from.declaredOrder.flatMap((n) => { const f = from.fields.find((x) => x.name === n); diff --git a/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts b/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts index aeefffe48..46074573d 100644 --- a/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts +++ b/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts @@ -298,7 +298,7 @@ function isHelperObject(lowerer: Lowerer, expr: ts.Expression): boolean { if (isStageCall(lowerer, expr)) return true; if (!ts.isIdentifier(expr)) return false; const symbol = lowerer.resolveValueSymbol(expr); - const decl = symbol ? lowerer.checker.valueDeclarationOf(symbol) : undefined; + const decl = symbol ? symbol.valueDeclaration : undefined; if ( !decl || !ts.isVariableDeclaration(decl) || @@ -1800,9 +1800,15 @@ function foldStages( ...inner(value), ]; } else { + // A take before this stage sets `done` as it delivers its last + // value, which still flattens in full: the inner walk runs with the + // flag clear, and keeps it set afterwards. + const afterTake = slots.slice(0, si).some((slot) => slot.kind === "take"); emit = (value) => { const innerT = s.fnT!.ret; const innerId = frame.local("fm", innerT, false); + const heldId = afterTake ? frame.local("held", BOOL, false) : undefined; + const walk = walkInner(lowerer, frame, innerId, innerT, done(), inner, loc); return [ { kind: "varDecl", @@ -1811,7 +1817,26 @@ function foldStages( loc, }, ...(s.cntId !== undefined ? [frame.bump(s.cntId)] : []), - ...walkInner(lowerer, frame, innerId, innerT, done(), inner, loc), + ...(heldId === undefined + ? walk + : [ + { kind: "varDecl", localId: heldId, init: done(), loc } as IrStmt, + { kind: "assign", localId: doneId, value: boolLit(false, loc), loc } as IrStmt, + ...walk, + { + kind: "assign", + localId: doneId, + value: { + kind: "logical", + op: "||", + left: varRef(heldId, BOOL, loc), + right: done(), + type: BOOL, + loc, + }, + loc, + } as IrStmt, + ]), ]; }; } diff --git a/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts b/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts index 832dc7def..39154a820 100644 --- a/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts +++ b/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts @@ -1,5 +1,12 @@ import { matchAllRowsReadByElements } from "./match-row-uses.js"; -import { dynUndefinedExpr, nodeThrowExpr, numLit, strLit, varRef } from "../../../ir/build.js"; +import { + boolLit, + dynUndefinedExpr, + nodeThrowExpr, + numLit, + strLit, + varRef, +} from "../../../ir/build.js"; import * as ts from "../../ts7/adapter.js"; import { BOOL, @@ -316,6 +323,21 @@ export function lowerStringIndexCall( ) { index = lowerer.coerceInto(indexNode, index, DYN); } + return stringIndexHelperCall(lowerer, call, method, receiver, receiverNode, index, indexNode); +} + +/** The relative-index / code-point helper call over an already lowered + * index (any type positionNumber converts — dyn indices included). */ +function stringIndexHelperCall( + lowerer: Lowerer, + call: ts.CallExpression, + method: "at" | "codePointAt", + receiver: IrExpr, + receiverNode: ts.Node, + index: IrExpr, + indexNode: ts.Expression | undefined, +): IrExpr { + const loc = locOf(call); const valueType = method === "at" ? STRING : F64; const resultType = lowerer.withUndefinedArmOf(valueType); if (!resultType || resultType.kind !== "union") @@ -1061,7 +1083,8 @@ export function lowerStringMethodCall( const replaceMethod = STRING_REPLACE_METHODS.has(access.name.text) ? (access.name.text as "replace" | "replaceAll") : null; - if (!entry && !indexMethod && !replaceMethod) return null; + const extraMethod = EXTRA_STRING_METHODS.has(access.name.text); + if (!entry && !indexMethod && !replaceMethod && !extraMethod) return null; // A validated dyn receiver (`pkg.name.replace(...)` on a JSON.parse // value) arrives pre-extracted through `dynReceiver`; its checker type // is `any`, so the type/symbol gates don't apply — the dyn value's @@ -1089,6 +1112,12 @@ export function lowerStringMethodCall( return null; if (!lowerer.isStdlibMember(access)) return null; } + if (extraMethod && argumentNodes === call.arguments.nodes) { + const receiver = dynReceiver + ? dynReceiver() + : lowerMethodReceiver(lowerer, access.expression, STRING, access.name.text); + return lowerExtraStringMethodCall(lowerer, call, access.name.text, receiver, access.expression); + } // The lib declares optional parameters beyond some lowered forms; fence // those arities instead of passing arguments the runtime doesn't take. if ( @@ -1386,3 +1415,393 @@ function lowerLocaleCompareCall( lowerer.badType(call.arguments.nodes[0]!, lowerer.typeOf(call.arguments.nodes[0]!)); return { kind: "libCall", fn: "str.localeCompare", args: [receiver, arg], type: F64, loc }; } + +/* ── String.prototype / Number.prototype members beyond the core tables ── + * substr, the locale case pair, the Annex B HTML methods, toPrecision and + * toExponential(fractionDigits) on typed receivers, plus the kind dispatch + * for every String/Number method a checked-dynamic receiver can name + * whose runtime dispatch had no answer (`character.codePointAt(0)` in a + * JS package, `n.toPrecision(3)` on a JS-inferred number). Each path + * replaces a compile-time refusal, a runtime 'not supported yet' fence or + * Node's is-not-a-function answer for a real method, so programs that + * never spell these names compile exactly as before. */ + +/** String.prototype members (Annex B included) whose typed-receiver + * lowering lives here. */ +export const EXTRA_STRING_METHODS: ReadonlySet = new Set([ + "substr", + "toLocaleLowerCase", + "toLocaleUpperCase", + "anchor", + "big", + "blink", + "bold", + "fixed", + "fontcolor", + "fontsize", + "italics", + "link", + "small", + "strike", + "sub", + "sup", +]); + +/** String methods a dyn receiver dispatches by runtime kind here: a string + * runs the typed lowering, every other kind takes the runtime dispatch + * (an object's own member, Node's is-not-a-function otherwise). Only names + * no other dyn kind's prototype declares: the shared ones (at, concat, + * slice, ...) already dispatch at run time for arrays, and a kind test in + * front of them would change array code that works today. The Annex B + * HTML names (bold, italics, link, ...) stay out too: chalk-style builders + * call them on functions and objects, whose calls must stay as they are. */ +const DYN_STRING_KIND_METHODS: ReadonlySet = new Set([ + "substr", + "toLocaleLowerCase", + "toLocaleUpperCase", + "codePointAt", + "isWellFormed", + "toWellFormed", + "trimLeft", + "trimRight", +]); + +/** Number methods a dyn receiver dispatches by runtime kind here. */ +const DYN_NUMBER_KIND_METHODS: ReadonlySet = new Set(["toPrecision", "toExponential"]); + +/** Annex B CreateHTML: the tag and the attribute (null for none). */ +const HTML_METHODS: Readonly> = { + anchor: ["a", "name"], + big: ["big", null], + blink: ["blink", null], + bold: ["b", null], + fixed: ["tt", null], + fontcolor: ["font", "color"], + fontsize: ["font", "size"], + italics: ["i", null], + link: ["a", "href"], + small: ["small", null], + strike: ["strike", null], + sub: ["sub", null], + sup: ["sup", null], +}; + +export function isDynPrimitiveKindMethod(name: string): boolean { + return DYN_STRING_KIND_METHODS.has(name) || DYN_NUMBER_KIND_METHODS.has(name); +} + +/** Evaluate `value` once into a hidden local (effect-free reads stay). */ +function hoist(lowerer: Lowerer, value: IrExpr, stmts: IrStmt[], name: string): IrExpr { + if ( + value.kind === "varRef" || + value.kind === "numLit" || + value.kind === "strLit" || + value.kind === "boolLit" || + value.kind === "unitLit" + ) + return value; + const local = lowerer.declareHiddenLocal(name, value.type); + stmts.push({ kind: "varDecl", localId: local.id, init: value, loc: value.loc }); + return varRef(local.id, value.type, value.loc); +} + +function seq(stmts: IrStmt[], result: IrExpr): IrExpr { + return stmts.length === 0 + ? result + : { kind: "seqExpr", stmts, result, type: result.type, loc: result.loc }; +} + +/** ToString of a method argument: the dyn coercion (symbols throw), the + * typed conversions elsewhere. */ +function stringArg(lowerer: Lowerer, value: IrExpr, node: ts.Node): IrExpr { + if (value.type.kind === "dyn") + return { + kind: "libCall", + fn: "dyn.toStringCoerce", + args: [value], + type: STRING, + loc: value.loc, + }; + return lowerer.ensureString(value, node); +} + +/** An optional numeric argument: its ToNumber value and whether it was + * supplied (undefined counts as absent, as the spec's checks do). */ +function optionalNumber( + lowerer: Lowerer, + value: IrExpr | undefined, + node: ts.Expression, + stmts: IrStmt[], + subject: string, + loc: SrcLoc, +): { value: IrExpr; has: IrExpr } { + if (!value || value.type.kind === "undefinedT" || value.type.kind === "void") + return { value: numLit(0, loc), has: boolLit(false, loc) }; + if ( + value.type.kind === "f64" || + value.type.kind === "string" || + value.type.kind === "bool" || + value.type.kind === "nullT" + ) + return { + value: positionNumber(lowerer, value, numLit(0, loc), node, subject), + has: boolLit(true, loc), + }; + const boxed = + value.type.kind === "dyn" + ? value + : hoist(lowerer, lowerer.coerceToExpected(value, DYN), stmts, "%numArg"); + const absent: IrExpr = { kind: "dynTest", test: "undefined", value: boxed, type: BOOL, loc }; + return { + value: positionNumber(lowerer, boxed, numLit(0, loc), node, subject), + has: { kind: "unary", op: "!", operand: absent, type: BOOL, loc }, + }; +} + +/** The typed result of a String.prototype member over a STRING receiver + * and already evaluated arguments, or null when the name is not one of + * this module's. */ +function buildStringMethod( + lowerer: Lowerer, + call: ts.CallExpression, + name: string, + s: IrExpr, + args: IrExpr[], + receiverNode: ts.Node, + stmts: IrStmt[], +): IrExpr | null { + const loc = locOf(call); + const node = (i: number): ts.Expression => call.arguments.nodes[i] ?? call; + if (name === "substr") { + const start = args[0] + ? positionNumber(lowerer, args[0], numLit(0, loc), node(0), "substr start") + : numLit(0, loc); + const length = optionalNumber(lowerer, args[1], node(1), stmts, "substr length", loc); + return { + kind: "libCall", + fn: "str.substr", + args: [s, start, length.value, length.has], + type: STRING, + loc, + }; + } + if (name === "toLocaleLowerCase" || name === "toLocaleUpperCase") { + const locales = args[0] ? lowerer.coerceToExpected(args[0], DYN) : dynUndefinedExpr(loc); + return { + kind: "libCall", + fn: "str.localeCase", + args: [s, locales, boolLit(name === "toLocaleUpperCase", loc)], + type: STRING, + loc, + }; + } + const html = Object.hasOwn(HTML_METHODS, name) ? HTML_METHODS[name] : undefined; + if (html) { + const [tag, attribute] = html; + const subject = hoist(lowerer, s, stmts, "%htmlSubject"); + const pieces: IrExpr[] = [strLit(`<${tag}`, loc)]; + if (attribute !== null) { + const text = args[0] ? stringArg(lowerer, args[0], node(0)) : strLit("undefined", loc); + pieces.push(strLit(` ${attribute}="`, loc)); + pieces.push({ + kind: "libCall", + fn: "str.replaceLiteral", + args: [text, strLit('"', loc), strLit(""", loc), boolLit(true, loc)], + type: STRING, + loc, + }); + pieces.push(strLit('"', loc)); + } + pieces.push(strLit(">", loc), subject, strLit(``, loc)); + return pieces.reduce((left, right) => ({ kind: "strConcat", left, right, type: STRING, loc })); + } + if (name === "codePointAt") + return stringIndexHelperCall( + lowerer, + call, + name, + s, + receiverNode, + args[0] ?? numLit(0, loc), + call.arguments.nodes[0], + ); + if (name === "isWellFormed" || name === "toWellFormed") + return { + kind: "strIntrinsic", + method: name, + receiver: s, + args: [], + type: name === "isWellFormed" ? BOOL : STRING, + loc, + }; + if (name === "trimLeft" || name === "trimRight") + return { + kind: "strIntrinsic", + method: name === "trimLeft" ? "trimStart" : "trimEnd", + receiver: s, + args: [], + type: STRING, + loc, + }; + return null; +} + +/** The typed result of toPrecision/toExponential over an F64 receiver. */ +function buildNumberMethod( + lowerer: Lowerer, + call: ts.CallExpression, + name: string, + x: IrExpr, + args: IrExpr[], + stmts: IrStmt[], +): IrExpr | null { + const loc = locOf(call); + if (name !== "toPrecision" && name !== "toExponential") return null; + const digits = optionalNumber( + lowerer, + args[0], + call.arguments.nodes[0] ?? call, + stmts, + `${name} digits`, + loc, + ); + if (name === "toPrecision") + return { + kind: "libCall", + fn: "num.toPrecision", + args: [x, digits.value, digits.has], + type: STRING, + loc, + }; + const shortest: IrExpr = { + kind: "libCall", + fn: "num.toExponential", + args: [x], + type: STRING, + loc, + }; + if (digits.has.kind === "boolLit" && !digits.has.value) return shortest; + const fixed: IrExpr = { + kind: "libCall", + fn: "num.toExponentialDigits", + args: [x, digits.value], + type: STRING, + loc, + }; + if (digits.has.kind === "boolLit") return fixed; + return { kind: "ternary", cond: digits.has, then: fixed, else_: shortest, type: STRING, loc }; +} + +/** Evaluate the call's arguments, in order, into hidden locals. */ +function evaluatedArgs( + lowerer: Lowerer, + call: ts.CallExpression, + stmts: IrStmt[], + expected: IrType | undefined, +): IrExpr[] { + return call.arguments.nodes.map((arg) => + hoist( + lowerer, + expected ? lowerer.lowerExprExpecting(arg, expected) : lowerer.lowerExpr(arg), + stmts, + "%methodArg", + ), + ); +} + +/** A typed STRING receiver calling one of EXTRA_STRING_METHODS: receiver, + * then arguments, then the method's own conversions — the spec order. */ +export function lowerExtraStringMethodCall( + lowerer: Lowerer, + call: ts.CallExpression, + name: string, + receiver: IrExpr, + receiverNode: ts.Node, +): IrExpr | null { + if (call.arguments.nodes.some(ts.isSpreadElement)) return null; + const stmts: IrStmt[] = []; + const s = hoist(lowerer, receiver, stmts, "%stringReceiver"); + const args = evaluatedArgs(lowerer, call, stmts, undefined); + const result = buildStringMethod(lowerer, call, name, s, args, receiverNode, stmts); + return result ? seq(stmts, result) : null; +} + +/** A typed F64 receiver calling toPrecision(p) or toExponential(f). */ +export function lowerExtraNumberMethodCall( + lowerer: Lowerer, + call: ts.CallExpression, + name: string, + receiver: IrExpr, +): IrExpr | null { + if (call.arguments.nodes.some(ts.isSpreadElement) || call.arguments.nodes.length > 1) return null; + if (name !== "toPrecision" && !(name === "toExponential" && call.arguments.nodes.length === 1)) + return null; + const stmts: IrStmt[] = []; + const x = hoist(lowerer, receiver, stmts, "%numberReceiver"); + const args = evaluatedArgs(lowerer, call, stmts, undefined); + const result = buildNumberMethod(lowerer, call, name, x, args, stmts); + return result ? seq(stmts, result) : null; +} + +/** `recv.m(args)` on a checked-dynamic receiver for a String/Number method + * the runtime dispatch cannot answer: the receiver and arguments evaluate + * once, a string (or number) receiver runs the typed lowering, and every + * other kind keeps the runtime dispatch's answer. Null for other names. */ +export function lowerDynPrimitiveMethodCall( + lowerer: Lowerer, + call: ts.CallExpression, + name: string, + calleeName: string, + recv: IrExpr, +): IrExpr | null { + const stringMethod = DYN_STRING_KIND_METHODS.has(name); + const numberMethod = DYN_NUMBER_KIND_METHODS.has(name); + if (!stringMethod && !numberMethod) return null; + if (call.arguments.nodes.some(ts.isSpreadElement)) return null; + const loc = locOf(call); + const stmts: IrStmt[] = []; + const local = lowerer.declareHiddenLocal("%primitiveReceiver", DYN); + stmts.push({ kind: "varDecl", localId: local.id, init: recv, loc }); + const r = varRef(local.id, DYN, loc); + const args = evaluatedArgs(lowerer, call, stmts, DYN); + let result: IrExpr = { + kind: "dynInvoke", + recv: r, + method: name, + calleeName, + args, + type: DYN, + loc, + }; + const branch = ( + test: "string" | "number", + build: (value: IrExpr, branchStmts: IrStmt[]) => IrExpr | null, + ): void => { + const branchStmts: IrStmt[] = []; + const value: IrExpr = { + kind: "dynCheck", + value: r, + type: test === "string" ? STRING : F64, + loc, + }; + const built = build(value, branchStmts); + if (!built) return; + result = { + kind: "ternary", + cond: { kind: "dynTest", test, value: r, type: BOOL, loc }, + then: { kind: "dynFrom", value: seq(branchStmts, built), type: DYN, loc }, + else_: result, + type: DYN, + loc, + }; + }; + if (numberMethod) + branch("number", (x, branchStmts) => + buildNumberMethod(lowerer, call, name, x, args, branchStmts), + ); + if (stringMethod) + branch("string", (s, branchStmts) => + buildStringMethod(lowerer, call, name, s, args, call.expression, branchStmts), + ); + return seq(stmts, result); +} diff --git a/packages/compiler/src/frontend/lowering/cycle-initialization.ts b/packages/compiler/src/frontend/lowering/cycle-initialization.ts index 1e8b9e5a5..fb62fb998 100644 --- a/packages/compiler/src/frontend/lowering/cycle-initialization.ts +++ b/packages/compiler/src/frontend/lowering/cycle-initialization.ts @@ -41,6 +41,10 @@ export interface CycleInitFlag { * own module's early calls is checked opportunistically: whatever cannot * be checked keeps the unchecked behavior instead of refusing. */ cycle: boolean; + /** A pointer-guarded binding declared without an initializer: its + * declaration initializes it (to undefined, which the pointer cannot + * hold), so the flag alone decides its writes (markUninitializedWrites). */ + uninitialized?: true; /** A class binding: its references are the class's own IR forms and its * static field storage (`global` is the flag itself). */ className?: string; @@ -107,6 +111,30 @@ export function markCycleEarlyBindings(lowerer: Lowerer): void { registerInitFlag(lowerer, binding, global, false); } } + // `let f: T;` with a pointer-guarded slot: once the declaration ran the + // binding is initialized while the slot is still empty, so a write + // checks the flag (where code can run before the declaration) instead of + // the empty pointer. `f = () => f(...)` is the shape. + for (const [id, entry] of lowerer.uninitializedLexicalGlobals) { + const global = lowerer.globalsList.find((g) => g.id === id); + if (global === undefined) continue; + if (!lowerer.cycleInitFlags.has(id)) { + registerInitFlag( + lowerer, + { + symbol: entry.symbol, + name: entry.name, + kind: "lexical", + module: entry.module, + reference: entry.name, + settled: new Set(), + }, + global, + false, + ); + } + lowerer.cycleInitFlags.get(id)!.uninitialized = true; + } } /** A class that its module's early-running code can reach before the class @@ -304,6 +332,22 @@ export function checkCycleBindingAccesses(lowerer: Lowerer, mod: IrModule): void const fileOfInit = new Map(); for (const [sf, name] of lowerer.initNameOf) fileOfInit.set(name, sf); for (const fn of mod.functions) rewriteFunction(lowerer, fn, fileOfInit.get(fn.name)); + markUninitializedWrites(lowerer, mod); +} + +/** Writes to a binding declared without an initializer that run after its + * declaration store into an initialized binding whose slot may still be + * empty: the pointer guard must not refuse them. Writes that can run + * earlier carry the flag check rewriteFunction inserted. */ +function markUninitializedWrites(lowerer: Lowerer, mod: IrModule): void { + const ids = new Set(); + for (const [id, flag] of lowerer.cycleInitFlags) if (flag.uninitialized) ids.add(id); + if (ids.size === 0) return; + const mark = (s: IrStmt): IrStmt => + s.kind === "assign" && !s.initializes && ids.has(s.localId) ? { ...s, initializes: true } : s; + const rewriteExpr = (e: IrExpr): IrExpr => mapExprChildren(e, rewriteExpr, rewriteStmt); + const rewriteStmt = (s: IrStmt): IrStmt => mark(mapStmtChildren(s, rewriteExpr, rewriteStmt)); + for (const fn of mod.functions) fn.body = fn.body.map(rewriteStmt); } function rewriteFunction( @@ -444,6 +488,7 @@ function rewriteFunction( kind: "assign", localId: s.localId, value: { kind: "varRef", localId: t, type: s.value.type, loc: s.loc }, + ...(flag.uninitialized ? { initializes: true as const } : {}), loc: s.loc, }; return { diff --git a/packages/compiler/src/frontend/lowering/deferred-module-initializers.ts b/packages/compiler/src/frontend/lowering/deferred-module-initializers.ts index 6b0b54b55..e3ca2270d 100644 --- a/packages/compiler/src/frontend/lowering/deferred-module-initializers.ts +++ b/packages/compiler/src/frontend/lowering/deferred-module-initializers.ts @@ -286,7 +286,7 @@ function registryInitializer( ) return true; const symbol = lowerer.resolveValueSymbol(callee); - const declaration = symbol && lowerer.checker.valueDeclarationOf(symbol); + const declaration = symbol && symbol.valueDeclaration; if (!symbol || !declaration || !bindingNeverReassigned(lowerer, symbol, declaration)) return false; if (ts.isFunctionDeclaration(declaration)) return true; @@ -346,7 +346,7 @@ function registryInitializer( } if (packageSource && ts.isPropertyAccessExpression(node)) { const symbol = lowerer.checker.getSymbolAtLocation(node.name); - const declaration = symbol && lowerer.checker.valueDeclarationOf(symbol); + const declaration = symbol && symbol.valueDeclaration; const owner = ts.isIdentifier(node.expression) ? lowerer.resolveValueSymbol(node.expression) : undefined; @@ -394,7 +394,7 @@ function registryInitializer( ) return true; const symbol = lowerer.resolveValueSymbol(node); - const functionDeclaration = symbol && lowerer.checker.valueDeclarationOf(symbol); + const functionDeclaration = symbol && symbol.valueDeclaration; if ( functionDeclaration && ts.isFunctionDeclaration(functionDeclaration) && @@ -449,7 +449,7 @@ function registryOwnerIsData( symbol: ts.Symbol, statement: ts.Statement, ): boolean { - const declaration = lowerer.checker.valueDeclarationOf(symbol); + const declaration = symbol.valueDeclaration; if ( !declaration || !ts.isVariableDeclaration(declaration) || diff --git a/packages/compiler/src/frontend/lowering/divergences.ts b/packages/compiler/src/frontend/lowering/divergences.ts index ceea9a3e3..36fbf48b9 100644 --- a/packages/compiler/src/frontend/lowering/divergences.ts +++ b/packages/compiler/src/frontend/lowering/divergences.ts @@ -23,6 +23,7 @@ import { } from "../../diagnostics/diagnostic.js"; import { isCollationCovered } from "./collation-repertoire.js"; import type { Lowerer } from "./lowerer.js"; +import { keptIdentityAssertion } from "./assertion-identity.js"; /** How an identity-sensitive binding is used after a copy could replace it. */ interface IdentityUse { @@ -179,7 +180,7 @@ export function scanDivergences( if (!candidates.some((fn) => identityUsesOf(fn).some((u) => u !== null))) continue; const signature = safely(() => checker.getResolvedSignature(call)); if (signature === undefined) continue; - const decl = safely(() => checker.signatureDeclaration(signature)); + const decl = safely(() => signature.declaration); if (decl === undefined || !candidates.includes(decl as ts.FunctionLikeDeclaration)) continue; const fn = decl as ts.FunctionLikeDeclaration; const uses = identityUsesOf(fn); @@ -217,6 +218,8 @@ export function scanDivergences( } for (const { decl, source, type } of castDecls) { + // An assertion of the global object keeps that object: no copy. + if (keptIdentityAssertion(lowerer, decl.initializer!) !== undefined) continue; const name = (decl.name as ts.Identifier).text; const scope = enclosingScope(decl); if (scope === undefined) continue; @@ -232,11 +235,9 @@ export function scanDivergences( const left = assignment.left as ts.Identifier; const symbol = safely(() => checker.getSymbolAtLocation(left)); if (symbol === undefined) continue; - const decl = checker - .declarationsOf(symbol) - .find( - (d): d is ts.VariableDeclaration => ts.isVariableDeclaration(d) && d.type !== undefined, - ); + const decl = symbol.declarations.find( + (d): d is ts.VariableDeclaration => ts.isVariableDeclaration(d) && d.type !== undefined, + ); if (decl === undefined) continue; const scope = enclosingScope(decl); if (scope === undefined) continue; @@ -287,7 +288,7 @@ export function scanDivergences( if (ts.isObjectLiteralExpression(inner)) lit = inner; else if (ts.isIdentifier(inner)) { const symbol = safely(() => checker.getSymbolAtLocation(inner)); - const decl = symbol === undefined ? undefined : checker.valueDeclarationOf(symbol); + const decl = symbol === undefined ? undefined : symbol.valueDeclaration; if ( decl !== undefined && ts.isVariableDeclaration(decl) && @@ -373,8 +374,7 @@ export function scanDivergences( const name = calleeName(call); if (name === null || !fnsByName.has(name)) continue; const signature = safely(() => checker.getResolvedSignature(call)); - const decl = - signature === undefined ? undefined : safely(() => checker.signatureDeclaration(signature)); + const decl = signature === undefined ? undefined : safely(() => signature.declaration); const sites = decl === undefined ? undefined : downcasts.get(decl); if (sites === undefined) continue; for (const site of sites) { @@ -423,7 +423,7 @@ function dateStringLiterals(checker: ts.TypeChecker, arg: ts.Expression): ts.Exp if (stringLiteralText(arg) !== null) return [arg]; if (!ts.isIdentifier(arg)) return []; const symbol = safely(() => checker.getSymbolAtLocation(arg)); - const decl = symbol === undefined ? undefined : checker.valueDeclarationOf(symbol); + const decl = symbol === undefined ? undefined : symbol.valueDeclaration; const list = decl?.parent; const loop = list?.parent; if ( @@ -438,7 +438,7 @@ function dateStringLiterals(checker: ts.TypeChecker, arg: ts.Expression): ts.Exp let iterated: ts.Expression = loop.expression; if (ts.isIdentifier(iterated)) { const source = safely(() => checker.getSymbolAtLocation(iterated)); - const init = source === undefined ? undefined : checker.valueDeclarationOf(source); + const init = source === undefined ? undefined : source.valueDeclaration; if (init === undefined || !ts.isVariableDeclaration(init) || init.initializer === undefined) return []; if (init.parent === undefined || (init.parent.flags & ts.NodeFlags.Const) === 0) return []; @@ -743,7 +743,7 @@ function isProgramClassType(checker: ts.TypeChecker, type: ts.Type): boolean { return ( symbol !== undefined && (symbol.flags & ts.SymbolFlags.Class) !== 0 && - checker.declarationsOf(symbol).some((d) => !d.getSourceFile().isDeclarationFile) + symbol.declarations.some((d) => !d.getSourceFile().isDeclarationFile) ); } @@ -756,7 +756,7 @@ function isPlainRecordType(checker: ts.TypeChecker, type: ts.Type): boolean { const symbol = type.getSymbol(); if (symbol !== undefined && (symbol.flags & ts.SymbolFlags.Class) !== 0) return false; // Library-declared object types (Map, Date, Promise, ...) are not records. - const decls = symbol === undefined ? [] : checker.declarationsOf(symbol); + const decls = symbol === undefined ? [] : symbol.declarations; if (decls.some((d) => d.getSourceFile().isDeclarationFile)) return false; return true; } @@ -769,7 +769,7 @@ function namedObjectDeclaration(checker: ts.TypeChecker, type: ts.Type): ts.Node const symbol = alias ?? type.getSymbol(); if (symbol === undefined) return null; if ((symbol.flags & ts.SymbolFlags.Class) !== 0) return null; - const decl = checker.declarationsOf(symbol)[0]; + const decl = symbol.declarations[0]; if (decl === undefined || decl.getSourceFile().isDeclarationFile) return null; return decl; } @@ -781,7 +781,11 @@ function keyOrderFinding( written: readonly string[], assignedNames: ReadonlySet, ): { native: string; node: string; detail: "reordered" | "added-later" } | null { - const present = declared.filter((name) => written.includes(name)); + // A shape whose table the witness pass re-chose (key-order-witness.ts) + // enumerates that table, not the checker's declaration order. + const table = witnessTable(lowerer, contextual); + const native = table ?? declared; + const present = native.filter((name) => written.includes(name) && declared.includes(name)); const writtenKnown = written.filter((name) => declared.includes(name)); if (present.join("\0") !== writtenKnown.join("\0") && writtenKnown.length >= 2) { return { native: present.join(", "), node: writtenKnown.join(", "), detail: "reordered" }; @@ -797,12 +801,21 @@ function keyOrderFinding( const prop = checker.getPropertyOfType(contextual, name); if (prop === undefined || (prop.flags & ts.SymbolFlags.Optional) === 0) continue; const nodeOrder = [...writtenKnown, name]; - const nativeOrder = declared.filter((d) => nodeOrder.includes(d)); + const nativeOrder = native.filter((d) => nodeOrder.includes(d)); + if (table !== null && nativeOrder.join("\0") === nodeOrder.join("\0")) continue; return { native: nativeOrder.join(", "), node: nodeOrder.join(", "), detail: "added-later" }; } return null; } +/** The re-chosen enumeration table of the record shape a contextual type + * lowered to, or null when the witness pass kept today's table. */ +function witnessTable(lowerer: Lowerer, contextual: ts.Type): readonly string[] | null { + const mapped = safely(() => lowerer.mapTypeOf(contextual)); + if (mapped?.kind !== "record" || !lowerer.keyOrder.changed.has(mapped.shapeId)) return null; + return lowerer.shapes.get(mapped.shapeId)?.declaredOrder ?? null; +} + function classDeclarationOfTypeNode( lowerer: Lowerer, node: ts.TypeNode, @@ -810,9 +823,7 @@ function classDeclarationOfTypeNode( if (!ts.isTypeReferenceNode(node) || !ts.isIdentifier(node.typeName)) return null; const symbol = lowerer.checker.getSymbolAtLocation(node.typeName); if (symbol === undefined || (symbol.flags & ts.SymbolFlags.Class) === 0) return null; - const decl = lowerer.checker - .declarationsOf(symbol) - .find((d) => ts.isClassDeclaration(d) || ts.isClassExpression(d)); + const decl = symbol.declarations.find((d) => ts.isClassDeclaration(d) || ts.isClassExpression(d)); if (decl === undefined || decl.getSourceFile().isDeclarationFile) return null; return decl as ts.ClassLikeDeclaration; } @@ -823,9 +834,7 @@ function classDeclarationOfType( ): ts.ClassLikeDeclaration | null { const symbol = type.getSymbol(); if (symbol === undefined || (symbol.flags & ts.SymbolFlags.Class) === 0) return null; - const decl = checker - .declarationsOf(symbol) - .find((d) => ts.isClassDeclaration(d) || ts.isClassExpression(d)); + const decl = symbol.declarations.find((d) => ts.isClassDeclaration(d) || ts.isClassExpression(d)); if (decl === undefined || decl.getSourceFile().isDeclarationFile) return null; return decl as ts.ClassLikeDeclaration; } @@ -850,9 +859,9 @@ function classExtends( (symbol.flags & ts.SymbolFlags.Alias) !== 0 ? lowerer.checker.getAliasedSymbol(symbol) : symbol; - const decl = lowerer.checker - .declarationsOf(resolved) - .find((d) => ts.isClassDeclaration(d) || ts.isClassExpression(d)); + const decl = resolved.declarations.find( + (d) => ts.isClassDeclaration(d) || ts.isClassExpression(d), + ); cur = decl === undefined ? null : (decl as ts.ClassLikeDeclaration); } return false; diff --git a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts index 216069362..6238479e0 100644 --- a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts +++ b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts @@ -24,6 +24,7 @@ import { PoisonError, neverTaintedJsType } from "../lowerer.js"; import type { Lowerer } from "../lowerer.js"; import { lowerIndexMergeHelper } from "../containers/indexed-objects.js"; import type { IndexMergeContributor } from "../containers/indexed-objects.js"; +import type { KeyOrderSegment } from "../key-order-witness.js"; import { PERF_ENTRY_MARKER, isGenericCallableMemberType, @@ -1596,6 +1597,8 @@ export function lowerObjectLiteral( // Member names DROPPED from the shape (JS deferral — unlowerable pure // member reads narrow away; see the catch in the property loop). const droppedNames = new Set(); + // The order this literal creates its keys in (key-order-witness.ts). + const orderSegments: KeyOrderSegment[] = []; for (const prop of expr.properties.nodes) { // A later contributor can mutate an earlier source or overwrite a // value with effects. Snapshot pending fields at their source position, @@ -1651,6 +1654,7 @@ export function lowerObjectLiteral( v = lowerer.coerceInto(valueNode, v, fieldType); if (!typeEquals(v.type, fieldType)) lowerer.badType(valueNode, lowerer.typeOf(valueNode)); conditionalNames.add(name); + orderSegments.push({ kind: "key", name, definite: false }); fields.push({ name, value: { @@ -1758,6 +1762,7 @@ export function lowerObjectLiteral( } } const srcRef = (): IrExpr => srcLowered ?? lowerer.lowerExpr(srcNode); + orderSegments.push({ kind: "spread", shapeId: recArm.shapeId, maybeAbsent: true }); for (const f of srcShape.fields) { if (laterNames.has(f.name)) continue; const targetType = fieldTypes.get(f.name); @@ -1993,6 +1998,12 @@ export function lowerObjectLiteral( laterNames.add(propNameText(lowerer, later.name)); } } + // A checked-dynamic source's keys are a run-time fact. + orderSegments.push( + srcLowered.type.kind === "dyn" + ? { kind: "opaque" } + : { kind: "spread", shapeId: srcType.shapeId, maybeAbsent: false }, + ); for (const f of srcShape.fields) { if (laterNames.has(f.name)) continue; let targetType = fieldTypes.get(f.name); @@ -2307,6 +2318,7 @@ export function lowerObjectLiteral( const at = fields.findIndex((x) => x.name === name); if (at >= 0) fields.splice(at, 1); fields.push({ name, value }); + orderSegments.push({ kind: "key", name, definite: true }); } // Optional fields may be omitted: the field holds its absent state. // tsc rejects omission of required fields before lowering. @@ -2335,7 +2347,10 @@ export function lowerObjectLiteral( fields.push({ name: f.name, value: absent }); } } - const result: IrExpr = { kind: "recordLit", fields, type, loc }; + const result: IrExpr = lowerer.keyOrder.literal( + { kind: "recordLit", fields, type, loc }, + orderSegments, + ); return spreadStmts.length === 0 ? result : { kind: "seqExpr", stmts: spreadStmts, result, type, loc }; @@ -3047,7 +3062,7 @@ export function lowerShorthandValue( return lowerer.maybeNarrow({ kind: "varRef", localId: g.id, type: g.type, loc }, propName); } const sig = lowerer.fnSigsBySymbol.get(resolved); - const decl = lowerer.checker.declarationsOf(resolved)[0]; + const decl = resolved.declarations[0]; if ( sig && decl && diff --git a/packages/compiler/src/frontend/lowering/function-returns.ts b/packages/compiler/src/frontend/lowering/function-returns.ts index f413f4680..c0e70013c 100644 --- a/packages/compiler/src/frontend/lowering/function-returns.ts +++ b/packages/compiler/src/frontend/lowering/function-returns.ts @@ -1,3 +1,4 @@ +import { mapAsyncGeneratorAnnotation } from "../type-mapper.js"; import { dynUndefinedExpr } from "../../ir/build.js"; import * as ts from "../ts7/adapter.js"; import type { Lowerer } from "./lowerer.js"; @@ -146,6 +147,12 @@ export function declaredReturnType( // (`.action(() => { throw ... })`). `never` VALUES stay unmapped. if (retTsType.flags & ts.TypeFlags.Never) return VOID; let mappedReturn = lowerer.mapTypeOf(retTsType); + if ( + mappedReturn === null && + decl.asteriskToken && + decl.modifiers?.nodes.some((modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword) + ) + mappedReturn = mapAsyncGeneratorAnnotation(retTsType, lowerer.typeCtx); if ( isJsSourceFile(decl.getSourceFile()) && decl.modifiers?.nodes.some((modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword) && diff --git a/packages/compiler/src/frontend/lowering/generic-functions.ts b/packages/compiler/src/frontend/lowering/generic-functions.ts index bf5f2563d..f588faf6d 100644 --- a/packages/compiler/src/frontend/lowering/generic-functions.ts +++ b/packages/compiler/src/frontend/lowering/generic-functions.ts @@ -187,9 +187,9 @@ function requireStableGenericBinding( name: string, ): void { const isConst = (ts.getCombinedNodeFlags(decl) & ts.NodeFlags.Const) !== 0; - const redeclared = lowerer.checker - .declarationsOf(symbol) - .some((d) => d !== decl && ts.isVariableDeclaration(d) && d.initializer !== undefined); + const redeclared = symbol.declarations.some( + (d) => d !== decl && ts.isVariableDeclaration(d) && d.initializer !== undefined, + ); if (!isConst && (redeclared || !bindingNeverReassigned(lowerer, symbol, decl))) { lowerer.unsupported( "SC1090", @@ -266,7 +266,7 @@ export function genericCallInstance( // honest story (monomorphize per implementation signature with the // reconcile bridge, like the non-generic path). { - const rdecl = rsig ? lowerer.checker.signatureDeclaration(rsig) : undefined; + const rdecl = rsig ? rsig.declaration : undefined; if ( rdecl && (ts.isFunctionDeclaration(rdecl) || ts.isMethodDeclaration(rdecl)) && @@ -451,7 +451,7 @@ function localClassOfResult( ): ts.ClassLikeDeclaration | null { const symbol = type.getSymbol(); if (!symbol || !(symbol.flags & ts.SymbolFlags.Class)) return null; - const decl = lowerer.checker.valueDeclarationOf(symbol); + const decl = symbol.valueDeclaration; if (!decl || !(ts.isClassExpression(decl) || ts.isClassDeclaration(decl))) return null; for (let parent = decl.parent; parent && !ts.isSourceFile(parent); parent = parent.parent) if (parent === info.decl) return decl; @@ -1549,9 +1549,9 @@ function implicitDeclaredReturn(lowerer: Lowerer, info: GenericFnInfo): IrType | const symbol = lowerer.resolveValueSymbol(node.expression); if ( symbol && - lowerer.checker - .declarationsOf(symbol) - .some((decl) => ts.isVariableDeclaration(decl) && jsBindingHasOpenWrites(lowerer, decl)) + symbol.declarations.some( + (decl) => ts.isVariableDeclaration(decl) && jsBindingHasOpenWrites(lowerer, decl), + ) ) openReturn = true; return undefined; @@ -2041,9 +2041,9 @@ export function implicitLocalFnNodeOf( const sym = lowerer.checker.getSymbolAtLocation(decl.name); if (!sym) return null; const isConst = (ts.getCombinedNodeFlags(decl) & ts.NodeFlags.Const) !== 0; - const redeclared = lowerer.checker - .declarationsOf(sym) - .some((d) => d !== decl && ts.isVariableDeclaration(d) && d.initializer !== undefined); + const redeclared = sym.declarations.some( + (d) => d !== decl && ts.isVariableDeclaration(d) && d.initializer !== undefined, + ); if (redeclared) return null; if (!isConst && !bindingNeverReassigned(lowerer, sym, decl)) return null; // The capture scan: instances are module functions with no frame. @@ -2059,7 +2059,7 @@ export function implicitLocalFnNodeOf( } if (ts.isIdentifier(n)) { const s = lowerer.checker.getSymbolAtLocation(n); - const d = s ? lowerer.checker.valueDeclarationOf(s) : undefined; + const d = s ? s.valueDeclaration : undefined; if ( d && d.getSourceFile() === decl.getSourceFile() && @@ -2146,7 +2146,7 @@ export function objLitGenericFnNodeOf( fnNode: ts.MethodDeclaration | ts.FunctionExpression | ts.ArrowFunction; literal: ts.ObjectLiteralExpression; } | null { - const decl = lowerer.checker.valueDeclarationOf(propSym); + const decl = propSym.valueDeclaration; if (!decl) return null; if (ts.isMethodDeclaration(decl) && ts.isObjectLiteralExpression(decl.parent)) { return decl.typeParameters !== undefined && decl.body !== undefined @@ -2420,7 +2420,7 @@ export function requireObjLitGenericReceiver( ); if (!ts.isIdentifier(recv)) fenceReceiver(); const recvSym = lowerer.resolveValueSymbol(recv); - const recvDecl = recvSym ? lowerer.checker.valueDeclarationOf(recvSym) : undefined; + const recvDecl = recvSym ? recvSym.valueDeclaration : undefined; if ( !recvDecl || !ts.isVariableDeclaration(recvDecl) || diff --git a/packages/compiler/src/frontend/lowering/js-dynamic-objects.ts b/packages/compiler/src/frontend/lowering/js-dynamic-objects.ts index 8f17a2448..d39e5cceb 100644 --- a/packages/compiler/src/frontend/lowering/js-dynamic-objects.ts +++ b/packages/compiler/src/frontend/lowering/js-dynamic-objects.ts @@ -190,9 +190,9 @@ export function lowerValueReturningConstructorNew( if (symbol !== null && (symbol.flags & ts.SymbolFlags.Alias) !== 0) symbol = lowerer.checker.getAliasedSymbol(symbol); const decl = symbol - ? lowerer.checker - .declarationsOf(symbol) - .find((node): node is ts.ClassDeclaration => ts.isClassDeclaration(node) && isDynamic(node)) + ? symbol.declarations.find( + (node): node is ts.ClassDeclaration => ts.isClassDeclaration(node) && isDynamic(node), + ) : undefined; if (decl === undefined) return null; const loc = locOf(expr); diff --git a/packages/compiler/src/frontend/lowering/key-order-witness.test.ts b/packages/compiler/src/frontend/lowering/key-order-witness.test.ts new file mode 100644 index 000000000..01f632cf3 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/key-order-witness.test.ts @@ -0,0 +1,329 @@ +import { expect, test } from "vitest"; +import { F64, type IrExpr, type IrFunction, type IrStmt, type IrType } from "../../ir/ir.js"; +import { ShapeRegistry } from "../type-mapper.js"; +import { + applyKeyOrderWitnesses, + KeyOrderNotes, + type KeyOrderSegment, +} from "./key-order-witness.js"; +import type { Lowerer } from "./lowerer.js"; + +const loc = { file: "order.ts", start: 0, end: 1 }; +/** `number | undefined`: the optional-field slot type. */ +const OPT: IrType = { kind: "union", unionId: "u0" }; +const num = (value: number): IrExpr => ({ kind: "numLit", value, type: F64, loc }); +const absent: IrExpr = { kind: "fieldAbsent", unionId: "u0", type: OPT, loc }; + +interface Fixture { + lowerer: Lowerer; + shapes: ShapeRegistry; + notes: KeyOrderNotes; + record(fields: [string, IrType][], order: string[]): IrType & { kind: "record" }; + literal(type: IrType, keys: string[], segments?: KeyOrderSegment[]): IrExpr; + run(body: IrStmt[]): void; + order(type: IrType & { kind: "record" }): string[] | undefined; +} + +function fixture(): Fixture { + const shapes = new ShapeRegistry(); + const notes = new KeyOrderNotes(); + const lowerer = { + shapes, + keyOrder: notes, + unions: new Map([["u0", { id: "u0", arms: [F64, { kind: "undefinedT" }] }]]), + classes: new Map(), + armTag: (unionId: string, arm: IrType): number => + unionId === "u0" && arm.kind === "undefinedT" ? 1 : -1, + } as unknown as Lowerer; + const record = (fields: [string, IrType][], order: string[]): IrType & { kind: "record" } => ({ + kind: "record", + shapeId: shapes.intern( + [...fields].sort((a, b) => (a[0] < b[0] ? -1 : 1)).map(([name, type]) => ({ name, type })), + false, + undefined, + order, + ), + }); + const literal = (type: IrType, keys: string[], segments?: KeyOrderSegment[]): IrExpr => { + const shape = shapes.get((type as IrType & { kind: "record" }).shapeId)!; + const fields = keys.map((name) => ({ name, value: num(1) })); + for (const f of shape.fields) + if (!keys.includes(f.name)) fields.push({ name: f.name, value: absent }); + return notes.literal( + { kind: "recordLit", fields, type, loc }, + segments ?? keys.map((name) => ({ kind: "key", name, definite: true })), + ); + }; + const run = (body: IrStmt[]): void => { + const fn: IrFunction = { name: "main", params: [], returnType: F64, locals: [], body, loc }; + applyKeyOrderWitnesses(lowerer, [fn]); + }; + const order = (type: IrType & { kind: "record" }): string[] | undefined => + shapes.get(type.shapeId)?.declaredOrder; + return { lowerer, shapes, notes, record, literal, run, order }; +} + +const observe = (value: IrExpr): IrStmt => ({ + kind: "exprStmt", + expr: { kind: "jsonStringify", value, type: { kind: "string" }, loc }, + loc, +}); +const decl = (localId: string, init: IrExpr): IrStmt => ({ kind: "varDecl", localId, init, loc }); +const ref = (localId: string, type: IrType): IrExpr => ({ kind: "varRef", localId, type, loc }); +const set = (obj: IrExpr, field: string): IrStmt => ({ + kind: "recordSet", + obj, + shapeId: (obj.type as IrType & { kind: "record" }).shapeId, + field, + value: num(2), + loc, +}); + +test("a shape takes the one order its literals write", () => { + const f = fixture(); + const ab = f.record( + [ + ["a", F64], + ["b", F64], + ], + ["a", "b"], + ); + f.run([observe(f.literal(ab, ["b", "a"])), observe(f.literal(ab, ["b", "a"]))]); + expect(f.order(ab)).toEqual(["b", "a"]); + expect(f.notes.changed.has(ab.shapeId)).toBe(true); +}); + +test("literals that agree with the table leave the same array in place", () => { + const f = fixture(); + const ab = f.record( + [ + ["a", F64], + ["b", F64], + ], + ["a", "b"], + ); + const before = f.order(ab); + f.run([observe(f.literal(ab, ["a", "b"]))]); + expect(f.order(ab)).toBe(before); + expect(f.notes.changed.size).toBe(0); +}); + +test("values built in two orders keep today's table", () => { + const f = fixture(); + const ab = f.record( + [ + ["a", F64], + ["b", F64], + ], + ["a", "b"], + ); + f.run([observe(f.literal(ab, ["b", "a"])), observe(f.literal(ab, ["a", "b"]))]); + expect(f.order(ab)).toEqual(["a", "b"]); +}); + +test("an unobserved shape keeps today's table", () => { + const f = fixture(); + const ab = f.record( + [ + ["a", F64], + ["b", F64], + ], + ["a", "b"], + ); + f.run([decl("x", f.literal(ab, ["b", "a"]))]); + expect(f.order(ab)).toEqual(["a", "b"]); +}); + +test("a synthesized construction pins today's table", () => { + const f = fixture(); + const ab = f.record( + [ + ["a", F64], + ["b", F64], + ], + ["a", "b"], + ); + const synthesized: IrExpr = { + kind: "recordLit", + fields: [ + { name: "a", value: num(1) }, + { name: "b", value: num(2) }, + ], + type: ab, + loc, + }; + f.run([observe(f.literal(ab, ["b", "a"])), observe(synthesized)]); + expect(f.order(ab)).toEqual(["a", "b"]); +}); + +test("a checked conversion pins today's table", () => { + const f = fixture(); + const ab = f.record( + [ + ["a", F64], + ["b", F64], + ], + ["a", "b"], + ); + const parsed: IrExpr = { kind: "dynCheck", value: ref("j", { kind: "dyn" }), type: ab, loc }; + f.run([observe(f.literal(ab, ["b", "a"])), observe(parsed)]); + expect(f.order(ab)).toEqual(["a", "b"]); +}); + +test("a spread contributes its source's order, and an override keeps its position", () => { + const f = fixture(); + const src = f.record( + [ + ["a", F64], + ["b", F64], + ], + ["a", "b"], + ); + const out = f.record( + [ + ["a", F64], + ["b", F64], + ["c", F64], + ], + ["a", "b", "c"], + ); + const spread: KeyOrderSegment = { kind: "spread", shapeId: src.shapeId, maybeAbsent: false }; + f.run([ + decl("s", f.literal(src, ["b", "a"])), + // { ...s, a: 1, c: 2 } — `a` stays where the spread put it. + observe( + f.literal( + out, + ["a", "b", "c"], + [ + spread, + { kind: "key", name: "a", definite: true }, + { kind: "key", name: "c", definite: true }, + ], + ), + ), + ]); + expect(f.order(out)).toEqual(["b", "a", "c"]); +}); + +test("a spread of a checked-conversion source cannot be ordered statically", () => { + const f = fixture(); + const src = f.record( + [ + ["a", F64], + ["b", F64], + ], + ["a", "b"], + ); + const out = f.record( + [ + ["a", F64], + ["b", F64], + ["c", F64], + ], + ["c", "a", "b"], + ); + const parsed: IrExpr = { kind: "dynCheck", value: ref("j", { kind: "dyn" }), type: src, loc }; + const spread: KeyOrderSegment = { kind: "spread", shapeId: src.shapeId, maybeAbsent: false }; + f.run([ + decl("s", parsed), + observe(f.literal(out, ["a", "b", "c"], [spread, { kind: "key", name: "c", definite: true }])), + ]); + expect(f.order(out)).toEqual(["c", "a", "b"]); +}); + +test("a late-only optional key follows the keys values are built with", () => { + const f = fixture(); + const o = f.record( + [ + ["a", OPT], + ["b", F64], + ], + ["a", "b"], + ); + f.run([decl("o", f.literal(o, ["b"])), set(ref("o", o), "a"), observe(ref("o", o))]); + expect(f.order(o)).toEqual(["b", "a"]); +}); + +test("straight-line writes of late-only keys keep program order", () => { + const f = fixture(); + const o = f.record( + [ + ["id", F64], + ["x", OPT], + ["y", OPT], + ], + ["id", "x", "y"], + ); + f.run([ + decl("o", f.literal(o, ["id"])), + set(ref("o", o), "y"), + set(ref("o", o), "x"), + observe(ref("o", o)), + ]); + expect(f.order(o)).toEqual(["id", "y", "x"]); +}); + +test("late-only keys written through different bindings keep today's table", () => { + const f = fixture(); + const o = f.record( + [ + ["id", F64], + ["x", OPT], + ["y", OPT], + ], + ["id", "x", "y"], + ); + f.run([ + decl("o", f.literal(o, ["id"])), + set(ref("o", o), "y"), + set(ref("p", o), "x"), + observe(ref("o", o)), + ]); + expect(f.order(o)).toEqual(["id", "x", "y"]); +}); + +test("a late-only key written in a loop over an outer binding keeps today's table", () => { + const f = fixture(); + const o = f.record( + [ + ["id", F64], + ["x", OPT], + ["y", OPT], + ], + ["id", "x", "y"], + ); + f.run([ + decl("o", f.literal(o, ["id"])), + set(ref("o", o), "y"), + { + kind: "while", + cond: { kind: "boolLit", value: false, type: { kind: "bool" }, loc }, + body: [set(ref("o", o), "x")], + loc, + }, + observe(ref("o", o)), + ]); + expect(f.order(o)).toEqual(["id", "x", "y"]); +}); + +test("re-adding a key a literal creates earlier keeps today's table", () => { + const f = fixture(); + const o = f.record( + [ + ["a", OPT], + ["b", F64], + ], + ["a", "b"], + ); + const del: IrStmt = { + kind: "recordSet", + obj: ref("o", o), + shapeId: o.shapeId, + field: "a", + value: absent, + loc, + }; + f.run([decl("o", f.literal(o, ["a", "b"])), del, set(ref("o", o), "a"), observe(ref("o", o))]); + expect(f.order(o)).toEqual(["a", "b"]); +}); diff --git a/packages/compiler/src/frontend/lowering/key-order-witness.ts b/packages/compiler/src/frontend/lowering/key-order-witness.ts new file mode 100644 index 000000000..ba8001792 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/key-order-witness.ts @@ -0,0 +1,675 @@ +/* Record enumeration order chosen from the orders values are built in. + * + * Node enumerates an object's string keys as canonical array indices + * ascending, then every other key in creation order. A record stores its + * fields name-sorted and enumerates them through one per-shape table, + * `IrRecordShape.declaredOrder`, which the type mapper fills with the + * checker's property order of the first type interned to the shape. That + * table is exact only while every value of the shape is created in that + * order. This pass replaces it, after lowering, with the order the + * program's construction sites actually create, when that order is one + * order. It changes the table's contents only: storage layout, shape + * identity and every helper's structure stay as they are, so the generated + * code differs only in the constant order the enumeration surfaces walk. + * + * Witnesses, per shape: + * - an object literal contributes the order it writes its keys in. Spread + * segments contribute their source shape's order (an inheritance edge, + * never the desugar's field list), and a key keeps the position of its + * first occurrence, so an override inside a spread stays where the + * spread put it. A key whose earlier occurrence may be absent at run + * time contributes every position it can take; + * - a width copy or a rest binding contributes its source shape's order; + * - any other construction (synthesized builtin results, opaque runtime + * producers) pins today's table; + * - checked conversions from dynamic values (typed JSON.parse, values + * from JavaScript) pin today's table: the record-source table keeps + * their own key order relative to it, and another table would make + * values parsed in today's order keep a source view. Their order is + * unknown, so a copy out of such a shape cannot be ordered statically; + * - a write to an optional key that may be absent appends it in Node. A + * key no construction creates ("late-only") goes after every key a + * construction can create; any other moving write goes after every key. + * Two late-only keys are ordered only by a straight-line sequence of + * writes through one binding. + * + * The orders merge into a precedence graph per shape, topologically sorted + * with ties broken by today's table, so a shape whose witnesses agree with + * the table keeps it untouched. A cycle means the order depends on the + * value: that shape, and every shape copied from it, keeps today's table + * (and today's SC6002 warnings). Shapes that no order surface observes + * keep today's table too. */ +import type { IrExpr, IrFunction, IrRecordShape, IrStmt, IrType } from "../../ir/ir.js"; +import { shapeHasAccessorSlots, typeEquals, UNDEFINED_T } from "../../ir/ir.js"; +import { everyExprChild, everyStmtChild, everyTypeChild } from "../../ir/traverse.js"; +import type { Lowerer } from "./lowerer.js"; + +/** One property of an object literal, in source order. */ +export type KeyOrderSegment = + /** An explicit key (`definite`), or a conditional spread's key, which + * exists only when its condition holds. */ + | { kind: "key"; name: string; definite: boolean } + /** `...source` of a record shape; `maybeAbsent` when the whole source + * can be undefined (`...options` of `T | undefined`). */ + | { kind: "spread"; shapeId: string; maybeAbsent: boolean } + /** A spread whose keys are only known at run time. */ + | { kind: "opaque" }; + +/** Witness notes the lowering attaches to the IR nodes it builds. Keyed by + * node identity, so nodes a speculative lowering discards are never walked, + * and a node some later pass rebuilds falls back to pinning. */ +export class KeyOrderNotes { + private readonly literals = new Map(); + private readonly copies = new Map(); + /** Shapes a frontend order surface (keys, values/entries, inspect, + * index-signature merges) walks. */ + readonly observed = new Set(); + /** Shapes whose table this pass replaced. */ + readonly changed = new Set(); + + literal(node: IrExpr, segments: KeyOrderSegment[]): IrExpr { + this.literals.set(node, segments); + return node; + } + + copy(node: IrExpr, sourceShapeId: string): IrExpr { + this.copies.set(node, sourceShapeId); + return node; + } + + observe(type: IrType): void { + if (type.kind === "record") this.observed.add(type.shapeId); + } + + literalOf(node: IrExpr): KeyOrderSegment[] | undefined { + return this.literals.get(node); + } + + copyOf(node: IrExpr): string | undefined { + return this.copies.get(node); + } +} + +const isArrayIndex = (name: string): boolean => { + const n = Number(name); + return Number.isInteger(n) && n >= 0 && n < 4294967295 && String(n) === name; +}; + +interface WriteSite { + field: string; + kind: "write" | "delete" | "clone"; + fn: string; + binding: string | null; + loop: string; + pos: number; +} + +interface BindingInit { + count: number; + fn: string; + loop: string; + pos: number; + fresh: boolean; +} + +interface ShapeFacts { + literals: KeyOrderSegment[][]; + copies: { source: string; keys: Set }[]; + /** A construction in today's order (synthesized literal). */ + pinned: boolean; + /** An opaque runtime producer: today's order, any key may be absent. */ + opaque: boolean; + /** A checked conversion from a dynamic value. */ + runtime: boolean; + clones: boolean; + present: Set; + absent: Set; + deleted: Set; + writes: WriteSite[]; +} + +type Resolved = { order: string[] } | "unknown"; + +/** `structuredClone(v)` checked back into v's own type: the copy is built + * from v's dyn view, so it creates exactly v's keys in v's order. */ +function orderPreservingClone(node: IrExpr & { kind: "dynCheck" }): boolean { + const value = node.value; + if (value.kind !== "libCall" || value.fn !== "dyn.structuredClone") return false; + const source = value.args[0]; + return ( + source !== undefined && source.kind === "dynFrom" && typeEquals(source.value.type, node.type) + ); +} + +/** Chooses each in-scope record shape's enumeration table from its + * witnesses. Runs once retained reachability has settled the historical + * tables and derived metadata, before the order-snapshotting helper bodies + * rebuild. */ +export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunction[]): void { + const notes = lowerer.keyOrder; + const shapes = lowerer.shapes; + const facts = new Map(); + const factsOf = (shapeId: string): ShapeFacts => { + let f = facts.get(shapeId); + if (f === undefined) { + f = { + literals: [], + copies: [], + pinned: false, + opaque: false, + runtime: false, + clones: false, + present: new Set(), + absent: new Set(), + deleted: new Set(), + writes: [], + }; + facts.set(shapeId, f); + } + return f; + }; + const observedRoots: IrType[] = []; + const inits = new Map(); + const undefinedArmed = (t: IrType): boolean => + t.kind === "union" && lowerer.armTag(t.unionId, UNDEFINED_T) >= 0; + const containsAbsent = (value: IrExpr): boolean => + value.kind === "fieldAbsent" || + (value.kind === "ternary" && (containsAbsent(value.then) || containsAbsent(value.else_))); + + /** Every record shape a type can hold, through fields, arms, elements, + * callables, promises and class fields. */ + /** Each walk shares one `seen` set per result set, so every type graph + * is walked once however many roots reach it. */ + const shapesIn = (type: IrType, out: Set, seen: Set): void => { + const visit = (t: IrType): boolean => { + if (t.kind === "record") { + if (seen.has("r:" + t.shapeId)) return true; + seen.add("r:" + t.shapeId); + out.add(t.shapeId); + const shape = shapes.get(t.shapeId); + if (shape) { + for (const f of shape.fields) visit(f.type); + if (shape.indexValue) visit(shape.indexValue); + } + return true; + } + if (t.kind === "union") { + if (seen.has("u:" + t.unionId)) return true; + seen.add("u:" + t.unionId); + for (const arm of lowerer.unions.get(t.unionId)?.arms ?? []) visit(arm); + return true; + } + if (t.kind === "object") { + if (seen.has("c:" + t.className)) return true; + seen.add("c:" + t.className); + for (const f of lowerer.classes.get(t.className)?.def.fields ?? []) visit(f.type); + return true; + } + return everyTypeChild(t, visit); + }; + visit(type); + }; + const opaqueShapes = new Set(); + const opaqueSeen = new Set(); + const runtimeShapes = new Set(); + const runtimeSeen = new Set(); + const producedBy = (type: IrType, opaque: boolean): void => { + if (opaque) shapesIn(type, opaqueShapes, opaqueSeen); + else shapesIn(type, runtimeShapes, runtimeSeen); + }; + + // ── collection ────────────────────────────────────────────────────── + let pos = 0; + let fnName = ""; + const loops: number[] = []; + let loopIds = 0; + const loopKey = (): string => loops.join("/"); + const noteInit = (id: string, init: IrExpr): void => { + const previous = inits.get(id); + if (previous !== undefined) { + previous.count++; + return; + } + const fresh = + init.kind === "recordLit" || + init.kind === "recordClone" || + (init.kind === "seqExpr" && init.result.kind === "recordLit"); + inits.set(id, { count: 1, fn: fnName, loop: loopKey(), pos, fresh }); + }; + const visitExpr = (e: IrExpr): void => { + switch (e.kind) { + case "recordLit": { + if (e.type.kind !== "record") return; + const f = factsOf(e.type.shapeId); + const presentKeys = new Set(); + for (const field of e.fields) { + if (field.overflow) continue; + if (field.value.kind === "fieldAbsent") { + f.absent.add(field.name); + continue; + } + presentKeys.add(field.name); + f.present.add(field.name); + if (containsAbsent(field.value)) f.absent.add(field.name); + } + const segments = notes.literalOf(e); + const copy = notes.copyOf(e); + if (segments !== undefined) f.literals.push(segments); + else if (copy !== undefined) f.copies.push({ source: copy, keys: presentKeys }); + else f.pinned = true; + return; + } + case "recordClone": { + if (e.type.kind !== "record") return; + const f = factsOf(e.type.shapeId); + f.clones = true; + for (const o of e.overrides) + f.writes.push({ field: o.name, kind: "clone", fn: fnName, binding: null, loop: "", pos }); + return; + } + case "assignExpr": + noteInit(e.localId, e.value); + return; + case "jsonStringify": + case "dynFrom": + observedRoots.push(e.value.type); + return; + case "recordKeyGet": + observedRoots.push(e.obj.type); + return; + case "libCall": + // Runtime surfaces (console, util.format, workers) that take a + // record may walk its keys; runtime builders make new records. + for (const arg of e.args) observedRoots.push(arg.type); + producedBy(e.type, true); + return; + case "intrinsic": + // Intrinsics hand back values built elsewhere, except allSettled's + // result records; console output renders through the inspect + // helpers, which note their shapes. + if (e.name === "promise.allSettled") producedBy(e.type, true); + return; + case "ffiCall": + case "strIntrinsic": + case "regexIntrinsic": + case "bytesIntrinsic": + producedBy(e.type, true); + return; + case "dynCheck": + if (!orderPreservingClone(e)) producedBy(e.type, false); + return; + case "dynDestrCheck": + case "caughtCheck": + case "caughtNarrow": + producedBy(e.type, false); + return; + default: + return; + } + }; + const visitStmt = (st: IrStmt): void => { + switch (st.kind) { + case "recordSet": + if (st.value.kind === "fieldAbsent") factsOf(st.shapeId).deleted.add(st.field); + factsOf(st.shapeId).writes.push({ + field: st.field, + kind: st.value.kind === "fieldAbsent" ? "delete" : "write", + fn: fnName, + binding: st.obj.kind === "varRef" ? st.obj.localId : null, + loop: loopKey(), + pos, + }); + return; + case "varDecl": + if (st.init !== null) noteInit(st.localId, st.init); + return; + case "assign": + noteInit(st.localId, st.value); + return; + default: + return; + } + }; + const expr = (e: IrExpr): boolean => { + pos++; + visitExpr(e); + return everyExprChild(e, expr, stmt); + }; + const stmt = (st: IrStmt): boolean => { + pos++; + visitStmt(st); + if (st.kind === "while" || st.kind === "doWhile" || st.kind === "for" || st.kind === "forOf") { + loops.push(++loopIds); + everyStmtChild(st, expr, stmt); + loops.pop(); + return true; + } + return everyStmtChild(st, expr, stmt); + }; + for (const fn of fns) { + fnName = fn.name; + for (const st of fn.body) stmt(st); + } + for (const id of opaqueShapes) { + const f = factsOf(id); + f.pinned = true; + f.opaque = true; + } + for (const id of runtimeShapes) factsOf(id).runtime = true; + + // ── scope ─────────────────────────────────────────────────────────── + // Only plain declared-field shapes with a table take part. + const inScope = (shape: IrRecordShape | undefined): shape is IrRecordShape => + shape !== undefined && + !shape.tuple && + shape.indexValue === undefined && + shape.declaredOrder !== undefined && + !shapeHasAccessorSlots(shape); + const tableOf = (shape: IrRecordShape): string[] => + shape.declaredOrder!.filter((k) => !k.startsWith("%") && !isArrayIndex(k)); + // A checked conversion builds values in their source's own order, which + // the record-source table reproduces relative to the shape's table. A new + // table would make every value parsed in today's order keep a source + // view, so such shapes keep today's table. + for (const f of facts.values()) if (f.runtime) f.pinned = true; + + // Nothing to choose: every witness agrees with today's table and no + // optional key is written late. + const interesting = [...facts].some(([id, f]) => { + const shape = shapes.get(id); + if (!inScope(shape)) return false; + if (f.writes.some((w) => w.kind !== "delete") || f.copies.length > 0) return true; + const table = tableOf(shape).join("\0"); + return f.literals.some((segments) => { + const keys: string[] = []; + for (const segment of segments) { + if (segment.kind !== "key") return true; + if (!isArrayIndex(segment.name)) keys.push(segment.name); + } + return keys.join("\0") !== table; + }); + }); + if (!interesting) return; + + // ── presence classes and late-write constraints ───────────────────── + const maybeAbsent = (id: string, key: string): boolean => { + const shape = shapes.get(id); + const field = shape?.fields.find((x) => x.name === key); + if (!field || !undefinedArmed(field.type)) return false; + const f = facts.get(id); + if (f === undefined) return true; + return f.absent.has(key) || f.runtime || f.opaque || f.deleted.has(key); + }; + /** Edges every value of the shape satisfies whatever its sources' + * orders are, or "unknown" when late writes make the order depend on + * the value. */ + const lateEdges = (id: string, keys: readonly string[]): [string, string][] | "unknown" => { + const f = facts.get(id); + if (f === undefined) return []; + const edges: [string, string][] = []; + const keySet = new Set(keys); + const createdKeys = (k: string): boolean => f.present.has(k) || f.runtime || f.opaque; + const required = keys.filter((k) => !maybeAbsent(id, k)); + const created = keys.filter((k) => maybeAbsent(id, k) && createdKeys(k)); + const lateOnly = new Set(keys.filter((k) => maybeAbsent(id, k) && !createdKeys(k))); + // The constraint depends on the written key only, not on the site. + const moving = new Set(); + for (const w of f.writes) + if (w.kind !== "delete" && keySet.has(w.field) && maybeAbsent(id, w.field)) + moving.add(w.field); + for (const field of moving) { + const before = lateOnly.has(field) ? [...required, ...created] : keys; + for (const k of before) if (k !== field) edges.push([k, field]); + } + const lateSites = f.writes.filter((w) => lateOnly.has(w.field)); + const lateWritten = new Set(lateSites.filter((w) => w.kind !== "delete").map((w) => w.field)); + if (lateWritten.size < 2) return edges; + // Two or more late-only keys: each value gains them in the order its + // writes run, so every write must belong to one straight-line + // sequence through one binding initialized once, in the same function + // and loop body as its writes. + const groups = new Map(); + for (const w of lateSites) { + if (w.kind === "clone" || w.binding === null) return "unknown"; + const init = inits.get(w.binding); + if ( + init === undefined || + init.count !== 1 || + init.fn !== w.fn || + init.loop !== w.loop || + init.pos > w.pos + ) + return "unknown"; + const group = groups.get(w.binding) ?? []; + group.push(w); + groups.set(w.binding, group); + } + if (groups.size > 1 && [...groups.keys()].some((b) => !inits.get(b)!.fresh)) return "unknown"; + for (const group of groups.values()) { + const seen = new Set(); + for (const w of group) { + if (seen.has(w.field)) return "unknown"; + seen.add(w.field); + } + const writes = group.filter((w) => w.kind === "write").sort((a, b) => a.pos - b.pos); + for (let i = 0; i + 1 < writes.length; i++) + edges.push([writes[i]!.field, writes[i + 1]!.field]); + } + return edges; + }; + + // ── witness sequences ─────────────────────────────────────────────── + /** The key sequences one literal can create, or null when a spread + * source's order is unknown. A key sits at its first occurrence that + * certainly defines it; each earlier occurrence that may define it is + * another possible position. */ + const literalSequences = ( + segments: readonly KeyOrderSegment[], + keySet: ReadonlySet, + resolved: (id: string) => Resolved, + ): string[][] | null => { + const occurrences: { key: string; definite: boolean }[] = []; + for (const s of segments) { + if (s.kind === "opaque") return null; + if (s.kind === "key") { + if (keySet.has(s.name)) occurrences.push({ key: s.name, definite: s.definite }); + continue; + } + const source = resolved(s.shapeId); + if (source === "unknown") return null; + for (const k of source.order) { + if (keySet.has(k)) + occurrences.push({ key: k, definite: !s.maybeAbsent && !maybeAbsent(s.shapeId, k) }); + } + } + // Per key, the occurrences that can create it: every one up to and + // including the first definite occurrence. The first such position is + // the base; each other one is an alternative. + const positions = new Map(); + const settled = new Set(); + occurrences.forEach((o, i) => { + if (settled.has(o.key)) return; + const list = positions.get(o.key) ?? []; + list.push(i); + positions.set(o.key, list); + if (o.definite) settled.add(o.key); + }); + const base = new Map(); + for (const [key, list] of positions) base.set(key, list[0]!); + const sequenceWith = (at: Map): string[] => + [...at].sort((a, b) => a[1] - b[1]).map(([k]) => k); + const out = [sequenceWith(base)]; + for (const [key, list] of positions) { + for (const alternative of list.slice(1)) { + const moved = new Map(base); + moved.set(key, alternative); + out.push(sequenceWith(moved)); + } + } + return out; + }; + + /** Today's table with every witness edge applied, or "unknown". */ + const merge = (table: readonly string[], edges: readonly [string, string][]): Resolved => { + const rank = new Map(table.map((k, i) => [k, i])); + const succ = new Map>(table.map((k) => [k, new Set()])); + const indeg = new Map(table.map((k) => [k, 0])); + for (const [a, b] of edges) { + if (a === b || !rank.has(a) || !rank.has(b)) continue; + const s = succ.get(a)!; + if (s.has(b)) continue; + s.add(b); + indeg.set(b, indeg.get(b)! + 1); + } + const order: string[] = []; + const ready = table.filter((k) => indeg.get(k) === 0); + while (ready.length > 0) { + ready.sort((a, b) => rank.get(a)! - rank.get(b)!); + const k = ready.shift()!; + order.push(k); + for (const next of succ.get(k)!) { + indeg.set(next, indeg.get(next)! - 1); + if (indeg.get(next) === 0) ready.push(next); + } + } + return order.length === table.length ? { order } : "unknown"; + }; + + const chain = (seq: readonly string[], out: [string, string][]): void => { + for (let i = 0; i + 1 < seq.length; i++) out.push([seq[i]!, seq[i + 1]!]); + }; + + // ── resolution over inheritance edges ─────────────────────────────── + const result = new Map(); + const late = new Map(); + const dependencies = (id: string): string[] => { + const f = facts.get(id); + if (f === undefined) return []; + const deps = new Set(); + for (const segments of f.literals) + for (const s of segments) if (s.kind === "spread") deps.add(s.shapeId); + for (const c of f.copies) deps.add(c.source); + return [...deps]; + }; + /** One shape's order given its sources' current orders. */ + const resolveOne = (id: string, current: (source: string) => Resolved): Resolved => { + const shape = shapes.get(id); + if (!inScope(shape)) return "unknown"; + const table = tableOf(shape); + const f = facts.get(id); + // A shape the program never constructs natively gets its values from + // somewhere this pass cannot see. + if (f === undefined || !(f.literals.length > 0 || f.copies.length > 0 || f.pinned || f.runtime)) + return "unknown"; + // Clones copy their source's own key order, which a checked + // conversion leaves unknown. + if (f.runtime && f.clones) return "unknown"; + const keySet = new Set(table); + const edges: [string, string][] = []; + if (f.pinned) chain(table, edges); + for (const segments of f.literals) { + const sequences = literalSequences(segments, keySet, current); + if (sequences === null) return "unknown"; + for (const seq of sequences) chain(seq, edges); + } + for (const c of f.copies) { + const source = current(c.source); + if (source === "unknown") return "unknown"; + chain( + source.order.filter((k) => keySet.has(k) && c.keys.has(k)), + edges, + ); + } + const cached = late.get(id); + const lateFor = cached !== undefined ? cached : lateEdges(id, table); + if (cached === undefined) late.set(id, lateFor); + if (lateFor === "unknown") return "unknown"; + for (const edge of lateFor) edges.push(edge); + return merge(table, edges); + }; + /** A source's order as its copies see it: values of a shape built by a + * checked conversion keep their source's own order. */ + const asSource = (id: string, resolved: Resolved): Resolved => + facts.get(id)?.runtime === true ? "unknown" : resolved; + + // Tarjan's SCCs over "depends on" edges: sources finish first. + const index = new Map(); + const low = new Map(); + const stack: string[] = []; + const onStack = new Set(); + let counter = 0; + const sccs: string[][] = []; + const strong = (v: string): void => { + index.set(v, counter); + low.set(v, counter); + counter++; + stack.push(v); + onStack.add(v); + for (const w of dependencies(v)) { + if (!index.has(w)) { + strong(w); + low.set(v, Math.min(low.get(v)!, low.get(w)!)); + } else if (onStack.has(w)) low.set(v, Math.min(low.get(v)!, index.get(w)!)); + } + if (low.get(v) === index.get(v)) { + const scc: string[] = []; + let w: string; + do { + w = stack.pop()!; + onStack.delete(w); + scc.push(w); + } while (w !== v); + sccs.push(scc); + } + }; + for (const id of facts.keys()) if (!index.has(id)) strong(id); + const external = (id: string): Resolved => { + const known = result.get(id); + return known === undefined ? "unknown" : asSource(id, known); + }; + for (const scc of sccs) { + const members = new Set(scc); + const guess = new Map(); + for (const id of scc) { + const shape = shapes.get(id); + guess.set(id, inScope(shape) ? { order: tableOf(shape) } : "unknown"); + } + const lookup = (id: string): Resolved => + members.has(id) ? asSource(id, guess.get(id)!) : external(id); + let stable = false; + for (let round = 0; round < scc.length + 3 && !stable; round++) { + stable = true; + for (const id of scc) { + const next = resolveOne(id, lookup); + const prev = guess.get(id)!; + const same = + next === "unknown" + ? prev === "unknown" + : prev !== "unknown" && prev.order.join("\0") === next.order.join("\0"); + if (!same) { + stable = false; + guess.set(id, next); + } + } + } + for (const id of scc) result.set(id, stable ? guess.get(id)! : "unknown"); + } + + // ── observation ───────────────────────────────────────────────────── + const observed = new Set(); + const observedSeen = new Set(); + for (const id of notes.observed) + shapesIn({ kind: "record", shapeId: id }, observed, observedSeen); + for (const root of observedRoots) shapesIn(root, observed, observedSeen); + + // ── apply ─────────────────────────────────────────────────────────── + for (const [id, resolved] of result) { + if (resolved === "unknown" || !observed.has(id)) continue; + const shape = shapes.get(id); + if (!inScope(shape)) continue; + const table = tableOf(shape); + if (resolved.order.join("\0") === table.join("\0")) continue; + shape.declaredOrder = [...shape.declaredOrder!.filter(isArrayIndex), ...resolved.order]; + notes.changed.add(id); + } +} diff --git a/packages/compiler/src/frontend/lowering/lower-assert.ts b/packages/compiler/src/frontend/lowering/lower-assert.ts index 8c49db594..cb31ecc79 100644 --- a/packages/compiler/src/frontend/lowering/lower-assert.ts +++ b/packages/compiler/src/frontend/lowering/lower-assert.ts @@ -870,7 +870,7 @@ export function lowerAssertDirectCall( const module = lowerer.builtinNamespaceModuleOf(callee); if (module !== "assert" && module !== "assert/strict") return null; const calleeSym = lowerer.checker.getSymbolAtLocation(callee); - const decl = calleeSym ? lowerer.checker.declarationsOf(calleeSym)[0] : undefined; + const decl = calleeSym ? calleeSym.declarations[0] : undefined; if (decl && ts.isNamespaceImport(decl)) { lowerer.unsupported( "SC1013", diff --git a/packages/compiler/src/frontend/lowering/lower-async-iteration.ts b/packages/compiler/src/frontend/lowering/lower-async-iteration.ts index c20f32480..f095c5b9c 100644 --- a/packages/compiler/src/frontend/lowering/lower-async-iteration.ts +++ b/packages/compiler/src/frontend/lowering/lower-async-iteration.ts @@ -20,7 +20,7 @@ import { VOID, } from "../../ir/ir.js"; import { locOf } from "../program.js"; -import { extractIteratorValue } from "./lower-generators.js"; +import { extractIteratorValue, type GenType } from "./lower-generators.js"; import { streamSidesOf } from "./lower-stream.js"; import { staticReadableStreamElementType } from "./lower-stdlib.js"; import { genResultRecord } from "../type-mapper.js"; @@ -282,6 +282,43 @@ function classAsyncIteratorPlan( }; } +/** `for await (const x of obj)` where obj's class declares (or inherits) + * `async *[Symbol.asyncIterator]()`: the loop opens the iterator with that + * method call, evaluated once, and then drives the async generator it + * returns exactly like `for await (const x of obj[Symbol.asyncIterator]())` + * (pi's EventStream). Null for other sources. */ +export function classAsyncGeneratorIterator( + lowerer: Lowerer, + expression: ts.Expression, +): (IrExpr & { type: GenType }) | null { + const sourceType = lowerer.mapTypeOf(lowerer.typeOf(expression)); + if (sourceType?.kind !== "object") return null; + const info = lowerer.classes.get(sourceType.className); + const open = info ? lowerer.findMethodOn(info, "sym:asyncIterator") : null; + if ( + !open || + open.sig.abstract === true || + open.sig.params.length !== 0 || + open.sig.ret.kind !== "generator" || + !open.sig.ret.async + ) + return null; + const retT = open.sig.ret; + const source = lowerer.lowerExpr(expression); + if (source.type.kind !== "object") return null; + return { + ...lowerer.accessorCall( + sourceType.className, + "sym:asyncIterator", + source, + [], + retT, + locOf(expression), + ), + type: retT, + }; +} + export function checkForAwaitBinding( lowerer: Lowerer, stmt: ts.ForOfStatement, diff --git a/packages/compiler/src/frontend/lowering/lower-builtin-values.ts b/packages/compiler/src/frontend/lowering/lower-builtin-values.ts index c05430ad3..5e04ee83c 100644 --- a/packages/compiler/src/frontend/lowering/lower-builtin-values.ts +++ b/packages/compiler/src/frontend/lowering/lower-builtin-values.ts @@ -132,13 +132,11 @@ export function isNativeBuiltinValueInitializer( const symbol = lowerer.resolveValueSymbol(expr); if (!symbol || seen.has(symbol)) return false; seen.add(symbol); - return lowerer.checker - .declarationsOf(symbol) - .some( - (decl) => - ts.isVariableDeclaration(decl) && - isNativeBuiltinValueInitializer(lowerer, decl.initializer, seen), - ); + return symbol.declarations.some( + (decl) => + ts.isVariableDeclaration(decl) && + isNativeBuiltinValueInitializer(lowerer, decl.initializer, seen), + ); } /** The stored Array.isArray function shares the direct call's native test. */ diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 67ca01b3f..5a4838782 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -1,4 +1,5 @@ import { guardNullProto, nullProtoRecordShape } from "./null-prototype.js"; +import { refuseModuleMemberThis } from "./module-member-this.js"; import { lowerUnionEquality, tagEqualityMayMissAlias } from "./strict-equality.js"; import { lowerHashStreamCall } from "./builtins/hash-stream.js"; import { lowerResponseStaticJson } from "./lower-stdlib.js"; @@ -114,6 +115,9 @@ import { lowerStringMethodCall, lowerStringPaddingCall, lowerStringSplitCall, + isDynPrimitiveKindMethod, + lowerDynPrimitiveMethodCall, + lowerExtraNumberMethodCall, } from "./containers/string-and-regexp.js"; import { createRequireSpecOf } from "./builtins/module-bindings.js"; import { @@ -126,7 +130,9 @@ import { lowerRequireResolveCall, } from "./builtins/module-resolution.js"; import { + isWebCryptoReceiver, lowerCipherMethodCall, + lowerCryptoGetRandomValues, lowerCryptoHashMethodCall, lowerPromisifiedCryptoCall, } from "./builtins/crypto.js"; @@ -145,7 +151,10 @@ import { lowerPerfHooksCall, perfEntryTag, } from "./builtins/performance.js"; -import { lowerProcStreamMethodCall } from "./builtins/process.js"; +import { + lowerProcStreamMethodCall, + lowerProcessBuiltinModuleNamespace, +} from "./builtins/process.js"; import { recordMayKeepSource, throughRecordSource } from "./record-sources.js"; import { lowerReflectCall } from "./builtins/reflection.js"; import { @@ -163,6 +172,7 @@ import { isSafeToDiscard } from "./expressions/evaluation-safety.js"; import { tryLowerExpression } from "./expressions/try-lower-expression.js"; import { classToNumberConversion } from "./class-to-primitive.js"; import { hasRuntimeStatics } from "./class-runtime-statics.js"; +import { structuralReadType } from "./class-unions.js"; import { httpClientFnBindingOf, lowerCompatReqStreamOptionalCall, @@ -420,6 +430,15 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { return { kind: "libCall", fn: "http2.streamNoop", args: [], type: VOID, loc }; } + // `process.getBuiltinModule("node:os")` typed as the module namespace, + // including pi's cast-and-probe spelling + // `(process as P).getBuiltinModule?.("node:os")` (the member always + // exists on Node 24's process object). + { + const namespace = lowerProcessBuiltinModuleNamespace(lowerer, expr); + if (namespace) return namespace; + } + // Optional-chain call forms: `f?.()` (the token on the call) and // `a?.m()` (the token on the member access). The handled markers keep // the chain lowering's re-entrant dispatch from looping. @@ -680,12 +699,23 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { }; // The declared result is the value's own type (the generic's T): // validate the dyn copy back into it when the type can be checked; - // dyn-typed and unmappable results stay dyn values (JS files). + // dyn-typed and unmappable results stay dyn values (JS files). An + // empty record (`{}` inferred from a JavaScript `payload = {}` + // default) is the checker's supertype of whatever arrives, not a + // layout: checking into it would drop every member of the copy. const resultT = lowerer.mapTypeOf(lowerer.typeOf(expr)); + const resultShape = + resultT?.kind === "record" ? lowerer.shapes.get(resultT.shapeId) : undefined; + const emptyRecord = + resultShape !== undefined && + !resultShape.tuple && + resultShape.indexValue === undefined && + resultShape.fields.length === 0; if ( resultT !== null && resultT.kind !== "dyn" && resultT.kind !== "void" && + !emptyRecord && canDynCheckTo( resultT, (id) => lowerer.shapes.get(id), @@ -1154,7 +1184,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { lowerer.noteEdge(sig.name); const args = lowerer.completeArgs(expr.arguments.nodes, sig.params, loc, expr); const symbol = lowerer.resolveValueSymbol(expr.expression); - const declaration = symbol ? lowerer.checker.valueDeclarationOf(symbol) : undefined; + const declaration = symbol ? symbol.valueDeclaration : undefined; // A bare call supplies undefined as this, even inside a callback // currently carrying a receiver. The value-call ABI saves and // restores that ambient receiver around the function invocation. @@ -1249,6 +1279,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { if (facadeServed) return facadeServed; const memberSym = lowerer.checker.getSymbolAtLocation(nsMember); if (memberSym) fenceEarlyNsMemberRef(lowerer, expr.expression, memberSym); + refuseModuleMemberThis(lowerer, expr, nsMember); const generic = lowerer.genericFnOf(nsMember); if (generic) return lowerer.lowerGenericCall(expr, generic); const sig = lowerer.fnSigOf(nsMember); @@ -1312,6 +1343,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { if (!ts.isIdentifier(nameId)) { lowerer.unsupported("SC1090", nameId, "private-named module members"); } + refuseModuleMemberThis(lowerer, expr, nameId); const generic = lowerer.genericFnOf(nameId); if (generic) return lowerer.lowerGenericCall(expr, generic); const sig = lowerer.fnSigOf(nameId); @@ -1489,6 +1521,9 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { lowerer.fenceStaticReadableStreamMember(expr.expression, "call") ?? lowerDisposableStackUseCall(lowerer, expr, expr.expression) ?? lowerer.lowerStdlibMethodCall(expr, expr.expression) ?? + // crypto.getRandomValues on the WebCrypto global (untyped in + // fallback builds, so before the dyn receiver path) or webcrypto. + lowerWebCryptoGetRandomValues(lowerer, expr, expr.expression) ?? // Dyn receivers (JSON.parse-derived `unknown`/`any` values) — lowerDynReceiverMethodCall(lowerer, expr, expr.expression) ?? @@ -3416,6 +3451,23 @@ function lowerDynReceiverMethodCall( lowerStringMethodCall(lowerer, call, access, () => recv) ); } + // A checker-untyped JS binding holding `string | undefined` (string- + // width's `const visibleSegment = baseVisible(segment)` after an + // `=== undefined` guard the checker cannot see through): no typed + // lowering claims the union, so a String method boxes it and takes the + // dyn receiver path, whose undefined/null arms throw Node's read errors. + if ( + checkerUntyped && + recv.type.kind === "union" && + isJsSourceFile(call.getSourceFile()) && + isStringMethodName(access.name.text) && + lowerer.unions + .get(recv.type.unionId) + ?.arms.every((arm) => arm.kind === "string" || isUnitType(arm)) === true && + lowerer.dynConvertible(recv.type) + ) { + recv = lowerer.coerceToExpected(recv, DYN); + } // A native handle read through `as any` (`(hash as any).update(x)`) // dispatches through the handle, as the same value held in an // 'unknown' slot does. @@ -3512,9 +3564,9 @@ function lowerDynReceiverMethodCall( // Those are not stored members of a checked-dynamic object, so // dynKeyGet cannot stand in for JavaScript's prototype lookup. Only // an authored declaration can justify the own-member path. - const authored = lowerer.checker - .declarationsOf(prop) - .some((decl) => !lowerer.isStdlibFile(decl.getSourceFile())); + const authored = prop.declarations.some( + (decl) => !lowerer.isStdlibFile(decl.getSourceFile()), + ); return ( authored && lowerer.checker.getCallSignatures(lowerer.checker.getTypeOfSymbol(prop)).length > 0 @@ -3540,6 +3592,26 @@ function lowerDynReceiverMethodCall( lowerStringMethodCall(lowerer, call, access, checked) ); } + // String/Number methods the runtime dispatch has no answer for + // (codePointAt, substr, toLocaleLowerCase, toPrecision, ...): a string + // or number receiver runs the typed lowering, every other kind keeps the + // runtime dispatch. + if ( + !ownCallable && + !call.questionDotToken && + !access.questionDotToken && + isDynPrimitiveKindMethod(access.name.text) && + !lowerer.chainBlocked(access, call) + ) { + const primitive = lowerDynPrimitiveMethodCall( + lowerer, + call, + access.name.text, + access.getText(), + recv, + ); + if (primitive) return primitive; + } // The argument is a radix for bigint and an encoding for Buffer. // Preserve its runtime value so receiver dispatch can choose correctly. if (access.name.text === "toString" && call.arguments.nodes.length <= 1) { @@ -3595,9 +3667,9 @@ function lowerDynReceiverMethodCall( return true; return ( prop !== undefined && - lowerer.checker - .declarationsOf(prop) - .some((decl) => ts.isPropertyDeclaration(decl) || ts.isPropertySignature(decl)) + prop.declarations.some( + (decl) => ts.isPropertyDeclaration(decl) || ts.isPropertySignature(decl), + ) ); })(); // Protocol methods keep their static signature: instances of the @@ -3632,8 +3704,14 @@ function lowerDynReceiverMethodCall( // own or inherited member exactly like a non-prototype name (chalk's // `chalk.red.bold(...)` reads a prototype getter of a function), so // only a string receiver keeps the refusal, deferred to run time. + // The same holds when only the MEMBER is checker-untyped: TypeScript + // importing chalk's JavaScript source types the default export as a + // plain function, so `chalk.bold` reads as `any` on a typed receiver. const stringOnlyName = - checkerUntyped && !ownCallable && DYN_STRING_PROTO_ONLY_NAMES.has(access.name.text); + (checkerUntyped || + (lowerer.typeOf(access).flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0) && + !ownCallable && + DYN_STRING_PROTO_ONLY_NAMES.has(access.name.text); if (DYN_PROTO_METHOD_NAMES.has(access.name.text) && !ownCallable && !stringOnlyName) return null; // Optional forms (`obj.cb?.()`, `obj?.cb()`) belong to the chain // machinery's short-circuit semantics — not modeled here yet. @@ -4117,6 +4195,19 @@ export function lowerDynDispatchMethodCall( }; } +/** Every String.prototype method name a typed or dyn string lowering + * answers. */ +function isStringMethodName(name: string): boolean { + return ( + Object.hasOwn(STR_METHODS, name) || + STRING_INDEX_METHODS.has(name) || + DYN_STRING_ONLY_METHODS.has(name) || + isDynPrimitiveKindMethod(name) || + name === "normalize" || + name === "lastIndexOf" + ); +} + /** STR_METHODS ∪ the regex-form names, MINUS everything Array (or any * other dyn kind's prototype) also declares. */ const DYN_STRING_ONLY_METHODS = new Set([ @@ -4239,9 +4330,7 @@ function lowerCaughtPredicateCall( const callee = call.expression; if (!ts.isIdentifier(callee)) return null; const symbol = lowerer.resolveValueSymbol(callee); - const decl = symbol - ? lowerer.checker.declarationsOf(symbol).find(ts.isFunctionDeclaration) - : undefined; + const decl = symbol ? symbol.declarations.find(ts.isFunctionDeclaration) : undefined; if (!symbol || !decl || !decl.body) return null; if (!decl.type || !ts.isTypePredicateNode(decl.type)) return null; if (decl.parameters.nodes.length !== 1) return null; @@ -4588,6 +4677,21 @@ function lowerPrimitiveProtoCall( isJsSourceFile(call.getSourceFile()) ) { const operand = lowerer.lowerExpr(recv); + if ( + name === "toExponential" && + call.arguments.nodes.length === 1 && + (operand.type.kind === "dyn" || + (operand.type.kind === "union" && lowerer.dynConvertible(operand.type))) + ) { + const dispatched = lowerDynPrimitiveMethodCall( + lowerer, + call, + name, + `${recv.getText()}.${name}`, + lowerer.coerceToExpected(operand, DYN), + ); + if (dispatched) return dispatched; + } if ( operand.type.kind === "dyn" || (operand.type.kind === "union" && lowerer.dynConvertible(operand.type)) @@ -4602,6 +4706,32 @@ function lowerPrimitiveProtoCall( loc: locOf(call), }; } + if ( + name === "toPrecision" && + call.arguments.nodes.length <= 1 && + !call.arguments.nodes.some(ts.isSpreadElement) && + isJsSourceFile(call.getSourceFile()) && + recvKind === "union" + ) { + const operand = lowerer.lowerExpr(recv); + if (operand.type.kind === "union" && lowerer.dynConvertible(operand.type)) { + const dispatched = lowerDynPrimitiveMethodCall( + lowerer, + call, + name, + `${recv.getText()}.${name}`, + lowerer.coerceToExpected(operand, DYN), + ); + if (dispatched) return dispatched; + } + } + if ( + recvKind === "f64" && + (name === "toPrecision" || (name === "toExponential" && call.arguments.nodes.length === 1)) + ) { + const typed = lowerExtraNumberMethodCall(lowerer, call, name, lowerer.lowerExpr(recv)); + if (typed) return typed; + } if (recvKind !== "f64" && recvKind !== "bool" && recvKind !== "string") return null; const loc = locOf(call); if (name === "toString" && call.arguments.nodes.length === 0) { @@ -4769,7 +4899,7 @@ function implicitReceiverOverloadReturn( if (!member) return null; const signatures = lowerer.checker.getCallSignatures(lowerer.checker.getTypeOfSymbol(member)); const matches = signatures.filter((signature) => { - const decl = lowerer.checker.signatureDeclaration(signature); + const decl = signature.declaration; if (!decl || !(ts.isFunctionDeclaration(decl) || ts.isMethodDeclaration(decl)) || decl.body) { return false; } @@ -4821,7 +4951,7 @@ function reconcileOverloadReturn( call: IrExpr, ): IrExpr { const rsig = lowerer.checker.getResolvedSignature(expr); - const rdecl = rsig ? lowerer.checker.signatureDeclaration(rsig) : undefined; + const rdecl = rsig ? rsig.declaration : undefined; // Nominal covariant overrides share the base's return ABI, but retain // their declared subclass result at the call site. Unannotated fluent // JS overrides use the same bridge for their proven receiver result. @@ -6520,6 +6650,7 @@ export function recordKeysArrayCall( loc: SrcLoc, ): IrExpr { const resultT = arrayOf(STRING); + lowerer.keyOrder.observe(argIr); const key = `obj.keys:${argIr.shapeId}:${typeKey(resultT)}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { @@ -7762,7 +7893,7 @@ function lowerObjectStaticCall( ? lowerer.checker.getSymbolAtLocation(argNode) : undefined; const namespace = symbol - ? lowerer.checker.declarationsOf(symbol).some((decl) => ts.isNamespaceImport(decl)) + ? symbol.declarations.some((decl) => ts.isNamespaceImport(decl)) : false; const names = namespace ? [...UTIL_TYPES_KEYS, "default"].sort() : UTIL_TYPES_KEYS; const loc = locOf(call); @@ -8000,6 +8131,7 @@ function lowerObjectStaticCall( valueT = tupleShape.fields.find((f) => f.name === "1")!.type; } + lowerer.keyOrder.observe(argIr); const key = `obj.${member}:${argIr.shapeId}:${typeKey(resultT)}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { @@ -8405,7 +8537,7 @@ function isOverloadedMemberAccess(lowerer: Lowerer, access: ts.PropertyAccessExp function resolvedOverloadResultType(lowerer: Lowerer, call: ts.CallExpression): IrType { const { checker } = lowerer; const signature = checker.getResolvedSignature(call); - const decl = signature ? checker.signatureDeclaration(signature) : undefined; + const decl = signature ? signature.declaration : undefined; const declared = decl ? checker.getSignatureFromDeclaration(decl) : undefined; const source = declared && declared.getTypeParameters().length === 0 ? declared : signature; const result = source ? lowerer.mapTypeOf(checker.getReturnTypeOfSignature(source)) : null; @@ -8597,7 +8729,7 @@ const SP_BRAND_METHODS = new Set([ function arrowSignatureOf(lowerer: Lowerer, node: ts.Expression): ts.Signature | null { const signatures = lowerer.checker.getCallSignatures(lowerer.typeOf(node)); if (signatures.length !== 1) return null; - const declaration = lowerer.checker.signatureDeclaration(signatures[0]!); + const declaration = signatures[0]!.declaration; return declaration !== undefined && ts.isArrowFunction(declaration) ? signatures[0]! : null; } @@ -8850,7 +8982,7 @@ function lowerObjLitGenericMethodCall( const recvT = lowerer.typeOf(access.expression); const propSym = lowerer.checker.getPropertyOfType(recvT, name); if (!propSym) return null; - const propertyDecl = lowerer.checker.valueDeclarationOf(propSym); + const propertyDecl = propSym.valueDeclaration; if ( propertyDecl && ts.isShorthandPropertyAssignment(propertyDecl) && @@ -8874,13 +9006,11 @@ function lowerObjLitGenericMethodCall( // claimed compilable ones; a class that failed collection keeps its // own diagnostics and the generic method-call fence downstream). if ( - lowerer.checker - .declarationsOf(propSym) - .some( - (d) => - d.parent !== undefined && - (ts.isClassDeclaration(d.parent) || ts.isClassExpression(d.parent)), - ) + propSym.declarations.some( + (d) => + d.parent !== undefined && + (ts.isClassDeclaration(d.parent) || ts.isClassExpression(d.parent)), + ) ) { return null; } @@ -8947,9 +9077,9 @@ function lowerObjLitGenericMethodCall( // receiver's runtime class must be provable — name that discipline // instead of the object-literal wording when the method lives on an // interface. - const onInterface = lowerer.checker - .declarationsOf(propSym) - .some((d) => d.parent !== undefined && ts.isInterfaceDeclaration(d.parent)); + const onInterface = propSym.declarations.some( + (d) => d.parent !== undefined && ts.isInterfaceDeclaration(d.parent), + ); // A receiver held as a checked-dynamic value (a class-backed interface) // dispatches by name to the runtime object's member, which a generic // method answers with its erased instance; the result converts to this @@ -9003,7 +9133,9 @@ export function lowerObjectMethodCall( access: ts.PropertyAccessExpression, ): IrExpr | null { if (lowerer.chainBlocked(access, call)) return null; - let mappedReceiver = lowerer.mapTypeOf(lowerer.typeOf(access.expression)); + let mappedReceiver = + structuralReadType(lowerer, access.expression) ?? + lowerer.mapTypeOf(lowerer.typeOf(access.expression)); // EventEmitter's static side (the class, a value of it, or the events // module, which is the class) keeps its static surface on the emitter // dispatch (`EventEmitter.setMaxListeners(n)`), which tells the statics @@ -9617,6 +9749,44 @@ function unionObjectMethodHelper( if (existing) return existing; const name = `%union.call.${lowerer.unionCallHelpers.size}`; lowerer.unionCallHelpers.set(key, name); + // A probe that catches a refusal while the body lowers must not leave + // the name registered for a helper that was never emitted. + try { + lowerer.liftedFns.push( + unionObjectMethodFunction( + lowerer, + name, + sourceCall, + receiverT, + method, + plans, + shapes, + resultT, + loc, + ), + ); + } catch (e) { + lowerer.unionCallHelpers.delete(key); + throw e; + } + return name; +} + +function unionObjectMethodFunction( + lowerer: Lowerer, + name: string, + sourceCall: ts.CallExpression, + receiverT: IrType & { kind: "union" }, + method: string, + plans: { + arm: IrType & { kind: "object" }; + info: ClassInfo; + found: NonNullable>; + }[], + shapes: readonly ParamShape[], + resultT: IrType, + loc: SrcLoc, +): IrFunction { const receiver = varRef("this.0", receiverT, loc); const argRefs = shapes.map((shape, i) => varRef(`a.${i}`, shape.type, loc)); const branch = (plan: (typeof plans)[number]): IrStmt[] => { @@ -9691,8 +9861,7 @@ function unionObjectMethodHelper( type: param.type, mutable: false, })); - lowerer.liftedFns.push({ name, params, returnType: resultT, locals, body, loc }); - return name; + return { name, params, returnType: resultT, locals, body, loc }; } /** The argument's lowered type for a message (what the static form saw), @@ -9885,6 +10054,22 @@ export function lowerStaticNumberGlobal( return null; } +/** `crypto.getRandomValues(view)` on the WebCrypto global or webcrypto. */ +function lowerWebCryptoGetRandomValues( + lowerer: Lowerer, + call: ts.CallExpression, + access: ts.PropertyAccessExpression, +): IrExpr | null { + if ( + access.name.text !== "getRandomValues" || + call.questionDotToken || + access.questionDotToken || + !isWebCryptoReceiver(lowerer, access.expression) + ) + return null; + return lowerCryptoGetRandomValues(lowerer, call); +} + /** A then handler's promise-armed result (`.then(() => cached ?? load())` * typed `T | Promise`, or a `Promise | void` callback's result): * the lifted async body adopts the promise arm and fulfills with the diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index 770c74840..8a5170a7a 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -1,5 +1,10 @@ import { lowerValueReturningConstructorNew } from "./js-dynamic-objects.js"; import { dynUndefinedExpr, nodeThrowExpr, varRef } from "../../ir/build.js"; +import { + constructedFromValue, + constructedValue, + superIntoResultSlot, +} from "./constructor-returns.js"; import { checkedIterableSpread } from "./checked-iterable-spread.js"; import { lowerReadlineUiNew } from "./builtins/readline.js"; import { checkedClassUnionAssertion } from "./class-assertions.js"; @@ -86,7 +91,7 @@ import { isNodeTypesPath, locOf, } from "../program.js"; -import { PoisonError, dynFallbackType, newFnCtx, own } from "./lowerer.js"; +import { PoisonError, dynFallbackType, jsFuncNameOf, newFnCtx, own } from "./lowerer.js"; import { isAssertClassNew } from "./assert-instances.js"; import { lowerArrayConstructor } from "./containers/array-construction.js"; import { lowerMapSeedNew, lowerSetSeedNew } from "./containers/collection-methods.js"; @@ -178,6 +183,39 @@ export function returnsOnlyThis(member: ts.MethodDeclaration): boolean { return valid; } +/** returnsOnlyThis, also accepting a comma expression whose last operand + * is `this` (the minifier's `return a(), this.x = v, this`, jose's fluent + * claim setters). Used only to narrow a call's result back to the + * receiver's class. */ +export function returnsOnlyThisValue(member: ts.MethodDeclaration): boolean { + const body = member.body; + const last = body?.statements?.nodes.at(-1); + if (!body || !last || !ts.isReturnStatement(last)) return false; + let valid = true; + const visit = (node: ts.Node): void => { + if (ts.isFunctionLike(node)) return; + if (ts.isReturnStatement(node)) { + let value = node.expression; + for (;;) { + while (value && ts.isParenthesizedExpression(value)) value = value.expression; + if ( + value && + ts.isBinaryExpression(value) && + value.operatorToken.kind === ts.SyntaxKind.CommaToken + ) { + value = value.right; + continue; + } + break; + } + if (!value || value.kind !== ts.SyntaxKind.ThisKeyword) valid = false; + } + node.forEachChild(visit); + }; + body.forEachChild(visit); + return valid; +} + function functionLocalClass(decl: ts.ClassLikeDeclaration): boolean { for ( let parent: ts.Node | undefined = decl.parent; @@ -221,14 +259,12 @@ export function storedClassValueType( const symbol = lowerer.resolveValueSymbol(expression); const declaration = symbol && - lowerer.checker - .declarationsOf(symbol) - .find( - (decl) => - ts.isVariableDeclaration(decl) && - decl.initializer && - (ts.getCombinedNodeFlags(decl) & ts.NodeFlags.Const) !== 0, - ); + symbol.declarations.find( + (decl) => + ts.isVariableDeclaration(decl) && + decl.initializer && + (ts.getCombinedNodeFlags(decl) & ts.NodeFlags.Const) !== 0, + ); if (declaration && ts.isVariableDeclaration(declaration) && declaration.initializer) { const value = storedClassValueType(lowerer, declaration.initializer, visiting); if (value?.kind === "classval") return value; @@ -243,7 +279,7 @@ export function storedClassValueType( const mapped = lowerer.mapTypeOf(type); if (mapped?.kind === "classval" && !lowerer.classes.has(mapped.className)) { for (const signature of lowerer.checker.getConstructSignatures(type)) { - const constructor = lowerer.checker.signatureDeclaration(signature); + const constructor = signature.declaration; const declaration = constructor && ts.isConstructorDeclaration(constructor) ? constructor.parent : undefined; if ( @@ -284,14 +320,12 @@ export function storedClassValueType( const symbol = lowerer.resolveValueSymbol(callee); const declaration = symbol && - lowerer.checker - .declarationsOf(symbol) - .find( - (decl) => - ts.isVariableDeclaration(decl) && - decl.initializer && - (ts.getCombinedNodeFlags(decl) & ts.NodeFlags.Const) !== 0, - ); + symbol.declarations.find( + (decl) => + ts.isVariableDeclaration(decl) && + decl.initializer && + (ts.getCombinedNodeFlags(decl) & ts.NodeFlags.Const) !== 0, + ); if (declaration && ts.isVariableDeclaration(declaration) && declaration.initializer) { callee = declaration.initializer; continue; @@ -353,7 +387,7 @@ export function storedClassValueType( // Cache the module-level closure so its captures and lifted bodies are // exactly the ones used when the declaration executes. for (const signature of lowerer.checker.getConstructSignatures(type)) { - const declaration = lowerer.checker.signatureDeclaration(signature); + const declaration = signature.declaration; const cls = declaration && ts.isConstructorDeclaration(declaration) ? declaration.parent : null; if (!cls || (!ts.isClassExpression(cls) && !ts.isClassDeclaration(cls))) continue; let factory: ts.ArrowFunction | ts.FunctionExpression | undefined; @@ -725,7 +759,7 @@ export interface GenericClassInfo { * or the ElementAccessExpression itself). Null when no declaration has * that shape (well-known-symbol members like `[Symbol.iterator]`). */ function lateBoundKeySymOf(lowerer: Lowerer, p: ts.Symbol): ts.Symbol | null { - for (const d of lowerer.checker.declarationsOf(p)) { + for (const d of p.declarations) { const access = ts.isBinaryExpression(d) && ts.isElementAccessExpression(d.left) ? d.left @@ -990,7 +1024,7 @@ export function builtinStreamInfoOf( // member to the stdlib class symbol. The declaration itself is // alias plumbing (streamClassAliasDecl — both declaration walks // skip it). - const decl = lowerer.checker.valueDeclarationOf(symbol); + const decl = symbol.valueDeclaration; if ( decl && ts.isVariableDeclaration(decl) && @@ -1012,7 +1046,7 @@ export function builtinStreamInfoOf( if (rec.lib === symbol.name) irName = name; } if (!irName) return null; - const declared = lowerer.checker.declarationsOf(symbol).some((d) => { + const declared = symbol.declarations.some((d) => { if (!ts.isClassDeclaration(d)) return false; if (isNodeTypesPath(d.getSourceFile().fileName)) return false; let node: ts.Node | undefined = d.parent; @@ -1034,15 +1068,13 @@ function jsFieldInitializerType(lowerer: Lowerer, type: IrType, expression: ts.E const symbol = lowerer.checker.getSymbolAtLocation(expression); if ( symbol && - lowerer.checker - .declarationsOf(symbol) - .some( - (decl) => - ts.isParameter(decl) && - ((ts.isConstructorDeclaration(decl.parent) && - !checkedJsDocumentedParameter(lowerer, decl)) || - (lowerer.typeOf(expression).flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0), - ) + symbol.declarations.some( + (decl) => + ts.isParameter(decl) && + ((ts.isConstructorDeclaration(decl.parent) && + !checkedJsDocumentedParameter(lowerer, decl)) || + (lowerer.typeOf(expression).flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0), + ) ) return DYN; } @@ -1102,7 +1134,7 @@ function jsFieldInitializerType(lowerer: Lowerer, type: IrType, expression: ts.E const symbol = lowerer.checker.getSymbolAtLocation(part.name); if ( symbol && - lowerer.checker.declarationsOf(symbol).some((decl) => { + symbol.declarations.some((decl) => { const member = ts.isBinaryExpression(decl) ? decl.left : decl; return ( ts.isPropertyAccessExpression(member) && @@ -1244,13 +1276,10 @@ function returnsWidenedJsField( : undefined; if ( symbol && - lowerer.checker - .declarationsOf(symbol) - .some( - (declaration) => - ts.isMethodDeclaration(declaration) && - lowerer.checkedVirtualJsMethods.has(declaration), - ) + symbol.declarations.some( + (declaration) => + ts.isMethodDeclaration(declaration) && lowerer.checkedVirtualJsMethods.has(declaration), + ) ) return true; } @@ -1263,14 +1292,12 @@ function returnsWidenedJsField( const symbol = lowerer.checker.getSymbolAtLocation(expression.name); if ( symbol && - lowerer.checker - .declarationsOf(symbol) - .some( - (declaration) => - (ts.isGetAccessorDeclaration(declaration) || - ts.isSetAccessorDeclaration(declaration)) && - lowerer.checkedVirtualJsAccessors.has(declaration), - ) + symbol.declarations.some( + (declaration) => + (ts.isGetAccessorDeclaration(declaration) || + ts.isSetAccessorDeclaration(declaration)) && + lowerer.checkedVirtualJsAccessors.has(declaration), + ) ) return true; } @@ -1303,11 +1330,11 @@ function returnsWidenedJsField( * (genResultRecord's list, including scalar-backed Date values). */ function undefArmedFieldType(lowerer: Lowerer, p: ts.Symbol): IrType | null { const t = lowerer.checker.getTypeOfSymbol(p); - const site = lowerer.checker.declarationsOf(p)[0]; + const site = p.declarations[0]; let mapped = lowerer.mapTypeOf(t) ?? (site ? dynFallbackType(lowerer, site, t) : null); if (mapped && site) mapped = jsOpenObjectType(site, mapped, lowerer.shapes, lowerer.unions); if (!mapped || mapped.kind === "void") return null; - for (const declaration of lowerer.checker.declarationsOf(p)) { + for (const declaration of p.declarations) { const assignment = ts.isBinaryExpression(declaration) ? declaration : declaration.parent; if ( assignment && @@ -1354,7 +1381,7 @@ export function builtinEmitterInfoOf( // follow the member off the module namespace. The declaration // itself is alias plumbing (builtinMemberRequireDecl — both // declaration walks skip it). - const decl = lowerer.checker.valueDeclarationOf(symbol); + const decl = symbol.valueDeclaration; if ( decl !== undefined && ts.isVariableDeclaration(decl) && @@ -1370,7 +1397,7 @@ export function builtinEmitterInfoOf( return null; } if (symbol.name !== "EventEmitter") return null; - const declared = lowerer.checker.declarationsOf(symbol).some((d) => { + const declared = symbol.declarations.some((d) => { if (!ts.isClassDeclaration(d) && !ts.isInterfaceDeclaration(d)) return false; let node: ts.Node | undefined = d.parent; while (node) { @@ -1539,7 +1566,7 @@ function collectClassShapeDeferring(lowerer: Lowerer, decl: ts.ClassDeclaration) const baseSymbol = reference && ts.isIdentifier(reference) ? lowerer.resolveValueSymbol(reference) : undefined; const baseDeclaration = baseSymbol - ? lowerer.checker.declarationsOf(baseSymbol).find(ts.isClassDeclaration) + ? baseSymbol.declarations.find(ts.isClassDeclaration) : undefined; if ( baseDeclaration && @@ -1886,8 +1913,7 @@ export function collectClassShapeInner( return true; } const signatures = lowerer.checker.getCallSignatures(lowerer.typeOf(expression)); - const source = - signatures.length === 1 ? lowerer.checker.signatureDeclaration(signatures[0]!) : undefined; + const source = signatures.length === 1 ? signatures[0]!.declaration : undefined; if ( !source || !isJsSourceFile(source.getSourceFile()) || @@ -2023,12 +2049,9 @@ export function collectClassShapeInner( // what actually blocks it). const rebinds = memberSym !== undefined && - lowerer.checker - .declarationsOf(memberSym) - .filter( - (d) => - ts.isBinaryExpression(d) && d.operatorToken.kind === ts.SyntaxKind.EqualsToken, - ).length > 1; + memberSym.declarations.filter( + (d) => ts.isBinaryExpression(d) && d.operatorToken.kind === ts.SyntaxKind.EqualsToken, + ).length > 1; if (rebinds) { lowerer.unsupported( "SC1090", @@ -4015,14 +4038,12 @@ export function collectClassShapeInner( if (fields.has(p.name) || methods.has(p.name)) continue; if (hasAccessor(p.name)) continue; if (base && (base.fields.has(p.name) || lowerer.findMethodOn(base, p.name))) continue; - const site = lowerer.checker - .declarationsOf(p) - .find( - (d) => - ts.isPropertyAccessExpression(d) || - ts.isBinaryExpression(d) || - ts.isElementAccessExpression(d), - ); + const site = p.declarations.find( + (d) => + ts.isPropertyAccessExpression(d) || + ts.isBinaryExpression(d) || + ts.isElementAccessExpression(d), + ); if (!site) continue; // Late-bound properties: the ones the scan above collected are // real fields under their printable names — skip. The rest keep @@ -4726,8 +4747,7 @@ export function analyzeClassDecoration(lowerer: Lowerer, info: ClassInfo): void : null; if (ambientCallee !== null) { const sym = lowerer.resolveValueSymbol(ambientCallee); - const vdecl = - sym && !lowerer.isStdlibSymbol(sym) ? lowerer.checker.declarationsOf(sym)[0] : undefined; + const vdecl = sym && !lowerer.isStdlibSymbol(sym) ? sym.declarations[0] : undefined; const ambientVar = vdecl !== undefined && ts.isVariableDeclaration(vdecl) && @@ -5747,7 +5767,7 @@ export function exactClassOfReceiver(lowerer: Lowerer, expr: ts.Expression): Cla // static write would create an own property in JS. The general // class-value write fence answers instead. if (direct) return direct.classDecorators?.valueGlobalId !== undefined ? null : direct; - const decl = lowerer.checker.valueDeclarationOf(symbol); + const decl = symbol.valueDeclaration; if ( !decl || !ts.isVariableDeclaration(decl) || @@ -5795,7 +5815,7 @@ export function propertyAssignedClassInfoOf( symbol.flags & ts.SymbolFlags.Alias ? lowerer.checker.getAliasedSymbol(symbol) : symbol; const registered = lowerer.classBySymbol.get(resolved); if (registered) return registered; - const decls = lowerer.checker.declarationsOf(resolved); + const decls = resolved.declarations; // The class expression's OWN symbol (tsgo's answer through CJS export // aliases): the declaration is the expression itself. Its top-level // assignment statement must be the binding's ONLY producer. The @@ -6259,7 +6279,7 @@ function capturesEnclosingScope( if (captured) return; if (ts.isIdentifier(node)) { const symbol = lowerer.checker.getSymbolAtLocation(node); - const decl = symbol ? lowerer.checker.valueDeclarationOf(symbol) : undefined; + const decl = symbol ? symbol.valueDeclaration : undefined; // The enclosing function's own name is the same binding for every // evaluation. if (decl && decl !== scope && inside(decl, scope) && !inside(decl, member)) captured = true; @@ -6665,7 +6685,7 @@ export function collectInheritedJsFieldWrites( for (const base of lowerer.checker.getBaseTypes(target)) { for (const [name, expressions] of writes) { const property = lowerer.checker.getPropertyOfType(base, name); - for (const declaration of property ? lowerer.checker.declarationsOf(property) : []) { + for (const declaration of property ? property.declarations : []) { if (ts.isMethodDeclaration(declaration) || ts.isAccessor(declaration)) continue; let owner: ts.Node | undefined = declaration.parent; while (owner && !ts.isClassDeclaration(owner) && !ts.isClassExpression(owner)) @@ -6724,7 +6744,7 @@ export function collectVirtualJsMethods(lowerer: Lowerer, files: readonly ts.Sou for (const member of accessors) { if (!ts.isIdentifier(member.name)) continue; const property = lowerer.checker.getPropertyOfType(base, member.name.text); - for (const declaration of property ? lowerer.checker.declarationsOf(property) : []) { + for (const declaration of property ? property.declarations : []) { if ( (ts.isGetAccessorDeclaration(declaration) || ts.isSetAccessorDeclaration(declaration)) && @@ -6767,7 +6787,7 @@ export function collectVirtualJsMethods(lowerer: Lowerer, files: readonly ts.Sou for (const member of members) { if (!ts.isIdentifier(member.name)) continue; const property = lowerer.checker.getPropertyOfType(base, member.name.text); - for (const declaration of property ? lowerer.checker.declarationsOf(property) : []) { + for (const declaration of property ? property.declarations : []) { if ( ts.isMethodDeclaration(declaration) && isJsSourceFile(declaration.getSourceFile()) && @@ -6877,7 +6897,7 @@ export function exactInstanceClassOf(lowerer: Lowerer, expr: ts.Expression): Cla if (direct) return direct; if (!ts.isIdentifier(e)) return null; const symbol = lowerer.resolveValueSymbol(e); - const decl = symbol ? lowerer.checker.valueDeclarationOf(symbol) : undefined; + const decl = symbol ? symbol.valueDeclaration : undefined; if ( !decl || !ts.isVariableDeclaration(decl) || @@ -6988,19 +7008,43 @@ export function lowerClassGenericMethodCall( } : upcastTo(lowerer, receiver, declarer.def.name); const args = lowerer.completeArgs(call.arguments.nodes, instance.params, loc, call); - return { + const result: IrExpr = { kind: "call", callee: instance.name, args: [thisArg, ...args], type: instance.returnType, loc, }; + // A fluent JavaScript method (every return ends in `this`, jose's claim + // setters) answers the receiver: the instance returns the declarer's + // class, a used result's checker type is the receiver's subclass. + if ( + result.type.kind === "object" && + !ts.isExpressionStatement(call.parent) && + ts.isMethodDeclaration(info.decl) && + isJsSourceFile(info.decl.getSourceFile()) && + returnsOnlyThisValue(info.decl) + ) { + return lowerer.maybeNarrow(result, call); + } + return result; } /** Wraps a derived-class expression in an upcast when the target base * class differs (a no-op reinterpret at runtime; keeps IR types exact). */ export function upcastTo(lowerer: Lowerer, expr: IrExpr, className: string): IrExpr { if (expr.type.kind === "object" && expr.type.className !== className) { + // A value lowered at an ANCESTOR of the class the site needs (a + // fluent `this` result whose lowering kept the method's declaring + // class, the receiver of the next call in a chain): the checker + // vouches for the subclass, so the bridge is the verified downcast, + // never an upcast the validator rejects (an internal error before). + if ( + !lowerer.isSubclassOf(expr.type.className, className) && + lowerer.isSubclassOf(className, expr.type.className) + ) { + return { kind: "downcast", value: expr, type: { kind: "object", className }, loc: expr.loc }; + } return { kind: "upcast", value: expr, type: { kind: "object", className }, loc: expr.loc }; } return expr; @@ -8303,7 +8347,7 @@ export function superCallStmt( )[0]!; } lowerer.noteSuperConstructorEdge(`%${base.def.name}.constructor`); - return { + const call: IrStmt = { kind: "exprStmt", expr: { kind: "call", @@ -8314,6 +8358,14 @@ export function superCallStmt( }, loc, }; + return superIntoResultSlot( + lowerer, + base, + info, + call, + lowerer.upcastTo({ ...thisRef }, base.def.name), + info.ctor ?? info.decl!, + ); } /** `super.method(args)`: the base chain's implementation, called @@ -8711,21 +8763,21 @@ function lowerProgramClassNew( : lowerer.completeArgs(expr.arguments?.nodes ?? [], info.ctorParams, loc, expr); for (let current: ClassInfo | null = info; current; current = current.base) { if (current.def.baseValueGlobal) - return { + return constructedValue(lowerer, info, { kind: "newValue", callee: classValueRef(lowerer, info, expr), args, type: { kind: "object", className: info.def.name }, loc, - }; + }); } - return { + return constructedValue(lowerer, info, { kind: "new", className: info.def.name, args, type: { kind: "object", className: info.def.name }, loc, - }; + }); } function assignedThisFieldType(lowerer: Lowerer, expr: ts.NewExpression): IrType | null { @@ -8767,7 +8819,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { let intlConstructor = expr.expression; if (ts.isIdentifier(intlConstructor)) { const symbol = lowerer.resolveValueSymbol(intlConstructor); - const declaration = symbol ? lowerer.checker.valueDeclarationOf(symbol) : undefined; + const declaration = symbol ? symbol.valueDeclaration : undefined; if (declaration && ts.isVariableDeclaration(declaration) && declaration.initializer) intlConstructor = declaration.initializer; } @@ -8791,9 +8843,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { const symbol = lowerer.resolveValueSymbol(expr.expression); const declaration = symbol && - lowerer.checker - .declarationsOf(symbol) - .find((node) => ts.isFunctionDeclaration(node) && node.body !== undefined); + symbol.declarations.find((node) => ts.isFunctionDeclaration(node) && node.body !== undefined); if (declaration && ts.isFunctionDeclaration(declaration) && declaration.body) { const last = declaration.body.statements.nodes.at(-1); if ( @@ -9026,13 +9076,18 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { loc, expr, ); - return { - kind: "newValue", - callee, - args, - type: { kind: "object", className: info.def.name }, - loc, - }; + return constructedFromValue( + lowerer, + info, + { + kind: "newValue", + callee, + args, + type: { kind: "object", className: info.def.name }, + loc, + }, + expr, + ); } } } @@ -9659,7 +9714,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { const a0 = args[0]!; if (ts.isIdentifier(a0) && a0.text === "setImmediate") { const sym = lowerer.checker.getSymbolAtLocation(a0); - const decls = sym ? lowerer.checker.declarationsOf(sym) : []; + const decls = sym ? sym.declarations : []; if (decls.length > 0 && decls.every((d) => lowerer.isStdlibFile(d.getSourceFile()))) { // The settled value is the undefined dyn value — the result // is promise whatever T the checker inferred for the @@ -9955,13 +10010,18 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { loc, expr, ); - const built: IrExpr = { - kind: "newValue", - callee, - args, - type: { kind: "object", className: info.def.name }, - loc, - }; + const built = constructedFromValue( + lowerer, + info, + { + kind: "newValue", + callee, + args, + type: { kind: "object", className: info.def.name }, + loc, + }, + expr, + ); // A runtime class slot widened from a union (`[Readable, Writable]`'s // elements hold EventEmitter values): the checker types the result as // the union of the arms' instances, which the checked class view @@ -10028,7 +10088,15 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { ((callee.type.kind === "func" || callee.type.kind === "classval") && lowerer.dynConvertible(callee.type))) ) { - return lowerCheckedConstructor(lowerer, expr, lowerer.coerceToExpected(callee, DYN)); + let boxed = lowerer.coerceToExpected(callee, DYN); + // A plain function's box carries its name: the construction's + // prototype records it as `constructor`, which util.inspect prints + // (`F { a: 2 }`). + if (boxed.kind === "dynFrom" && boxed.fnName === undefined && callee.type.kind === "func") { + const name = jsFuncNameOf(expr.expression, lowerer); + if (name !== null) boxed = { ...boxed, fnName: name }; + } + return lowerCheckedConstructor(lowerer, expr, boxed); } lowerer.unsupported( "SC1090", diff --git a/packages/compiler/src/frontend/lowering/lower-comptime.ts b/packages/compiler/src/frontend/lowering/lower-comptime.ts index f811d13b6..41c6704c5 100644 --- a/packages/compiler/src/frontend/lowering/lower-comptime.ts +++ b/packages/compiler/src/frontend/lowering/lower-comptime.ts @@ -162,7 +162,7 @@ export function rejectComptimeCaptures( const symbol = ts.isShorthandPropertyAssignment(parent) ? lowerer.checker.getShorthandAssignmentValueSymbol(parent) : lowerer.checker.getSymbolAtLocation(node); - const decls = symbol ? lowerer.checker.declarationsOf(symbol) : undefined; + const decls = symbol ? symbol.declarations : undefined; if (symbol && decls && decls.length > 0) { const ownLocal = decls.every( (d) => d.getSourceFile() === sf && d.pos >= cb.pos && d.end <= cb.end, diff --git a/packages/compiler/src/frontend/lowering/lower-enums.ts b/packages/compiler/src/frontend/lowering/lower-enums.ts index 03e22e684..159d72f32 100644 --- a/packages/compiler/src/frontend/lowering/lower-enums.ts +++ b/packages/compiler/src/frontend/lowering/lower-enums.ts @@ -73,7 +73,7 @@ export function lowerEnumAccess( if (ts.isPropertyAccessExpression(expr)) { const sym = lowerer.checker.getSymbolAtLocation(expr.name); if (sym && sym.flags & ts.SymbolFlags.EnumMember) { - const decl = lowerer.checker.valueDeclarationOf(sym); + const decl = sym.valueDeclaration; if (decl && ts.isEnumMember(decl) && effectFreeReceiver(expr.expression)) { return lowerEnumMemberRead(lowerer, expr, decl); } @@ -86,9 +86,9 @@ export function lowerEnumAccess( if (!ts.isIdentifier(recv)) return null; const recvSym = lowerer.resolveValueSymbol(recv); if (!recvSym || !(recvSym.flags & ts.SymbolFlags.Enum)) return null; - const decls = lowerer.checker - .declarationsOf(recvSym) - .filter((d): d is ts.EnumDeclaration => ts.isEnumDeclaration(d)); + const decls = recvSym.declarations.filter((d): d is ts.EnumDeclaration => + ts.isEnumDeclaration(d), + ); if (decls.length === 0) return null; let key = expr.argumentExpression; while (ts.isParenthesizedExpression(key)) key = key.expression; @@ -222,7 +222,7 @@ function foldConstIndex(lowerer: Lowerer, e: ts.Expression): number | null { if (ts.isPropertyAccessExpression(x)) { const sym = lowerer.checker.getSymbolAtLocation(x.name); if (sym && sym.flags & ts.SymbolFlags.EnumMember) { - const decl = lowerer.checker.valueDeclarationOf(sym); + const decl = sym.valueDeclaration; if (decl && ts.isEnumMember(decl)) { const v = lowerer.checker.getConstantValue(decl); if (typeof v === "number") return v; diff --git a/packages/compiler/src/frontend/lowering/lower-event-emitter.ts b/packages/compiler/src/frontend/lowering/lower-event-emitter.ts index b8297fe91..06e9c4ab1 100644 --- a/packages/compiler/src/frontend/lowering/lower-event-emitter.ts +++ b/packages/compiler/src/frontend/lowering/lower-event-emitter.ts @@ -420,7 +420,7 @@ function isErrorMonitorRef(lowerer: Lowerer, arg: ts.Expression): boolean { // binding's type in JavaScript; its source property still names it. if (!ts.isIdentifier(arg)) return false; const symbol = lowerer.resolveValueSymbol(arg); - const declaration = symbol ? lowerer.checker.valueDeclarationOf(symbol) : undefined; + const declaration = symbol ? symbol.valueDeclaration : undefined; if (!declaration || !ts.isBindingElement(declaration)) return false; const pattern = declaration.parent; const variable = pattern?.parent; @@ -475,7 +475,7 @@ function staticStringPrefixOf( const symbol = lowerer.resolveValueSymbol(expression); if (!symbol || seen.has(symbol)) return null; seen.add(symbol); - const declaration = lowerer.checker.valueDeclarationOf(symbol); + const declaration = symbol.valueDeclaration; if ( declaration && ts.isVariableDeclaration(declaration) && @@ -509,7 +509,7 @@ function staticStringSuffixOf( const symbol = lowerer.resolveValueSymbol(expression); if (!symbol || seen.has(symbol)) return null; seen.add(symbol); - const declaration = lowerer.checker.valueDeclarationOf(symbol); + const declaration = symbol.valueDeclaration; if ( declaration && ts.isVariableDeclaration(declaration) && diff --git a/packages/compiler/src/frontend/lowering/lower-expando.ts b/packages/compiler/src/frontend/lowering/lower-expando.ts index 5e4c43021..5ed97d4d7 100644 --- a/packages/compiler/src/frontend/lowering/lower-expando.ts +++ b/packages/compiler/src/frontend/lowering/lower-expando.ts @@ -97,7 +97,7 @@ function expandoFnSymbolOf(lowerer: Lowerer, recv: ts.Expression): ts.Symbol | n // existing member paths stay put. const sym = lowerer.resolveValueSymbol(recv); if (!sym) return null; - const decl = lowerer.checker.valueDeclarationOf(sym); + const decl = sym.valueDeclaration; if (!decl || decl.getSourceFile().isDeclarationFile) return null; if (isJsSourceFile(decl.getSourceFile())) return null; if (ts.isFunctionDeclaration(decl)) { @@ -202,7 +202,7 @@ export function collectExpandoMembers(lowerer: Lowerer, sf: ts.SourceFile): void const memberName = typeof w.key === "string" ? w.key - : `sym%${w.key.name}%${lowerer.checker.declarationsOf(w.key)[0]?.getStart() ?? 0}`; + : `sym%${w.key.name}%${w.key.declarations[0]?.getStart() ?? 0}`; const g: IrGlobal = { id: `%g.${tag}${fnName}%.${memberName}`, name: `${fnName}.${memberName}`, @@ -239,7 +239,7 @@ function nsOwnedMember( const nameNode = ts.isPropertyAccessExpression(access) ? access.name : access.argumentExpression; const sym = lowerer.checker.getSymbolAtLocation(nameNode); if (!sym) return false; - for (const d of lowerer.checker.declarationsOf(sym)) { + for (const d of sym.declarations) { for ( let p: ts.Node | undefined = d.parent; p !== undefined && !ts.isSourceFile(p); diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index afcecb2c5..5881a9b39 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -6,13 +6,14 @@ import { lowerEventObjectProperty, lowerEventObjectPropertyWrite } from "./lower import { lowerDisposableStackInstanceOf } from "./builtins/disposable-stack.js"; import { lowerEventStaticProperty } from "./builtins/numbers.js"; import { lowerUnionEquality, tagEqualityMayMissAlias } from "./strict-equality.js"; -import { isOptionalProcessStreamProperty } from "./builtins/process.js"; +import { isOptionalProcessStreamProperty, NODE_COMPAT_VERSION } from "./builtins/process.js"; import { lowerWorkerMetadata } from "./builtins/workers.js"; import { esmImportBindingKind } from "./builtins/module-bindings.js"; import { lowerReadlineProperty } from "./builtins/readline.js"; import { lowerBlobInstanceof } from "./builtins/blob.js"; import { lowerWebAssemblyTypeof, refuseWebAssemblyUse } from "./builtins/webassembly.js"; import { lowerKeyObjectInstanceof } from "./builtins/key-object.js"; +import { lowerCryptoKeyInstanceof } from "./builtins/webcrypto.js"; import { lowerNetAddressInstanceof, lowerNetAddressStaticRead } from "./builtins/net-address.js"; import { isExecExceptionType, refuseExecExceptionCode } from "./builtins/errors.js"; import { STATS_TYPE_TESTS } from "./builtins/filesystem.js"; @@ -33,6 +34,7 @@ import { literalUnionArm } from "../union-discriminants.js"; * (FieldTarget). */ import { isFsStreamMember, lowerFsStreamProperty } from "./builtins/fs-streams.js"; import * as ts from "../ts7/adapter.js"; +import { builtinFunctionFenceValue } from "./builtin-function-fences.js"; import { isZlibStreamMember, lowerZlibInstanceOf, @@ -68,7 +70,12 @@ import { } from "./maybe-promise.js"; import { checkedClassAssertion, checkedClassUnionAssertion } from "./class-assertions.js"; import { checkedArmExtraction } from "./coercions/unions.js"; -import { narrowClassUnion, narrowStoredClassValue } from "./class-unions.js"; +import { + narrowClassUnion, + narrowStoredClassValue, + structuralNarrowing, + structuralReadType, +} from "./class-unions.js"; import { lowerUnionFieldRead, lowerUnionFieldWrite } from "./expressions/union-fields.js"; import { captureContextArguments, declareContextLocal } from "./function-context.js"; import { OBJECT_CALLABLE_VALUES } from "./surfaces.js"; @@ -149,6 +156,7 @@ import { unsupportedDiag, } from "../../diagnostics/diagnostic.js"; import { PoisonError, dynFallbackType, jsFuncNameOf, neverTaintedJsType, own } from "./lowerer.js"; +import { assertsGlobalObject, extractionCopiesReference } from "./assertion-identity.js"; import { lowerCollectionSpread } from "./containers/collection-methods.js"; import { lowerHelperReturnCall } from "./containers/iterator-helpers.js"; import { @@ -216,6 +224,7 @@ import { errorPropertyRead, errorPropertyWrite, errorToStringCall } from "./erro import { lowerStreamProperty, lowerStreamStateProperty, streamSidesOf } from "./lower-stream.js"; import { unionWideningTags } from "../../ir/analysis.js"; import { isSafeToDiscard, isSafeToRepeat } from "./expressions/evaluation-safety.js"; +import { isNavigatorGlobal, lowerNavigatorMember, refuseNavigator } from "./builtins/navigator.js"; import { globalSymbolKey } from "./expressions/global-symbols.js"; import { lowerShortCircuitAssignment } from "./expressions/nullish-assignment.js"; import { @@ -346,23 +355,41 @@ export function abstractPropertyDeclOf( ): boolean { const sym = lowerer.checker.getSymbolAtLocation(expr.name); if (!sym) return false; - return lowerer.checker - .declarationsOf(sym) - .some( - (d) => - ts.isPropertyDeclaration(d) && - ts.getModifiers(d)?.some((m) => m.kind === ts.SyntaxKind.AbstractKeyword) === true, - ); + return sym.declarations.some( + (d) => + ts.isPropertyDeclaration(d) && + ts.getModifiers(d)?.some((m) => m.kind === ts.SyntaxKind.AbstractKeyword) === true, + ); } /** A field declared by node:util's parseArgs checked-dynamic type family. * Narrowing a ParseArgsToken exposes one anonymous union arm, so its alias * identity no longer reaches mapType; declaration ancestry is the stable * provenance check. */ +/** The Uint8Array base64/hex prototype methods (ES2026) Node 24 lacks. */ +const UINT8ARRAY_BASE64_METHODS: ReadonlySet = new Set([ + "toBase64", + "toHex", + "setFromBase64", + "setFromHex", +]); + +/** `Uint8Array.prototype.` read off the global. */ +function isUint8ArrayBase64Probe(lowerer: Lowerer, expr: ts.PropertyAccessExpression): boolean { + if (!UINT8ARRAY_BASE64_METHODS.has(expr.name.text)) return false; + const proto = expr.expression; + return ( + ts.isPropertyAccessExpression(proto) && + !proto.questionDotToken && + proto.name.text === "prototype" && + stdlibGlobalNameOf(lowerer, proto.expression) === "Uint8Array" + ); +} + function isParseArgsDynProperty(lowerer: Lowerer, expr: ts.PropertyAccessExpression): boolean { const sym = lowerer.checker.getSymbolAtLocation(expr.name); if (!sym) return false; - return lowerer.checker.declarationsOf(sym).some((d) => { + return sym.declarations.some((d) => { if (!lowerer.isStdlibFile(d.getSourceFile())) return false; let inParseArgsType = false; for (let node: ts.Node | undefined = d.parent; node; node = node.parent) { @@ -390,7 +417,7 @@ function isStatFsDynProperty(lowerer: Lowerer, expr: ts.PropertyAccessExpression const symbol = lowerer.checker.getSymbolAtLocation(expr.name); return ( !!symbol && - lowerer.checker.declarationsOf(symbol).some((declaration) => { + symbol.declarations.some((declaration) => { if (!lowerer.isStdlibFile(declaration.getSourceFile())) return false; const parent = declaration.parent; if (!ts.isInterfaceDeclaration(parent)) return false; @@ -647,6 +674,8 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { return { kind: "strLit", value: "object", type: STRING, loc }; if (lowerer.isStdlibGlobal(expr.expression, "fetch")) return { kind: "strLit", value: "function", type: STRING, loc }; + if (isNavigatorGlobal(lowerer, expr.expression)) + return { kind: "strLit", value: "object", type: STRING, loc }; const performance = lowerPerfHooksTypeof(lowerer, expr.expression); if (performance) return performance; if (isNodeModuleValue(lowerer, expr.expression)) { @@ -709,7 +738,7 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { lowerer.isStdlibMember(tested) ) { const sym = lowerer.checker.getSymbolAtLocation(tested.name); - const decls = sym ? lowerer.checker.declarationsOf(sym) : []; + const decls = sym ? sym.declarations : []; if ( decls.some(isBuiltinModuleValueDecl) && decls.every((decl) => builtinModuleExportParent(decl) !== undefined) @@ -723,6 +752,31 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { } } } + // `typeof globalThis.AbortSignal` / `typeof Headers`: Node 24's Web + // platform constructor globals are functions. JavaScript sources + // represent such a value as an identity token, and the fallback + // declarations type some (AbortSignal) without a construct signature, + // so neither the operand nor the type can answer; the read has no + // effects. + if (constructorGlobalTypeof(lowerer, tested) !== null) + return { kind: "strLit", value: "function", type: STRING, loc }; + // `typeof globalThis.AbortSignal.any` (ky's feature probe): a static + // member of a standard-library global, declared callable, whose value + // has no representation. The read has no effects and Node's answer + // for every such function is "function"; members that lower as values + // keep their own answer. + if (ts.isPropertyAccessExpression(tested) && !tested.questionDotToken) { + const owner = stdlibGlobalNameOf(lowerer, tested.expression); + if (owner !== null && lowerer.isStdlibMember(tested)) { + const t = lowerer.typeOf(tested); + if ( + (lowerer.checker.getCallSignatures(t).length > 0 || + lowerer.checker.getConstructSignatures(t).length > 0) && + tryLowerExpression(lowerer, tested) === null + ) + return { kind: "strLit", value: "function", type: STRING, loc }; + } + } if (ts.isIdentifier(tested)) { const sym = lowerer.checker.getSymbolAtLocation(tested); if (!sym) { @@ -973,7 +1027,7 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { recordTextCodecClass(lowerer.shapes.get(local.type.shapeId)!) !== null ) { const symbol = lowerer.resolveValueSymbol(expr); - const decl = symbol ? lowerer.checker.valueDeclarationOf(symbol) : undefined; + const decl = symbol ? symbol.valueDeclaration : undefined; if ( decl && ts.isVariableDeclaration(decl) && @@ -1316,6 +1370,8 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { if (bi.module === "os" && bi.member === "constants") { return { kind: "libCall", fn: "os.constants", args: [], type: DYN, loc }; } + if (bi.module === "fs" && bi.member === "constants") + return { kind: "libCall", fn: "fs.constants", args: [], type: DYN, loc }; // `import { O_RDONLY } from "constants"`: the member of the frozen // module object. if (bi.module === "constants") { @@ -1342,6 +1398,9 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { // the same identity-token story as stdlib globals above (the // harness adds worker_threads.Worker to its identity Set). if (isJsSourceFile(expr.getSourceFile())) { + // A function member is a function value that fences when called. + if (builtinModuleFnOf(lowerer, bi.module, bi.member)) + return builtinFunctionFenceValue(lowerer, bi, expr, loc); return { kind: "strLit", value: `[builtin ${bi.module}.${bi.member}]`, @@ -1555,7 +1614,7 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { // is above); only user-file declares do. { const sym = lowerer.resolveValueSymbol(expr); - const decl = sym ? lowerer.checker.declarationsOf(sym)[0] : undefined; + const decl = sym ? sym.declarations[0] : undefined; if ( decl !== undefined && ts.isVariableDeclaration(decl) && @@ -1667,6 +1726,14 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { const ns = lowerNsIdentifierValue(lowerer, expr); if (ns) return ns; } + if (isNavigatorGlobal(lowerer, expr)) { + refuseNavigator( + lowerer, + expr, + "the navigator object as a value", + lowerer.checker.getSymbolAtLocation(expr), + ); + } // Preflight guarantees no unresolved identifiers; anything else here // is a blocked declaration's binding (the SC2004 cascade) or a // binding form we don't model yet. @@ -1930,6 +1997,10 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { if (ts.isConditionalExpression(expr)) return lowerTernary(lowerer, expr); if (ts.isPropertyAccessExpression(expr)) { + if (!expr.questionDotToken) { + const navigator = lowerNavigatorMember(lowerer, expr, NODE_COMPAT_VERSION); + if (navigator) return navigator; + } // `require("./x").member` over a program module's export table: the // module's %init at this position, then the member read exactly like // a `const m = require("./x")` binding's (cjs-inline-require.ts). @@ -2128,6 +2199,10 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { ) { return dynUndefinedExpr(loc); } + // `Uint8Array.prototype.toBase64` and its siblings, the base64/hex + // feature probes libraries such as jose make: the methods are newer + // than Node 24, whose Uint8Array prototype answers undefined for them. + if (isUint8ArrayBase64Probe(lowerer, expr)) return dynUndefinedExpr(loc); if (expr.questionDotToken && expr.name.text === "electron") { const processProperty = lowerer.lowerProcessProperty(expr); if (processProperty) return processProperty; @@ -2618,13 +2693,11 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { (recvSym?.name === "IteratorResult" || recvSym?.name === "IteratorYieldResult" || recvSym?.name === "IteratorReturnResult") && - lowerer.checker - .declarationsOf(recvSym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isTypeAliasDeclaration(d)) && - lowerer.isStdlibFile(d.getSourceFile()), - ) + recvSym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isTypeAliasDeclaration(d)) && + lowerer.isStdlibFile(d.getSourceFile()), + ) ) { const recv = lowerer.lowerExpr(expr.expression); if (recv.type.kind === "dyn") @@ -3318,15 +3391,13 @@ export function maybeNarrow(lowerer: Lowerer, expr: IrExpr, node: ts.Node): IrEx const symbol = lowerer.checker.getSymbolAtLocation(node.name); if ( symbol && - lowerer.checker - .declarationsOf(symbol) - .some( - (decl) => - ts.isPropertyDeclaration(decl) && - !decl.initializer && - !decl.type && - isJsSourceFile(decl.getSourceFile()), - ) + symbol.declarations.some( + (decl) => + ts.isPropertyDeclaration(decl) && + !decl.initializer && + !decl.type && + isJsSourceFile(decl.getSourceFile()), + ) ) { // Inference describes later writes, not the initial undefined. // A use that requires the inferred type must validate the value. @@ -3394,48 +3465,44 @@ export function maybeNarrow(lowerer: Lowerer, expr: IrExpr, node: ts.Node): IrEx const symbol = lowerer.resolveValueSymbol(node); if ( symbol && - lowerer.checker - .declarationsOf(symbol) - .some( - (decl) => - ts.isParameter(decl) && - ts.isMethodDeclaration(decl.parent) && - lowerer.checkedVirtualJsMethods.has(decl.parent), - ) + symbol.declarations.some( + (decl) => + ts.isParameter(decl) && + ts.isMethodDeclaration(decl.parent) && + lowerer.checkedVirtualJsMethods.has(decl.parent), + ) ) return expr; if ( symbol && - lowerer.checker - .declarationsOf(symbol) - .some((decl) => ts.isParameter(decl) && ts.isConstructorDeclaration(decl.parent)) + symbol.declarations.some( + (decl) => ts.isParameter(decl) && ts.isConstructorDeclaration(decl.parent), + ) ) return expr; if ( symbol && - lowerer.checker - .declarationsOf(symbol) - .some((decl) => ts.isVariableDeclaration(decl) && jsBindingHasOpenWrites(lowerer, decl)) + symbol.declarations.some( + (decl) => ts.isVariableDeclaration(decl) && jsBindingHasOpenWrites(lowerer, decl), + ) ) return expr; if ( symbol && - lowerer.checker - .declarationsOf(symbol) - .some( - (decl) => - ts.isParameter(decl) && - decl.initializer && - !decl.type && - !/@(?:param|type)\b/.test( - decl.getSourceFile().text.slice(decl.parent?.pos ?? decl.pos, decl.getStart()), - ), - ) + symbol.declarations.some( + (decl) => + ts.isParameter(decl) && + decl.initializer && + !decl.type && + !/@(?:param|type)\b/.test( + decl.getSourceFile().text.slice(decl.parent?.pos ?? decl.pos, decl.getStart()), + ), + ) ) return expr; if ( symbol && - lowerer.checker.declarationsOf(symbol).some((decl) => { + symbol.declarations.some((decl) => { if (!ts.isParameter(decl) || decl.type) return false; const fn = decl.parent; if (!ts.isFunctionExpression(fn) && !ts.isArrowFunction(fn)) return false; @@ -3508,6 +3575,9 @@ export function maybeNarrow(lowerer: Lowerer, expr: IrExpr, node: ts.Node): IrEx narrowed.className !== expr.type.className && lowerer.isSubclassOf(narrowed.className, expr.type.className) ) { + const reduced = structuralNarrowing(lowerer, node, expr.type.className, narrowed); + const union = reduced ? narrowClassUnion(lowerer, expr, reduced) : null; + if (union) return union; if (lowerer.narrowing.untrusted(node, narrowed)) { const checked = checkedClassAssertion(lowerer, expr, narrowed, expr.loc); if (checked) return checked; @@ -7061,6 +7131,10 @@ export function lowerElementAccess(lowerer: Lowerer, expr: ts.ElementAccessExpre } } } + if (key.type.kind === "f64") { + const safe = lowerOptionalArrayIndexRead(lowerer, expr, value, key); + if (safe) return safe; + } if (key.type.kind === "string" || key.type.kind === "f64") { const lit = ts.isStringLiteral(expr.argumentExpression) ? expr.argumentExpression.text @@ -8022,6 +8096,22 @@ export function lowerAbsenceProbe(lowerer: Lowerer, node: ts.Expression): IrExpr } return null; } + // `m[2]` on a JavaScript `RegExpExecArray | null` that a previous + // statement ruled non-null without narrowing it (jose's secs()). + if ( + receiverT?.kind === "union" && + isJsSourceFile(expr.getSourceFile()) && + !expr.questionDotToken && + !hasOptionalChainGuard(expr.expression) + ) { + const arms = lowerer.unions.get(receiverT.unionId)?.arms ?? []; + const present = arms.filter((arm) => !isUnitType(arm)); + if (present.length !== 1 || present[0]!.kind !== "array") return null; + const value = lowerer.lowerExpr(expr.expression); + const index = lowerer.lowerExpr(expr.argumentExpression); + if (index.type.kind !== "f64") return null; + return lowerOptionalArrayIndexRead(lowerer, expr, value, index); + } if (receiverT?.kind !== "array") return null; const arr = lowerer.lowerExpr(expr.expression); if (arr.type.kind !== "array") return null; @@ -9025,6 +9115,76 @@ export function lowerEnvironmentKey(lowerer: Lowerer, node: ts.Expression): IrEx return lowerRecordPropertyKey(lowerer, key, node); } +/** `typeof` of a Node 24 Web-platform constructor global or one of its + * static methods: "function", decided by name. The read has no effects. + * Static methods answer by name because after a `typeof + * globalThis.AbortSignal === "function" &&` guard the checker narrows a + * fallback-typed receiver to never, hiding the member's declaration. */ +function constructorGlobalTypeof(lowerer: Lowerer, tested: ts.Expression): string | null { + while (ts.isParenthesizedExpression(tested)) tested = tested.expression; + const globalName = stdlibGlobalNameOf(lowerer, tested); + if (globalName !== null && NODE_CONSTRUCTOR_GLOBALS.has(globalName)) return "function"; + if (!ts.isPropertyAccessExpression(tested) || tested.questionDotToken) return null; + const owner = stdlibGlobalNameOf(lowerer, tested.expression); + return owner !== null && + Object.hasOwn(NODE_CONSTRUCTOR_STATICS, owner) && + NODE_CONSTRUCTOR_STATICS[owner]!.includes(tested.name.text) + ? "function" + : null; +} + +/** `typeof globalThis.AbortSignal.any === "function"`: the comparison + * folds with constructorGlobalTypeof's answer. */ +function lowerConstructorGlobalTypeofTest( + lowerer: Lowerer, + expr: ts.BinaryExpression, + loc: SrcLoc, +): IrExpr | null { + const negated = expr.operatorToken.kind === ts.SyntaxKind.ExclamationEqualsEqualsToken; + for (const [a, b] of operandPairs(expr)) { + if (!ts.isTypeOfExpression(a) || !ts.isStringLiteral(b)) continue; + const answer = constructorGlobalTypeof(lowerer, a.expression); + if (answer !== null) + return { kind: "boolLit", value: (answer === b.text) !== negated, type: BOOL, loc }; + } + return null; +} + +/** Static methods of Node 24's Web-platform constructor globals. */ +const NODE_CONSTRUCTOR_STATICS: Readonly> = { + AbortSignal: ["abort", "any", "timeout"], + Response: ["error", "json", "redirect"], + URL: ["canParse", "createObjectURL", "parse", "revokeObjectURL"], +}; + +/** Node 24's Web-platform constructor globals: `typeof` answers + * "function" for each. */ +const NODE_CONSTRUCTOR_GLOBALS: ReadonlySet = new Set([ + "AbortController", + "AbortSignal", + "Blob", + "BroadcastChannel", + "CustomEvent", + "DOMException", + "Event", + "EventTarget", + "File", + "FormData", + "Headers", + "MessageChannel", + "MessageEvent", + "MessagePort", + "ReadableStream", + "Request", + "Response", + "TextDecoder", + "TextEncoder", + "TransformStream", + "URL", + "URLSearchParams", + "WritableStream", +]); + /** An operand of `+` with a string: ToString of ToPrimitive with the * default hint, which tries a class instance's valueOf before toString. */ function concatOperand(lowerer: Lowerer, e: IrExpr, node: ts.Node): IrExpr { @@ -9528,6 +9688,11 @@ export function lowerAsExpression( return inner; } } + // The global object: the checked extraction would build a separate + // value, so the assertion keeps the object and its member operations + // reach it (assertion-identity.ts). + if (extractionCopiesReference(lowerer, target) && assertsGlobalObject(lowerer, expr)) + return inner; if ( target.kind === "void" || !canDynCheckTo( @@ -10593,6 +10758,7 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr op === ts.SyntaxKind.ExclamationEqualsEqualsToken ) { const test = + lowerConstructorGlobalTypeofTest(lowerer, expr, loc) ?? lowerWebAssemblyTypeofTest(lowerer, expr, loc) ?? lowerErrorCodeTypeofTest(lowerer, expr, loc) ?? lowerer.lowerCaughtTypeofTest(expr, loc) ?? @@ -11729,6 +11895,25 @@ function lowerLogicalPair(lowerer: Lowerer, expr: ts.BinaryExpression, left: IrE } } } + // `&&` over an always-truthy plain left operand (an array, object or + // function value) whose result union has no arm for it: every JS + // object is truthy, so `list && list[0]` evaluates the left for its + // effects and answers the right. Coercing the left into that union + // would trap at run time where Node answers the right operand. + if ( + op === ts.SyntaxKind.AmpersandAmpersandToken && + target?.kind === "union" && + REF_TRUTHY_KINDS.has(left.type.kind) && + lowerer.armTag(target.unionId, left.type) < 0 + ) { + return { + kind: "seqExpr", + stmts: [{ kind: "exprStmt", expr: left, loc }], + result: lowerer.coerceInto(expr.right, right, target), + type: target, + loc, + }; + } if (target?.kind === "union") { // `&&` whose left does NOT fit the result union: the checker // built that result by DROPPING left arms which are always @@ -12239,7 +12424,7 @@ function aliasTypeofArmOf( const a = strip(a0); if (!ts.isIdentifier(a)) continue; const aliasSym = lowerer.resolveValueSymbol(a); - const aliasDecl = aliasSym ? lowerer.checker.valueDeclarationOf(aliasSym) : undefined; + const aliasDecl = aliasSym ? aliasSym.valueDeclaration : undefined; if ( !aliasSym || !aliasDecl || @@ -12253,7 +12438,7 @@ function aliasTypeofArmOf( if (!ts.isIdentifier(opnd)) continue; if (!bindingNeverReassigned(lowerer, aliasSym, aliasDecl)) continue; const valSym = lowerer.resolveValueSymbol(opnd); - const valDecl = valSym ? lowerer.checker.valueDeclarationOf(valSym) : undefined; + const valDecl = valSym ? valSym.valueDeclaration : undefined; if (!valSym || !valDecl) continue; // v1: identifier PARAMETERS only — initialized at function entry, // before any alias can capture their typeof. @@ -12535,9 +12720,7 @@ function lowerReplacedExportsRead( * rare enough to keep fenced). */ function cjsExportAccessorRead(lowerer: Lowerer, ident: ts.Identifier): IrExpr | null { const symbol = lowerer.resolveValueSymbol(ident); - const getter = symbol - ? lowerer.checker.declarationsOf(symbol).find(ts.isGetAccessorDeclaration) - : undefined; + const getter = symbol ? symbol.declarations.find(ts.isGetAccessorDeclaration) : undefined; if (!getter) return null; if (!ts.isObjectLiteralExpression(getter.parent) || !isCjsExportTableLiteral(getter.parent)) { return null; @@ -12924,6 +13107,8 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc if (blob) return blob; const keyObject = lowerKeyObjectInstanceof(lowerer, expr); if (keyObject) return keyObject; + const cryptoKey = lowerCryptoKeyInstanceof(lowerer, expr); + if (cryptoKey) return cryptoKey; const netAddress = lowerNetAddressInstanceof(lowerer, expr); if (netAddress) return netAddress; } @@ -14540,7 +14725,7 @@ function staticRegexPatternOf(lowerer: Lowerer, e: ts.Expression): string | null * destructured bindings, and unresolvable names. */ function constInitializerOf(lowerer: Lowerer, ident: ts.Identifier): ts.Expression | null { const sym = lowerer.resolveValueSymbol(ident); - const decl = sym ? lowerer.checker.valueDeclarationOf(sym) : undefined; + const decl = sym ? sym.valueDeclaration : undefined; if (decl === undefined || !ts.isVariableDeclaration(decl) || decl.initializer === undefined) return null; if (!ts.isVariableDeclarationList(decl.parent) || (decl.parent.flags & ts.NodeFlags.Const) === 0) @@ -14656,7 +14841,7 @@ function matchProducerRegexOf(lowerer: Lowerer, e: ts.Expression): ts.Expression // The for-of binding over a matchAll drain: `for (const m of // s.matchAll(re))` (or over a stored const rows). const sym = lowerer.resolveValueSymbol(expr); - const decl = sym ? lowerer.checker.valueDeclarationOf(sym) : undefined; + const decl = sym ? sym.valueDeclaration : undefined; if ( decl !== undefined && ts.isVariableDeclaration(decl) && @@ -15103,13 +15288,11 @@ export function lowerFieldRead(lowerer: Lowerer, expr: ts.PropertyAccessExpressi isGenericCallableMemberType(lowerer.checker.getTypeOfSymbol(propSym), lowerer.checker) && // CLASS members keep the class-path fences (a poisoned class's own // diagnostics, the bound-method fence above). - !lowerer.checker - .declarationsOf(propSym) - .some( - (d) => - d.parent !== undefined && - (ts.isClassDeclaration(d.parent) || ts.isClassExpression(d.parent)), - ) + !propSym.declarations.some( + (d) => + d.parent !== undefined && + (ts.isClassDeclaration(d.parent) || ts.isClassExpression(d.parent)), + ) ) { const found = objLitGenericFnNodeOf(lowerer, propSym); if (!found) { @@ -15199,7 +15382,9 @@ export function lowerUnionProperty( expr: ts.PropertyAccessExpression, ): IrExpr | null { if (lowerer.chainBlocked(expr)) return null; - const receiverIr = lowerer.mapTypeOf(lowerer.typeOf(expr.expression)); + const receiverIr = + structuralReadType(lowerer, expr.expression) ?? + lowerer.mapTypeOf(lowerer.typeOf(expr.expression)); if (receiverIr?.kind !== "union") return null; // Lower the receiver first: its runtime value can still carry the full // binding union even when the checker has removed several variants. @@ -15406,6 +15591,62 @@ export function lowerUnionProperty( ); } +/** A number-keyed read in JavaScript on an array that may be undefined or + * null outside an optional chain (`m[1]` on a `RegExpExecArray | null` + * that an earlier statement ruled non-null without narrowing it): the + * array arm is an ordinary JavaScript element read, so a hole, a present + * undefined (a regex group that did not participate) or an invalid index + * answers undefined, as lowerSafeIndexRead does for a plain array. The + * unit arms keep the keyed read's answer. The receiver and key evaluate + * once, in source order. Null when the union is not exactly one array arm + * beside unit arms; optional-chain tails and TypeScript keep the keyed + * read. */ +function lowerOptionalArrayIndexRead( + lowerer: Lowerer, + expr: ts.ElementAccessExpression, + value: IrExpr, + key: IrExpr, +): IrExpr | null { + if ( + !isJsSourceFile(expr.getSourceFile()) || + expr.questionDotToken || + hasOptionalChainGuard(expr.expression) + ) + return null; + if (value.type.kind !== "union") return null; + const unionId = value.type.unionId; + const arms = lowerer.unions.get(unionId)?.arms; + if (!arms) return null; + const present = arms.filter((arm) => !isUnitType(arm)); + const arm = present[0]; + if (present.length !== 1 || arm?.kind !== "array" || arm.elem.kind === "void") return null; + const tag = arms.indexOf(arm); + const loc = locOf(expr); + const stmts: IrStmt[] = []; + const stable = (e: IrExpr, name: string): IrExpr => { + if (e.kind === "varRef" || e.kind === "numLit") return e; + const local = lowerer.declareHiddenLocal(name, e.type); + stmts.push({ kind: "varDecl", localId: local.id, init: e, loc }); + return varRef(local.id, e.type, loc); + }; + if (lowerUnionKeyedRead(lowerer, expr, unionId, value, key, null) === null) return null; + const recv = stable(value, "%indexReceiver"); + const index = stable(key, "%indexKey"); + const keyed = lowerUnionKeyedRead(lowerer, expr, unionId, recv, index, null)!; + const narrowed: IrExpr = { kind: "unionNarrow", unionId, tag, value: recv, type: arm, loc }; + const read = lowerSafeIndexRead(lowerer, narrowed, index, loc); + if (read === null) return null; + const result: IrExpr = { + kind: "ternary", + cond: { kind: "unionIsTag", unionId, tag, value: recv, negated: false, type: BOOL, loc }, + then: lowerer.coerceToExpected(read, keyed.type), + else_: keyed, + type: keyed.type, + loc, + }; + return stmts.length === 0 ? result : { kind: "seqExpr", stmts, result, type: keyed.type, loc }; +} + /** The unionDisc generalization: a keyed read on a union receiver whose * arms answer DIFFERENT (but joinable) types. Each arm contributes its * declared answer — an array element's type (number keys), a declared field's type (literal keys), an @@ -15631,14 +15872,12 @@ function classFieldTarget( ); if ( property && - lowerer.checker - .declarationsOf(property) - .some( - (declaration) => - ts.isPropertyAccessExpression(declaration) || - ts.isBinaryExpression(declaration) || - ts.isElementAccessExpression(declaration), - ) + property.declarations.some( + (declaration) => + ts.isPropertyAccessExpression(declaration) || + ts.isBinaryExpression(declaration) || + ts.isElementAccessExpression(declaration), + ) ) { return { container: "dynamic", @@ -15842,7 +16081,7 @@ export function fieldTarget( // fences: JS finds the prototype function, never an overflow miss. const canonicalized = (): boolean => { if (!nameSym) return false; - const nameDecls = lowerer.checker.declarationsOf(nameSym); + const nameDecls = nameSym.declarations; if (!nameDecls.length) return false; if (!nameDecls.every((d) => ts.isPropertySignature(d))) return false; const declared = lowerer.mapTypeOf(lowerer.typeOf(access)); @@ -15915,7 +16154,7 @@ export function uniqueSymbolKeyOf( // which a runtime-identity symbol value can never satisfy. if (!(t.flags & (ts.TypeFlags.UniqueESSymbol | ts.TypeFlags.ESSymbol))) return null; const sym = lowerer.resolveValueSymbol(key); - const decl = sym ? lowerer.checker.valueDeclarationOf(sym) : undefined; + const decl = sym ? sym.valueDeclaration : undefined; if (!sym || !decl || !ts.isVariableDeclaration(decl)) return null; if (!(ts.getCombinedNodeFlags(decl) & ts.NodeFlags.Const)) return null; // Module level only: a unique-symbol const inside a FUNCTION is a @@ -16992,7 +17231,7 @@ function jsFunctionExpandoMember(lowerer: Lowerer, access: ts.PropertyAccessExpr const recvType = lowerer.typeOf(access.expression); if (lowerer.checker.getCallSignatures(recvType).length === 0) return false; const member = lowerer.checker.getPropertyOfType(recvType, access.name.text); - const decls = member ? lowerer.checker.declarationsOf(member) : []; + const decls = member ? member.declarations : []; return ( decls.length > 0 && decls.every((d) => { diff --git a/packages/compiler/src/frontend/lowering/lower-functions.ts b/packages/compiler/src/frontend/lowering/lower-functions.ts index 7cc448994..0ef601672 100644 --- a/packages/compiler/src/frontend/lowering/lower-functions.ts +++ b/packages/compiler/src/frontend/lowering/lower-functions.ts @@ -59,7 +59,7 @@ function declareCallReceiver( export function isTopLevelFnSymbol(lowerer: Lowerer, ident: ts.Identifier): boolean { const symbol = lowerer.resolveValueSymbol(ident); if (symbol && lowerer.staticCallables.get(symbol)?.kind === "declared-function") return true; - const decl = symbol ? lowerer.checker.declarationsOf(symbol)[0] : undefined; + const decl = symbol ? symbol.declarations[0] : undefined; return ( !!decl && ts.isFunctionDeclaration(decl) && diff --git a/packages/compiler/src/frontend/lowering/lower-generators.ts b/packages/compiler/src/frontend/lowering/lower-generators.ts index 6bf414f29..b1d0a1aa5 100644 --- a/packages/compiler/src/frontend/lowering/lower-generators.ts +++ b/packages/compiler/src/frontend/lowering/lower-generators.ts @@ -203,14 +203,12 @@ function isGeneratorMember(lowerer: Lowerer, access: ts.PropertyAccessExpression const member = lowerer.checker.getSymbolAtLocation(access.name); return ( member !== undefined && - lowerer.checker - .declarationsOf(member) - .some( - (d) => - ts.isInterfaceDeclaration(d.parent) && - (d.parent.name.text === "Generator" || d.parent.name.text === "AsyncGenerator") && - lowerer.isStdlibFile(d.getSourceFile()), - ) + member.declarations.some( + (d) => + ts.isInterfaceDeclaration(d.parent) && + (d.parent.name.text === "Generator" || d.parent.name.text === "AsyncGenerator") && + lowerer.isStdlibFile(d.getSourceFile()), + ) ); } @@ -268,28 +266,24 @@ export function isSettledIteratorValueRead( const member = lowerer.checker.getSymbolAtLocation(node.name); if ( !member || - !lowerer.checker - .declarationsOf(member) - .some( - (d) => - ts.isPropertySignature(d) && - ts.isInterfaceDeclaration(d.parent) && - (d.parent.name.text === "IteratorYieldResult" || - d.parent.name.text === "IteratorReturnResult") && - lowerer.isStdlibFile(d.getSourceFile()), - ) + !member.declarations.some( + (d) => + ts.isPropertySignature(d) && + ts.isInterfaceDeclaration(d.parent) && + (d.parent.name.text === "IteratorYieldResult" || + d.parent.name.text === "IteratorReturnResult") && + lowerer.isStdlibFile(d.getSourceFile()), + ) ) return false; const receiver = lowerer.typeOf(node.expression); const stdlib = (sym: ts.Symbol | undefined): boolean => sym !== undefined && - lowerer.checker - .declarationsOf(sym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isTypeAliasDeclaration(d)) && - lowerer.isStdlibFile(d.getSourceFile()), - ); + sym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isTypeAliasDeclaration(d)) && + lowerer.isStdlibFile(d.getSourceFile()), + ); const alias = receiver.getAliasSymbol(); const symbol = receiver.getSymbol(); if ( diff --git a/packages/compiler/src/frontend/lowering/lower-inspect.ts b/packages/compiler/src/frontend/lowering/lower-inspect.ts index be7f21c1c..245d1b676 100644 --- a/packages/compiler/src/frontend/lowering/lower-inspect.ts +++ b/packages/compiler/src/frontend/lowering/lower-inspect.ts @@ -10,6 +10,7 @@ import { DYN, F64, type IrExpr, + type IrFunction, type IrStmt, type IrType, RUNTIME_ERROR_CLASSES, @@ -992,6 +993,7 @@ function inspectHelper(lowerer: Lowerer, t: IrType, loc: SrcLoc): string { // Object.keys' declared order (SEMANTICS.md 36's stance). Retained // reachability can settle this shape after the helper is first // interned, so rebuild the field walk once metadata is final. + lowerer.keyOrder.observe(t); rebuildShapeOrderBody = (): void => { const current = lowerer.shapes.get(t.shapeId) ?? shape; const order = current.declaredOrder ?? current.fields.map((f) => f.name); @@ -1342,7 +1344,10 @@ function inspectHelper(lowerer: Lowerer, t: IrType, loc: SrcLoc): string { prependCycleGuard(); } - const fn = { + // Typed as the IR function itself: the finalizer below replaces the + // body of the very object liftedFns holds (natively, a differently typed + // literal would be copied into the list). + const fn: IrFunction = { name, params: [ { localId: "v.0", name: "v", type: t }, @@ -1768,7 +1773,7 @@ function directCallableInspect(lowerer: Lowerer, node: ts.Expression, loc: SrcLo const base = cls.base ? ` extends ${cls.base.def.name.replace(/^%/, "")}` : ""; return strLit(`[class ${cls.def.name}${base}]`, loc); } - const decl = lowerer.checker.declarationsOf(sym)[0]; + const decl = sym.declarations[0]; if (!decl) return null; if (ts.isFunctionDeclaration(decl) && decl.name) { return strLit(`[Function: ${decl.name.text}]`, loc); diff --git a/packages/compiler/src/frontend/lowering/lower-mixins.ts b/packages/compiler/src/frontend/lowering/lower-mixins.ts index 5677b2812..90789b4a0 100644 --- a/packages/compiler/src/frontend/lowering/lower-mixins.ts +++ b/packages/compiler/src/frontend/lowering/lower-mixins.ts @@ -223,7 +223,7 @@ export function mixinFnOfCallee(lowerer: Lowerer, callee: ts.Expression): MixinF let sym = lowerer.checker.getSymbolAtLocation(callee) ?? null; if (sym && sym.flags & ts.SymbolFlags.Alias) sym = lowerer.checker.getAliasedSymbol(sym); if (!sym) return null; - const decls = lowerer.checker.declarationsOf(sym); + const decls = sym.declarations; if (decls.length !== 1 || decls[0] === undefined) return null; // overloads / merged declarations const d = decls[0]; if (ts.isFunctionDeclaration(d)) return mixinFnShapeOf(lowerer, d); @@ -348,7 +348,7 @@ export function mixinResultBindingClassOf( sym: ts.Symbol | null | undefined, ): ClassInfo | null { if (!sym) return null; - const decls = lowerer.checker.declarationsOf(sym); + const decls = sym.declarations; if (decls.length !== 1 || decls[0] === undefined || !ts.isVariableDeclaration(decls[0])) return null; const decl = decls[0]; @@ -541,7 +541,7 @@ export function mixinIntersectionInstanceType(lowerer: Lowerer, widened: ts.Type const plainParts: string[] = []; for (const part of parts) { const sym = part.getSymbol(); - const decl = sym ? lowerer.checker.valueDeclarationOf(sym) : undefined; + const decl = sym ? sym.valueDeclaration : undefined; const node = decl && (ts.isClassDeclaration(decl) || ts.isClassExpression(decl)) && diff --git a/packages/compiler/src/frontend/lowering/lower-modules.ts b/packages/compiler/src/frontend/lowering/lower-modules.ts index 86fab82d4..ba33811f9 100644 --- a/packages/compiler/src/frontend/lowering/lower-modules.ts +++ b/packages/compiler/src/frontend/lowering/lower-modules.ts @@ -93,6 +93,7 @@ import { import { isPromisifyCall, registerBuiltinCallableAlias } from "./builtins/callable-aliases.js"; import { registerBuiltinClassAlias } from "./builtins/class-values.js"; import { stripTypeCasts } from "./builtins/arguments.js"; +import { isWebCryptoMemberType } from "./builtins/webcrypto.js"; import { bindingContextualGenericFnNodeOf, bindingGenericFnAliasInfoOf, @@ -113,6 +114,7 @@ import { isVarDeclared, numericIteratorSourceOf, provenanceElidedConstDecl, + stdlibBufferVarAlias, } from "./lower-stmts.js"; import { objectFactoryGlobalType } from "./object-factory-new.js"; import { streamClassAliasDecl } from "./lower-stream.js"; @@ -144,6 +146,7 @@ import { workerTargetPaths } from "../worker-target.js"; import { isNativeProxyInitializer } from "./expressions/native-proxy.js"; import { handleEventReaderPrelude } from "./lower-event-target.js"; import { cjsAccessorSnapshotStores, jsHoldsDynObject } from "./lower-exprs.js"; +import { keptIdentityAssertion } from "./assertion-identity.js"; /** One file's declarations, split for collection and init-body lowering. */ export interface FileParts { @@ -651,9 +654,7 @@ function collectDynamicImports(lowerer: Lowerer, sf: ts.SourceFile): void { const modSym = lowerer.checker.getSymbolAtLocation(node.arguments.nodes[0]); const ownModule = modSym !== undefined && - lowerer.checker - .declarationsOf(modSym) - .some((d) => ts.isSourceFile(d) && !d.isDeclarationFile); + modSym.declarations.some((d) => ts.isSourceFile(d) && !d.isDeclarationFile); const builtin = canonicalBuiltinModule(spec); const res = ownModule ? ({ kind: "program-module" } as const) @@ -691,7 +692,7 @@ export function collectJsonImports(lowerer: Lowerer, parts: FileParts[]): void { const nameSym = lowerer.checker.getSymbolAtLocation(clause.name); if (!nameSym || !(nameSym.flags & ts.SymbolFlags.Alias)) continue; const target = lowerer.checker.getAliasedSymbol(nameSym); - const jsonSf = lowerer.checker.declarationsOf(target)[0]?.getSourceFile(); + const jsonSf = target.declarations[0]?.getSourceFile(); if (!jsonSf || !jsonSf.fileName.endsWith(".json")) continue; try { const tsType = lowerer.typeOf(clause.name); @@ -1053,7 +1054,7 @@ function checkedGlobalReference(lowerer: Lowerer, expression: ts.Expression): bo ) return false; const root = lowerer.checker.getSymbolAtLocation(expression.expression); - const namespace = root && lowerer.checker.declarationsOf(root).some(ts.isNamespaceImport); + const namespace = root && root.declarations.some(ts.isNamespaceImport); return ( (namespace || lowerer.cjsLocalModuleBindingOf(expression.expression)) && lowerer.globalOf(expression.name)?.type.kind === "dyn" @@ -1261,7 +1262,7 @@ export function collectGlobals( if (!symbol) continue; if (symbol.flags & ts.SymbolFlags.Alias) { const target = lowerer.checker.getAliasedSymbol(symbol); - const vd = lowerer.checker.valueDeclarationOf(target); + const vd = target.valueDeclaration; const mutableVar = vd !== undefined && ts.isVariableDeclaration(vd) && @@ -1370,8 +1371,7 @@ export function collectGlobals( if (vG) { if (!vG.mutable) { lowerer.globalsBySymbol.set(symbol, vG); - for (const d of lowerer.checker.declarationsOf(symbol)) - lowerer.globalsByDeclNode.set(d, vG); + for (const d of symbol.declarations) lowerer.globalsByDeclNode.set(d, vG); return; } const g: IrGlobal = { @@ -1381,8 +1381,7 @@ export function collectGlobals( mutable: false, }; lowerer.globalsBySymbol.set(symbol, g); - for (const d of lowerer.checker.declarationsOf(symbol)) - lowerer.globalsByDeclNode.set(d, g); + for (const d of symbol.declarations) lowerer.globalsByDeclNode.set(d, g); lowerer.globalsList.push(g); return; } @@ -1392,8 +1391,7 @@ export function collectGlobals( lowerer.globalsBySymbol.set(symbol, g); // Importer aliases resolve to a DISTINCT late-bound symbol with // the same declaration — key the node too (globalOf's fallback). - for (const d of lowerer.checker.declarationsOf(symbol)) - lowerer.globalsByDeclNode.set(d, g); + for (const d of symbol.declarations) lowerer.globalsByDeclNode.set(d, g); lowerer.globalsList.push(g); } catch (e) { if (!(e instanceof PoisonError)) throw e; @@ -1536,7 +1534,7 @@ export function collectGlobals( ) continue; const target = lowerer.checker.getAliasedSymbol(aliasSym); - const vd = lowerer.checker.valueDeclarationOf(target); + const vd = target.valueDeclaration; const mutableVar = vd !== undefined && ts.isVariableDeclaration(vd) && @@ -1895,6 +1893,7 @@ export function collectGlobals( // same test). const stableStdlibAlias = isConst || + stdlibBufferVarAlias(lowerer, decl) || ((list.flags & ts.NodeFlags.Let) !== 0 && ts.isIdentifier(decl.name) && stdlibGlobalAliasNameOf(lowerer, decl.initializer) !== null && @@ -1919,7 +1918,8 @@ export function collectGlobals( decl.initializer && !decl.type && !hasJsTypeAnnotation(decl) && - !lowerer.mapTypeOf(lowerer.typeOf(nameNode)) + (!lowerer.mapTypeOf(lowerer.typeOf(nameNode)) || + isWebCryptoMemberType(lowerer, lowerer.typeOf(nameNode))) ) { const symbol = lowerer.checker.getSymbolAtLocation(nameNode); if (symbol && !lowerer.globalsBySymbol.has(symbol)) { @@ -1970,6 +1970,30 @@ export function collectGlobals( } continue; } + // A file-scope binding of an assertion of the global object + // (`const state = globalThis as { [KEY]?: State }`): the binding + // holds that object, so writes through it reach the object other + // modules read (assertion-identity.ts). + const asserted = decl.initializer + ? keptIdentityAssertion(lowerer, decl.initializer) + : undefined; + if (nameNode === decl.name && !decl.type && asserted) { + const symbol = lowerer.checker.getSymbolAtLocation(nameNode); + if (symbol && !lowerer.globalsBySymbol.has(symbol)) { + const g: IrGlobal = { + id: `%g.${tag}${nsPrefix}${nameNode.text}`, + name: nameNode.text, + type: DYN, + mutable: isLet, + source: bindingSource(nameNode), + ...(!isVarDeclared(decl) ? { tdz: true as const } : {}), + }; + lowerer.globalsBySymbol.set(symbol, g); + lowerer.globalsList.push(g); + if (isVarDeclared(decl)) noteVarGlobalEntryInit(lowerer, sf, g); + } + continue; + } // A JS file-scope evolving ARRAY (`const mustCallChecks = [];` // — test/common's exit-accounting ledger): the strict type // (any[]) has no mapping, but the VALUE is the dyn array the @@ -2248,8 +2272,7 @@ export function collectGlobals( ts.isCallExpression(decl.initializer) ? lowerer.checker.getResolvedSignature(decl.initializer) : undefined; - const callDeclaration = - callSignature && lowerer.checker.signatureDeclaration(callSignature); + const callDeclaration = callSignature && callSignature.declaration; const callType = callDeclaration && isJsSourceFile(callDeclaration.getSourceFile()) ? lowerer.mapTypeOf(lowerer.typeOf(nameNode)) @@ -2516,6 +2539,11 @@ export function collectGlobals( }; lowerer.globalsBySymbol.set(symbol, g); lowerer.globalsList.push(g); + // `let f: T;` initializes the binding (to undefined) where the + // declaration runs: later writes are ordinary stores, which the + // pointer guard alone would refuse (markCycleEarlyBindings). + if (g.tdz && !decl.initializer) + lowerer.uninitializedLexicalGlobals.set(g.id, { name: nameNode, module: sf, symbol }); // Mutable checked-dynamic LET globals ride the same entry // init: a closure called above the declaration statement // reads the dyn undefined instead of faulting on NULL — the @@ -2885,11 +2913,16 @@ export function lowerFileInit( // a function called above the declaration reads `undefined`, exactly // Node); checked-dynamic var globals hold the dyn undefined the same // way. After the guard: a cache-hit revisit must not reset them. + // A union slot the body retypes checked-dynamic (lowerVarDecl: a + // JavaScript var initialized with an existing checked-dynamic object) + // gets the dyn undefined once the body has lowered. + const unionEntryInits: { at: number; g: IrGlobal }[] = []; for (const g of lowerer.varGlobalEntryInits.get(sf) ?? []) { if (g.type.kind !== "union" && g.type.kind !== "dyn") continue; const wrapped = g.type.kind === "dyn" ? dynUndefinedExpr(loc0) : lowerer.unassignedSlotInit(g.type, loc0); if (wrapped) { + if (g.type.kind === "union") unionEntryInits.push({ at: header.length, g }); // A let global whose early reads are checked (initFlag) only // gets its storage filled here: this is not the source write, so // it must neither run the TDZ check nor set the flag. @@ -2984,6 +3017,16 @@ export function lowerFileInit( ); at++; } + for (const { at, g } of unionEntryInits) { + if (g.type.kind !== "dyn") continue; + body[at] = { + kind: "assign", + localId: g.id, + value: dynUndefinedExpr(loc0), + ...(g.initFlag !== undefined ? { initializes: true as const } : {}), + loc: loc0, + }; + } const moduleRef = cjsModuleRef(lowerer, sf); if (moduleRef !== null) { const guardedBody = body.splice(guardId !== undefined ? 2 : 0); diff --git a/packages/compiler/src/frontend/lowering/lower-namespaces.ts b/packages/compiler/src/frontend/lowering/lower-namespaces.ts index 7c1d70ed0..23b7d0f57 100644 --- a/packages/compiler/src/frontend/lowering/lower-namespaces.ts +++ b/packages/compiler/src/frontend/lowering/lower-namespaces.ts @@ -96,7 +96,7 @@ function nsSymbolRuntimeKind( if (!(sym.flags & (ts.SymbolFlags.ValueModule | ts.SymbolFlags.NamespaceModule))) return null; let sawTypeOnly = false; let sawNamespace = false; - for (const d of lowerer.checker.declarationsOf(sym)) { + for (const d of sym.declarations) { if (!ts.isModuleDeclaration(d)) continue; sawNamespace = true; let body = d.body; @@ -189,7 +189,7 @@ export function nsPathPrefix(node: ts.Node, modifierDecl?: ts.Node): string { * null. The walk stops at class/function boundaries: a static member of a * class INSIDE a namespace is the class's business, not the namespace's. */ function nsBlockKindOfSymbol(lowerer: Lowerer, sym: ts.Symbol): "flattened" | "typeOnly" | null { - for (const d of lowerer.checker.declarationsOf(sym)) { + for (const d of sym.declarations) { // TYPE declarations don't make a member a namespace VALUE: a class // static `C.B` whose name also carries `namespace C { export // interface B }` is the static's business — value references consult @@ -253,7 +253,7 @@ function moduleNsSourceFileOf(lowerer: Lowerer, e: ts.Expression): ts.SourceFile } if (!sym) return null; if (sym.flags & ts.SymbolFlags.Alias) sym = lowerer.checker.getAliasedSymbol(sym); - for (const d of lowerer.checker.declarationsOf(sym)) { + for (const d of sym.declarations) { if ( sym.flags & ts.SymbolFlags.ValueModule && ts.isSourceFile(d) && @@ -322,7 +322,7 @@ export function ambientNsRootOf(lowerer: Lowerer, e: ts.Expression): ts.Identifi if (!sym || !(sym.flags & (ts.SymbolFlags.ValueModule | ts.SymbolFlags.NamespaceModule))) return null; if (lowerer.isStdlibSymbol(sym)) return null; - const decls = lowerer.checker.declarationsOf(sym); + const decls = sym.declarations; if (decls.length === 0) return null; for (const d of decls) { // A namespace declaration, or the fundule/clazzdule merge partners @@ -427,7 +427,7 @@ export function ambientUndefVarRootOf(lowerer: Lowerer, e: ts.Expression): ts.Id if (lowerer.trapBindings.has(sym)) return root; if (lowerer.isStdlibSymbol(sym)) return null; if (ambientUndefinedFnSymbolOf(lowerer, root) !== null) return root; - const decls = lowerer.checker.declarationsOf(sym); + const decls = sym.declarations; if (decls.length === 0) return null; for (const d of decls) { if (!ts.isVariableDeclaration(d) || d.initializer !== undefined) return null; @@ -608,7 +608,7 @@ export function ambientUndefinedFnSymbolOf( if (!sym || !(sym.flags & ts.SymbolFlags.Function)) return null; if (lowerer.isStdlibSymbol(sym)) return null; let sawFn = false; - for (const d of lowerer.checker.declarationsOf(sym)) { + for (const d of sym.declarations) { if (ts.isInterfaceDeclaration(d) || ts.isTypeAliasDeclaration(d)) continue; // type-world merge partners if (ts.isFunctionDeclaration(d)) { if (d.body) return null; // an implementation exists — not ambient-undefined @@ -656,7 +656,7 @@ function initExecutingPosition(node: ts.Node): boolean { function earliestSameFileDeclStart(lowerer: Lowerer, sym: ts.Symbol, sf: ts.SourceFile): number { let earliest = Infinity; - for (const d of lowerer.checker.declarationsOf(sym)) { + for (const d of sym.declarations) { if (d.getSourceFile() === sf) earliest = Math.min(earliest, d.getStart()); } return earliest; @@ -669,7 +669,7 @@ function earliestSameFileDeclStart(lowerer: Lowerer, sym: ts.Symbol, sf: ts.Sour export function fenceEarlyAliasUse(lowerer: Lowerer, ident: ts.Identifier, refNode: ts.Node): void { const sym = lowerer.checker.getSymbolAtLocation(ident); if (!sym || !(sym.flags & ts.SymbolFlags.Alias)) return; - const aliasDecl = lowerer.checker.declarationsOf(sym).find(ts.isImportEqualsDeclaration); + const aliasDecl = sym.declarations.find(ts.isImportEqualsDeclaration); if (!aliasDecl || ts.isExternalModuleReference(aliasDecl.moduleReference)) return; if (!initExecutingPosition(refNode)) return; const sf = refNode.getSourceFile(); @@ -766,7 +766,7 @@ export function fenceCrossBlockNsRef( ) { if (lowerer.nsBlocks.has(q)) refBlocks.push(q); } - for (const d of lowerer.checker.declarationsOf(symbol)) { + for (const d of symbol.declarations) { for ( let q: ts.Node | undefined = d.parent; q !== undefined && !ts.isSourceFile(q); diff --git a/packages/compiler/src/frontend/lowering/lower-node-module.ts b/packages/compiler/src/frontend/lowering/lower-node-module.ts index 68f11b948..fee25b10f 100644 --- a/packages/compiler/src/frontend/lowering/lower-node-module.ts +++ b/packages/compiler/src/frontend/lowering/lower-node-module.ts @@ -42,7 +42,7 @@ function cjsGlobal(lowerer: Lowerer, node: ts.Expression, name: "module" | "requ if (!ts.isIdentifier(node) || node.text !== name) return false; if (lowerer.isStdlibGlobal(node, name)) return true; const symbol = lowerer.checker.getSymbolAtLocation(node); - const decls = symbol ? lowerer.checker.declarationsOf(symbol) : []; + const decls = symbol ? symbol.declarations : []; const sourceShadow = decls.some( (decl) => !decl.getSourceFile().isDeclarationFile && @@ -204,16 +204,14 @@ function nodeModuleType(lowerer: Lowerer, node: ts.Expression): boolean { if ( symbol && (symbol.name === "Module" || symbol.name === "ScriptcModule") && - lowerer.checker - .declarationsOf(symbol) - .some((decl) => lowerer.isStdlibFile(decl.getSourceFile())) + symbol.declarations.some((decl) => lowerer.isStdlibFile(decl.getSourceFile())) ) { return true; } const property = lowerer.checker.getPropertyOfType(type, "filename"); const declaredByModule = property && - lowerer.checker.declarationsOf(property).some((decl) => { + property.declarations.some((decl) => { for (let parent: ts.Node | undefined = decl.parent; parent; parent = parent.parent) { if ( (ts.isInterfaceDeclaration(parent) || ts.isClassDeclaration(parent)) && diff --git a/packages/compiler/src/frontend/lowering/lower-server.ts b/packages/compiler/src/frontend/lowering/lower-server.ts index aedd123c6..0e1438fc9 100644 --- a/packages/compiler/src/frontend/lowering/lower-server.ts +++ b/packages/compiler/src/frontend/lowering/lower-server.ts @@ -5051,7 +5051,7 @@ function clientFnRefOf( * `const` declaration, or a parameter/let whose enclosing scope contains * no assignment or ++/-- targeting it. */ function neverReassigned(lowerer: Lowerer, sym: ts.Symbol): boolean { - const decl = lowerer.checker.declarationsOf(sym)[0]; + const decl = sym.declarations[0]; if (!decl) return false; if ( ts.isVariableDeclaration(decl) && diff --git a/packages/compiler/src/frontend/lowering/lower-stdlib.ts b/packages/compiler/src/frontend/lowering/lower-stdlib.ts index 92d9f0690..2f0e1c66f 100644 --- a/packages/compiler/src/frontend/lowering/lower-stdlib.ts +++ b/packages/compiler/src/frontend/lowering/lower-stdlib.ts @@ -177,15 +177,13 @@ function requestInitConstBacked( if (!symbol || seen.has(symbol)) return false; if (requestInitPropMutatedSymbols(lowerer).has(symbol)) return false; seen.add(symbol); - const declarations = lowerer.checker - .declarationsOf(symbol) - .filter( - (declaration): declaration is ts.VariableDeclaration => - ts.isVariableDeclaration(declaration) && - declaration.initializer !== undefined && - ts.isVariableDeclarationList(declaration.parent) && - (declaration.parent.flags & ts.NodeFlags.Const) !== 0, - ); + const declarations = symbol.declarations.filter( + (declaration): declaration is ts.VariableDeclaration => + ts.isVariableDeclaration(declaration) && + declaration.initializer !== undefined && + ts.isVariableDeclarationList(declaration.parent) && + (declaration.parent.flags & ts.NodeFlags.Const) !== 0, + ); return ( declarations.length > 0 && declarations.every((declaration) => @@ -300,7 +298,7 @@ function visitRequestInitConstSelectedValues( seen.add(symbol); let sawDeclaration = false; let defines = true; - for (const declaration of lowerer.checker.declarationsOf(symbol)) { + for (const declaration of symbol.declarations) { if ( ts.isVariableDeclaration(declaration) && declaration.initializer !== undefined && @@ -413,7 +411,7 @@ function fenceRequestInitProperty( : lowerer.checker.getPropertyOfType(lowerer.typeOf(access.expression), key); if (!symbol || seen.has(symbol)) return; seen.add(symbol); - for (const declaration of lowerer.checker.declarationsOf(symbol)) { + for (const declaration of symbol.declarations) { if (ts.isPropertyAssignment(declaration)) { fenceRequestInitValueInner(lowerer, declaration.initializer, seen); } else if (ts.isShorthandPropertyAssignment(declaration) && ts.isIdentifier(declaration.name)) { @@ -533,7 +531,7 @@ function fenceRequestInitValueInner( if (!symbol || seen.has(symbol)) return; if (requestInitPropMutatedSymbols(lowerer).has(symbol)) return; seen.add(symbol); - for (const declaration of lowerer.checker.declarationsOf(symbol)) { + for (const declaration of symbol.declarations) { if (ts.isBindingElement(declaration)) { const source = requestInitConstBindingSource(lowerer, declaration); if (source !== null) { @@ -1618,9 +1616,9 @@ export function lowerStaticAbortControllerCall( const symbol = receiverTs.getAliasSymbol() ?? receiverTs.getSymbol(); if ( symbol?.name !== "AbortController" || - !lowerer.checker - .declarationsOf(symbol) - .some((d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(d.getSourceFile())) + !symbol.declarations.some( + (d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(d.getSourceFile()), + ) ) { return null; } @@ -1672,9 +1670,9 @@ export function lowerStaticReadableStreamControllerCall( const symbol = receiverTs.getAliasSymbol() ?? receiverTs.getSymbol(); if ( symbol?.name !== "ReadableStreamDefaultController" || - !lowerer.checker - .declarationsOf(symbol) - .some((d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(d.getSourceFile())) + !symbol.declarations.some( + (d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(d.getSourceFile()), + ) ) { return null; } @@ -1780,9 +1778,9 @@ export function lowerStaticAbortSignalListenerCall( const symbol = receiverTs.getAliasSymbol() ?? receiverTs.getSymbol(); if ( (symbol?.name !== "AbortSignal" && symbol?.name !== "EventTarget") || - !lowerer.checker - .declarationsOf(symbol) - .some((d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(d.getSourceFile())) + !symbol.declarations.some( + (d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(d.getSourceFile()), + ) ) { return null; } @@ -2127,9 +2125,9 @@ export function lowerStaticReadableStreamReaderCall( const symbol = receiverTs.getSymbol(); if ( symbol?.name !== "ReadableStreamDefaultReader" || - !lowerer.checker - .declarationsOf(symbol) - .some((d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(d.getSourceFile())) + !symbol.declarations.some( + (d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(d.getSourceFile()), + ) ) { return null; } @@ -2183,7 +2181,7 @@ export function lowerDynamicImportCall(lowerer: Lowerer, call: ts.CallExpression /** The checker-resolved program source behind one literal import(). */ function programImportTarget(lowerer: Lowerer, arg: ts.StringLiteralLike): ts.SourceFile | null { const modSym = lowerer.checker.getSymbolAtLocation(arg); - for (const d of modSym ? lowerer.checker.declarationsOf(modSym) : []) { + for (const d of modSym ? modSym.declarations : []) { if (ts.isSourceFile(d) && !d.isDeclarationFile) return d; } return null; @@ -2543,7 +2541,7 @@ export function npmMemberFence(lowerer: Lowerer, access: ts.PropertyAccessExpres * npmPackageOf, also asked directly for import-alias bindings (whose * aliased symbol is the package's export). */ export function npmPackageOfSymbol(lowerer: Lowerer, sym: ts.Symbol | undefined): string | null { - const decls = sym ? lowerer.checker.declarationsOf(sym) : undefined; + const decls = sym ? sym.declarations : undefined; if (!decls || decls.length === 0) return null; if (!decls.every((d) => lowerer.isNpmFile(d.getSourceFile()))) return null; return npmPackageNameOf(decls[0]!.getSourceFile().fileName); diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index 4a6fb15a3..f740d23e2 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -2,7 +2,7 @@ import { matchAllRowsReadByElements, storedMatchAllRowsReadByElements, } from "./containers/match-row-uses.js"; -import { dynUndefinedExpr, numLit, varRef } from "../../ir/build.js"; +import { dynUndefinedExpr, nodeThrowExpr, numLit, varRef } from "../../ir/build.js"; import { lowerCollectionSpread } from "./containers/collection-methods.js"; import { lowerEventObjectPropertyWrite } from "./lower-event-target.js"; import { lowerDynObjectLiteral } from "./expressions/object-literals.js"; @@ -19,6 +19,7 @@ import { processDataMemberValue } from "./builtins/process.js"; import { requireInitFlagStmt } from "./require-initialization.js"; import { isJsonRequireDecl } from "./json-require.js"; import { bindingSource } from "../binding-source.js"; +import { lowerConstructorReturn } from "./constructor-returns.js"; import type { Lowerer } from "./lowerer.js"; import { cjsExportValueByName, @@ -47,7 +48,7 @@ import { lowerYieldStarStatement, type GenType, } from "./lower-generators.js"; -import { lowerForAwaitBuiltin } from "./lower-async-iteration.js"; +import { classAsyncGeneratorIterator, lowerForAwaitBuiltin } from "./lower-async-iteration.js"; import { BOOL, BYTES_U8, @@ -88,6 +89,7 @@ import { isJsSourceFile, isNodeEsmFile, locOf, + requireEarlyReference, requireSpecOf, } from "../program.js"; import { @@ -99,6 +101,7 @@ import { sideEffectFreeOptionValue, stdlibGlobalAliasDecl, stdlibGlobalAliasNameOf, + bufferNamespaceMember, stdlibGlobalNameOf, } from "./surfaces.js"; import { isNativeBuiltinValueInitializer } from "./lower-builtin-values.js"; @@ -192,10 +195,12 @@ import { import { globalSymbolKey } from "./expressions/global-symbols.js"; import { lowerShortCircuitAssignment } from "./expressions/nullish-assignment.js"; import { + jsHoldsDynObject, lowerEnvironmentKey, lowerNativeFunctionAssignment, lowerStrictEquality, } from "./lower-exprs.js"; +import { keptIdentityAssertion } from "./assertion-identity.js"; import { isNativeProxyInitializer } from "./expressions/native-proxy.js"; import { tryLowerExpression } from "./expressions/try-lower-expression.js"; import { lowerUnionFieldWrite } from "./expressions/union-fields.js"; @@ -347,8 +352,7 @@ function stdlibTypeNamed(lowerer: Lowerer, node: ts.Node, name: string): boolean if (t.flags & ts.TypeFlags.Union) return constituentTypes(t).some(visit); const sym = t.getAliasSymbol() ?? t.getSymbol(); return ( - sym?.name === name && - lowerer.checker.declarationsOf(sym).some((d) => lowerer.isStdlibFile(d.getSourceFile())) + sym?.name === name && sym.declarations.some((d) => lowerer.isStdlibFile(d.getSourceFile())) ); }; return visit(lowerer.typeOf(node)); @@ -911,7 +915,7 @@ const TDZ_KINDS = new Set([ * is empty. Mutable bindings also guard writes; only the declaration's * initializing assignment may leave the temporal dead zone. */ export function predeclareForwardCapture(lowerer: Lowerer, symbol: ts.Symbol): boolean { - const decl = lowerer.checker.valueDeclarationOf(symbol); + const decl = symbol.valueDeclaration; if ( !decl || !ts.isVariableDeclaration(decl) || @@ -991,7 +995,7 @@ export function predeclareForwardCapture(lowerer: Lowerer, symbol: ts.Symbol): b * lower the declaration under the owning statement list's context, then * resume the caller and thread its captures normally. */ export function predeclareForwardFnDecl(lowerer: Lowerer, symbol: ts.Symbol): boolean { - const decl = lowerer.checker.valueDeclarationOf(symbol); + const decl = symbol.valueDeclaration; if (!decl || !ts.isFunctionDeclaration(decl) || !decl.body || decl.typeParameters?.nodes) return false; for (let i = lowerer.activeStmtLists.length - 1; i >= 0; i--) { @@ -1068,13 +1072,124 @@ function hostVariableDeclarationOf(name: ts.Identifier): ts.VariableDeclaration export function forOfVarTarget( lowerer: Lowerer, decl: ts.VariableDeclaration, + valueType?: IrType, ): { id: string; type: IrType } | null { if (!ts.isIdentifier(decl.name) || !isVarDeclared(decl)) return null; const symbol = lowerer.checker.getSymbolAtLocation(decl.name); if (!symbol) return null; const g = lowerer.globalsBySymbol.get(symbol); - if (g) return g; - return hoistVarBinding(lowerer, symbol, decl.name); + if (g) { + if (valueType?.kind === "dyn" && jsBindingKeepsCheckedObject(lowerer, decl.name, g.type)) + g.type = DYN; + return g; + } + return hoistVarBinding(lowerer, symbol, decl.name, valueType); +} + +/** JavaScript: whether an unannotated variable binding whose static type + * is `type` keeps a checked-dynamic value unconverted. The checker types + * such a binding by the object it receives (an element of an evolving + * `[]`, a `find` result, a member of a checked-dynamic object), but the + * value is an existing checked-dynamic object or array: a record- or + * array-typed slot would hold a converted copy, so writes through the + * binding would miss the object other references see, and `===` against + * them would fail. Block-scoped bindings with an identifier name keep such + * values by the same rule in lowerVarDecl (preservesObjectIdentity). */ +function jsBindingKeepsCheckedObject( + lowerer: Lowerer, + nameNode: ts.Identifier, + type: IrType, +): boolean { + if (!isJsSourceFile(nameNode.getSourceFile())) return false; + const host = hostVariableDeclarationOf(nameNode); + if (!host || host.type || hasJsTypeAnnotation(host)) return false; + const arms = type.kind === "union" ? (lowerer.unions.get(type.unionId)?.arms ?? []) : [type]; + return ( + arms.some((arm) => arm.kind === "record" || arm.kind === "array") && + arms.every((arm) => arm.kind === "record" || arm.kind === "array" || isUnitType(arm)) + ); +} + +/** Whether `node` mentions the binding `symbol` anywhere, nested functions + * included. */ +function mentionsSymbol(lowerer: Lowerer, node: ts.Node, symbol: ts.Symbol): boolean { + if (ts.isIdentifier(node)) return lowerer.checker.getSymbolAtLocation(node) === symbol; + return ( + node.forEachChild((child) => (mentionsSymbol(lowerer, child, symbol) ? true : undefined)) === + true + ); +} + +/** Whether `node` evaluates to an existing value rather than constructing + * one whose layout a destination directs: not a literal, `new`, function + * or class expression, nor a conditional with such an arm (an arm that + * is `null` or `undefined` constructs nothing). */ +function existingValueForm(node: ts.Expression, armOfConditional = false): boolean { + while (ts.isParenthesizedExpression(node)) node = node.expression; + if (ts.isConditionalExpression(node)) + return existingValueForm(node.whenTrue, true) && existingValueForm(node.whenFalse, true); + if ( + armOfConditional && + (node.kind === ts.SyntaxKind.NullKeyword || + (ts.isIdentifier(node) && node.text === "undefined")) + ) + return true; + return !( + ts.isObjectLiteralExpression(node) || + ts.isArrayLiteralExpression(node) || + ts.isNewExpression(node) || + ts.isFunctionExpression(node) || + ts.isArrowFunction(node) || + ts.isClassExpression(node) || + ts.isLiteralExpression(node) || + node.kind === ts.SyntaxKind.NullKeyword + ); +} + +/** JavaScript `var x = e` whose binding could hold an existing + * checked-dynamic object (jsBindingKeepsCheckedObject): lowers `e` BEFORE + * minting the hoisted slot, as a block-scoped declaration does, so a + * checked-dynamic value gets a checked-dynamic slot instead of a converted + * copy. Other values coerce into the planned slot exactly as + * lowerExprExpecting would. Null when the declaration does not qualify: + * the slot already exists, the initializer is fresh construction whose + * layout the slot type directs (existingValueForm; a literal holding an + * existing checked-dynamic object qualifies), or it refers to the binding + * itself (hoist-before-init resolves that reference). */ +function lowerJsVarFromExisting( + lowerer: Lowerer, + decl: ts.VariableDeclaration, + symbol: ts.Symbol, +): IrStmt | null { + if (!ts.isIdentifier(decl.name) || !decl.initializer) return null; + if (!isJsSourceFile(decl.getSourceFile()) || lowerer.ctx.hoistedVars.has(symbol)) return null; + if (lowerer.bindingIn(lowerer.ctx, symbol)) return null; + let node: ts.Expression = decl.initializer; + while (ts.isParenthesizedExpression(node) || ts.isSatisfiesExpression(node)) + node = node.expression; + // A literal holding an existing checked-dynamic object builds as a + // checked-dynamic literal without a destination (lowerArrayLiteral, + // lowerObjectLiteral): a record- or array-typed slot would copy it. + const holdsChecked = + (ts.isArrayLiteralExpression(node) || ts.isObjectLiteralExpression(node)) && + jsHoldsDynObject(lowerer, node); + if (!holdsChecked && !existingValueForm(node)) return null; + const planned = varBindingType(lowerer, decl.name); + if (!planned || !jsBindingKeepsCheckedObject(lowerer, decl.name, planned)) return null; + if (mentionsSymbol(lowerer, node, symbol)) return null; + const value = lowerer.lowerExpr(node); + const local = hoistVarBinding( + lowerer, + symbol, + decl.name, + value.type.kind === "dyn" ? DYN : undefined, + ); + return { + kind: "assign", + localId: local.id, + value: lowerer.coerceExistingInto(node, value, local.type), + loc: locOf(decl), + }; } /** The `var` symbol's function-scoped binding TYPE — the declared type at @@ -1130,7 +1245,12 @@ function varBindingType(lowerer: Lowerer, nameNode: ts.Identifier): IrType | nul * a loop re-ASSIGNS the one binding per pass and never resets it — the * classic capture semantics fall out: closures made in a loop share the * single boxed binding, where `let` gets a fresh box per iteration. */ -function hoistVarBinding(lowerer: Lowerer, symbol: ts.Symbol, nameNode: ts.Identifier): IrLocal { +function hoistVarBinding( + lowerer: Lowerer, + symbol: ts.Symbol, + nameNode: ts.Identifier, + valueType?: IrType, +): IrLocal { const existing = lowerer.ctx.hoistedVars.get(symbol); if (existing) return existing; // The parameter merge: the symbol already binds a function-root local. @@ -1139,7 +1259,15 @@ function hoistVarBinding(lowerer: Lowerer, symbol: ts.Symbol, nameNode: ts.Ident lowerer.ctx.hoistedVars.set(symbol, bound); return bound; } - const type = varBindingType(lowerer, nameNode); + let type = varBindingType(lowerer, nameNode); + // The first value stored is an existing checked-dynamic object: keep it + // (jsBindingKeepsCheckedObject). A checked-dynamic slot holds undefined + // natively, so no runtime-optional widening applies. + const keepsChecked = + valueType?.kind === "dyn" && + type !== null && + jsBindingKeepsCheckedObject(lowerer, nameNode, type); + if (keepsChecked) type = DYN; if (!type) lowerer.badType(nameNode, lowerer.typeOf(nameNode)); const root = lowerer.activeStmtLists.find((e) => e.ctx === lowerer.ctx); if (!root) @@ -1156,7 +1284,7 @@ function hoistVarBinding(lowerer: Lowerer, symbol: ts.Symbol, nameNode: ts.Ident }; lowerer.ctx.locals.push(local); lowerer.ctx.scopes[0]!.set(symbol, local); - noteEarlyReadVar(lowerer, symbol, local); + if (!keepsChecked) noteEarlyReadVar(lowerer, symbol, local); // A checked-dynamic slot holds the dyn undefined (a NULL dyn is a // trap); everything else rides unassignedSlotInit (interned union arm, @@ -1185,7 +1313,7 @@ function hoistVarBinding(lowerer: Lowerer, symbol: ts.Symbol, nameNode: ts.Ident export function predeclareForwardVar(lowerer: Lowerer, symbol: ts.Symbol): boolean { // Module-scope vars already have their global storage. if (lowerer.globalsBySymbol.has(symbol)) return false; - const decl = lowerer.checker.valueDeclarationOf(symbol); + const decl = symbol.valueDeclaration; if (!decl || !ts.isVariableDeclaration(decl) || !isVarDeclared(decl)) return false; const nameNode = ts.isIdentifier(decl.name) ? decl.name : null; if (!nameNode) { @@ -1451,6 +1579,11 @@ export function lowerStmt(lowerer: Lowerer, stmt: ts.Statement): IrStmt | IrStmt // incompatible boundary keeps the ordinary per-statement refusal. let value: IrExpr | null = null; if (stmt.expression) { + // A constructor's returned object replaces the constructed `this`. + if (lowerer.currentClass?.ctor) { + const constructorReturn = lowerConstructorReturn(lowerer, stmt); + if (constructorReturn !== null) return constructorReturn; + } return lowerer.lowerReturnStmt(stmt.expression, locOf(stmt)); } else if (lowerer.ctx.returnType.kind === "union") { value = lowerer.wrappedUndefined(lowerer.ctx.returnType, locOf(stmt)); @@ -1926,11 +2059,7 @@ export function isParseArgsDynCheckerType(lowerer: Lowerer, type: ts.Type): bool return false; }; const sym = widened.getAliasSymbol() ?? widened.getSymbol(); - if ( - sym && - isParseArgsDynTypeName(sym.name) && - lowerer.checker.declarationsOf(sym).some(declarationBelongs) - ) { + if (sym && isParseArgsDynTypeName(sym.name) && sym.declarations.some(declarationBelongs)) { return true; } // A discriminant check erases the token alias and leaves one anonymous @@ -1938,7 +2067,44 @@ export function isParseArgsDynCheckerType(lowerer: Lowerer, type: ts.Type): bool // is the same stable signal the dotted-member bridge uses. return lowerer.checker .getPropertiesOfType(widened) - .some((p) => lowerer.checker.declarationsOf(p).some(declarationBelongs)); + .some((p) => p.declarations.some(declarationBelongs)); +} + +/** True when an `any` value comes from a JavaScript function's result: + * a call (possibly awaited) whose callee is declared in a JavaScript file, + * or a const bound to one. A TypeScript program importing a JavaScript + * package sees the package's inferred types (`jwtVerify` from jose + * answers `any`), and its object destructuring reads the members of that + * checked-dynamic value as JavaScript does — member reads on the same + * value already compile. Other `any` sources keep the fence. */ +function javaScriptAnyOrigin(lowerer: Lowerer, node: ts.Expression, depth: number): boolean { + let expr = node; + while (ts.isParenthesizedExpression(expr) || ts.isAwaitExpression(expr)) expr = expr.expression; + if (ts.isCallExpression(expr)) { + const callee = ts.isPropertyAccessExpression(expr.expression) + ? expr.expression.name + : expr.expression; + if (!ts.isIdentifier(callee)) return false; + const symbol = lowerer.resolveValueSymbol(callee); + const declarations = symbol ? symbol.declarations : []; + return ( + declarations.length > 0 && + declarations.every((d) => isJsSourceFile(d.getSourceFile()) && ts.isFunctionLike(d)) + ); + } + if (ts.isIdentifier(expr) && depth === 0) { + const symbol = lowerer.resolveValueSymbol(expr); + const declaration = symbol ? symbol.valueDeclaration : undefined; + return ( + declaration !== undefined && + ts.isVariableDeclaration(declaration) && + ts.isIdentifier(declaration.name) && + declaration.initializer !== undefined && + (ts.getCombinedNodeFlags(declaration) & ts.NodeFlags.Const) !== 0 && + javaScriptAnyOrigin(lowerer, declaration.initializer, depth + 1) + ); + } + return false; } /** Destructuring declarations, desugared as sugar over indexed/field @@ -1985,7 +2151,12 @@ export function lowerDestructuringDecl( lowerer.anyOrigin(decl.initializer) && !isJsSourceFile(decl.getSourceFile()) && init.type.kind !== "record" && - init.type.kind !== "array" + init.type.kind !== "array" && + !( + init.type.kind === "dyn" && + ts.isObjectBindingPattern(decl.name) && + javaScriptAnyOrigin(lowerer, decl.initializer, 0) + ) ) { lowerer.anyOpFence("destructuring", decl); } @@ -2907,6 +3078,10 @@ export function lowerBindingPattern( lowerMatchResultObjectPattern(lowerer, pattern, srcRef, srcType, isLet, out); return; } + if (srcType.kind === "array" && isArrayPositionPattern(lowerer, pattern)) { + lowerArrayPositionObjectPattern(lowerer, pattern, srcRef, srcType, isLet, out); + return; + } if (srcType.kind !== "record") { lowerer.unsupported( "SC1031", @@ -3237,19 +3412,8 @@ function lowerMatchResultObjectPattern( ); } let value: IrExpr; - if (/^(?:0|[1-9]\d{0,8})$/.test(propName)) { - const index = Number(propName); - const position: IrExpr = { kind: "numLit", value: index, type: F64, loc }; - value = lowerSafeIndexRead(lowerer, srcRef(), position, loc) ?? { - kind: "arrayGet", - arr: srcRef(), - index: position, - type: srcType.elem, - loc, - }; - if (el.initializer) { - value = arrayPositionDefault(lowerer, el, value, srcRef, index, srcType.elem, out); - } + if (ARRAY_POSITION_KEY.test(propName)) { + value = arrayPositionBinding(lowerer, el, srcRef, srcType, Number(propName), out); } else if (propName === "index" || propName === "input") { if (el.initializer) { lowerer.unsupported( @@ -3295,6 +3459,83 @@ function lowerMatchResultObjectPattern( } } +/** A canonical array index key (the numeric positions an object pattern + * reads off an array). */ +const ARRAY_POSITION_KEY = /^(?:0|[1-9]\d{0,8})$/; + +/** The positional read `{ : x }` takes off an array: the element, + * undefined past the end, with the binding default applied. */ +function arrayPositionBinding( + lowerer: Lowerer, + el: ts.BindingElement, + srcRef: () => IrExpr, + srcType: IrType & { kind: "array" }, + index: number, + out: IrStmt[], +): IrExpr { + const loc = locOf(el); + const position: IrExpr = { kind: "numLit", value: index, type: F64, loc }; + const value = lowerSafeIndexRead(lowerer, srcRef(), position, loc) ?? { + kind: "arrayGet", + arr: srcRef(), + index: position, + type: srcType.elem, + loc, + }; + return el.initializer + ? arrayPositionDefault(lowerer, el, value, srcRef, index, srcType.elem, out) + : value; +} + +/** Whether every element of an object pattern over an array names a + * numeric position or `length` (`const { 0: a, 1: b, length } = + * s.split(".")`), with no rest element. */ +function isArrayPositionPattern(lowerer: Lowerer, pattern: ts.ObjectBindingPattern): boolean { + return pattern.elements.nodes.every((el) => { + if (el.name === undefined) return true; + if (el.dotDotDotToken) return false; + const prop = el.propertyName ?? el.name; + if ( + !ts.isIdentifier(prop) && + !ts.isComputedPropertyName(prop) && + !ts.isStringLiteralLike(prop) && + !ts.isNumericLiteral(prop) + ) + return false; + const name = patternKeyNameOf(lowerer, prop as ts.PropertyName); + return name !== null && (name === "length" || ARRAY_POSITION_KEY.test(name)); + }); +} + +/** An object pattern of positions and `length` over an array: each + * position is the read the equivalent array pattern makes, and `length` + * the array's length (always present, so its default never runs). */ +function lowerArrayPositionObjectPattern( + lowerer: Lowerer, + pattern: ts.ObjectBindingPattern, + srcRef: () => IrExpr, + srcType: IrType & { kind: "array" }, + isLet: boolean, + out: IrStmt[], +): void { + for (const el of pattern.elements.nodes) { + if (el.name === undefined) continue; + const name = patternKeyNameOf(lowerer, (el.propertyName ?? el.name) as ts.PropertyName)!; + const value: IrExpr = + name === "length" + ? { + kind: "arrIntrinsic", + method: "length", + receiver: srcRef(), + args: [], + type: F64, + loc: locOf(el), + } + : arrayPositionBinding(lowerer, el, srcRef, srcType, Number(name), out); + lowerer.bindPatternTarget(el.name, value, isLet, out); + } +} + /** Decode one non-rest object-assignment property into its static source * field, destination expression, and optional default initializer. */ function destructuringPropertyOf( @@ -4015,7 +4256,7 @@ function objectRestValue( ), }; }); - return { kind: "recordLit", fields, type: restT, loc }; + return lowerer.keyOrder.copy({ kind: "recordLit", fields, type: restT, loc }, srcType.shapeId); } /** The fresh record for `{ a, ...rest }` over a CLASS-INSTANCE source: @@ -4198,6 +4439,8 @@ export function bindPatternTarget( const symbol = lowerer.checker.getSymbolAtLocation(name); const g = symbol ? lowerer.globalsBySymbol.get(symbol) : undefined; if (g) { + if (value.type.kind === "dyn" && jsBindingKeepsCheckedObject(lowerer, name, g.type)) + g.type = DYN; out.push({ kind: "assign", localId: g.id, @@ -4214,7 +4457,7 @@ export function bindPatternTarget( // here by declareParams) carry no block-scope flag either, but their // bindings are the parameters' own. if (symbol && hostVariableDeclarationOf(name) !== null && isVarDeclared(name)) { - const hoisted = hoistVarBinding(lowerer, symbol, name); + const hoisted = hoistVarBinding(lowerer, symbol, name, value.type); out.push({ kind: "assign", localId: hoisted.id, @@ -4229,7 +4472,7 @@ export function bindPatternTarget( value.type.kind === "dyn" && (() => { const mapped = lowerer.mapTypeOf(lowerer.typeOf(name)); - return mapped === null; + return mapped === null || jsBindingKeepsCheckedObject(lowerer, name, mapped); })(); let type = dynStays ? DYN @@ -4565,6 +4808,7 @@ export function lowerVarDecl( // alias plumbing (see stdlibGlobalAliasDecl), no storage, no code. const stableStdlibAlias = !isLet || + stdlibBufferVarAlias(lowerer, decl) || (ts.isVariableDeclarationList(decl.parent) && (decl.parent.flags & ts.NodeFlags.Let) !== 0 && ts.isIdentifier(decl.name) && @@ -4712,7 +4956,11 @@ export function lowerVarDecl( !decl.type && !hasJsTypeAnnotation(decl) && raw.type.kind === "dyn" && - (g.type.kind === "record" || g.type.kind === "object") + (g.type.kind === "record" || + g.type.kind === "object" || + (ts.isIdentifier(decl.name) && + jsBindingKeepsCheckedObject(lowerer, decl.name, g.type) && + (existingValueForm(decl.initializer) || jsHoldsDynObject(lowerer, decl.initializer)))) ) g.type = DYN; g.type = inferredOptionalRecordType(lowerer, decl, g.type, raw.type); @@ -4759,6 +5007,8 @@ export function lowerVarDecl( // types already hold the interned undefined arm), and a declaration // re-run in a loop must not reset the persisting value. if (isVarDeclared(decl) && declSymbol) { + const fromExisting = lowerJsVarFromExisting(lowerer, decl, declSymbol); + if (fromExisting) return fromExisting; const local = hoistVarBinding(lowerer, declSymbol, decl.name); if (!decl.initializer) return null; const init = lowerer.lowerExprExpecting(decl.initializer, local.type); @@ -4919,9 +5169,16 @@ export function lowerVarDecl( lowerer.unions.get(inferredObjectType.unionId)?.arms.every(isUnitType))))) && !decl.type && !hasJsTypeAnnotation(decl); + // The binding of an assertion of the global object holds that object, + // whatever its asserted type (assertion-identity.ts). + const keepsAssertedObject = + init.type.kind === "dyn" && + !decl.type && + keptIdentityAssertion(lowerer, decl.initializer) !== undefined; if ( expandsObject || preservesObjectIdentity || + keepsAssertedObject || (isJsSourceFile(decl.getSourceFile()) && !decl.type && isNativeBuiltinValueInitializer(lowerer, decl.initializer)) @@ -5134,7 +5391,7 @@ export function lowerVarDecl( ? npmStaticOptionalSelfWriteType(lowerer, decl, settledType) : null; settledType = - expandsObject || preservesObjectIdentity + expandsObject || preservesObjectIdentity || keepsAssertedObject ? DYN : lowerer.runtimeOptionalBindingType(decl.name, settledType); if (optionalSelfWriteType !== null) settledType = optionalSelfWriteType; @@ -5978,6 +6235,28 @@ function lowerDeleteStatement(lowerer: Lowerer, expr: ts.DeleteExpression): IrSt }; } let obj = lowerer.lowerExpr(target.expression); + // `delete this.stack` in an Error subclass that declares a `stack` + // accessor (quickjs-wasi's JSException): V8 gives each error an own + // `stack` that shadows the accessor, and the delete removes it so reads + // reach the accessor. Compiled errors keep the trace internally and + // class dispatch already reads the accessor, so only the receiver's + // evaluation remains. + if ( + obj.type.kind === "object" && + ts.isPropertyAccessExpression(target) && + target.name.text === "stack" + ) { + let accessor = false; + let errorBase = false; + for (let c = lowerer.classes.get(obj.type.className); c; c = c.base ?? undefined) { + if (c.builtinError) { + errorBase = true; + break; + } + if (c.methods.has("get:stack")) accessor = true; + } + if (accessor && errorBase) return { kind: "exprStmt", expr: obj, loc }; + } if (isJsSourceFile(expr.getSourceFile()) && obj.type.kind === "object") { const info = lowerer.classes.get(obj.type.className); const keyType = ts.isElementAccessExpression(target) @@ -6402,10 +6681,7 @@ function lowerCjsSingleValueExport( // own statement already evaluated it), and a builtin global // (`module.exports = TypeError`) is the global itself: requirer // bindings alias straight through to it, so nothing is stored. - if ( - lowerer.checker.declarationsOf(sym).some((d) => ts.isSourceFile(d)) || - lowerer.isStdlibSymbol(sym) - ) { + if (sym.declarations.some((d) => ts.isSourceFile(d)) || lowerer.isStdlibSymbol(sym)) { return { kind: "block", body: [], loc }; } const g = lowerer.globalsBySymbol.get(sym); @@ -6524,10 +6800,7 @@ export function lowerExprStatement(lowerer: Lowerer, expr: ts.Expression): IrStm // positions keep the first-class-namespace fences. if (ts.isIdentifier(expr)) { const sym = lowerer.checker.getSymbolAtLocation(expr); - if ( - sym !== undefined && - lowerer.checker.declarationsOf(sym).some((d) => ts.isNamespaceImport(d)) - ) { + if (sym !== undefined && sym.declarations.some((d) => ts.isNamespaceImport(d))) { return { kind: "block", body: [], loc: locOf(expr) }; } } @@ -6601,16 +6874,12 @@ export function lowerExprStatement(lowerer: Lowerer, expr: ts.Expression): IrStm if (!isRelativeSpecifier(spec) && canonicalBuiltinModule(spec) === null) { const refusal = probeNodeRequireRefusal(sf.fileName, spec); if (refusal !== null) { + // The same throw as the expression form (cjs-inline-require): + // an Error carrying Node's MODULE_NOT_FOUND code. const loc = locOf(expr); return { - kind: "throw", - value: { - kind: "libCall", - fn: "error.new", - args: [{ kind: "strLit", value: refusal.message, type: STRING, loc }], - type: { kind: "object", className: "%Error" }, - loc, - }, + kind: "exprStmt", + expr: nodeThrowExpr(0, "MODULE_NOT_FOUND", refusal.message, VOID, loc), loc, }; } @@ -7036,6 +7305,32 @@ export function lowerExprStatement(lowerer: Lowerer, expr: ts.Expression): IrStm loc, }; } + // process.emitWarning = fn: an ordinary writable property. Later + // process.emitWarning(...) calls, and the warnings Node's own + // modules emit through process.emitWarning, call the replacement + // instead (silencing them when it does nothing). + if (flag === "emitWarning") { + const loc = locOf(expr); + let rhs: ts.Expression = expr.right; + while ( + ts.isParenthesizedExpression(rhs) || + ts.isAsExpression(rhs) || + ts.isSatisfiesExpression(rhs) + ) + rhs = rhs.expression; + const value = lowerer.lowerExprExpecting(rhs, DYN); + return { + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "process.setEmitWarning", + args: [value], + type: VOID, + loc, + }, + loc, + }; + } // process.title = name: libuv's uv_set_process_title (the // runtime truncates to the original argument area on POSIX). if (flag === "title") { @@ -7322,6 +7617,15 @@ export function lowerExprStatement(lowerer: Lowerer, expr: ts.Expression): IrStm if (droppedPureStdlibStatement(lowerer, expr)) { return { kind: "block", body: [], loc: locOf(expr) }; } + if ( + stmtNode !== undefined && + ts.isExpressionStatement(stmtNode) && + ts.isBinaryExpression(expr) && + (expr.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken || + expr.operatorToken.kind === ts.SyntaxKind.BarBarToken) + ) { + return lowerDiscardedLogical(lowerer, expr); + } const value = lowerer.lowerExpr(expr); // A bare unit-literal statement (`undefined;`): a pure no-op in JS, // and the IR has no bare-unit VALUE outside a union wrap — drop it @@ -7330,6 +7634,59 @@ export function lowerExprStatement(lowerer: Lowerer, expr: ts.Expression): IrStm return { kind: "exprStmt", expr: value, loc: locOf(expr) }; } +/** A discarded `a && b` / `a || b` statement (the minifier's `if`). The + * value form lowers first, exactly as before. Only when it fails on the + * logical's own result type — the operands have no common representation + * (jose's `(!m || m[4] && m[1]) && invalid()` joins a boolean, a string + * and undefined into the checker's `string`) — does the statement lower + * as the control flow it is: `if (a) b;` / `if (!a) b;`, a condition test + * that needs no result value. Diagnostics outside the statement, and + * internal errors, keep the original failure. */ +function lowerDiscardedLogical(lowerer: Lowerer, expr: ts.BinaryExpression): IrStmt { + const diagsBefore = lowerer.diags.length; + try { + const value = lowerer.lowerExpr(expr); + if (value.kind === "unitLit") return { kind: "block", body: [], loc: locOf(expr) }; + return { kind: "exprStmt", expr: value, loc: locOf(expr) }; + } catch (e) { + // A probe (a capture buffer is active) keeps its failure: its owner + // picks another lowering, exactly as before. + if (!(e instanceof PoisonError) || lowerer.diagSink !== null) throw e; + const added = lowerer.diags.slice(diagsBefore); + const file = expr.getSourceFile().fileName; + const start = expr.getStart(); + const local = + added.length > 0 && + added.every( + (d) => + d.code !== "SC9001" && + d.loc.file === file && + d.loc.start >= start && + d.loc.end <= expr.end, + ); + if (!local) throw e; + lowerer.diags.length = diagsBefore; + } + const and = expr.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken; + const cond = lowerer.lowerCondition(expr.left); + if (cond.kind === "boolLit") { + return cond.value === and + ? lowerExprStatement(lowerer, expr.right) + : { kind: "block", body: [], loc: locOf(expr) }; + } + const right = (): IrStmt[] => + lowerer.narrowingAliases(aliasTypeofNarrows(lowerer, expr.left, and), () => + and + ? withRuntimeOptionalNarrowed(lowerer, runtimeOptionalTrueIds(lowerer, expr.left), () => [ + lowerExprStatement(lowerer, expr.right), + ]) + : [lowerExprStatement(lowerer, expr.right)], + ); + return and + ? { kind: "if", cond, then: right(), else_: [], loc: locOf(expr) } + : { kind: "if", cond, then: [], else_: right(), loc: locOf(expr) }; +} + /** True for a statement-position expression whose evaluation cannot be * observed once the value is discarded: * - a property/element READ (`?.` included) whose receiver IS a @@ -7927,22 +8284,25 @@ function destructuringAssignInto( ), ); } - const packed: IrExpr = { - kind: "recordLit", - fields: remaining.map((f): Extract["fields"][number] => ({ - name: f.name, - value: { - kind: "recordGet", - obj: { kind: "varRef", localId: tmp.id, type: srcType, loc: locOf(prop) }, - shapeId: srcType.shapeId, - field: f.name, - type: f.type, - loc: locOf(prop), - }, - })), - type: { kind: "record", shapeId: restShapeId }, - loc: locOf(prop), - }; + const packed: IrExpr = lowerer.keyOrder.copy( + { + kind: "recordLit", + fields: remaining.map((f): Extract["fields"][number] => ({ + name: f.name, + value: { + kind: "recordGet", + obj: { kind: "varRef", localId: tmp.id, type: srcType, loc: locOf(prop) }, + shapeId: srcType.shapeId, + field: f.name, + type: f.type, + loc: locOf(prop), + }, + })), + type: { kind: "record", shapeId: restShapeId }, + loc: locOf(prop), + }, + srcType.shapeId, + ); if (!ts.isIdentifier(restTo)) { // `({ a, ...box.rest } = src)` — the rest packs identically and // lands through the property/element write machinery. @@ -8546,6 +8906,10 @@ export function lowerForOf(lowerer: Lowerer, stmt: ts.ForOfStatement): IrStmt { } } } + { + const opened = classAsyncGeneratorIterator(lowerer, stmt.expression); + if (opened) return lowerForAwaitGenerator(lowerer, stmt, opened, labels); + } if ( ts.isPropertyAccessExpression(stmt.expression) && lowerer.stdlibGlobalMember(stmt.expression, "process") === "stdin" @@ -9240,7 +9604,7 @@ export function lowerForOf(lowerer: Lowerer, stmt: ts.ForOfStatement): IrStmt { // local and the body opens by ASSIGNING it into the one hoisted // binding — closures capture the shared slot, and the value persists // after the loop (both Node-exact for var). - const varTarget = forOfVarTarget(lowerer, decl); + const varTarget = forOfVarTarget(lowerer, decl, elemValueT); if (varTarget) { const elemType = elemValueT; const loc = locOf(decl.name); @@ -9976,7 +10340,7 @@ function lowerForOfDyn( binds, ); } else if (ts.isIdentifier(decl.name)) { - const varTarget = forOfVarTarget(lowerer, decl); + const varTarget = forOfVarTarget(lowerer, decl, DYN); if (varTarget) { // `for (var x of d)`: the element mechanics stay on a hidden // per-iteration local; the body opens by assigning the ONE @@ -10820,3 +11184,25 @@ export function lowerForStatement(lowerer: Lowerer, stmt: ts.ForStatement): IrSt lowerer.scopes.pop(); } } + +/** safe-buffer's `var Buffer = buffer.Buffer` at file scope: a `var` alias + * of the global Buffer is the same alias plumbing as the const spelling + * when nothing reassigns it and no code that runs before the declaration + * can read it (the require-binding early-reference analysis), so the + * hoisted undefined is unobservable. */ +export function stdlibBufferVarAlias(lowerer: Lowerer, decl: ts.VariableDeclaration): boolean { + if ( + !ts.isIdentifier(decl.name) || + !ts.isVariableDeclarationList(decl.parent) || + (decl.parent.flags & (ts.NodeFlags.Let | ts.NodeFlags.Const)) !== 0 || + decl.initializer === undefined || + !bufferNamespaceMember(lowerer, decl.initializer) + ) + return false; + const symbol = lowerer.checker.getSymbolAtLocation(decl.name); + return ( + symbol !== undefined && + bindingNeverReassigned(lowerer, symbol, decl) && + requireEarlyReference(lowerer.program, decl) === null + ); +} diff --git a/packages/compiler/src/frontend/lowering/lower-stream.ts b/packages/compiler/src/frontend/lowering/lower-stream.ts index 7639e0974..5769e1e0a 100644 --- a/packages/compiler/src/frontend/lowering/lower-stream.ts +++ b/packages/compiler/src/frontend/lowering/lower-stream.ts @@ -492,7 +492,7 @@ function followOptionsArg( } catch { return node; } - const decl = sym ? lowerer.checker.valueDeclarationOf(sym) : undefined; + const decl = sym ? sym.valueDeclaration : undefined; if (!sym || !decl || !ts.isVariableDeclaration(decl) || decl.initializer === undefined) return node; if ((ts.getCombinedNodeFlags(decl) & ts.NodeFlags.Const) === 0) return node; diff --git a/packages/compiler/src/frontend/lowering/lower-test.ts b/packages/compiler/src/frontend/lowering/lower-test.ts index 5c6011425..32870c2cf 100644 --- a/packages/compiler/src/frontend/lowering/lower-test.ts +++ b/packages/compiler/src/frontend/lowering/lower-test.ts @@ -334,7 +334,7 @@ function staticFunctionName(lowerer: Lowerer, node: ts.Expression): string | nul if (!ts.isIdentifier(node)) return null; const symbol = lowerer.resolveValueSymbol(node); if (!symbol) return null; - for (const decl of lowerer.checker.declarationsOf(symbol)) { + for (const decl of symbol.declarations) { if (ts.isFunctionDeclaration(decl)) return decl.name?.text ?? "default"; if (ts.isVariableDeclaration(decl) && ts.isIdentifier(decl.name) && decl.initializer) { let init = decl.initializer; @@ -728,7 +728,7 @@ export function lowerTestDirectCall( if (!ts.isIdentifier(callee)) return null; if (lowerer.builtinNamespaceModuleOf(callee) !== "test") return null; const calleeSym = lowerer.checker.getSymbolAtLocation(callee); - const decl = calleeSym ? lowerer.checker.declarationsOf(calleeSym)[0] : undefined; + const decl = calleeSym ? calleeSym.declarations[0] : undefined; if (decl && ts.isNamespaceImport(decl)) { lowerer.unsupported( "SC1013", diff --git a/packages/compiler/src/frontend/lowering/lower-timers.ts b/packages/compiler/src/frontend/lowering/lower-timers.ts index 740ff66f1..22983fbb4 100644 --- a/packages/compiler/src/frontend/lowering/lower-timers.ts +++ b/packages/compiler/src/frontend/lowering/lower-timers.ts @@ -330,7 +330,7 @@ function isRealTimerCallee(lowerer: Lowerer, callee: ts.Expression): boolean { if (ts.isIdentifier(callee)) return lowerer.builtinImportOf(callee)?.module === "timers"; if (ts.isPropertyAccessExpression(callee) && ts.isIdentifier(callee.expression)) { const sym = lowerer.checker.getSymbolAtLocation(callee.expression); - const decl = sym ? lowerer.checker.declarationsOf(sym)[0] : undefined; + const decl = sym ? sym.declarations[0] : undefined; return decl !== undefined && ts.isNamespaceImport(decl); } return false; diff --git a/packages/compiler/src/frontend/lowering/lowerer.test.ts b/packages/compiler/src/frontend/lowering/lowerer.test.ts index 093c78632..bacdc7941 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.test.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.test.ts @@ -241,25 +241,26 @@ test("optional union widening keeps refused and missing contracts current", () = }); test("stdlib provenance is cached by symbol identity, including merged and shadowed declarations", () => { - const builtin = {} as Symbol, - shadow = {} as Symbol; const userFile = { fileName: "user.ts" } as SourceFile; const library = { fileName: "library.d.ts" } as SourceFile; const userDeclaration = { getSourceFile: () => userFile } as Node; const libraryDeclaration = { getSourceFile: () => library } as Node; - const declarationsOf = vi.fn((symbol: Symbol) => - symbol === builtin ? [userDeclaration, libraryDeclaration] : [userDeclaration], - ); - const lowerer = new Lowerer( - { getTypeChecker: () => ({ declarationsOf }) } as unknown as Program, - userFile, - [], - ); + let declarationReads = 0; + const symbolWith = (declarations: readonly Node[]): Symbol => + ({ + get declarations() { + declarationReads++; + return declarations; + }, + }) as unknown as Symbol; + const builtin = symbolWith([userDeclaration, libraryDeclaration]), + shadow = symbolWith([userDeclaration]); + const lowerer = new Lowerer({ getTypeChecker: () => ({}) } as unknown as Program, userFile, []); vi.spyOn(lowerer, "isStdlibFile").mockImplementation((file) => file === library); for (let i = 0; i < 20; i++) { expect(lowerer.isStdlibSymbol(builtin)).toBe(true); expect(lowerer.isStdlibSymbol(shadow)).toBe(false); expect(lowerer.isStdlibSymbol(undefined)).toBe(false); } - expect(declarationsOf).toHaveBeenCalledTimes(2); + expect(declarationReads).toBe(2); }); diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 1f905cae4..9d6bf89d7 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -4,6 +4,7 @@ import { lowerLifetimeRegionCall, lowerLifetimeRegionNew, } from "./lifetime-regions.js"; +import type { ConstructorReturnPlan } from "./constructor-returns.js"; import { internTableType, lowerInternTableCall, lowerInternTableNew } from "./intern-tables.js"; import { lowerThreadSlotsCall, lowerThreadSlotsNew, threadSlotsType } from "./thread-slots.js"; import { isExperimentalDeclarationFile } from "../experimental-import.js"; @@ -20,6 +21,7 @@ import { } from "./require-initialization.js"; import { identityPreservingWidening } from "./coercions/identity.js"; import { scanDivergences } from "./divergences.js"; +import { applyKeyOrderWitnesses, KeyOrderNotes } from "./key-order-witness.js"; import { checkedClassAssertion } from "./class-assertions.js"; import { narrowStoredClassValue, narrowGenericClassValue } from "./class-unions.js"; import { isOptionalProcessStreamProperty } from "./builtins/process.js"; @@ -31,6 +33,7 @@ import { refuseWebAssemblyUse } from "./builtins/webassembly.js"; import { discoverWasmModules, type WasmModuleReport } from "../../wasm/discover.js"; import { resolveRequireRuntime } from "../runtime-resolve.js"; import { lowerKeyObjectCall } from "./builtins/key-object.js"; +import { fenceWebCryptoMember, lowerWebCryptoCall } from "./builtins/webcrypto.js"; import { hashStreamOf } from "./builtins/hash-stream.js"; import { lowerNetAddressCall, lowerNetAddressNew } from "./builtins/net-address.js"; import { dynUndefinedExpr, nodeThrowExpr, numLit, varRef } from "../../ir/build.js"; @@ -72,6 +75,7 @@ import { uninhabitedArmRetagHelper, strandedUnitTrap, strandedCoercionTrap, + recordUnionRouteHelper, recordUnionWrapHelper, unionRetagHelper, narrowedArmHelper, @@ -474,6 +478,8 @@ import { lowerConsoleNew } from "./lower-console.js"; import { lowerNumberStaticCall, lowerNumberStaticProperty } from "./builtins/numbers.js"; import { dateJsonType, isDateJsonSource, lowerDateCall } from "./builtins/dates.js"; import { + fsConstantMemberRead, + fsConstantsObject, lowerFsConstantsProperty, lowerBuiltinConstantsProperty, builtinConstantBindingOf, @@ -1193,9 +1199,9 @@ export function staticImportNamespaceType( const symbol = lowerer.checker.getSymbolAtLocation(arg); const source = symbol && - lowerer.checker - .declarationsOf(symbol) - .find((decl): decl is ts.SourceFile => ts.isSourceFile(decl) && !decl.isDeclarationFile); + symbol.declarations.find( + (decl): decl is ts.SourceFile => ts.isSourceFile(decl) && !decl.isDeclarationFile, + ); if ( source && !source.fileName.endsWith(".cts") && @@ -1450,7 +1456,7 @@ function jsFallbackFunctionType(lowerer: Lowerer, node: ts.Node, t: ts.Type): Ir // slot. JS inference reports the body type (`boolean` for // `enabled = true`) rather than that call-site type, so recover the // syntactic optionality from the parameter declaration. - const decl = lowerer.checker.valueDeclarationOf(p); + const decl = p.valueDeclaration; mapped = jsOpenObjectType(decl, mapped, lowerer.shapes, lowerer.unions); const optional = decl !== undefined && @@ -1462,7 +1468,7 @@ function jsFallbackFunctionType(lowerer: Lowerer, node: ts.Node, t: ts.Type): Ir return mapped; }); const retT = lowerer.checker.getReturnTypeOfSignature(sig); - const sigDecl = lowerer.checker.signatureDeclaration(sig); + const sigDecl = sig.declaration; const jsUnitReturn = sigDecl !== undefined && isJsSourceFile(sigDecl.getSourceFile()) && @@ -1491,7 +1497,7 @@ function jsArgumentsFunctionType(lowerer: Lowerer, t: ts.Type): IrType | null { lowerer.checker.getPropertiesOfType(t).length !== 0 ) return null; - const decl = lowerer.checker.signatureDeclaration(sigs[0]!); + const decl = sigs[0]!.declaration; if ( decl === undefined || !( @@ -1537,14 +1543,14 @@ function pureSingleCallSignatureOf(lowerer: Lowerer, t: ts.Type): ts.Signature | lowerer.checker.getConstructSignatures(t).length === 0 && sigs[0]!.getTypeParameters().length === 0 && sigs[0]!.getParameters().every((p) => { - const pDecl = lowerer.checker.valueDeclarationOf(p); + const pDecl = p.valueDeclaration; return !pDecl || !ts.isParameter(pDecl) || pDecl.dotDotDotToken === undefined; }) && // A SYNTHESIZED rest param (tsc's `arguments` inference — no // valueDeclaration to carry the dotDotDot): param-count mismatch // against the signature's declaration; the whole value stays dyn. (() => { - const sigDecl = lowerer.checker.signatureDeclaration(sigs[0]!); + const sigDecl = sigs[0]!.declaration; const declParams = sigDecl !== undefined && ts.isFunctionLike(sigDecl) ? sigDecl.parameters.nodes : undefined; if (declParams !== undefined && declParams.length !== sigs[0]!.getParameters().length) @@ -1600,7 +1606,7 @@ export function jsFuncNameOf(node: ts.Node, lowerer?: Lowerer): string | null { const symbol = lowerer.resolveValueSymbol(n); if (!symbol || seen.has(symbol)) break; seen.add(symbol); - const declaration = lowerer.checker.valueDeclarationOf(symbol); + const declaration = symbol.valueDeclaration; if (declaration && ts.isFunctionDeclaration(declaration)) { n = declaration; break; @@ -1718,7 +1724,7 @@ export class Lowerer { get stringIndexBounds(): StringIndexBounds { this.stringIndexBoundsCache ??= new StringIndexBounds( (node) => this.checker.getSymbolAtLocation(node) ?? null, - (symbol) => this.checker.valueDeclarationOf(symbol), + (symbol) => symbol.valueDeclaration, (node) => (this.typeOf(node).flags & ts.TypeFlags.StringLike) !== 0, ); return this.stringIndexBoundsCache; @@ -2080,7 +2086,7 @@ export class Lowerer { // directly from the aliased declaration as a final read-side hook, so // the initializer cannot fall back to a narrowing adapter. if (ts.isIdentifier(node) && fallback?.kind === "func") { - const decl = this.checker.valueDeclarationOf(symbol); + const decl = symbol.valueDeclaration; if ( decl && ts.isVariableDeclaration(decl) && @@ -2439,6 +2445,9 @@ export class Lowerer { * Reachability lowers inits before the declarations they discover, so * these rebuild after the worklist restores historical shape metadata. */ readonly shapeOrderHelperFinalizers: (() => void)[] = []; + /** Construction witnesses that choose each shape's enumeration order + * once lowering settles (key-order-witness.ts). */ + readonly keyOrder = new KeyOrderNotes(); /** Emit-override specializations (`%C.emit:` — lower-event-emitter.ts's * emit-overrides block): interned names, the drive-loop queue, and the * currently-lowering specialization's context (the super-forward @@ -2450,6 +2459,9 @@ export class Lowerer { /** Generated value helpers, keyed by operation and type. Callers own * their key prefixes; this includes more than structural conversions. */ readonly valueHelpers = new Map(); + /** structuralNarrowing's assignable classes, keyed by static and + * narrowed class name. */ + readonly structuralClassPeers = new Map(); /** Interned node:assert helpers (deep-equality comparisons keyed by * typeKey, throws wrappers keyed by callback type + expected class) — @@ -2535,6 +2547,8 @@ export class Lowerer { * so named imports, namespace reads, CommonJS bindings, and alias chains * all reuse one immortal zero-capture closure. */ readonly builtinCallableValueFns = new Map(); + /** Each builtin function-value adapter's Node name, for boxed values. */ + readonly builtinCallableValueNames = new Map(); /** Optional-chain lowering state. While a chain body lowers, the guarded * receiver NODE reads as a chainRecv (typed by the narrowed arm) instead * of re-lowering, its checker type reads non-nullish (typeOf), and the @@ -2820,6 +2834,16 @@ export class Lowerer { * by class name (cycle-initialization.ts). */ readonly classInitFlags = new Map(); readonly cycleInitDeclarations = new Map(); + /** Each class's constructor return plan by class name + * (constructor-returns.ts); null when no return can yield an object. */ + readonly constructorReturnPlans = new Map(); + /** Pointer-guarded (`tdz`) module bindings declared without an + * initializer (`let f: T;`): an initialization flag tracks their + * declaration (markCycleEarlyBindings). */ + readonly uninitializedLexicalGlobals = new Map< + string, + { name: ts.Identifier; module: ts.SourceFile; symbol: ts.Symbol } + >(); /** CommonJS require declarations whose bindings code running before the * require can read, and those references (require-initialization.ts). */ readonly requireInitFlags = new Map(); @@ -3154,15 +3178,38 @@ export class Lowerer { /** Declared class fields may store a DYN value only when its static type * is `unknown` or `object` (also with `null` or `undefined`, or an array - * of those), names checked protocol identity, or is an owned runtime - * handle (a Worker or MessagePort, or a fetch Headers/Request/Response); - * `any` fields stay refused. */ + * of those), names checked protocol identity, is an owned runtime + * handle (a Worker or MessagePort, or a fetch Headers/Request/Response), + * 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) || this.classImplementedProtocol(type) || isWorkerHandleType(type, this.typeCtx) || - isFetchHandleType(type, this.typeCtx) + isFetchHandleType(type, this.typeCtx) || + this.weakCollectionType(type) + ); + } + + /** A stdlib WeakMap or WeakSet, alone or with `null`/`undefined`. */ + private weakCollectionType(type: ts.Type): boolean { + const weak = (part: ts.Type): boolean => { + const symbol = part.getSymbol(); + return ( + (symbol?.name === "WeakMap" || symbol?.name === "WeakSet") && + symbol.declarations + .some((d) => ts.isInterfaceDeclaration(d) && this.isStdlibFile(d.getSourceFile())) + ); + }; + if (!type.isUnionType()) return weak(type); + const parts = ts.constituentTypes(type); + return ( + parts.some(weak) && + parts.every( + (part) => (part.flags & (ts.TypeFlags.Null | ts.TypeFlags.Undefined)) !== 0 || weak(part), + ) ); } @@ -3191,7 +3238,7 @@ export class Lowerer { let sym = type.getSymbol(); if (!sym) return null; if (sym.flags & ts.SymbolFlags.Alias) sym = this.checker.getAliasedSymbol(sym); - for (const decl of this.checker.declarationsOf(sym)) { + for (const decl of sym.declarations) { if ( ts.isSourceFile(decl) && !decl.isDeclarationFile && @@ -3455,7 +3502,7 @@ export class Lowerer { * every project-module spelling. */ private moduleSourceFileOf(from: ts.SourceFile, spec: ts.StringLiteral): ts.SourceFile | null { const moduleSymbol = this.checker.getSymbolAtLocation(spec); - for (const decl of moduleSymbol ? this.checker.declarationsOf(moduleSymbol) : []) { + for (const decl of moduleSymbol ? moduleSymbol.declarations : []) { if (ts.isSourceFile(decl)) return decl; } return resolveImport(this.program, from, spec.text); @@ -3500,9 +3547,7 @@ export class Lowerer { } if (!ts.isIdentifier(value)) return null; const symbol = this.checker.getSymbolAtLocation(value); - const namespaceDecl = symbol - ? this.checker.declarationsOf(symbol).find(ts.isNamespaceImport) - : undefined; + const namespaceDecl = symbol ? symbol.declarations.find(ts.isNamespaceImport) : undefined; if (namespaceDecl === undefined) return null; const importDecl = namespaceDecl.parent?.parent; if (!ts.isImportDeclaration(importDecl) || !ts.isStringLiteral(importDecl.moduleSpecifier)) @@ -3522,7 +3567,7 @@ export class Lowerer { ): string | null { if (symbol === undefined || seenSymbols.has(symbol)) return null; seenSymbols.add(symbol); - const declarations = this.checker.declarationsOf(symbol); + const declarations = symbol.declarations; // Route-aware alias hops run before declaration-file ownership. An // exact import must keep the specifier it actually named, rather than @@ -3591,9 +3636,9 @@ export class Lowerer { let sym: ts.Symbol | undefined = alias; for (let hop = 0; sym !== undefined && hop < 32; hop++) { if (hop > 0 && sym.flags & ts.SymbolFlags.Alias && this.globalsBySymbol.has(sym)) return sym; - const d = this.checker - .declarationsOf(sym) - .find((x) => ts.isImportClause(x) || ts.isImportSpecifier(x) || ts.isExportSpecifier(x)); + const d = sym.declarations.find( + (x) => ts.isImportClause(x) || ts.isImportSpecifier(x) || ts.isExportSpecifier(x), + ); let spec: ts.Expression | undefined; let name: string | undefined; if (d && ts.isImportClause(d) && ts.isImportDeclaration(d.parent)) { @@ -3640,7 +3685,7 @@ export class Lowerer { * see resolveValueSymbol. Null when `symbol` is not such a property (or * the property's value is not a plain identifier reference). */ private cjsExportValueSymbol(symbol: ts.Symbol): ts.Symbol | null { - const d = this.checker.declarationsOf(symbol)[0]; + const d = symbol.declarations[0]; if (!d) return null; // MEMBER-form class exports (`exports.C = C` — commander's error.js): // alias plumbing exactly like a table entry, so importers land on the @@ -3686,7 +3731,7 @@ export class Lowerer { let value = this.checker.getSymbolAtLocation(assign.right); if (!value) return null; if (value.flags & ts.SymbolFlags.Alias) value = this.checker.getAliasedSymbol(value); - const isClass = this.checker.declarationsOf(value).some((decl) => ts.isClassDeclaration(decl)); + const isClass = value.declarations.some((decl) => ts.isClassDeclaration(decl)); return isClass ? value : null; } @@ -3732,7 +3777,7 @@ export class Lowerer { * member-symbol fallback and namespace classification in lockstep. */ private localModuleBindingDepOf(expr: ts.Identifier): ts.SourceFile | null { const sym = this.checker.getSymbolAtLocation(expr); - const decls = sym ? this.checker.declarationsOf(sym) : []; + const decls = sym ? sym.declarations : []; const decl = decls.find(ts.isImportClause) ?? decls[0]; if (!decl) return null; if (ts.isImportClause(decl)) return this.cjsDefaultImportDepOf(decl); @@ -3760,31 +3805,27 @@ export class Lowerer { * static storage or callable/class metadata. */ private createRequireWholeExportSymbolOf(ident: ts.Identifier): ts.Symbol | null { const binding = this.checker.getSymbolAtLocation(ident); - const decl = binding - ? this.checker.declarationsOf(binding).find(ts.isVariableDeclaration) - : undefined; + const decl = binding ? binding.declarations.find(ts.isVariableDeclaration) : undefined; if (!decl || !ts.isIdentifier(decl.name) || !decl.initializer) return null; const target = createRequireProgramModuleOf(this, decl.initializer); if (target === null) return null; const moduleSym = this.checker.getSymbolAtLocation(target.dep); let symbol = moduleSym?.getExports().get("export=" as ts.__String); if (!symbol) return null; - const wholeAssignment = this.checker - .declarationsOf(symbol) - .find((d): d is ts.BinaryExpression => { - if ( - !ts.isBinaryExpression(d) || - !ts.isExpressionStatement(d.parent) || - !ts.isSourceFile(d.parent.parent) - ) - return false; - const exported = cjsExportAssignmentOf(d.parent); - return ( - exported?.kind === "table" && - exported.obj === null && - cjsExportDiscardReason(d.parent) === null - ); - }); + const wholeAssignment = symbol.declarations.find((d): d is ts.BinaryExpression => { + if ( + !ts.isBinaryExpression(d) || + !ts.isExpressionStatement(d.parent) || + !ts.isSourceFile(d.parent.parent) + ) + return false; + const exported = cjsExportAssignmentOf(d.parent); + return ( + exported?.kind === "table" && + exported.obj === null && + cjsExportDiscardReason(d.parent) === null + ); + }); if (wholeAssignment === undefined) return null; let rhs: ts.Expression = wholeAssignment.right; while (ts.isParenthesizedExpression(rhs)) rhs = rhs.expression; @@ -3811,9 +3852,7 @@ export class Lowerer { * target module's real export symbol. */ private createRequireDestructuredExportSymbolOf(ident: ts.Identifier): ts.Symbol | null { const binding = this.checker.getSymbolAtLocation(ident); - const element = binding - ? this.checker.declarationsOf(binding).find(ts.isBindingElement) - : undefined; + const element = binding ? binding.declarations.find(ts.isBindingElement) : undefined; if ( !element || element.name === undefined || @@ -3861,7 +3900,7 @@ export class Lowerer { if (sym && sym.flags & ts.SymbolFlags.Alias) { // `module.exports = TypeError`: the binding IS the builtin global. if (this.isStdlibSymbol(this.checker.getAliasedSymbol(sym))) return false; - const d = this.checker.declarationsOf(this.checker.getAliasedSymbol(sym))[0]; + const d = this.checker.getAliasedSymbol(sym).declarations[0]; if (d && (ts.isClassDeclaration(d) || ts.isFunctionDeclaration(d))) return false; if (d && ts.isVariableDeclaration(d)) { let init = d.initializer; @@ -3926,7 +3965,7 @@ export class Lowerer { if (g.type.kind === "func") g.type = this.runtimeOptionalBindingType(ident, g.type); return g; } - for (const d of this.checker.declarationsOf(symbol)) { + for (const d of symbol.declarations) { const byDecl = this.globalsByDeclNode.get(d); if (byDecl) return byDecl; } @@ -4442,7 +4481,7 @@ export class Lowerer { }; const recordFlows = new RecordFlowClasses(sourceFiles, { symbolOf, - declarationOf: (symbol) => this.checker.valueDeclarationOf(symbol), + declarationOf: (symbol) => symbol.valueDeclaration, mayBeRecord: (node) => { if (node.parent && ts.isParameter(node.parent)) return plainRecordShape(parameterType(node)) !== null; @@ -4473,7 +4512,7 @@ export class Lowerer { const nanCoded = new NanCodedReads( { symbolOf, - declarationOf: (symbol) => this.checker.valueDeclarationOf(symbol), + declarationOf: (symbol) => symbol.valueDeclaration, bytesElemOf: (receiver) => { const t = mappedOrNull(receiver); return t?.kind === "bytes" ? t.elem : null; @@ -4525,7 +4564,7 @@ export class Lowerer { const seen = new Set(); while (symbol && !signatureBySymbol.has(symbol) && !seen.has(symbol)) { seen.add(symbol); - const declaration = this.checker.valueDeclarationOf(symbol); + const declaration = symbol.valueDeclaration; if (declaration && ts.isMethodDeclaration(declaration)) { symbol = symbolOf(declaration.name); } else if ( @@ -4670,7 +4709,7 @@ export class Lowerer { if (ts.isIdentifier(e)) { // A binding initialized from such a read and never reassigned. const symbol = symbolOf(e); - const decl = symbol ? this.checker.valueDeclarationOf(symbol) : undefined; + const decl = symbol ? symbol.valueDeclaration : undefined; return ( symbol !== null && !reassigned.has(symbol) && @@ -4711,7 +4750,7 @@ export class Lowerer { const read = peel(node); if (!ts.isElementAccessExpression(read) || !ts.isIdentifier(read.expression)) return false; const source = symbolOf(read.expression); - const declaration = source ? this.checker.valueDeclarationOf(source) : undefined; + const declaration = source ? source.valueDeclaration : undefined; if (!declaration || !ts.isVariableDeclaration(declaration) || !declaration.initializer) return false; const init = peel(declaration.initializer); @@ -4940,7 +4979,7 @@ export class Lowerer { * exactly `T | undefined`: an assignment from a missing read stores the * undefined arm while the checker narrows later reads to `T`. */ const optionalInstanceField = (symbol: ts.Symbol): boolean => { - const decl = this.checker.valueDeclarationOf(symbol); + const decl = symbol.valueDeclaration; if (!decl || !ts.isPropertyDeclaration(decl) || !ts.isIdentifier(decl.name)) return false; if (ts.getCombinedModifierFlags(decl) & ts.ModifierFlags.Static) return false; const sf = decl.getSourceFile(); @@ -5024,13 +5063,42 @@ export class Lowerer { return out; }; const scanPattern = (name: ts.BindingName, init: ts.Expression): boolean => { - if (ts.isObjectBindingPattern(name)) return scanRecordPattern(name, init); + if (ts.isObjectBindingPattern(name)) { + // `const { 0: a, 1: b, length } = parts` reads positions like the + // equivalent array pattern: each may sit past the end. + // Regex match results keep their own pattern lowering. + const arrayType = this.mapTypeOf(this.typeOf(init)); + const initType = this.checker.getNonNullableType(this.typeOf(init)); + const initSymbol = initType.getAliasSymbol() ?? initType.getSymbol(); + const matchResult = + initSymbol !== undefined && + (initSymbol.name === "RegExpExecArray" || initSymbol.name === "RegExpMatchArray"); + if (arrayType?.kind !== "array" || matchResult) return scanRecordPattern(name, init); + const positions = name.elements.nodes.filter((el) => { + const key = el.propertyName; + return ( + key !== undefined && ts.isNumericLiteral(key) && /^(?:0|[1-9]\d{0,8})$/.test(key.text) + ); + }); + return scanPositions(positions, init, arrayType); + } if (!ts.isArrayBindingPattern(name)) return false; const sourceType = this.mapTypeOf(this.typeOf(init)); if (sourceType?.kind !== "array") return false; + return scanPositions( + name.elements.nodes.filter((el): el is ts.BindingElement => !ts.isOmittedExpression(el)), + init, + sourceType, + ); + }; + const scanPositions = ( + elements: readonly ts.BindingElement[], + init: ts.Expression, + sourceType: IrType & { kind: "array" }, + ): boolean => { let changed = false; - name.elements.nodes.forEach((el) => { - if (ts.isOmittedExpression(el) || el.name === undefined || el.dotDotDotToken) return; + elements.forEach((el) => { + if (el.name === undefined || el.dotDotDotToken) return; if (ts.isIdentifier(el.name)) { const symbol = symbolOf(el.name); if (symbol && !el.initializer && !optionalSymbols.has(symbol)) { @@ -5279,7 +5347,7 @@ export class Lowerer { // monomorphic ABI or poisons valid generic value programs. if (this.genericFnsBySymbol.has(symbol)) return false; seen.add(symbol); - const declaration = this.checker.valueDeclarationOf(symbol); + const declaration = symbol.valueDeclaration; if (declaration && ts.isVariableDeclaration(declaration) && declaration.initializer) { if (promoteHofCallback(peel(declaration.initializer), parameterIndices, seen)) changed = true; @@ -5413,7 +5481,7 @@ export class Lowerer { seen.add(symbol); const fn = functionDeclBySymbol.get(symbol); if (fn) return bodyReturnsOptional(fn); - const decl = this.checker.valueDeclarationOf(symbol); + const decl = symbol.valueDeclaration; return ( !!decl && ts.isVariableDeclaration(decl) && @@ -5494,7 +5562,7 @@ export class Lowerer { }; const ownership = new ArrayOwnership(sourceFiles, { symbolOf, - declarationOf: (symbol) => this.checker.valueDeclarationOf(symbol), + declarationOf: (symbol) => symbol.valueDeclaration, typeOf: typeOrNull, instanceAssignableTo: (owner, target) => panicSafe( @@ -5519,7 +5587,7 @@ export class Lowerer { // flow type such as `Array.isArray(x)`'s `any[]` only narrows it. const e = peel(node); const symbol = ts.isIdentifier(e) ? symbolOf(e) : null; - const declaration = symbol ? this.checker.valueDeclarationOf(symbol) : undefined; + const declaration = symbol ? symbol.valueDeclaration : undefined; const declared = declaration && (ts.isVariableDeclaration(declaration) || ts.isParameter(declaration)) && @@ -5841,7 +5909,7 @@ export class Lowerer { const assertion = forwardedAssertion(node.right); const symbol = assertion ? symbolOf(node.left) : null; // Only a variable's storage is promoted (see the final pass). - const declaration = symbol ? this.checker.valueDeclarationOf(symbol) : undefined; + const declaration = symbol ? symbol.valueDeclaration : undefined; if ( assertion && symbol && @@ -5927,9 +5995,7 @@ export class Lowerer { let nativeArrayReceiver = receiver?.kind === "array" || tuple; if (nativeArrayReceiver && ts.isIdentifier(receiverNode)) { const receiverSymbol = symbolOf(receiverNode); - const declaration = receiverSymbol - ? this.checker.valueDeclarationOf(receiverSymbol) - : undefined; + const declaration = receiverSymbol ? receiverSymbol.valueDeclaration : undefined; if (declaration && ts.isVariableDeclaration(declaration)) { const declared = this.mapTypeOf(this.typeOf(declaration.name)); if ( @@ -5989,7 +6055,7 @@ export class Lowerer { // number) => ...`): a possibly missing argument widens that // parameter, as for a callback the same value is passed as. if (!ts.isIdentifier(node.expression) || reassigned.has(symbol)) return; - const declaration = this.checker.valueDeclarationOf(symbol); + const declaration = symbol.valueDeclaration; const value = declaration && ts.isVariableDeclaration(declaration) && declaration.initializer ? peel(declaration.initializer) @@ -6216,7 +6282,7 @@ export class Lowerer { // there, whatever its declared type. for (const symbol of earlyReadVarSymbols(this, sourceFiles)) { // Text codec bindings keep their own presence-checked storage. - const declaration = this.checker.valueDeclarationOf(symbol); + const declaration = symbol.valueDeclaration; const mapped = declaration && ts.isVariableDeclaration(declaration) ? mappedOrNull(declaration.name) @@ -7433,6 +7499,13 @@ export class Lowerer { this.settleGenericDemandPriorities(); this.shapes.settleDeclaredOrderPriorities(); for (const finalize of this.shapeOrderMetadataFinalizers) finalize(); + applyKeyOrderWitnesses(this, [ + ...orderedUnits, + ...initFunctions, + ...instanceFunctions, + ...this.liftedFns, + ...this.implicitFns, + ]); for (const finalize of this.shapeOrderHelperFinalizers) finalize(); const functions = [ ...orderedUnits, @@ -7755,13 +7828,11 @@ export class Lowerer { const ownSym = widened.getSymbol(); if ( ownSym && - this.checker - .declarationsOf(ownSym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - this.isStdlibFile(d.getSourceFile()), - ) + ownSym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + this.isStdlibFile(d.getSourceFile()), + ) ) { this.pushDiag(noLoweringDiag(this.checker.typeToString(type), locOf(node))); throw new PoisonError(); @@ -7803,13 +7874,11 @@ export class Lowerer { const stdlibOwnSym = widened.getSymbol(); const stdlibNominal = stdlibOwnSym !== undefined && - this.checker - .declarationsOf(stdlibOwnSym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - this.isStdlibFile(d.getSourceFile()), - ); + stdlibOwnSym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + this.isStdlibFile(d.getSourceFile()), + ); // OVERLOADED call signatures (SC2007) — after the generic branch, so a // generic overload set keeps the monomorphization story: a value of a // multi-signature type has no single compiled signature to hold. @@ -7905,7 +7974,11 @@ export class Lowerer { access: ts.PropertyAccessExpression | ts.ElementAccessExpression, use: "read" | "call", ): IrExpr | null { - return fenceStaticResponseMember(this, access, use) ?? fenceStaticBlobMember(this, access, use); + return ( + fenceStaticResponseMember(this, access, use) ?? + fenceStaticBlobMember(this, access, use) ?? + fenceWebCryptoMember(this, access, use) + ); } fenceUnsupportedFetchConstructorMember( @@ -8022,7 +8095,7 @@ export class Lowerer { // JSDoc and initializer inference are declaration types, not flow // proofs. A checked specialization must not reinstate those // stale layouts (mutable/null slots and heterogeneous JS arrays). - const declaration = sym && this.checker.valueDeclarationOf(sym); + const declaration = sym && sym.valueDeclaration; const declared = declaration && ts.isParameter(declaration) ? this.mapTypeOf(this.checker.getTypeAtLocation(declaration.name)) @@ -8612,12 +8685,17 @@ export class Lowerer { liveReference(expr.type) || (expr.type.kind === "union" && (this.unions.get(expr.type.unionId)?.arms.some(liveReference) ?? false)); + // A builtin function's adapter boxes under the builtin's name, the + // name Node's function carries (`[Function: randomUUID]`). + const builtinName = + expr.kind === "closure" ? this.builtinCallableValueNames.get(expr.fnName) : undefined; return { kind: "dynFrom", value: expr, type: DYN, loc: expr.loc, ...(liveRef ? { liveRef: true as const } : {}), + ...(builtinName !== undefined ? { fnName: builtinName } : {}), }; } return expr; @@ -9160,6 +9238,14 @@ export class Lowerer { return recordUnionWrapHelper(this, source, toId, loc); } + recordUnionRouteHelper( + source: IrType & { kind: "record" }, + toId: string, + loc: SrcLoc, + ): string | null { + return recordUnionRouteHelper(this, source, toId, loc); + } + unionRetagHelper( fromId: string, toId: string, @@ -9356,7 +9442,7 @@ export class Lowerer { if (!ts.isIdentifier(n)) return null; const sym = this.resolveValueSymbol(n); if (!sym) return null; - const decls = this.checker.declarationsOf(sym); + const decls = sym.declarations; if (decls.length === 0) return null; const units = new Set(); let allConst = true; @@ -9660,16 +9746,23 @@ export class Lowerer { } } } - let e = this.lowerExpr(node); + const e = this.lowerExpr(node); + return expected ? this.coerceExistingInto(node, e, expected) : e; + } + + /** The conversion lowerExprExpecting applies to `e`, the lowered value + * of `node` when it is not fresh construction the destination directs + * (a literal, conditional, or empty collection). */ + coerceExistingInto(node: ts.Expression, e: IrExpr, expected: IrType): IrExpr { if ( - (expected?.kind === "union" && this.armTag(expected.unionId, UNDEFINED_T) >= 0) || - (expected?.kind === "dyn" && isJsSourceFile(node.getSourceFile())) + (expected.kind === "union" && this.armTag(expected.unionId, UNDEFINED_T) >= 0) || + (expected.kind === "dyn" && isJsSourceFile(node.getSourceFile())) ) { // A destination that accepts undefined must retain an unchecked // read's storage value even when the checker still calls it present. e = this.runtimeOptionalSourceValue(node, e) ?? e; } - return expected ? this.coerceInto(node, e, expected) : e; + return this.coerceInto(node, e, expected); } /** An unannotated empty collection has no existing elements or aliases @@ -10138,7 +10231,7 @@ export class Lowerer { // declaration poisons under a source-anchored diagnostic, deferred // like any collection fence — never a crashed CLI. if (isCheckerPanic(e)) { - const decl = symbol ? this.checker.declarationsOf(symbol)[0] : undefined; + const decl = symbol ? symbol.declarations[0] : undefined; sink.push( checkerPanicDiag( e.message.split("\n", 1)[0]!, @@ -11063,7 +11156,7 @@ export class Lowerer { // symbol — which is what declareParams registered the local under. // On a miss, normalize to the declaration's key and retry (ordinary // bindings never reach this — their two sides intern to one symbol). - const vd = this.checker.valueDeclarationOf(symbol); + const vd = symbol.valueDeclaration; if ( vd && ts.isParameter(vd) && @@ -11114,9 +11207,7 @@ export class Lowerer { }); if (resolved.error !== null) { const location = - blame ?? - (symbol === undefined ? undefined : this.checker.declarationsOf(symbol)[0]) ?? - this.entry; + blame ?? (symbol === undefined ? undefined : symbol.declarations[0]) ?? this.entry; const message = `the binding '${resolved.origin!.name}' captured through a plain nested function (the declaration has no static storage a capture can thread — bind the value through a typed const, or read it in the declaring scope)`; this.unsupported("SC1090", location, message); } @@ -11198,7 +11289,7 @@ export class Lowerer { // concrete signature — name the shape instead of the generic // binding-form text. if (symbol) { - const d = this.checker.valueDeclarationOf(symbol); + const d = symbol.valueDeclaration; if (d && ts.isVariableDeclaration(d) && d.initializer === undefined) { const t = this.checker.getTypeOfSymbol(symbol); const parts = t.isUnionType() ? ts.constituentTypes(t) : [t]; @@ -11220,7 +11311,7 @@ export class Lowerer { // assignment are `undefined`, and this binding's type has no slot for // that value — name the shape instead of the generic no-lowering text. if (symbol) { - const d = this.checker.valueDeclarationOf(symbol); + const d = symbol.valueDeclaration; if ( d && ts.isVariableDeclaration(d) && @@ -11744,6 +11835,7 @@ export class Lowerer { lowerStaticReadableStreamReaderCall(this, expr) ?? lowerStaticResponseCall(this, expr) ?? lowerStaticBlobCall(this, expr) ?? + lowerWebCryptoCall(this, expr) ?? lowerKeyObjectCall(this, expr) ?? lowerNetAddressCall(this, expr) ?? lowerFetchElementMethodCall(this, expr) ?? @@ -12079,9 +12171,21 @@ export class Lowerer { if (mapped?.kind === "moduleNs" && mapped.moduleId.startsWith("builtin:")) { return mapped.moduleId.slice("builtin:".length); } + // A nullable namespace binding (`const os: typeof NodeOs | null`, a + // probed process.getBuiltinModule result) read where the checker + // narrowed it present: its members key the same tables. + if (stored?.kind === "union") { + const present = this.unions.get(stored.unionId)?.arms.filter((arm) => !isUnitType(arm)); + const only = present?.length === 1 ? present[0] : undefined; + if (only?.kind === "moduleNs" && only.moduleId.startsWith("builtin:")) { + const narrowed = this.mapTypeOf(this.typeOf(expr)); + if (narrowed?.kind === "moduleNs" && narrowed.moduleId === only.moduleId) + return only.moduleId.slice("builtin:".length); + } + } if (ts.isIdentifier(expr)) { const symbol = this.checker.getSymbolAtLocation(expr); - const decl = symbol ? this.checker.declarationsOf(symbol)[0] : undefined; + const decl = symbol ? symbol.declarations[0] : undefined; if (!decl) return null; // The CommonJS twin: `const fs = require("fs")` binds the same // namespace surface as `import * as fs from "node:fs"` — and the @@ -12146,7 +12250,7 @@ export class Lowerer { } if (ts.isIdentifier(expr)) { const symbol = this.checker.getSymbolAtLocation(expr); - const decl = symbol ? this.checker.declarationsOf(symbol)[0] : undefined; + const decl = symbol ? symbol.declarations[0] : undefined; if (!decl) return null; // The DEFAULT-import twin: Node's default export of a CJS builtin // IS the module object, so `import path from "node:path"` exposes @@ -12279,13 +12383,7 @@ export class Lowerer { COPYFILE_FICLONE_FORCE: 4, }; const value = own(MODES, expr.name.text); - if (value === undefined) { - this.noLowering( - `fs.constants.${expr.name.text}`, - expr, - "F_OK, R_OK, W_OK, X_OK, and COPYFILE_* are the lowered constants", - ); - } + if (value === undefined) return fsConstantMemberRead(expr); return { kind: "numLit", value, type: F64, loc }; } } @@ -12354,14 +12452,8 @@ export class Lowerer { const callable = this.lowerBuiltinCallableValue(bi, loc); if (callable) return callable; } - if (bi.member === "constants" && bi.module === "fs") { - // A bare `fs.constants` read (not one of the baked bits above). - this.noLowering( - `fs.constants`, - expr, - "F_OK, R_OK, W_OK, X_OK, and COPYFILE_* are the lowered constants", - ); - } + // A bare `fs.constants` read (not one of the baked bits above). + if (bi.member === "constants" && bi.module === "fs") return fsConstantsObject(loc); if (builtinModuleFnOf(this, bi.module, bi.member)) { this.unsupported( "SC1090", @@ -12422,11 +12514,12 @@ export class Lowerer { param.mode === "optional" ? `${param.mode}:${param.defaultValue}` : param.mode, ) .join(","); - const key = `${fn.fn}:${typeKey(funcType)}:${adapterShape}`; + const key = `${fn.valueFn ?? fn.fn}:${typeKey(funcType)}:${adapterShape}`; let fnName = this.builtinCallableValueFns.get(key); if (!fnName) { - fnName = `%builtin.value.${fn.fn}.${this.builtinCallableValueFns.size}`; + fnName = `%builtin.value.${fn.valueFn ?? fn.fn}.${this.builtinCallableValueFns.size}`; this.builtinCallableValueFns.set(key, fnName); + this.builtinCallableValueNames.set(fnName, display.slice(display.lastIndexOf(".") + 1)); const params = funcType.params.map((type, index) => ({ localId: `arg.${index}`, name: `arg${index}`, @@ -12491,7 +12584,7 @@ export class Lowerer { loc, }; }); - const call: IrExpr = { kind: "libCall", fn: fn.fn, args, type: fn.result, loc }; + const call: IrExpr = { kind: "libCall", fn: fn.valueFn ?? fn.fn, args, type: fn.result, loc }; const body: IrStmt[] = fn.result.kind === "void" ? [ diff --git a/packages/compiler/src/frontend/lowering/module-member-this.ts b/packages/compiler/src/frontend/lowering/module-member-this.ts new file mode 100644 index 000000000..205cc63e9 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/module-member-this.ts @@ -0,0 +1,64 @@ +/* A module export called through its module binding (`m.f()` where `m` is + * `const m = require("./x")` or `import * as m from "./x"`): Node passes + * the module object (the exports object, or the namespace) as `this`. The + * member call lowers to a direct call of the exported function, which has + * no module object to pass, so an export that reads `this` refuses instead + * of seeing another receiver. */ +import * as ts from "../ts7/adapter.js"; +import type { Lowerer } from "./lowerer.js"; + +/** The function an export binding holds, when its declaration shows it. */ +function exportedFunction(decl: ts.Node): ts.Node | null { + let node: ts.Node | undefined = decl; + if (ts.isPropertyAccessExpression(node) && node.parent && ts.isBinaryExpression(node.parent)) + node = node.parent; + if (ts.isBinaryExpression(node)) node = node.right; + else if (ts.isVariableDeclaration(node) || ts.isPropertyAssignment(node)) node = node.initializer; + while (node !== undefined && ts.isParenthesizedExpression(node)) node = node.expression; + if (node === undefined) return null; + if ( + ts.isFunctionDeclaration(node) || + ts.isFunctionExpression(node) || + ts.isMethodDeclaration(node) + ) + return node; + return null; +} + +/** Whether a function's own body reads `this` (arrows share it; other + * functions and classes have their own). */ +function readsOwnThis(fn: ts.Node): boolean { + let found = false; + const visit = (node: ts.Node): void => { + if (found) return; + if (node.kind === ts.SyntaxKind.ThisKeyword) { + found = true; + return; + } + if ((ts.isFunctionLike(node) && !ts.isArrowFunction(node)) || ts.isClassLikeDeclaration(node)) + return; + node.forEachChild(visit); + }; + fn.forEachChild(visit); + return found; +} + +export function refuseModuleMemberThis( + lowerer: Lowerer, + call: ts.CallExpression, + member: ts.Identifier, +): void { + let symbol = lowerer.checker.getSymbolAtLocation(member); + if (symbol !== undefined && (symbol.flags & ts.SymbolFlags.Alias) !== 0) + symbol = lowerer.checker.getAliasedSymbol(symbol); + if (symbol === undefined) return; + for (const decl of symbol.declarations) { + const fn = exportedFunction(decl); + if (fn === null || !readsOwnThis(fn)) continue; + lowerer.unsupported( + "SC1090", + call, + `calling a module export that reads 'this' through its module binding ('${member.text}': Node passes the module object as 'this', which this call has no value for)`, + ); + } +} diff --git a/packages/compiler/src/frontend/lowering/module-namespace-elements.ts b/packages/compiler/src/frontend/lowering/module-namespace-elements.ts index 368ce3c5f..d2425750b 100644 --- a/packages/compiler/src/frontend/lowering/module-namespace-elements.ts +++ b/packages/compiler/src/frontend/lowering/module-namespace-elements.ts @@ -31,7 +31,7 @@ export function lowerModuleNamespaceElement( ? lowerer.checker.getAliasedSymbol(exported) : exported; if (!(target.flags & ts.SymbolFlags.Value)) return; - const declaration = lowerer.checker.valueDeclarationOf(target); + const declaration = target.valueDeclaration; const identifier = declaration?.name as ts.Node | undefined; if (!identifier || !ts.isIdentifier(identifier)) { lowerer.unsupported("SC1090", expr, "computed namespace reads of unnamed exports"); diff --git a/packages/compiler/src/frontend/lowering/narrowing-stability.ts b/packages/compiler/src/frontend/lowering/narrowing-stability.ts index 500267263..01801470d 100644 --- a/packages/compiler/src/frontend/lowering/narrowing-stability.ts +++ b/packages/compiler/src/frontend/lowering/narrowing-stability.ts @@ -190,7 +190,7 @@ export class NarrowingStability { const { call, argument } = slot; const checker = this.lowerer.checker; const signature = checker.getResolvedSignature(call); - const declaration = signature ? checker.signatureDeclaration(signature) : undefined; + const declaration = signature ? signature.declaration : undefined; if (!declaration || !this.lowerer.isStdlibFile(declaration.getSourceFile())) return true; const callee = skipOuter(call.expression); if ( @@ -372,7 +372,7 @@ export class NarrowingStability { this.staleBindings.set(symbol, false); let result = false; try { - const decl = this.lowerer.checker.valueDeclarationOf(symbol); + const decl = symbol.valueDeclaration; if (decl !== undefined && ts.isVariableDeclaration(decl)) { if (decl.initializer !== undefined && this.mayBeStale(decl.initializer)) result = true; else { @@ -421,7 +421,7 @@ export class NarrowingStability { if (cached !== undefined) return cached; let result = false; try { - const decl = this.lowerer.checker.valueDeclarationOf(symbol); + const decl = symbol.valueDeclaration; const mutableBinding = decl !== undefined && ((ts.isVariableDeclaration(decl) && diff --git a/packages/compiler/src/frontend/lowering/native-ffi.ts b/packages/compiler/src/frontend/lowering/native-ffi.ts index 3ccd54b64..c9ff3300d 100644 --- a/packages/compiler/src/frontend/lowering/native-ffi.ts +++ b/packages/compiler/src/frontend/lowering/native-ffi.ts @@ -163,7 +163,7 @@ function ffiDeclarationDiagnostic( loc: SrcLoc, ): ScrDiagnostic | null { const { binding: bindingDiag, signature: signatureDiag } = ffiFlavor(lowerer); - const declarations = lowerer.checker.declarationsOf(symbol); + const declarations = symbol.declarations; const functionDecls = declarations.filter(ts.isFunctionDeclaration); if ( functionDecls.length === 0 || @@ -386,7 +386,7 @@ export function lowerFfiCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr // are untouched. if (lowerer.libraryCallbacks) { const symbol = lowerer.resolveValueSymbol(expr.expression); - const decls = symbol === null ? [] : lowerer.checker.declarationsOf(symbol); + const decls = symbol === null ? [] : symbol.declarations; const callbackShaped = decls.length > 0 && decls.every( @@ -427,7 +427,7 @@ export function lowerFfiCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr if (validSymbols === undefined) throw new PoisonError(); if (!validSymbols.has(symbol)) { if (lowerer.libraryCallbacks) { - const declarations = lowerer.checker.declarationsOf(symbol); + const declarations = symbol.declarations; const programDeclarations = declarations.filter((decl) => libraryCallbackOwnsFile(lowerer, decl.getSourceFile()), ); diff --git a/packages/compiler/src/frontend/lowering/object-factory-new.ts b/packages/compiler/src/frontend/lowering/object-factory-new.ts index ac83dccd8..ddd8d6d56 100644 --- a/packages/compiler/src/frontend/lowering/object-factory-new.ts +++ b/packages/compiler/src/frontend/lowering/object-factory-new.ts @@ -21,15 +21,13 @@ function isObjectFactoryNew(lowerer: Lowerer, expr: ts.NewExpression): boolean { lowerer.collecting = wasCollecting; } if (!symbol) return false; - const owner = lowerer.checker - .declarationsOf(symbol) - .find( - (node) => - (ts.isFunctionDeclaration(node) && node.body !== undefined) || - (ts.isVariableDeclaration(node) && - node.initializer && - ts.isFunctionExpression(node.initializer)), - ); + const owner = symbol.declarations.find( + (node) => + (ts.isFunctionDeclaration(node) && node.body !== undefined) || + (ts.isVariableDeclaration(node) && + node.initializer && + ts.isFunctionExpression(node.initializer)), + ); const declaration = owner && ts.isVariableDeclaration(owner) && diff --git a/packages/compiler/src/frontend/lowering/split-consumers.ts b/packages/compiler/src/frontend/lowering/split-consumers.ts index 11443051c..9624ac0aa 100644 --- a/packages/compiler/src/frontend/lowering/split-consumers.ts +++ b/packages/compiler/src/frontend/lowering/split-consumers.ts @@ -21,7 +21,7 @@ export function isPrivateSplitConsumer( const local = lowerer.bindingIn(lowerer.ctx, symbol); if (!local || local.boxed || local.tdz || local.mutable || local.id !== iterable.localId) return false; - const decl = lowerer.checker.valueDeclarationOf(symbol); + const decl = symbol.valueDeclaration; if ( !decl || !ts.isVariableDeclaration(decl) || diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts index e43748b39..7ae1371c2 100644 --- a/packages/compiler/src/frontend/lowering/surfaces.ts +++ b/packages/compiler/src/frontend/lowering/surfaces.ts @@ -22,7 +22,7 @@ import { VOID, arrayOf, } from "../../ir/ir.js"; -import { isNodeTypesPath, requireSpecOf } from "../program.js"; +import { isJsSourceFile, isNodeTypesPath, requireSpecOf } from "../program.js"; import { ZLIB_CONSTANTS } from "./zlib-constants.js"; /** zlib stream instances: runtime Transform streams (scr_zlib_stream.c). */ @@ -773,6 +773,7 @@ export const STATIC_NUMBER_METHODS: Record< { fns: readonly IrLibFn[]; minArgs: number; maxArgs: number } | undefined > = { toFixed: { fns: ["num.toFixed0", "num.toFixed"], minArgs: 0, maxArgs: 1 }, + toPrecision: { fns: ["num.toPrecision"], minArgs: 0, maxArgs: 1 }, }; /** One lowerable builtin-module FUNCTION: `fn`'s libCall with `params` @@ -793,6 +794,10 @@ export interface BuiltinModuleFn { * omission or explicit undefined; rest parameters pack into one typed * array slot. Entries with call-site-specific validation remain absent. */ valueParams?: BuiltinValueParam[]; + /** The libCall a function value's adapter makes when it differs from the + * direct-call row's `fn` (a direct call picks an arity-specific entry + * point; the value accepts every arity through one). */ + valueFn?: IrLibFn; } export type BuiltinValueParam = @@ -1502,7 +1507,15 @@ export const BUILTIN_MODULE_FNS: Record< userInfo: { fn: "os.userName", params: [], result: VOID }, }, crypto: { - randomUUID: { fn: "crypto.randomUUID", params: [], result: STRING }, + randomUUID: { + fn: "crypto.randomUUID", + params: [], + result: STRING, + // As a value (uuid's `export default { randomUUID }`), the optional + // options argument goes through the validating entry point. + valueFn: "crypto.randomUUIDOpts", + valueParams: [{ mode: "optional", type: DYN, defaultValue: undefined }], + }, // A real Buffer result. The composed randomBytes(n).toString(enc) // form KEEPS its one-libCall lowering (lowerCryptoComposedCall runs // first and the Buffer never materializes there); this entry covers @@ -1521,9 +1534,15 @@ export const BUILTIN_MODULE_FNS: Record< hash: { fn: "crypto.hashDigestStr", params: [STRING, STRING, STRING], result: STRING }, timingSafeEqual: { fn: "crypto.timingSafeEqual", params: [BYTES_U8, BYTES_U8], result: BOOL }, randomFillSync: { fn: "crypto.randomFill", params: [BYTES_U8, F64, F64], result: BYTES_U8 }, + getRandomValues: { fn: "crypto.getRandomValues", params: [BYTES_U8], result: BYTES_U8 }, randomInt: { fn: "crypto.randomInt", params: [F64, F64], result: F64 }, randomFill: { fn: "crypto.randomFillCb", params: [], result: VOID }, - randomUUIDv7: { fn: "crypto.randomUUIDv7", params: [DYN], result: STRING }, + randomUUIDv7: { + fn: "crypto.randomUUIDv7", + params: [DYN], + result: STRING, + valueParams: [{ mode: "optional", type: DYN, defaultValue: undefined }], + }, 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 }, @@ -2086,6 +2105,7 @@ export const BUILTIN_MODULE_FN_ALIASES: Record< "crypto.randomFillPromiseDyn", ], timingSafeEqual: ["crypto.timingSafeEqualDyn"], + getRandomValues: ["crypto.getRandomValuesDyn"], randomInt: ["crypto.randomIntMax", "crypto.randomIntCb", "crypto.randomIntPromise"], randomUUID: ["crypto.randomUUIDOpts"], }, @@ -2159,6 +2179,20 @@ export const AMBIENT_SURFACE_FNS: readonly AmbientSurfaceRow[] = [ fns: ["blob.fileNew"], note: "a Blob with name and lastModified (Date.now() when absent)", }, + { + id: "stdlib.webcrypto.subtle", + kind: "stdlib", + name: "globalThis.crypto.subtle", + fns: ["webcrypto.importKey", "webcrypto.sign", "webcrypto.verify", "webcrypto.digest"], + note: "importKey('raw') for HMAC keys, HMAC sign and verify, and SHA-1/SHA-2 digest; other methods, algorithms and key formats are refused", + }, + { + id: "stdlib.webcrypto.CryptoKey", + kind: "stdlib", + name: "CryptoKey", + fns: ["webcrypto.isCryptoKey"], + note: "secret HMAC keys with type, extractable, algorithm and usages, and instanceof CryptoKey", + }, { id: "stdlib.atomics", kind: "stdlib", @@ -2663,6 +2697,15 @@ export const AMBIENT_SURFACE_FNS: readonly AmbientSurfaceRow[] = [ fns: ["os.constants"], note: "the platform's dlopen, errno, signals and priority groups; members a platform lacks read as undefined", }, + // fs.constants as a value, and its members beyond the baked access-mode + // bits: the target libc's null-prototype constants object. + { + id: "node-builtin.fs.constants", + kind: "node-builtin", + name: "fs.constants", + fns: ["fs.constants"], + note: "node_constants.cc's fs group under the target libc's macros; members a platform lacks read as undefined", + }, ]; /** The win32-target overrides of the bare modules' constants: path's @@ -3220,7 +3263,7 @@ export function isChildSurfaceMember( ): boolean { if (isStdlibMember(lowerer, access)) return true; const sym = lowerer.checker.getSymbolAtLocation(access.name); - const decl = sym ? lowerer.checker.declarationsOf(sym)[0] : undefined; + const decl = sym ? sym.declarations[0] : undefined; const iface = decl?.parent; if (!decl || !iface || !ts.isInterfaceDeclaration(iface)) return false; if (lowerer.isStdlibFile(decl.getSourceFile())) return false; @@ -3233,10 +3276,7 @@ export function isChildSurfaceMember( * with lib interfaces, so a member can carry declarations from both. A * user's own declaration is in neither, so shadowing never matches. */ export function isStdlibSymbol(lowerer: Lowerer, symbol: ts.Symbol | undefined): boolean { - return ( - !!symbol && - lowerer.checker.declarationsOf(symbol).some((d) => lowerer.isStdlibFile(d.getSourceFile())) - ); + return !!symbol && symbol.declarations.some((d) => lowerer.isStdlibFile(d.getSourceFile())); } /** The canonical stdlib-global name `expr` denotes, or null. Three @@ -3262,9 +3302,7 @@ export function stdlibGlobalNameOf(lowerer: Lowerer, expr: ts.Expression): strin // symbol has no ordinary declarations for the provenance check. if ( expr.text === "globalThis" && - (!symbol || - lowerer.checker.declarationsOf(symbol).length === 0 || - lowerer.isStdlibSymbol(symbol)) + (!symbol || symbol.declarations.length === 0 || lowerer.isStdlibSymbol(symbol)) ) return "globalThis"; if (!symbol) return null; @@ -3272,7 +3310,7 @@ export function stdlibGlobalNameOf(lowerer: Lowerer, expr: ts.Expression): strin if (alias !== undefined) return alias; const classAlias = lowerer.builtinClassAliasOf(symbol); if (classAlias?.kind === "collection") return classAlias.which; - const decl = lowerer.checker.declarationsOf(symbol)[0]; + const decl = symbol.declarations[0]; if (decl !== undefined && ts.isImportClause(decl) && decl.name !== undefined) { const importDecl = decl.parent; if (ts.isImportDeclaration(importDecl) && ts.isStringLiteral(importDecl.moduleSpecifier)) { @@ -3288,7 +3326,21 @@ export function stdlibGlobalNameOf(lowerer: Lowerer, expr: ts.Expression): strin // `Buffer` resolves to a user-file declaration and still misses. const resolved = symbol.flags & ts.SymbolFlags.Alias ? lowerer.checker.getAliasedSymbol(symbol) : symbol; - if (!lowerer.isStdlibSymbol(resolved)) return null; + if (!lowerer.isStdlibSymbol(resolved)) { + // A CommonJS binding of the buffer module's Buffer (`var Buffer = + // require('buffer').Buffer`, `const { Buffer } = require('buffer')` + // — safe-buffer and compression): the module member IS the global + // constructor. + if ( + decl !== undefined && + (ts.isVariableDeclaration(decl) || ts.isBindingElement(decl)) && + isJsSourceFile(decl.getSourceFile()) + ) { + const imported = lowerer.builtinImportOf(expr); + if (imported?.module === "buffer" && imported.member === "Buffer") return "Buffer"; + } + return null; + } return resolved.name === "global" ? "globalThis" : resolved.name; } if (ts.isPropertyAccessExpression(expr) && !expr.questionDotToken) { @@ -3363,7 +3415,9 @@ export function stdlibGlobalAliasNameOf( const name = requireSpec === "process" || requireSpec === "node:process" ? "process" - : stdlibGlobalNameOf(lowerer, aliasExpr); + : bufferNamespaceMember(lowerer, aliasExpr) + ? "Buffer" + : stdlibGlobalNameOf(lowerer, aliasExpr); if (name === null) return null; // These globals have provenance-aware call or member lowering, so a // stable snapshot keeps the same dispatch as the original global. @@ -3384,6 +3438,14 @@ export function stdlibGlobalAliasNameOf( : null; } +/** `buffer.Buffer` through a buffer-module namespace binding (`var buffer + * = require('buffer')`): the module's Buffer IS the global constructor. */ +export function bufferNamespaceMember(lowerer: Lowerer, expr: ts.Expression): boolean { + if (!ts.isPropertyAccessExpression(expr) || expr.name.text !== "Buffer") return false; + const member = lowerer.builtinMemberOf(expr); + return member?.module === "buffer" && member.member === "Buffer"; +} + export function stdlibGlobalAliasDecl( lowerer: Lowerer, nameNode: ts.Node, @@ -3411,7 +3473,7 @@ export function stdlibGlobalAliasDecl( * the SC2020 fence's wording: "typed by @types/node", not "standard * library". */ export function nodeTypesOnlySymbol(lowerer: Lowerer, sym: ts.Symbol | null | undefined): boolean { - const decls = sym ? lowerer.checker.declarationsOf(sym) : undefined; + const decls = sym ? sym.declarations : undefined; if (!decls || decls.length === 0) return false; let viaNode = false; for (const d of decls) { diff --git a/packages/compiler/src/frontend/lowering/symbol-fields.ts b/packages/compiler/src/frontend/lowering/symbol-fields.ts index f4f536af9..5d7a20c06 100644 --- a/packages/compiler/src/frontend/lowering/symbol-fields.ts +++ b/packages/compiler/src/frontend/lowering/symbol-fields.ts @@ -26,7 +26,7 @@ function constantSymbolDescription( const symbol = lowerer.resolveValueSymbol(name); if (!symbol || seen.has(symbol)) return null; seen.add(symbol); - const declaration = lowerer.checker.valueDeclarationOf(symbol); + const declaration = symbol.valueDeclaration; if ( !declaration || !ts.isVariableDeclaration(declaration) || @@ -94,8 +94,8 @@ export function classSymbolKeyOfSymbol(lowerer: Lowerer, sym: ts.Symbol): ClassS const cached = cache.get(sym); if (cached !== undefined) return cached; cache.set(sym, null); - const decls = lowerer.checker.declarationsOf(sym); - const decl = lowerer.checker.valueDeclarationOf(sym); + const decls = sym.declarations; + const decl = sym.valueDeclaration; if (decls.length !== 1 || !decl || !ts.isVariableDeclaration(decl) || !ts.isIdentifier(decl.name)) return null; const list = decl.parent; diff --git a/packages/compiler/src/frontend/lowering/thread-slots.ts b/packages/compiler/src/frontend/lowering/thread-slots.ts index 6ec9af036..50b5ecf09 100644 --- a/packages/compiler/src/frontend/lowering/thread-slots.ts +++ b/packages/compiler/src/frontend/lowering/thread-slots.ts @@ -168,7 +168,7 @@ function declaringNew(lowerer: Lowerer, receiver: ts.Expression): ts.NewExpressi let symbol = checker.getSymbolAtLocation(ref); if (symbol !== undefined && (symbol.flags & ts.SymbolFlags.Alias) !== 0) symbol = checker.getAliasedSymbol(symbol); - const decl = symbol ? checker.declarationsOf(symbol)[0] : undefined; + const decl = symbol ? symbol.declarations[0] : undefined; if (decl === undefined || !ts.isVariableDeclaration(decl)) return null; const list = decl.parent; if ( diff --git a/packages/compiler/src/frontend/lowering/union-retag.test.ts b/packages/compiler/src/frontend/lowering/union-retag.test.ts index 0cb7b1b9f..8116d6378 100644 --- a/packages/compiler/src/frontend/lowering/union-retag.test.ts +++ b/packages/compiler/src/frontend/lowering/union-retag.test.ts @@ -1,7 +1,12 @@ import { describe, expect, test } from "vitest"; import { BOOL, F64, NULL_T, STRING, UNDEFINED_T, typeEquals, typeKey } from "../../ir/ir.js"; import type { IrRecordShape, IrType, IrUnionDef, SrcLoc } from "../../ir/ir.js"; -import { planUnionRetag, buildUnionRetag, planRecordUnionWrap } from "./union-retag.js"; +import { + planUnionRetag, + buildUnionRetag, + planRecordUnionLift, + planRecordUnionWrap, +} from "./union-retag.js"; import type { UnionRetagArm } from "./union-retag.js"; import type { WidthLift } from "./width-lift.js"; @@ -126,6 +131,30 @@ describe("record conversion with shared layouts", () => { f.shapes[2]!.fields.reverse(); expect(planRecordUnionWrap(f.shapes[1]!, f.to, f.shapeOf)).toBeNull(); }); + + test("routes a record that fits several arms by its discriminant", () => { + const f = fixture(); + expect(planRecordUnionLift(f.shapes[0]!, f.to, f.shapeOf, f.lift)).toEqual({ + field: "kind", + fieldType: STRING, + routes: [ + { tag: 0, lift: { how: "width" }, values: ["left"] }, + { tag: 1, lift: { how: "width" }, values: ["right"] }, + ], + }); + // Without the discriminant field, or without an arm it converts into, + // there is no route. + const unnamed: IrRecordShape = { id: "r3", fields: f.shapes[0]!.fields.slice(1) }; + expect(planRecordUnionLift(unnamed, f.to, f.shapeOf, f.lift)).toBeNull(); + const unrelated: IrRecordShape = { + id: "r4", + fields: [ + { name: "kind", type: STRING }, + { name: "other", type: F64 }, + ], + }; + expect(planRecordUnionLift(unrelated, f.to, f.shapeOf, f.lift)).toBeNull(); + }); }); describe("union conversion planning", () => { diff --git a/packages/compiler/src/frontend/lowering/union-retag.ts b/packages/compiler/src/frontend/lowering/union-retag.ts index 8d1c61d09..1c435ab36 100644 --- a/packages/compiler/src/frontend/lowering/union-retag.ts +++ b/packages/compiler/src/frontend/lowering/union-retag.ts @@ -86,6 +86,36 @@ export function planRecordUnionWrap( return { field: discriminant.field, fieldType, routes }; } +/** A record whose layout converts into more than one arm of a + * discriminated union. The IR keeps the discriminant field as a plain + * string, so its layout alone cannot say which variant it is; the value of + * the discriminant does, as in JavaScript. Every record arm the record + * converts into (by copy or width) becomes a route selected by that arm's + * discriminant values. A value no route names throws the catchable + * TypeError. */ +export function planRecordUnionLift( + source: IrRecordShape, + to: IrUnionDef, + shapeOf: (id: string) => IrRecordShape | undefined, + widthLift: (src: IrType, dst: IrType) => WidthLift | null, +): RecordUnionWrapPlan | null { + const discriminant = to.discriminant; + if (!discriminant || source.tuple || source.indexValue || shapeHasAccessorSlots(source)) + return null; + const fieldType = discriminantField(source, discriminant.field); + if (!fieldType || discriminantOwners(to, shapeOf) === null) return null; + const sourceType: IrType = { kind: "record", shapeId: source.id }; + const routes: UnionRetagRoute[] = []; + for (const entry of discriminant.cases) { + const arm = to.arms[entry.tag]; + if (!arm || arm.kind !== "record") continue; + const lift = arm.shapeId === source.id ? { how: "copy" as const } : widthLift(sourceType, arm); + if (lift === null) continue; + routes.push({ tag: entry.tag, lift, values: entry.values }); + } + return routes.length > 0 ? { field: discriminant.field, fieldType, routes } : null; +} + /** Pure conversion planning. A source storage tag need not denote just * one semantic variant: refinements can coalesce recursive record layouts. * When both unions name the same discriminant, its values select target diff --git a/packages/compiler/src/frontend/lowering/width-lift.ts b/packages/compiler/src/frontend/lowering/width-lift.ts index 40c533d72..d90f9ed35 100644 --- a/packages/compiler/src/frontend/lowering/width-lift.ts +++ b/packages/compiler/src/frontend/lowering/width-lift.ts @@ -7,6 +7,7 @@ export type WidthLift = | { how: "copy" } | { how: "wrap"; tag: number } | { how: "discriminantWrap" } + | { how: "discriminantRoute" } | { how: "retag" } | { how: "liftWrap"; tag: number; arm: IrType } | { how: "width" } diff --git a/packages/compiler/src/frontend/npm-static-subtyping.ts b/packages/compiler/src/frontend/npm-static-subtyping.ts index a81cbf263..04c66a559 100644 --- a/packages/compiler/src/frontend/npm-static-subtyping.ts +++ b/packages/compiler/src/frontend/npm-static-subtyping.ts @@ -16,14 +16,12 @@ export function isNpmStaticSubclassArgument( const symbol = type.getSymbol(); return ( symbol !== undefined && - checker - .declarationsOf(symbol) - .some( - (declaration) => - (ts.isClassDeclaration(declaration) || ts.isClassExpression(declaration)) && - /\.[cm]?js$/.test(declaration.getSourceFile().fileName) && - npmStaticPackageOfPath(declaration.getSourceFile().fileName) !== null, - ) + symbol.declarations.some( + (declaration) => + (ts.isClassDeclaration(declaration) || ts.isClassExpression(declaration)) && + /\.[cm]?js$/.test(declaration.getSourceFile().fileName) && + npmStaticPackageOfPath(declaration.getSourceFile().fileName) !== null, + ) ); }; const subtype = (actual: ts.Type, expected: ts.Type): boolean => { diff --git a/packages/compiler/src/frontend/npm.ts b/packages/compiler/src/frontend/npm.ts index 0afd1a59a..6cb1d3b5e 100644 --- a/packages/compiler/src/frontend/npm.ts +++ b/packages/compiler/src/frontend/npm.ts @@ -7,6 +7,7 @@ import { trackedReadFile, trackedRealpath, } from "./input-tracker.js"; +import { isUnloadedPackage } from "./package-shims.js"; import { resolveExports, resolvePackageImports } from "./resolve.js"; const NODE_IMPORT_CONDITIONS = new Set(["import", "node", "default"]); @@ -66,6 +67,13 @@ export function probeNodeImportRefusal( ): NodeImportRefusal | null { if (specifier.startsWith("#") || specifier.startsWith("node:")) return null; const name = packageNameOf(specifier); + // A package the project marks never loaded resolves like a missing one. + if (isUnloadedPackage(specifier)) { + return { + code: "ERR_MODULE_NOT_FOUND", + message: `Cannot find package '${name}' imported from ${resolve(fromFile)}`, + }; + } const parts = specifier.split("/"); const subparts = specifier.startsWith("@") ? parts.slice(2) : parts.slice(1); const subpath = subparts.length > 0 ? `./${subparts.join("/")}` : "."; @@ -152,6 +160,11 @@ export function probeNodeRequireRefusal( } const name = packageNameOf(specifier); const importer = resolve(fromFile); + // A package the project marks never loaded (package-shims.ts) throws + // exactly what Node throws when it is not installed. + if (isUnloadedPackage(specifier)) { + return { message: `Cannot find module '${specifier}'\nRequire stack:\n- ${importer}` }; + } for (let dir = dirname(importer); ;) { // A self-scope whose "name" matches: Node's require honors package // self-reference — conservative null (resolvable or its own story). diff --git a/packages/compiler/src/frontend/package-shims.ts b/packages/compiler/src/frontend/package-shims.ts new file mode 100644 index 000000000..06779064b --- /dev/null +++ b/packages/compiler/src/frontend/package-shims.ts @@ -0,0 +1,200 @@ +/** Package shims: npm packages that compile from an implementation module + * shipped with scriptc instead of from their own published JavaScript, and + * packages a project marks as never loaded. + * + * Two kinds of entries: + * + * - implementation: a built-in TypeScript module per supported major version + * (packages/compiler/shims//v.ts). The importing package's + * INSTALLED version selects the entry, so a program with undici 5.x in one + * workspace package and 7.x in another compiles both, each against its own + * version's behavior. A package that is not installed, or whose major + * version has no entry, is not shimmed and resolves exactly as before. + * + * The module edge goes through resolveProjectImport (preflight, module + * order, the CommonJS link) and resolveNpmImport7 / the pipeline's npm-edge + * walk skip the package. The checker sees the same module through the fs + * shadow: the installed package's types entry (index.d.ts) reads as a + * re-export of the shim, so types resolve per importer like the real + * package's declarations (tsconfig `paths` would map every importer to one + * file). + * + * - unloaded: a package the project declares never loaded + * (`"scriptc": { "packages": { "encoding": "unloaded" } }` in the entry's + * package.json). `require()` of it throws Node's MODULE_NOT_FOUND exactly + * as when the package is not installed, so the optional-dependency pattern + * `try { require("encoding") } catch {}` takes its absent branch, and the + * package never joins the program. + * + * A configured stand-in (for example a no-op tracer for dd-trace) is an + * implementation entry whose module the project supplies. Today a tsconfig + * `paths` mapping already does that: resolveProjectImport honors project + * paths for module edges and the checker uses the same map. A config + * spelling (`"dd-trace": "./native-shims/dd-trace.ts"`) would add the path + * as a project-owned implementation entry here. + */ +import { dirname, join, resolve } from "node:path"; +import { packageShimRoot, tsgoPath } from "./dts-paths.js"; +import { trackedDirectoryExists, trackedReadFile, trackedRealpath } from "./input-tracker.js"; +import { packageNameOfSpecifier } from "./workspace-registry.js"; + +interface ImplementationShim { + /** npm package name. */ + name: string; + /** Supported major versions and their entry modules, relative to the + * shim root. */ + versions: ReadonlyMap; +} + +const IMPLEMENTATION_SHIMS: ReadonlyMap = new Map([ + [ + "undici", + { + name: "undici", + versions: new Map([ + [5, "undici/v5.ts"], + [7, "undici/v7.ts"], + ]), + }, + ], +]); + +/** Package names the project marks never loaded. */ +let unloadedPackages: ReadonlySet = new Set(); + +/** Reads the project's package-shim configuration: the `scriptc.packages` + * map of the entry's nearest package.json. Called once per program load. */ +export function configurePackageShims(entryPath: string): void { + const packages = new Set(); + for (let dir = dirname(resolve(entryPath)); ;) { + const text = trackedReadFile(join(dir, "package.json")); + if (text !== null) { + let parsed: unknown = null; + try { + parsed = JSON.parse(text); + } catch { + parsed = null; + } + const config = + parsed !== null && typeof parsed === "object" + ? (parsed as Record)["scriptc"] + : undefined; + const map = + config !== null && typeof config === "object" + ? (config as Record)["packages"] + : undefined; + if (map !== null && typeof map === "object") { + for (const [name, kind] of Object.entries(map as Record)) { + if (kind === "unloaded") packages.add(name); + } + } + break; + } + const parent = dirname(dir); + if (parent === dir) break; + dir = parent; + } + unloadedPackages = packages; +} + +/** True for a package name the registry implements or the project marks + * never loaded: such a package never compiles its published JavaScript. */ +export function isPackageShimName(name: string): boolean { + return IMPLEMENTATION_SHIMS.has(name) || unloadedPackages.has(name); +} + +/** True when `specifier` names a package the project marks never loaded. */ +export function isUnloadedPackage(specifier: string): boolean { + if (unloadedPackages.size === 0) return false; + if (specifier.startsWith(".") || specifier.startsWith("/") || specifier.startsWith("#")) { + return false; + } + return unloadedPackages.has(packageNameOfSpecifier(specifier)); +} + +const installedCache = new Map(); + +/** The installed package directory Node's resolution would reach for + * `name` from `fromFile` (node_modules walked upward, realpath'd). */ +function installedPackageDir(fromFile: string, name: string): string | null { + const start = dirname(resolve(fromFile)); + const key = `${start}\0${name}`; + const cached = installedCache.get(key); + if (cached !== undefined) return cached; + let found: string | null = null; + for (let dir = start; ;) { + const candidate = join(dir, "node_modules", name); + if (trackedDirectoryExists(candidate)) { + found = trackedRealpath(candidate) ?? candidate; + break; + } + const parent = dirname(dir); + if (parent === dir) break; + dir = parent; + } + installedCache.set(key, found); + return found; +} + +/** The shim entry for an installed package directory, or null when the + * directory is not a registered package at a supported major version. */ +function shimEntryForPackageDir(pkgDir: string, expectedName: string | null): string | null { + const text = trackedReadFile(join(pkgDir, "package.json")); + if (text === null) return null; + let pkg: { name?: unknown; version?: unknown }; + try { + pkg = JSON.parse(text) as { name?: unknown; version?: unknown }; + } catch { + return null; + } + if (typeof pkg.name !== "string" || typeof pkg.version !== "string") return null; + if (expectedName !== null && pkg.name !== expectedName) return null; + const shim = IMPLEMENTATION_SHIMS.get(pkg.name); + if (shim === undefined) return null; + const major = Number(/^(\d+)\./.exec(pkg.version)?.[1] ?? NaN); + const entry = shim.versions.get(major); + return entry === undefined ? null : join(packageShimRoot(), entry); +} + +/** The shim module `specifier` resolves to from `fromFile`, or null. Only + * the package's main entry is shimmed; subpath imports resolve as before. */ +export function packageShimFor(fromFile: string, specifier: string): string | null { + if (!IMPLEMENTATION_SHIMS.has(specifier)) return null; + const dir = installedPackageDir(fromFile, specifier); + return dir === null ? null : shimEntryForPackageDir(dir, specifier); +} + +/** True when a bare specifier is served by a shim or marked unloaded: it is + * not an npm import for the npm-static machinery. */ +export function isShimmedImport(fromFile: string, specifier: string): boolean { + return isUnloadedPackage(specifier) || packageShimFor(fromFile, specifier) !== null; +} + +/** The fs-shadow answer for a shimmed package's declaration entry: the + * installed `index.d.ts` reads as a re-export of the version's shim module, + * so the checker types each importer against its own version. Undefined + * for every other path. */ +export function packageShimDeclarationText(path: string): string | undefined { + if (!path.endsWith("/index.d.ts")) return undefined; + const pkgDir = dirname(path); + const name = pkgDir.slice(pkgDir.lastIndexOf("/") + 1); + if (!IMPLEMENTATION_SHIMS.has(name)) return undefined; + const entry = shimEntryForPackageDir(pkgDir, name); + if (entry === null) return undefined; + const target = JSON.stringify(tsgoPath(entry)); + return `// scriptc: this package compiles from its built-in implementation.\nexport * from ${target};\n`; +} + +/** True for a file of the shipped package shims. They are compiler-owned + * implementations, type-checked by scriptc's own tests: like library + * declarations under skipLibCheck, a project's lib and @types/node choice + * must not turn their internals into the project's type errors. */ +export function isPackageShimFile(fileName: string): boolean { + const root = tsgoPath(packageShimRoot()); + return fileName.startsWith(root.endsWith("/") ? root : `${root}/`); +} + +/** Resets per-compile caches. */ +export function clearPackageShimCaches(): void { + installedCache.clear(); +} diff --git a/packages/compiler/src/frontend/pipeline.ts b/packages/compiler/src/frontend/pipeline.ts index 5081fc7be..29d40ccfa 100644 --- a/packages/compiler/src/frontend/pipeline.ts +++ b/packages/compiler/src/frontend/pipeline.ts @@ -23,6 +23,7 @@ import { npmStaticPackageOfPath, } from "./npm-static.js"; import { clearResolveCaches, resolveBareModule, resolveWorkspaceSourceModule } from "./resolve.js"; +import { isShimmedImport } from "./package-shims.js"; import { provenanceEntryFor } from "./provenance-registry.js"; import { isJsSourceFileName } from "./tsc-codes.js"; import { isRelativeSpecifier, packageNameOfSpecifier } from "./workspace-registry.js"; @@ -191,6 +192,9 @@ function detectAutoPackages( // Attested entries already resolve to program sources, so the // published package's declaration eligibility does not apply. if (provenanceEntryFor(spec) !== null) continue; + // Packages scriptc implements itself, or that the project marks never + // loaded, are not npm dependencies (package-shims.ts). + if (isShimmedImport(sf.fileName, spec)) continue; if (resolveWorkspaceSourceModule(sf.fileName, spec) !== null) continue; if (isRelativeSpecifier(spec) || spec.startsWith("node:") || spec.startsWith("#")) continue; // Bare builtin names ("fs", "path") are the builtin machinery's diff --git a/packages/compiler/src/frontend/program.ts b/packages/compiler/src/frontend/program.ts index 5c824c8ee..78e3d463b 100644 --- a/packages/compiler/src/frontend/program.ts +++ b/packages/compiler/src/frontend/program.ts @@ -89,6 +89,12 @@ import type { NpmStaticDeclarationProperties, NpmStaticOverloadSignature, } from "./npm-static-declaration-syntax.js"; +import { + configurePackageShims, + isPackageShimFile, + isPackageShimName, + isShimmedImport, +} from "./package-shims.js"; import { provenanceEntryFor, provenancePaths } from "./provenance-registry.js"; import { cjsLexedExportsOfFile, cjsVisibleNames } from "./cjs-syntax.js"; import { @@ -862,7 +868,13 @@ export function loadProgram( } externalTypes.set(specifier, declarationPath); } - const npmStaticPackages = [...new Set(opts?.npmStatic ?? [])]; + // Packages scriptc implements itself or the project marks never loaded + // (package-shims.ts) never compile their published JavaScript, even when + // named. + configurePackageShims(entryPath); + const npmStaticPackages = [...new Set(opts?.npmStatic ?? [])].filter( + (name) => !isPackageShimName(name), + ); // Resolve authored declaration entries before enabling the JS-only // package resolver. Safe overload metadata binds through the runtime // entry to one exact implementation file; it never contributes values @@ -1227,9 +1239,9 @@ function pureCjsExport7(program: ts.Program, stmt: ts.Statement): boolean { const symbol = checker.getSymbolAtLocation(value); return ( symbol !== undefined && - checker - .declarationsOf(symbol) - .some((decl) => ts.isFunctionDeclaration(decl) && decl.parent === stmt.parent) + symbol.declarations.some( + (decl) => ts.isFunctionDeclaration(decl) && decl.parent === stmt.parent, + ) ); } @@ -1238,9 +1250,9 @@ function sourceBinding7(program: ts.Program, ident: ts.Identifier): boolean { const symbol = checker.getSymbolAtLocation(ident); return ( symbol !== undefined && - checker - .declarationsOf(symbol) - .some((decl) => !decl.getSourceFile().isDeclarationFile && !ts.isSourceFile(decl)) + symbol.declarations.some( + (decl) => !decl.getSourceFile().isDeclarationFile && !ts.isSourceFile(decl), + ) ); } @@ -1958,7 +1970,7 @@ function stripRequireCasts7(expr: ts.Expression): ts.Expression { function isCreateRequireImport7(program: ts.Program, ident: ts.Identifier): boolean { const checker = program.getTypeChecker(); const symbol = checker.getSymbolAtLocation(ident); - const decl = symbol ? checker.declarationsOf(symbol)[0] : undefined; + const decl = symbol ? symbol.declarations[0] : undefined; if (decl === undefined || !ts.isImportSpecifier(decl)) return false; const importDecl = decl.parent?.parent?.parent; if (ts.isImportDeclaration(importDecl) && ts.isStringLiteral(importDecl.moduleSpecifier)) { @@ -1968,7 +1980,7 @@ function isCreateRequireImport7(program: ts.Program, ident: ts.Identifier): bool } if (symbol !== undefined && (symbol.flags & ts.SymbolFlags.Alias) !== 0) { const target = checker.getAliasedSymbol(symbol); - const targetDecl = checker.declarationsOf(target)[0]; + const targetDecl = target.declarations[0]; if (targetDecl !== undefined && targetDecl.getSourceFile().isDeclarationFile) { for ( let p: ts.Node | undefined = targetDecl.parent; @@ -1993,9 +2005,7 @@ export function isCreateRequireBinding7(program: ts.Program, callee: ts.Identifi const checker = program.getTypeChecker(); const symbol = checker.getSymbolAtLocation(callee); const decl = symbol - ? checker - .declarationsOf(symbol) - .find((d): d is ts.VariableDeclaration => ts.isVariableDeclaration(d)) + ? symbol.declarations.find((d): d is ts.VariableDeclaration => ts.isVariableDeclaration(d)) : undefined; if ( decl === undefined || @@ -2059,7 +2069,7 @@ function stableCreateRequireBindingReason7( } const checker = program.getTypeChecker(); const symbol = checker.getSymbolAtLocation(decl.name); - if (!symbol || checker.declarationsOf(symbol).length !== 1) + if (!symbol || symbol.declarations.length !== 1) return "its createRequire binding is redeclared"; if ((ts.getCombinedModifierFlags(decl) & ts.ModifierFlags.Export) !== 0) return "its createRequire result escapes through an exported binding"; @@ -2209,7 +2219,7 @@ function requireCallBindingKind7(program: ts.Program, node: ts.Node): RequireCal const checker = program.getTypeChecker(); const symbol = checker.getSymbolAtLocation(call.expression); if (symbol === undefined) return "ambient"; - const decls = checker.declarationsOf(symbol); + const decls = symbol.declarations; if (decls.every((d) => d.getSourceFile().isDeclarationFile)) return "ambient"; return isCreateRequireBinding7(program, call.expression) ? "createRequire" : "source"; } @@ -2364,7 +2374,7 @@ function selfImportTdzFences7( // The aliased declaration's TDZ horizon: the position after which reads // are initialized. Hoisted functions have none. const tdzEndOf = (aliased: ts.Symbol): number | null => { - const d = checker.valueDeclarationOf(aliased) ?? checker.declarationsOf(aliased)[0]; + const d = aliased.valueDeclaration ?? aliased.declarations[0]; if (d === undefined || d.getSourceFile() !== sf) return null; if (ts.isFunctionDeclaration(d)) return null; return d.getEnd(); @@ -2621,7 +2631,7 @@ function topLevelInertness7(program: ts.Program): (stmt: ts.Statement) => ts.Nod let sym = checker.getSymbolAtLocation(root); if (sym === undefined) return false; if (sym.flags & ts.SymbolFlags.Alias) sym = checker.getAliasedSymbol(sym); - const decls = checker.declarationsOf(sym); + const decls = sym.declarations; return decls.length > 0 && decls.every((d) => d.getSourceFile().isDeclarationFile); }; const hasDecorator = (n: ts.Node): boolean => @@ -3936,7 +3946,7 @@ function returnsOnlyClosuresOver7( if (symbol !== undefined && symbol.flags & ts.SymbolFlags.Alias) { symbol = checker.getAliasedSymbol(symbol); } - const decls = symbol === undefined ? [] : checker.declarationsOf(symbol); + const decls = symbol === undefined ? [] : symbol.declarations; if (decls.length !== 1) return false; const decl = decls[0]!; if (decl.getSourceFile().isDeclarationFile) return false; @@ -4059,7 +4069,7 @@ function laterJobCallee7(checker: ts.TypeChecker, callee: ts.Expression): boolea symbol = checker.getAliasedSymbol(symbol); } if (symbol === undefined) return false; - const decls = checker.declarationsOf(symbol); + const decls = symbol.declarations; if (decls.length === 0 || !decls.every((d) => d.getSourceFile().isDeclarationFile)) return false; if (LATER_JOB_BUILTINS7.has(name.text)) return true; // The JavaScript standard library's callbacks mostly run synchronously; @@ -4177,10 +4187,20 @@ function resolveNpmImport7( if (isRelativeSpecifier(specifier) || specifier.startsWith("node:")) { return null; } + // Node resolves a bare core-module name (`string_decoder`, `events`, + // `fs/promises`) to the builtin before it looks in node_modules, so an + // installed npm package of the same name never answers it. Prefix-only + // builtins (`node:test`) are listed with their prefix, so a bare `test` + // still names the package. + if (nodeBuiltinNames.has(specifier)) return null; // --provenance-sources: a registered specifier is NOT an npm import — // its attested source compiles as program modules (resolveProjectModule if (provenanceEntryFor(specifier) !== null) return null; + // Shimmed and never-loaded packages (package-shims.ts) are not npm + // imports either: the shim compiles as a program module, and an unloaded + // package's require() is Node's MODULE_NOT_FOUND. + if (isShimmedImport(fromFileName, specifier)) return null; const resolved = resolveBareModule(fromFileName, specifier, undefined, resolutionKind); if (!resolved) return null; if (!isNodeModulesPath(resolved.typesFile)) { @@ -4469,6 +4489,7 @@ function preflight7(load: LoadResult): { (!suppressedJsStrictness7(d) || enforcedInCheckedJs(p, d)) && !npmStaticFileSuppressed(d) && !nodeModulesJsSuppressed(d) && + (d.fileName === undefined || !isPackageShimFile(d.fileName)) && !namespaceCalleeSuppressed(p, d) && !inferredJsDiagnosticSuppressed(p, d) && !isNpmStaticSubclassArgument(p, d) && @@ -5684,18 +5705,16 @@ function analyzeEsmNamedImportLinks( if ( name === "default" && hasNodeTsDefaultInterfacePlaceholder7(dep) && - checker - .declarationsOf(resolved) - .some( - (declaration) => - ts.isInterfaceDeclaration(declaration) && - ts - .getModifiers(declaration) - ?.some((modifier) => modifier.kind === ts.SyntaxKind.DefaultKeyword) === true && - ts - .getModifiers(declaration) - ?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword) === true, - ) + resolved.declarations.some( + (declaration) => + ts.isInterfaceDeclaration(declaration) && + ts + .getModifiers(declaration) + ?.some((modifier) => modifier.kind === ts.SyntaxKind.DefaultKeyword) === true && + ts + .getModifiers(declaration) + ?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword) === true, + ) ) { return resolved; } diff --git a/packages/compiler/src/frontend/resolve.ts b/packages/compiler/src/frontend/resolve.ts index 36470a643..3d3d135b4 100644 --- a/packages/compiler/src/frontend/resolve.ts +++ b/packages/compiler/src/frontend/resolve.ts @@ -22,6 +22,7 @@ import { npmStaticPackageOfPath, npmStaticTransformPkgJson, } from "./npm-static.js"; +import { clearPackageShimCaches, packageShimFor } from "./package-shims.js"; import { provenanceEntryFor } from "./provenance-registry.js"; import { trackedAccessibleEntries, @@ -609,6 +610,10 @@ export function resolveProjectImport(fromFile: string, specifier: string): strin if (provenance !== null) return normalizeResolvedPath(provenance); const viaPaths = resolveViaProjectPaths(specifier); if (viaPaths !== null) return normalizeResolvedPath(viaPaths); + // A package scriptc implements itself (package-shims.ts): the installed + // version's shim module compiles in the package's place. + const shim = packageShimFor(fromFile, specifier); + if (shim !== null) return normalizeResolvedPath(shim); const pkgDir = nearestPkgDir(dirname(resolve(fromFile))); if (pkgDir === null) return null; const pkg = pkgJsonOf(pkgDir); @@ -1051,6 +1056,7 @@ export function resolveTypeDirective(name: string, fromFile: string): string | n /** Resolver metadata and the active project realm are shared within one * compile, never across compiles in a long-lived process. */ export function clearResolveCaches(): void { + clearPackageShimCaches(); pkgJsonCache.clear(); workspaceMembersCache.clear(); projectRealmPkgJson = null; diff --git a/packages/compiler/src/frontend/threads-import.ts b/packages/compiler/src/frontend/threads-import.ts index 12425c360..1944b2b81 100644 --- a/packages/compiler/src/frontend/threads-import.ts +++ b/packages/compiler/src/frontend/threads-import.ts @@ -11,7 +11,7 @@ export type ThreadsMember = "publish" | "sharesPublishedGraphs"; export function threadsImportOf(program: ts.Program, ident: ts.Identifier): ThreadsMember | null { const checker = program.getTypeChecker(); const symbol = checker.getSymbolAtLocation(ident); - const decl = symbol ? checker.declarationsOf(symbol)[0] : undefined; + const decl = symbol ? symbol.declarations[0] : undefined; if (decl === undefined || !ts.isImportSpecifier(decl)) return null; const importDecl = decl.parent?.parent?.parent; if ( diff --git a/packages/compiler/src/frontend/tier-analysis.ts b/packages/compiler/src/frontend/tier-analysis.ts index 248b3f922..2ed5c8c9b 100644 --- a/packages/compiler/src/frontend/tier-analysis.ts +++ b/packages/compiler/src/frontend/tier-analysis.ts @@ -474,7 +474,7 @@ class UnitResolver { let symbol = this.checker.getSymbolAtLocation(root); if (symbol !== undefined && (symbol.flags & ts.SymbolFlags.Alias) !== 0) symbol = this.checker.getAliasedSymbol(symbol); - const decls = symbol === undefined ? [] : this.checker.declarationsOf(symbol); + const decls = symbol === undefined ? [] : symbol.declarations; const decl = decls.find((d) => !d.getSourceFile().isDeclarationFile); if (decl === undefined) { // CommonJS `exports`/`module` as values: the module object. diff --git a/packages/compiler/src/frontend/ts7/checker-cache.ts b/packages/compiler/src/frontend/ts7/checker-cache.ts index b9a455a0d..1613272e1 100644 --- a/packages/compiler/src/frontend/ts7/checker-cache.ts +++ b/packages/compiler/src/frontend/ts7/checker-cache.ts @@ -44,9 +44,6 @@ export class CheckerCache { readonly managedSymbols = new Set(); readonly intrinsics = new Map(); readonly tupleTypeAnswer = new Map(); - readonly declsOf = new Map(); - readonly valueDeclOf = new Map(); - readonly sigDeclOf = new Map(); readonly callSigsOf = new Map(); readonly ctorSigsOf = new Map(); readonly constantValueOf = new Map(); @@ -90,9 +87,6 @@ export class CheckerCache { this.managedSymbols.clear(); this.intrinsics.clear(); this.tupleTypeAnswer.clear(); - this.declsOf.clear(); - this.valueDeclOf.clear(); - this.sigDeclOf.clear(); this.callSigsOf.clear(); this.ctorSigsOf.clear(); this.constantValueOf.clear(); diff --git a/packages/compiler/src/frontend/ts7/checker.test.ts b/packages/compiler/src/frontend/ts7/checker.test.ts index d9b6e44a1..7b3647b2d 100644 --- a/packages/compiler/src/frontend/ts7/checker.test.ts +++ b/packages/compiler/src/frontend/ts7/checker.test.ts @@ -80,7 +80,7 @@ test("unchanged AST identities receive independent semantic answers after a depe const oldType = old.getTypeAtLocation(name); const oldSymbol = old.getSymbolAtLocation(name)!; expect(old.typeToString(oldType)).toBe("number"); - expect(old.valueDeclarationOf(oldSymbol)).toBe(declaration(root, "result")); + expect(oldSymbol.valueDeclaration).toBe(declaration(root, "result")); h.paths.set(h.path("dependency.ts"), 'export const value: string = "changed";'); const second = h.session.updateSnapshot({ @@ -165,9 +165,6 @@ test("all public query and prefetch entry points reject a disposed facade before facade.typeToString(type); facade.prefetchSourceFile(root); const calls: Record unknown> = { - declarationsOf: () => facade.declarationsOf(symbol), - valueDeclarationOf: () => facade.valueDeclarationOf(symbol), - signatureDeclaration: () => facade.signatureDeclaration(signature), getCallSignatures: () => facade.getCallSignatures(type), getConstructSignatures: () => facade.getConstructSignatures(type), getPropertyOfType: () => facade.getPropertyOfType(type, "missing"), diff --git a/packages/compiler/src/frontend/ts7/checker.ts b/packages/compiler/src/frontend/ts7/checker.ts index f3ef51a3d..41eb0d0b9 100644 --- a/packages/compiler/src/frontend/ts7/checker.ts +++ b/packages/compiler/src/frontend/ts7/checker.ts @@ -1,5 +1,4 @@ import { CheckerCache } from "./checker-cache.js"; -import { InternalCompilerError } from "../../errors.js"; /* The checker facade: 5.9.3-shaped TypeChecker methods over 7.0.2's sync * client, built around the survey's feasibility verdict. Naive per-call use * of the 7.0.2 client costs 0.1-0.3 ms of IPC per query; the census counted @@ -238,60 +237,6 @@ export class CheckerFacade { this.cache.dispose(); } - /* ── the symbol-declaration surface (phase 3) ───────────────────────── - * 7's Symbol carries declarations as NodeHandles (server references), - * where 5.9.3 handed out the nodes themselves. The lowering reads - * symbol.declarations/valueDeclaration pervasively, so the facade owns - * the resolve step (NodeHandle.resolve into the client AST — identity- - * stable, probe-verified) and memoizes per symbol. Requires the project - * the symbols came from (options.project — Ts7Program supplies it). */ - - private requireProject(): Project { - const project = this.options.project; - if (!project) - throw new InternalCompilerError( - "CheckerFacade built without a project cannot resolve declarations", - ); - return project; - } - - /** 5.9.3's symbol.declarations (never undefined here: 7 answers an empty - * array where 5.9.3 answered undefined — callers treat them alike). */ - declarationsOf(symbol: Ts7Symbol): readonly Node[] { - this.ensureActive(); - let decls = this.cache.declsOf.get(symbol); - if (decls === undefined) { - const project = this.requireProject(); - const resolved: Node[] = []; - for (const handle of symbol.declarations) { - const node = handle.resolve(project); - if (node !== undefined) resolved.push(node); - } - decls = resolved; - this.cache.declsOf.set(symbol, decls); - } - return decls; - } - - /** 5.9.3's symbol.valueDeclaration. */ - valueDeclarationOf(symbol: Ts7Symbol): Node | undefined { - this.ensureActive(); - if (this.cache.valueDeclOf.has(symbol)) return this.cache.valueDeclOf.get(symbol); - const decl = symbol.valueDeclaration?.resolve(this.requireProject()); - this.cache.valueDeclOf.set(symbol, decl); - return decl; - } - - /** 5.9.3's signature.getDeclaration() (undefined for synthesized - * signatures — same contract as sig.declaration there). */ - signatureDeclaration(signature: Signature): Node | undefined { - this.ensureActive(); - if (this.cache.sigDeclOf.has(signature)) return this.cache.sigDeclOf.get(signature); - const decl = signature.declaration?.resolve(this.requireProject()); - this.cache.sigDeclOf.set(signature, decl); - return decl; - } - /** 5.9.3's type.getCallSignatures(). */ getCallSignatures(type: Type): readonly Signature[] { this.ensureActive(); diff --git a/packages/compiler/src/frontend/ts7/program-host.ts b/packages/compiler/src/frontend/ts7/program-host.ts index 2f2fdf76b..54c647220 100644 --- a/packages/compiler/src/frontend/ts7/program-host.ts +++ b/packages/compiler/src/frontend/ts7/program-host.ts @@ -40,6 +40,7 @@ import { trackedReadFile, trackedRealpath, } from "../input-tracker.js"; +import { packageShimDeclarationText } from "../package-shims.js"; /** The compiler options our createProgram accepts: TS7's CompilerOptions * shape (numeric enums for target/module/moduleResolution — the enums module @@ -154,6 +155,11 @@ export class Ts7Host { const replacement = shadow.readFile(fileName); if (replacement !== undefined) return stripSourceBom(replacement); } + // A package scriptc implements itself (package-shims.ts): its + // installed declaration entry re-exports the shim module, in every + // program the host creates. + const shim = packageShimDeclarationText(fileName); + if (shim !== undefined) return shim; const source = trackedReadFile(fileName); return source === null ? null : stripSourceBom(source); }, diff --git a/packages/compiler/src/frontend/ts7/semantic-model.ts b/packages/compiler/src/frontend/ts7/semantic-model.ts index 095021922..4920c96ae 100644 --- a/packages/compiler/src/frontend/ts7/semantic-model.ts +++ b/packages/compiler/src/frontend/ts7/semantic-model.ts @@ -294,8 +294,12 @@ export class SemanticSymbol { readonly name: string; readonly flags: number; readonly checkFlags: number; - readonly declarations: SemanticNodeHandle[]; - readonly valueDeclaration: SemanticNodeHandle | undefined; + /** The declarations' node handles, as the server sends them. */ + readonly declarationHandles: SemanticNodeHandle[]; + readonly valueDeclarationHandle: SemanticNodeHandle | undefined; + private declarationNodes: readonly AstNode[] | undefined; + private valueDeclarationNode: AstNode | undefined; + private valueDeclarationResolved = false; private readonly parent: number | undefined; private readonly exportSymbol: number | undefined; private membersCache: Map | undefined; @@ -310,16 +314,40 @@ export class SemanticSymbol { this.name = data.name.startsWith("___") ? data.name.slice(1) : data.name; this.flags = data.flags; this.checkFlags = data.checkFlags; - this.declarations = (data.declarations ?? []).map( + this.declarationHandles = (data.declarations ?? []).map( (handle) => new SemanticNodeHandle(handle, canonicalProject), ); - this.valueDeclaration = data.valueDeclaration + this.valueDeclarationHandle = data.valueDeclaration ? new SemanticNodeHandle(data.valueDeclaration, canonicalProject) : undefined; this.parent = data.parent; this.exportSymbol = data.exportSymbol; } + /** The declaration nodes, resolved in the symbol's project. A handle whose + * file the project doesn't have resolves to nothing and is left out. */ + get declarations(): readonly AstNode[] { + this.canonicalProject.ensureActive(); + if (this.declarationNodes === undefined) { + const nodes: AstNode[] = []; + for (const handle of this.declarationHandles) { + const node = handle.resolve(); + if (node !== undefined) nodes.push(node); + } + this.declarationNodes = nodes; + } + return this.declarationNodes; + } + + get valueDeclaration(): AstNode | undefined { + this.canonicalProject.ensureActive(); + if (!this.valueDeclarationResolved) { + this.valueDeclarationNode = this.valueDeclarationHandle?.resolve(); + this.valueDeclarationResolved = true; + } + return this.valueDeclarationNode; + } + getParent(): SemanticSymbol | undefined { return this.canonicalProject.fetchSymbol(this.id, "getParentOfSymbol", this.parent); } @@ -603,7 +631,10 @@ export class SemanticType { export class SemanticSignature { readonly id: number; readonly flags: number; - readonly declaration: SemanticNodeHandle | undefined; + /** The declaration's node handle, as the server sends it. */ + readonly declarationHandle: SemanticNodeHandle | undefined; + private declarationNode: AstNode | undefined; + private declarationResolved = false; constructor( private readonly data: SignatureResponse, @@ -611,11 +642,22 @@ export class SemanticSignature { ) { this.id = data.id; this.flags = data.flags; - this.declaration = data.declaration + this.declarationHandle = data.declaration ? new SemanticNodeHandle(data.declaration, project) : undefined; } + /** The declaration node, resolved in the signature's project; undefined + * for a signature the checker synthesized. */ + get declaration(): AstNode | undefined { + this.project.ensureActive(); + if (!this.declarationResolved) { + this.declarationNode = this.declarationHandle?.resolve(); + this.declarationResolved = true; + } + return this.declarationNode; + } + getTypeParameters(): SemanticType[] { return this.project.fetchTypes( this.id, diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index 123a5d1a0..90b5d1e9f 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -125,6 +125,19 @@ export function jsOpenObjectType( : type; } +/** The WebCrypto interfaces whose values are checked-dynamic: the crypto + * global, SubtleCrypto, CryptoKey handles and the algorithm dictionaries + * (scr_webcrypto.c). */ +export const WEBCRYPTO_DYN_TYPES: ReadonlySet = new Set([ + "Crypto", + "SubtleCrypto", + "CryptoKey", + "Algorithm", + "KeyAlgorithm", + "HmacImportParams", + "HmacKeyAlgorithm", +]); + export const FETCH_AMBIENT_TYPES = [ "Response", "Request", @@ -930,13 +943,11 @@ export function isWebAssemblyType( ): boolean { const symbol = type.getAliasSymbol() ?? type.getSymbol(); if (!symbol) return false; - return ctx.checker - .declarationsOf(symbol) - .some( - (d) => - ctx.isStdlibFile(d.getSourceFile()) && - isDeclaredInAmbientNamespace(d as ts.Declaration, "WebAssembly"), - ); + return symbol.declarations.some( + (d) => + ctx.isStdlibFile(d.getSourceFile()) && + isDeclaredInAmbientNamespace(d as ts.Declaration, "WebAssembly"), + ); } /** True when a type contains a record anywhere (the trigger for the @@ -1208,7 +1219,7 @@ export function isLegacyUrlSymbol( ctx: Pick, ): boolean { if (!symbol) return false; - for (const d of ctx.checker.declarationsOf(symbol)) { + for (const d of symbol.declarations) { if (!ctx.isStdlibFile(d.getSourceFile())) continue; let family = false; for (let n: ts.Node | undefined = d; n; n = n.parent) { @@ -1245,7 +1256,7 @@ export function isOsConstantsSymbol( types = false, ): boolean { if (!symbol) return false; - for (const d of ctx.checker.declarationsOf(symbol)) { + for (const d of symbol.declarations) { const alias = types && ts.isTypeAliasDeclaration(d) && d.name.text === "SignalConstants"; const signals = !types && ts.isVariableDeclaration(d) && ts.isIdentifier(d.name) && d.name.text === "signals"; @@ -1276,14 +1287,12 @@ function isWorkerHandleSymbol( symbol?.name === "MessagePort" || symbol?.name === "MessageChannel" || symbol?.name === "BroadcastChannel") && - ctx.checker - .declarationsOf(symbol) - .some( - (d) => - (ts.isClassDeclaration(d) || ts.isInterfaceDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && - isDeclaredInAmbientModule(d, "worker_threads"), - ) + symbol.declarations.some( + (d) => + (ts.isClassDeclaration(d) || ts.isInterfaceDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()) && + isDeclaredInAmbientModule(d, "worker_threads"), + ) ); } @@ -1297,9 +1306,7 @@ function isReceiveMessageResult( let found = false; for (const member of members) { if (member.flags & ts.TypeFlags.Undefined) continue; - const decl = member.getSymbol() - ? ctx.checker.declarationsOf(member.getSymbol()!)[0] - : undefined; + const decl = member.getSymbol() ? member.getSymbol()!.declarations[0] : undefined; let fn: ts.Node | undefined = decl?.parent; while (fn && ts.isUnionTypeNode(fn)) fn = fn.parent; if ( @@ -1339,6 +1346,31 @@ export function isWorkerHandleType( return handle; } +/** The return annotation of an `async *` declaration spelled with the + * protocol interfaces the generator object implements — + * `AsyncIterator`, `AsyncIterableIterator` or `AsyncIterable` (pi's + * `async *[Symbol.asyncIterator](): AsyncIterator`): the body always + * produces an async generator, so the declaration maps exactly like an + * `AsyncGenerator` annotation. Values of these types + * elsewhere stay unmapped (they can hold any object implementing the + * protocol). Null when the annotation is not one of them. */ +export function mapAsyncGeneratorAnnotation(type: ts.Type, ctx: TypeMapperCtx): IrType | null { + const { checker } = ctx; + const symbol = type.getSymbol(); + if ( + !symbol || + !["AsyncIterator", "AsyncIterableIterator", "AsyncIterable"].includes(symbol.name) || + !symbol.declarations + .some((d) => ts.isInterfaceDeclaration(d) && ctx.isStdlibFile(d.getSourceFile())) + ) + return null; + const args = checker.getTypeArguments(type as ts.TypeReference); + const channels = genChannels(args[0], args[1], args[2], ctx); + if (!channels) return null; + if (!genResultRecord(channels.yieldT, channels.retT, ctx.shapes, ctx.unions)) return null; + return { kind: "generator", async: true, ...channels }; +} + /** The native fetch handles (Headers, Request, Response), optionally in * arrays or with null/undefined: checked-dynamic handles wherever they are * stored, like the worker handles above. */ @@ -1358,13 +1390,11 @@ export function isFetchHandleType( if ( !symbol || (symbol.name !== "Headers" && symbol.name !== "Request" && symbol.name !== "Response") || - !ctx.checker - .declarationsOf(symbol) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()), - ) + !symbol.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()), + ) ) return false; } @@ -1439,15 +1469,14 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // TypeScript's intrinsic globalThis symbol has no declaration. A stored // reference needs native object identity, including var's undefined state. const intrinsic = type.getSymbol(); - if (intrinsic?.name === "globalThis" && checker.declarationsOf(intrinsic).length === 0) - return DYN; + if (intrinsic?.name === "globalThis" && intrinsic.declarations.length === 0) return DYN; if (ctx.jsDynamicDeclaration !== undefined && intrinsic !== undefined) { const objectLiteral = (intrinsic.flags & ts.SymbolFlags.ObjectLiteral) !== 0; const classInstance = (intrinsic.flags & ts.SymbolFlags.Class) !== 0 && type.isClassOrInterface(); if ( (objectLiteral || classInstance) && - checker.declarationsOf(intrinsic).some((decl) => ctx.jsDynamicDeclaration!(decl)) + intrinsic.declarations.some((decl) => ctx.jsDynamicDeclaration!(decl)) ) return DYN; } @@ -1465,7 +1494,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { const directAlias = type.getAliasSymbol(); if ( directAlias?.name === "ExecFileException" && - checker.declarationsOf(directAlias).some((decl) => ctx.isStdlibFile(decl.getSourceFile())) + directAlias.declarations.some((decl) => ctx.isStdlibFile(decl.getSourceFile())) ) { return { kind: "object", className: "%Error" }; } @@ -1475,14 +1504,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { const execException = type.getSymbol(); if ( execException?.name === "ExecException" && - checker - .declarationsOf(execException) - .some( - (decl) => - ts.isInterfaceDeclaration(decl) && - ctx.isStdlibFile(decl.getSourceFile()) && - isDeclaredInAmbientModule(decl, "child_process"), - ) + execException.declarations.some( + (decl) => + ts.isInterfaceDeclaration(decl) && + ctx.isStdlibFile(decl.getSourceFile()) && + isDeclaredInAmbientModule(decl, "child_process"), + ) ) { return { kind: "object", className: "%Error" }; } @@ -1578,7 +1605,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { const errorOptionsSymbol = widened.getSymbol(); if ( errorOptionsSymbol?.name === "ErrorOptions" && - checker.declarationsOf(errorOptionsSymbol).some((d) => ctx.isStdlibFile(d.getSourceFile())) + errorOptionsSymbol.declarations.some((d) => ctx.isStdlibFile(d.getSourceFile())) ) { return DYN; } @@ -1610,17 +1637,14 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { if ( parseArgsSym && PARSE_ARGS_DYN_TYPES.has(parseArgsSym.name) && - checker.declarationsOf(parseArgsSym).some(belongs) + parseArgsSym.declarations.some(belongs) ) return DYN; // ReturnType and instantiated conditional types can erase the alias. // Require every member to retain the util declaration provenance so // unrelated records with similar property names keep their own layout. const members = checker.getPropertiesOfType(widened); - if ( - members.length > 0 && - members.every((member) => checker.declarationsOf(member).some(belongs)) - ) + if (members.length > 0 && members.every((member) => member.declarations.some(belongs))) return DYN; } // The lib's BOXED wrapper interfaces used as TYPES (`const n: Number = @@ -1633,7 +1657,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { if ( boxSym && (boxSym.name === "Number" || boxSym.name === "String" || boxSym.name === "Boolean") && - checker.declarationsOf(boxSym).some((d) => ctx.isStdlibFile(d.getSourceFile())) + boxSym.declarations.some((d) => ctx.isStdlibFile(d.getSourceFile())) ) { return boxSym.name === "Number" ? F64 : boxSym.name === "String" ? STRING : BOOL; } @@ -1656,7 +1680,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { (ctorSym.name === "StringConstructor" || ctorSym.name === "NumberConstructor" || ctorSym.name === "BooleanConstructor") && - checker.declarationsOf(ctorSym).some((d) => ctx.isStdlibFile(d.getSourceFile())) + ctorSym.declarations.some((d) => ctx.isStdlibFile(d.getSourceFile())) ) { return funcOf( [STRING], @@ -1768,7 +1792,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { const classSym = widened.getSymbol(); const name = classSym?.name; if (name === "LifetimeRegion" || name === "InternTable" || name === "ThreadSlots") { - for (const d of checker.declarationsOf(classSym!)) { + for (const d of classSym!.declarations) { if (!ts.isClassDeclaration(d)) continue; const synthetic = name === "LifetimeRegion" @@ -1790,7 +1814,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // types-only package), so it maps structurally like a local declaration. // Values the package itself would produce stay refused at their import. const packageOnly = (sym: ts.Symbol | undefined): readonly ts.Node[] | null => { - const decls = sym ? checker.declarationsOf(sym) : undefined; + const decls = sym ? sym.declarations : undefined; return decls && decls.length > 0 && decls.every((d) => { @@ -2004,7 +2028,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // Class instances name a program class. The same symbol with construct // signatures names its static side, mapped to classval below. const widenedSym = widened.getSymbol(); - const classDecl = widenedSym ? checker.valueDeclarationOf(widenedSym) : undefined; + const classDecl = widenedSym ? widenedSym.valueDeclaration : undefined; if ( classDecl && (ts.isClassDeclaration(classDecl) || ts.isClassExpression(classDecl)) && @@ -2132,9 +2156,18 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { const psym = widened.getSymbol(); const isStdlibInterface = (name: string): boolean => psym?.name === name && - checker - .declarationsOf(psym) - .some((d) => ts.isInterfaceDeclaration(d) && ctx.isStdlibFile(d.getSourceFile())); + psym.declarations.some( + (d) => ts.isInterfaceDeclaration(d) && ctx.isStdlibFile(d.getSourceFile()), + ); + // The timer handles: an interface in the fallback and in @types/node 22+, + // a class (`class Timeout implements Timer`) in @types/node 20 and older. + const isStdlibTimerHandle = (name: string): boolean => + psym?.name === name && + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()), + ); // The builtin Error classes: references to the lib's Error interfaces map to the runtime-provided class // hierarchy (provenance, not the name — a user's own `class Error` // resolved through the class-instance branch above, and a user @@ -2148,9 +2181,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // fallback and in @types/node), not as a lib interface. if ( psym?.name === "AssertionError" && - checker - .declarationsOf(psym) - .some((d) => ts.isClassDeclaration(d) && ctx.isStdlibFile(d.getSourceFile())) + psym.declarations.some((d) => ts.isClassDeclaration(d) && ctx.isStdlibFile(d.getSourceFile())) ) return { kind: "object", className: "%AssertionError" }; // The lib's `Object` interface is a TOP type: tsc admits every @@ -2168,16 +2199,14 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // included, while unrelated user interfaces named Module stay structural. if ( (psym?.name === "ScriptcModule" || psym?.name === "Module") && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && - (psym.name === "ScriptcModule" || - isDeclaredInAmbientNamespace(d, "NodeJS") || - isDeclaredInAmbientModule(d, "module")), - ) + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()) && + (psym.name === "ScriptcModule" || + isDeclaredInAmbientNamespace(d, "NodeJS") || + isDeclaredInAmbientModule(d, "module")), + ) ) { return F64; } @@ -2190,14 +2219,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // et al. extend) is the same runtime value. Provenance-checked. if ( psym?.name === "EventEmitter" && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isClassDeclaration(d) || ts.isInterfaceDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && - (isDeclaredInAmbientModule(d, "events") || isDeclaredInAmbientNamespace(d, "NodeJS")), - ) + psym.declarations.some( + (d) => + (ts.isClassDeclaration(d) || ts.isInterfaceDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()) && + (isDeclaredInAmbientModule(d, "events") || isDeclaredInAmbientNamespace(d, "NodeJS")), + ) ) { return { kind: "object", className: RUNTIME_EMITTER_CLASS }; } @@ -2207,15 +2234,13 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // enclosing ambient module, like net.Server. if ( psym?.name === "Interface" && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && - (isDeclaredInAmbientModule(d, "readline") || - isDeclaredInAmbientModule(d, "readline/promises")), - ) + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()) && + (isDeclaredInAmbientModule(d, "readline") || + isDeclaredInAmbientModule(d, "readline/promises")), + ) ) { return F64; } @@ -2224,29 +2249,25 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // code member). if ( psym?.name === "Key" && - checker - .declarationsOf(psym) - .some( - (d) => - ts.isInterfaceDeclaration(d) && - ctx.isStdlibFile(d.getSourceFile()) && - (isDeclaredInAmbientModule(d, "readline") || - isDeclaredInAmbientModule(d, "readline/promises")), - ) + psym.declarations.some( + (d) => + ts.isInterfaceDeclaration(d) && + ctx.isStdlibFile(d.getSourceFile()) && + (isDeclaredInAmbientModule(d, "readline") || + isDeclaredInAmbientModule(d, "readline/promises")), + ) ) { return DYN; } // readline/promises.Readline: an f64 handle the same way. if ( psym?.name === "Readline" && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && - isDeclaredInAmbientModule(d, "readline/promises"), - ) + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()) && + isDeclaredInAmbientModule(d, "readline/promises"), + ) ) { return F64; } @@ -2258,14 +2279,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // and @types/node's class Channel. if ( psym?.name === "Channel" && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && - isDeclaredInAmbientModule(d, "diagnostics_channel"), - ) + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()) && + isDeclaredInAmbientModule(d, "diagnostics_channel"), + ) ) { return F64; } @@ -2274,14 +2293,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // process-lived; contexts ride the fiber machinery). if ( psym?.name === "AsyncLocalStorage" && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && - isDeclaredInAmbientModule(d, "async_hooks"), - ) + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()) && + isDeclaredInAmbientModule(d, "async_hooks"), + ) ) { return F64; } @@ -2289,21 +2306,19 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // runtime's tracing registry (five event channels per entry). if ( psym?.name === "TracingChannel" && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && - isDeclaredInAmbientModule(d, "diagnostics_channel"), - ) + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()) && + isDeclaredInAmbientModule(d, "diagnostics_channel"), + ) ) { return F64; } // Node names this result; the ambient WHATWG declaration uses a type literal. const encodeResult = psym && - checker.declarationsOf(psym).some((decl) => { + psym.declarations.some((decl) => { if (!ctx.isStdlibFile(decl.getSourceFile())) return false; if (ts.isInterfaceDeclaration(decl) && decl.name.text === "EncodeIntoResult") return true; const parent = decl.parent; @@ -2330,13 +2345,11 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // Their private encoding slot is omitted from enumeration and JSON. if ( (psym?.name === "TextEncoder" || psym?.name === "TextDecoder") && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()), - ) + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()), + ) ) { return { kind: "record", @@ -2366,13 +2379,11 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // record shape never surfaces. if ( psym?.name === "StringDecoder" && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()), - ) + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()), + ) ) { return { kind: "record", @@ -2392,7 +2403,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // record-holding binding forms compile. User-declared literals with // these member names keep the ordinary record path (their declarations // are not in declaration files). - const promiseDeclarations = psym ? checker.declarationsOf(psym) : []; + const promiseDeclarations = psym ? psym.declarations : []; if ( flags & ts.TypeFlags.Object && promiseDeclarations.length > 0 && @@ -2458,9 +2469,9 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { const alias = widened.getAliasSymbol(); if ( alias?.name === "ReadableStreamReadResult" && - checker - .declarationsOf(alias) - .some((d) => ts.isTypeAliasDeclaration(d) && ctx.isStdlibFile(d.getSourceFile())) + alias.declarations.some( + (d) => ts.isTypeAliasDeclaration(d) && ctx.isStdlibFile(d.getSourceFile()), + ) ) { const elemTs = widened.getAliasTypeArguments()?.[0]; const elem = @@ -2473,9 +2484,9 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { psym && (psym.name === "ReadableStreamReadValueResult" || psym.name === "ReadableStreamReadDoneResult") && - checker - .declarationsOf(psym) - .some((d) => ts.isInterfaceDeclaration(d) && ctx.isStdlibFile(d.getSourceFile())) + psym.declarations.some( + (d) => ts.isInterfaceDeclaration(d) && ctx.isStdlibFile(d.getSourceFile()), + ) ) { const elemTs = checker.getTypeArguments(widened as ts.TypeReference)[0]; const elem = @@ -2487,8 +2498,22 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // whose getters and methods dispatch through the handle. if ( (psym?.name === "SocketAddress" || psym?.name === "BlockList") && - checker - .declarationsOf(psym) + psym.declarations.some( + (d) => + (ts.isClassDeclaration(d) || ts.isInterfaceDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()), + ) + ) { + return DYN; + } + // WebCrypto: the crypto global and its subtle object are dynamic + // objects, CryptoKey values are native handles (scr_webcrypto.c), and + // the algorithm dictionaries the methods take and keys report are plain + // checked-dynamic objects. + if ( + psym && + WEBCRYPTO_DYN_TYPES.has(psym.name) && + psym.declarations .some( (d) => (ts.isClassDeclaration(d) || ts.isInterfaceDeclaration(d)) && @@ -2502,13 +2527,11 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // that export() returns is a plain checked-dynamic object. if ( (psym?.name === "KeyObject" || psym?.name === "JsonWebKey") && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isClassDeclaration(d) || ts.isInterfaceDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()), - ) + psym.declarations.some( + (d) => + (ts.isClassDeclaration(d) || ts.isInterfaceDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()), + ) ) { return DYN; } @@ -2517,17 +2540,15 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { (FETCH_AMBIENT_TYPES as readonly string[]).includes(psym.name) && !( psym.name === "ReadableStream" && - checker - .declarationsOf(psym) - .some((d) => ts.isInterfaceDeclaration(d) && isDeclaredInAmbientNamespace(d, "NodeJS")) - ) && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()), + psym.declarations.some( + (d) => ts.isInterfaceDeclaration(d) && isDeclaredInAmbientNamespace(d, "NodeJS"), ) + ) && + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()), + ) ) { return DYN; } @@ -2543,9 +2564,9 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { psym?.name === "Disposable" || psym?.name === "AsyncDisposable" || psym?.name === "DisposableStack") && - checker - .declarationsOf(psym) - .some((d) => ts.isInterfaceDeclaration(d) && ctx.isStdlibFile(d.getSourceFile())) + psym.declarations.some( + (d) => ts.isInterfaceDeclaration(d) && ctx.isStdlibFile(d.getSourceFile()), + ) ) { return DYN; } @@ -2555,7 +2576,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { if ( psym !== undefined && CIPHER_TYPE_NAMES.has(psym.name) && - checker.declarationsOf(psym).some((d) => ctx.isStdlibFile(d.getSourceFile())) + psym.declarations.some((d) => ctx.isStdlibFile(d.getSourceFile())) ) { return DYN; } @@ -2643,11 +2664,11 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // methods lower over that handle (loop-liveness bookkeeping); `Timeout | // null` becomes `number | null`, and clearTimeout/clearInterval take the // handle. Provenance-checked like the other named interfaces. - if (isStdlibInterface("Timeout")) return F64; + if (isStdlibTimerHandle("Timeout")) return F64; // NodeJS.Immediate / the fallback `Immediate` (setImmediate's return): // the same numeric-handle story over the check-phase queue — its own id // space, so clearTimeout of an Immediate no-ops like Node. - if (isStdlibInterface("Immediate")) return F64; + if (isStdlibTimerHandle("Immediate")) return F64; if ( isStdlibInterface("ArrayBuffer") || isStdlibInterface("SharedArrayBuffer") || @@ -2683,13 +2704,11 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // the same symbol); provenance-checked like URL. if ( psym?.name === "Buffer" && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()), - ) + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()), + ) ) { return bytesOf("u8"); } @@ -2700,13 +2719,11 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // record/class like any other. if ( psym?.name === "URL" && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()), - ) + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()), + ) ) { return { kind: "url" }; } @@ -2715,13 +2732,11 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // URLSearchParams` in the "url" module). Provenance-checked like URL. if ( psym?.name === "URLSearchParams" && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()), - ) + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()), + ) ) { return { kind: "searchParams" }; } @@ -2729,13 +2744,11 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // declarations' interface. Provenance-checked like URL. if ( psym?.name === "Stats" && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()), - ) + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()), + ) ) { return { kind: "stats" }; } @@ -2745,14 +2758,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { (psym?.name === "StatsFs" || psym?.name === "BigIntStatsFs" || psym?.name === "StatFsOptions") && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && - isDeclaredInAmbientModule(d, "fs"), - ) + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()) && + isDeclaredInAmbientModule(d, "fs"), + ) ) return DYN; if (isWorkerHandleSymbol(psym, ctx)) return DYN; @@ -2761,14 +2772,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // fallback and @types/node declare it in "fs/promises". if ( psym?.name === "FileHandle" && - checker - .declarationsOf(psym) - .some( - (d) => - ts.isInterfaceDeclaration(d) && - ctx.isStdlibFile(d.getSourceFile()) && - isDeclaredInAmbientModule(d, "fs/promises"), - ) + psym.declarations.some( + (d) => + ts.isInterfaceDeclaration(d) && + ctx.isStdlibFile(d.getSourceFile()) && + isDeclaredInAmbientModule(d, "fs/promises"), + ) ) { return { kind: "fileHandle" }; } @@ -2776,14 +2785,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // fallback and @types/node. if ( psym?.name === "Dir" && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && - isDeclaredInAmbientModule(d, "fs"), - ) + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()) && + isDeclaredInAmbientModule(d, "fs"), + ) ) { return { kind: "dir" }; } @@ -2793,13 +2800,11 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // Provenance-checked like Stats. if ( psym?.name === "SpawnSyncReturns" && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()), - ) + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()), + ) ) { return { kind: "spawnRes" }; } @@ -2811,14 +2816,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { (psym.name === "ChildProcess" || psym.name === "ChildProcessWithoutNullStreams" || psym.name === "ChildProcessByStdio") && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && - isDeclaredInAmbientModule(d, "child_process"), - ) + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()) && + isDeclaredInAmbientModule(d, "child_process"), + ) ) { return { kind: "child" }; } @@ -2831,7 +2834,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // site. if ( (psym?.name === "Server" || psym?.name === "Socket" || psym?.name === "TLSSocket") && - checker.declarationsOf(psym).some( + psym.declarations.some( (d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && ctx.isStdlibFile(d.getSourceFile()) && @@ -2857,14 +2860,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // "tls"`, and the shipped fallback mirrors it. if ( psym?.name === "SecureContext" && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && - isDeclaredInAmbientModule(d, "tls"), - ) + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()) && + isDeclaredInAmbientModule(d, "tls"), + ) ) { return { kind: "secureCtx" }; } @@ -2872,14 +2873,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // checked values so presence probes and repeated getter reads match Node. if ( psym?.name === "ScryptOptions" && - checker - .declarationsOf(psym) - .some( - (d) => - ts.isInterfaceDeclaration(d) && - ctx.isStdlibFile(d.getSourceFile()) && - isDeclaredInAmbientModule(d, "crypto"), - ) + psym.declarations.some( + (d) => + ts.isInterfaceDeclaration(d) && + ctx.isStdlibFile(d.getSourceFile()) && + isDeclaredInAmbientModule(d, "crypto"), + ) ) return DYN; // crypto.Hash / crypto.Hmac: native incremental digest handles. Keep @@ -2887,14 +2886,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // retain their ordinary structural/class representation. if ( (psym?.name === "Hash" || psym?.name === "Hmac") && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && - isDeclaredInAmbientModule(d, "crypto"), - ) + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()) && + isDeclaredInAmbientModule(d, "crypto"), + ) ) { return psym.name === "Hash" ? CRYPTOHASH_T : CRYPTOHMAC_T; } @@ -2904,14 +2901,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // mirrors it. if ( psym?.name === "FSWatcher" && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && - isDeclaredInAmbientModule(d, "fs"), - ) + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()) && + isDeclaredInAmbientModule(d, "fs"), + ) ) { return { kind: "fsWatcher" }; } @@ -2934,25 +2929,20 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // scalar; the lowered surface is write(string). if ( (psym?.name === "WriteStream" && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && - (isDeclaredInAmbientModule(d, "tty") || - (isDeclaredInAmbientModule(d, "process") && - isDeclaredInAmbientNamespace(d, "NodeJS"))), - )) || + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()) && + (isDeclaredInAmbientModule(d, "tty") || + (isDeclaredInAmbientModule(d, "process") && isDeclaredInAmbientNamespace(d, "NodeJS"))), + )) || (psym?.name === "WritableStream" && - checker - .declarationsOf(psym) - .some( - (d) => - ts.isInterfaceDeclaration(d) && - ctx.isStdlibFile(d.getSourceFile()) && - isDeclaredInAmbientNamespace(d, "NodeJS"), - )) + psym.declarations.some( + (d) => + ts.isInterfaceDeclaration(d) && + ctx.isStdlibFile(d.getSourceFile()) && + isDeclaredInAmbientNamespace(d, "NodeJS"), + )) ) { return PROCSTREAM_T; } @@ -2962,24 +2952,20 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // of the broad @types class fence where they are constructed. if ( (psym?.name === "ChildStdin" && - checker - .declarationsOf(psym) - .some( - (d) => - ts.isInterfaceDeclaration(d) && - ctx.isStdlibFile(d.getSourceFile()) && - isDeclaredInAmbientModule(d, "child_process"), - )) || + psym.declarations.some( + (d) => + ts.isInterfaceDeclaration(d) && + ctx.isStdlibFile(d.getSourceFile()) && + isDeclaredInAmbientModule(d, "child_process"), + )) || (psym?.name === "Writable" && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && - isNodeTypesPath(d.getSourceFile().fileName) && - isDeclaredInAmbientModule(d, "stream"), - )) + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()) && + isNodeTypesPath(d.getSourceFile().fileName) && + isDeclaredInAmbientModule(d, "stream"), + )) ) { return { kind: "childWriter" }; } @@ -2993,15 +2979,13 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { for (const [irName, rec] of RUNTIME_STREAM_CLASSES) { if ( psym?.name === rec.lib && - checker - .declarationsOf(psym) - .some( - (d) => - ts.isClassDeclaration(d) && - ctx.isStdlibFile(d.getSourceFile()) && - !isNodeTypesPath(d.getSourceFile().fileName) && - isDeclaredInAmbientModule(d, "stream"), - ) + psym.declarations.some( + (d) => + ts.isClassDeclaration(d) && + ctx.isStdlibFile(d.getSourceFile()) && + !isNodeTypesPath(d.getSourceFile().fileName) && + isDeclaredInAmbientModule(d, "stream"), + ) ) { return { kind: "object", className: irName }; } @@ -3011,15 +2995,13 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { if ( psym !== undefined && ZLIB_STREAM_MODES.has(psym.name) && - checker - .declarationsOf(psym) - .some( - (d) => - ts.isClassDeclaration(d) && - ctx.isStdlibFile(d.getSourceFile()) && - !isNodeTypesPath(d.getSourceFile().fileName) && - isDeclaredInAmbientModule(d, "zlib"), - ) + psym.declarations.some( + (d) => + ts.isClassDeclaration(d) && + ctx.isStdlibFile(d.getSourceFile()) && + !isNodeTypesPath(d.getSourceFile().fileName) && + isDeclaredInAmbientModule(d, "zlib"), + ) ) { return { kind: "object", className: "%Transform" }; } @@ -3028,37 +3010,31 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // (scr_fs_stream.c). if ( (psym?.name === "ReadStream" || psym?.name === "WriteStream") && - checker - .declarationsOf(psym) - .some( - (d) => - ts.isClassDeclaration(d) && - ctx.isStdlibFile(d.getSourceFile()) && - !isNodeTypesPath(d.getSourceFile().fileName) && - isDeclaredInAmbientModule(d, "fs"), - ) + psym.declarations.some( + (d) => + ts.isClassDeclaration(d) && + ctx.isStdlibFile(d.getSourceFile()) && + !isNodeTypesPath(d.getSourceFile().fileName) && + isDeclaredInAmbientModule(d, "fs"), + ) ) { return { kind: "object", className: psym.name === "ReadStream" ? "%Readable" : "%Writable" }; } if ( (psym?.name === "Readable" && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && - isDeclaredInAmbientModule(d, "stream"), - )) || + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()) && + isDeclaredInAmbientModule(d, "stream"), + )) || (psym?.name === "ReadableStream" && - checker - .declarationsOf(psym) - .some( - (d) => - ts.isInterfaceDeclaration(d) && - ctx.isStdlibFile(d.getSourceFile()) && - isDeclaredInAmbientNamespace(d, "NodeJS"), - )) + psym.declarations.some( + (d) => + ts.isInterfaceDeclaration(d) && + ctx.isStdlibFile(d.getSourceFile()) && + isDeclaredInAmbientNamespace(d, "NodeJS"), + )) ) { return { kind: "childStream" }; } @@ -3068,14 +3044,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // inside `declare module "dgram"`. if ( psym?.name === "Socket" && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && - isDeclaredInAmbientModule(d, "dgram"), - ) + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()) && + isDeclaredInAmbientModule(d, "dgram"), + ) ) { return { kind: "dgramSocket" }; } @@ -3087,14 +3061,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // both spellings). if ( (psym?.name === "TestContext" || psym?.name === "SuiteContext") && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && - isDeclaredInAmbientModule(d, "test"), - ) + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()) && + isDeclaredInAmbientModule(d, "test"), + ) ) { return { kind: "testCtx" }; } @@ -3103,14 +3075,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // { %call: F, mock } record through mapHybridCallableIntersection. if ( (psym?.name === "MockFunctionContext" || psym?.name === "MockFunctionCall") && - checker - .declarationsOf(psym) - .some( - (d) => - ts.isInterfaceDeclaration(d) && - ctx.isStdlibFile(d.getSourceFile()) && - isDeclaredInAmbientModule(d, "test"), - ) + psym.declarations.some( + (d) => + ts.isInterfaceDeclaration(d) && + ctx.isStdlibFile(d.getSourceFile()) && + isDeclaredInAmbientModule(d, "test"), + ) ) { const fnTs = checker.getTypeArguments(widened as ts.TypeReference)[0]; const fn = fnTs ? mapType(fnTs, ctx) : null; @@ -3126,9 +3096,9 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // Provenance-checked like the rest. if ( psym?.name === "ErrnoException" && - checker - .declarationsOf(psym) - .some((d) => ts.isInterfaceDeclaration(d) && ctx.isStdlibFile(d.getSourceFile())) + psym.declarations.some( + (d) => ts.isInterfaceDeclaration(d) && ctx.isStdlibFile(d.getSourceFile()), + ) ) { return { kind: "object", className: "%Error" }; } @@ -3140,14 +3110,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // Agent extends http's; both live in their ambient modules). if ( psym?.name === "Agent" && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && - (isDeclaredInAmbientModule(d, "http") || isDeclaredInAmbientModule(d, "https")), - ) + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()) && + (isDeclaredInAmbientModule(d, "http") || isDeclaredInAmbientModule(d, "https")), + ) ) { return { kind: "dyn" }; } @@ -3156,14 +3124,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // rule). if ( (psym?.name === "IncomingMessage" || psym?.name === "ServerResponse") && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && - isDeclaredInAmbientModule(d, "http"), - ) + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()) && + isDeclaredInAmbientModule(d, "http"), + ) ) { return psym.name === "IncomingMessage" ? { kind: "httpReq" } : { kind: "httpRes" }; } @@ -3179,14 +3145,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { (psym?.name === "Http2SecureServer" || psym?.name === "Http2ServerRequest" || psym?.name === "Http2ServerResponse") && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && - isDeclaredInAmbientModule(d, "http2"), - ) + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()) && + isDeclaredInAmbientModule(d, "http2"), + ) ) { return psym.name === "Http2SecureServer" ? { kind: "netServer" } @@ -3205,14 +3169,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { psym?.name === "ServerHttp2Session" || psym?.name === "ClientHttp2Stream" || psym?.name === "ServerHttp2Stream") && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && - isDeclaredInAmbientModule(d, "http2"), - ) + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()) && + isDeclaredInAmbientModule(d, "http2"), + ) ) { return psym.name === "Http2Server" ? { kind: "netServer" } @@ -3224,14 +3186,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // like IncomingMessage. if ( psym?.name === "ClientRequest" && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && - isDeclaredInAmbientModule(d, "http"), - ) + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()) && + isDeclaredInAmbientModule(d, "http"), + ) ) { return { kind: "httpClientReq" }; } @@ -3245,9 +3205,9 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // and interns the SAME shape. if ( psym?.name === "ProcessEnv" && - checker - .declarationsOf(psym) - .some((d) => ts.isInterfaceDeclaration(d) && ctx.isStdlibFile(d.getSourceFile())) + psym.declarations.some( + (d) => ts.isInterfaceDeclaration(d) && ctx.isStdlibFile(d.getSourceFile()), + ) ) { const value = withUndefinedArm(STRING, ctx.unions); if (!value) return null; @@ -3337,9 +3297,9 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { const alias = widened.getAliasSymbol(); if ( alias?.name === "IteratorResult" && - checker - .declarationsOf(alias) - .some((d) => ts.isTypeAliasDeclaration(d) && ctx.isStdlibFile(d.getSourceFile())) + alias.declarations.some( + (d) => ts.isTypeAliasDeclaration(d) && ctx.isStdlibFile(d.getSourceFile()), + ) ) { const targs = widened.getAliasTypeArguments() ?? []; const channels = genChannels(targs[0], targs[1], undefined, ctx); @@ -3474,14 +3434,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { psym?.name === "ReadOptions" || psym?.name === "ReadOptionsWithBuffer" || psym?.name === "WriteOptions") && - checker - .declarationsOf(psym) - .some( - (d) => - ts.isInterfaceDeclaration(d) && - ctx.isStdlibFile(d.getSourceFile()) && - (isDeclaredInAmbientModule(d, "fs/promises") || isDeclaredInAmbientModule(d, "fs")), - ) + psym.declarations.some( + (d) => + ts.isInterfaceDeclaration(d) && + ctx.isStdlibFile(d.getSourceFile()) && + (isDeclaredInAmbientModule(d, "fs/promises") || isDeclaredInAmbientModule(d, "fs")), + ) ) { const fields: { name: string; type: IrType }[] = []; for (const property of checker.getPropertiesOfType(widened)) { @@ -3505,16 +3463,14 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { psym?.name === "FileWritevResult" || psym?.name === "ReadVResult" || psym?.name === "WriteVResult") && - checker - .declarationsOf(psym) - .some( - (d) => - ts.isInterfaceDeclaration(d) && - ctx.isStdlibFile(d.getSourceFile()) && - (isDeclaredInAmbientModule(d, "fs/promises") || - ((psym.name === "ReadVResult" || psym.name === "WriteVResult") && - isDeclaredInAmbientModule(d, "fs"))), - ) + psym.declarations.some( + (d) => + ts.isInterfaceDeclaration(d) && + ctx.isStdlibFile(d.getSourceFile()) && + (isDeclaredInAmbientModule(d, "fs/promises") || + ((psym.name === "ReadVResult" || psym.name === "WriteVResult") && + isDeclaredInAmbientModule(d, "fs"))), + ) ) { const args = checker.getTypeArguments(widened as ts.TypeReference); if (args.length !== 1) return null; @@ -3572,13 +3528,11 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // provenance-checked, the Buffer/URL pattern). if ( psym?.name === "Dirent" && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()), - ) + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()), + ) ) { const args = checker.getTypeArguments(widened as ts.TypeReference); if (args.length > 1) return null; @@ -3626,18 +3580,16 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { }); if ( psym && - checker - .declarationsOf(psym) - .some( - (d) => - ts.isTypeLiteralNode(d) && - ts.isPropertySignature(d.parent) && - ts.isIdentifier(d.parent.name) && - d.parent.name.text === "times" && - ts.isInterfaceDeclaration(d.parent.parent) && - d.parent.parent.name.text === "CpuInfo" && - ctx.isStdlibFile(d.getSourceFile()), - ) + psym.declarations.some( + (d) => + ts.isTypeLiteralNode(d) && + ts.isPropertySignature(d.parent) && + ts.isIdentifier(d.parent.name) && + d.parent.name.text === "times" && + ts.isInterfaceDeclaration(d.parent.parent) && + d.parent.parent.name.text === "CpuInfo" && + ctx.isStdlibFile(d.getSourceFile()), + ) ) return cpuTimes(); if (isStdlibInterface("CpuInfo")) { @@ -3705,16 +3657,14 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // unmapped. if ( psym && - checker - .declarationsOf(psym) - .some( - (d) => - ts.isTypeLiteralNode(d) && - ctx.isStdlibFile(d.getSourceFile()) && - (isDeclaredInAmbientModule(d, "child_process") || - isDeclaredInAmbientModule(d, "util") || - isDeclaredInAmbientModule(d, "dns")), - ) + psym.declarations.some( + (d) => + ts.isTypeLiteralNode(d) && + ctx.isStdlibFile(d.getSourceFile()) && + (isDeclaredInAmbientModule(d, "child_process") || + isDeclaredInAmbientModule(d, "util") || + isDeclaredInAmbientModule(d, "dns")), + ) ) { const props = checker.getPropertiesOfType(widened); const names = @@ -3754,14 +3704,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // internal-slot object shows {} — the Dirent %dtype edge's cousin. if ( psym?.name === "X509Certificate" && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && - isDeclaredInAmbientModule(d, "crypto"), - ) + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()) && + isDeclaredInAmbientModule(d, "crypto"), + ) ) { return { kind: "record", @@ -3789,13 +3737,11 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { (psym?.name === "PerformanceEntry" || psym?.name === "PerformanceMark" || psym?.name === "PerformanceMeasure") && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()), - ) + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()), + ) ) { return { kind: "record", shapeId: perfEntryShape(ctx.shapes, ctx.unions) }; } @@ -4048,10 +3994,10 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { flags & ts.TypeFlags.Object && callSigs.length === 0 && psym !== undefined && - checker.declarationsOf(psym).length > 0 && - checker - .declarationsOf(psym) - .every((d) => ts.isInterfaceDeclaration(d) && !ctx.isStdlibFile(d.getSourceFile())) + psym.declarations.length > 0 && + psym.declarations.every( + (d) => ts.isInterfaceDeclaration(d) && !ctx.isStdlibFile(d.getSourceFile()), + ) ) { const CHILD_SURFACE = new Set([ "pid", @@ -4297,7 +4243,7 @@ function mapCallSignature(sig: ts.Signature, ctx: TypeMapperCtx): IrType | null // convention. Detected by the param-count mismatch against the // signature's own declaration, excluding an erased `this` parameter. { - const sigDecl = checker.signatureDeclaration(sig); + const sigDecl = sig.declaration; const declParams = sigDecl !== undefined && ts.isFunctionLike(sigDecl) ? sigDecl.parameters.nodes : undefined; if ( @@ -4317,7 +4263,7 @@ function mapCallSignature(sig: ts.Signature, ctx: TypeMapperCtx): IrType | null let typedRest = false; let dynRest = false; for (const p of sig.getParameters()) { - const decl = checker.valueDeclarationOf(p); + const decl = p.valueDeclaration; const rest = decl !== undefined && ts.isParameter(decl) && decl.dotDotDotToken !== undefined; // arity ABI selected by lambdaSignature. Treating an inferred any[] @@ -4353,9 +4299,7 @@ function mapCallSignature(sig: ts.Signature, ctx: TypeMapperCtx): IrType | null ts.isIdentifier(construction.expression) && construction.expression.text === "Promise" && constructorSymbol && - checker - .declarationsOf(constructorSymbol) - .some((site) => ctx.isStdlibFile(site.getSourceFile())); + constructorSymbol.declarations.some((site) => ctx.isStdlibFile(site.getSourceFile())); if ( !nativeExecutor && decl && @@ -4409,7 +4353,7 @@ function mapCallSignature(sig: ts.Signature, ctx: TypeMapperCtx): IrType | null // like declaredReturnType does for declarations. JS null/undefined // returns can read mutable fields, so callable values use the same // checked-native return ABI as their implementations. - const sigDecl = checker.signatureDeclaration(sig); + const sigDecl = sig.declaration; const jsUnitReturn = sigDecl !== undefined && isJsSourceFile(sigDecl.getSourceFile()) && @@ -4550,7 +4494,7 @@ function mapClassView( bases = ts.constituentTypes(type); } else { const symbol = type.getSymbol(); - const declarations = symbol ? checker.declarationsOf(symbol) : []; + const declarations = symbol ? symbol.declarations : []; if ( declarations.length === 0 || !declarations.every( @@ -4588,7 +4532,7 @@ function mapClassView( let representation: IrType | undefined; for (const base of bases) { const symbol = base.getSymbol(); - const declaration = symbol ? checker.valueDeclarationOf(symbol) : undefined; + const declaration = symbol ? symbol.valueDeclaration : undefined; const classBase = declaration && (ts.isClassDeclaration(declaration) || ts.isClassExpression(declaration)) && @@ -4612,8 +4556,8 @@ function mapClassView( const original = checker.getPropertyOfType(baseType, property.name); if (!original) return null; if (original === property) continue; - const originalDeclarations = checker.declarationsOf(original); - const declarations = checker.declarationsOf(property); + const originalDeclarations = original.declarations; + const declarations = property.declarations; // Instantiating an inherited member can create a new symbol without // changing its declaration or ABI. Only refinements need validation. if ( @@ -4651,7 +4595,7 @@ function mapCollectionView( if (!type.isTypeReference()) return null; const { checker } = ctx; const symbol = type.getSymbol(); - const declarations = symbol ? checker.declarationsOf(symbol) : []; + const declarations = symbol ? symbol.declarations : []; if ( declarations.length === 0 || !declarations.every( @@ -4704,16 +4648,14 @@ function mapCollectionView( const native = baseSymbol && ["Map", "ReadonlyMap", "Set", "ReadonlySet"].includes(baseSymbol.name) && - checker - .declarationsOf(baseSymbol) - .some((decl) => ts.isInterfaceDeclaration(decl) && ctx.isStdlibFile(decl.getSourceFile())); + baseSymbol.declarations.some( + (decl) => ts.isInterfaceDeclaration(decl) && ctx.isStdlibFile(decl.getSourceFile()), + ); const mapped = native ? mapType(base, baseCtx) : mapCollectionView(base, baseCtx, seen); if (mapped?.kind !== "map" && mapped?.kind !== "set") return null; const hasType = funcOf([mapped.kind === "map" ? mapped.key : mapped.elem], BOOL); for (const property of checker.getPropertiesOfType(type)) { - const own = checker - .declarationsOf(property) - .filter((decl) => !ctx.isStdlibFile(decl.getSourceFile())); + const own = property.declarations.filter((decl) => !ctx.isStdlibFile(decl.getSourceFile())); if (own.length === 0) continue; if ( property.name !== "has" || @@ -4863,8 +4805,8 @@ export function underConstAssertion(n: ts.Node): boolean { * panic repair): a property symbol declared by a property assignment whose * initializer is an EMPTY array literal inside a const assertion — its * type is provably `readonly []`, whatever the panicked query answered. */ -function constAssertedEmptyArrayProp(p: ts.Symbol, ctx: TypeMapperCtx): boolean { - const d = ctx.checker.valueDeclarationOf(p); +function constAssertedEmptyArrayProp(p: ts.Symbol): boolean { + const d = p.valueDeclaration; if (d === undefined || !ts.isPropertyAssignment(d)) return false; const init = d.initializer; // `a: [] as const` pins the field type by itself, wherever the literal sits. @@ -4887,9 +4829,9 @@ function mapGenericAwaitedAlias(type: ts.Type, ctx: TypeMapperCtx): IrType | nul if (aliasArgs.length !== 1 || !arg) return null; if (!(arg.flags & ts.TypeFlags.TypeParameter)) return null; // Provenance: the STANDARD LIBRARY's alias, not a user's shadowing one. - const isLibAlias = ctx.checker - .declarationsOf(alias) - .some((d) => ts.isTypeAliasDeclaration(d) && ctx.isStdlibFile(d.getSourceFile())); + const isLibAlias = alias.declarations.some( + (d) => ts.isTypeAliasDeclaration(d) && ctx.isStdlibFile(d.getSourceFile()), + ); if (!isLibAlias) return null; const bound = resolveTypeParam(arg); if (!bound) return null; @@ -4953,7 +4895,7 @@ function mapBoundMappedType(widened: ts.Type, ctx: TypeMapperCtx): IrType | null if (!resolveTypeParamTs || !isMappedShape(widened)) return null; const symbol = widened.getSymbol(); const node = symbol - ? checker.declarationsOf(symbol).find((d): d is ts.MappedTypeNode => ts.isMappedTypeNode(d)) + ? symbol.declarations.find((d): d is ts.MappedTypeNode => ts.isMappedTypeNode(d)) : undefined; const constraint = node?.typeParameter.constraint; if ( @@ -5039,9 +4981,7 @@ function evalMappedTemplate( const alias = type.getAliasSymbol(); const aliasArgs = type.getAliasTypeArguments(); const aliasDecl = alias - ? checker - .declarationsOf(alias) - .find((d): d is ts.TypeAliasDeclaration => ts.isTypeAliasDeclaration(d)) + ? alias.declarations.find((d): d is ts.TypeAliasDeclaration => ts.isTypeAliasDeclaration(d)) : undefined; if ( aliasDecl?.typeParameters?.nodes && @@ -5099,9 +5039,7 @@ function matchInferReference( const want = wantArgs[i]!; const symbol = want.flags & ts.TypeFlags.TypeParameter ? want.getSymbol() : undefined; const isInfer = symbol - ? checker - .declarationsOf(symbol) - .some((d) => d.parent !== undefined && ts.isInferTypeNode(d.parent)) + ? symbol.declarations.some((d) => d.parent !== undefined && ts.isInferTypeNode(d.parent)) : false; if (isInfer && symbol) inferred.set(symbol, gotArgs[i]!); else if (want !== gotArgs[i]) return undefined; @@ -5135,9 +5073,9 @@ function mapGenericUtilityAlias(widened: ts.Type, ctx: TypeMapperCtx): IrType | if (aliasArgs.length !== 1 || !arg) return null; if (!(arg.flags & ts.TypeFlags.TypeParameter)) return null; // Provenance: the STANDARD LIBRARY's alias, not a user's shadowing one. - const isLibAlias = ctx.checker - .declarationsOf(alias) - .some((d) => ts.isTypeAliasDeclaration(d) && ctx.isStdlibFile(d.getSourceFile())); + const isLibAlias = alias.declarations.some( + (d) => ts.isTypeAliasDeclaration(d) && ctx.isStdlibFile(d.getSourceFile()), + ); if (!isLibAlias) return null; const bound = resolveTypeParam(arg); if (!bound || bound.kind !== "record") return null; @@ -5390,7 +5328,7 @@ function mapErrorMetadataIntersection(type: ts.Type, ctx: TypeMapperCtx): IrType "AggregateError", "SuppressedError", ].includes(symbol.name) && - ctx.checker.declarationsOf(symbol).some((decl) => ctx.isStdlibFile(decl.getSourceFile())) + symbol.declarations.some((decl) => ctx.isStdlibFile(decl.getSourceFile())) ); }); if (!hasError) return null; @@ -5532,12 +5470,12 @@ function immutableSourceType(type: ts.Type, ctx: TypeMapperCtx): ts.Type | null if (!isMappedShape(type)) return null; const { checker } = ctx; const symbol = type.getSymbol(); - const decl = symbol ? checker.declarationsOf(symbol)[0] : undefined; + const decl = symbol ? symbol.declarations[0] : undefined; if (decl === undefined || !CONTRACT_TYPES_FILE.test(decl.getSourceFile().fileName)) return null; const props = checker.getPropertiesOfType(type); const seen = new Set(); for (const prop of props) { - const owner = checker.declarationsOf(prop)[0]?.parent; + const owner = prop.declarations[0]?.parent; if (owner === undefined || seen.has(owner)) continue; seen.add(owner); let source: ts.Type | null = null; @@ -5604,21 +5542,19 @@ function recordProvenanceOk(t: ts.Type, ctx: TypeMapperCtx): boolean { } if (isMappedShape(t)) return true; const tSym = t.getSymbol(); - const decls = tSym ? checker.declarationsOf(tSym) : undefined; + const decls = tSym ? tSym.declarations : undefined; if (!decls || decls.length === 0) { // Inferred destructuring parameters can have a synthesized anonymous // shape with no declaration or member symbols to trace to a source. return checker .getPropertiesOfType(t) .every((property) => - checker - .declarationsOf(property) - .every( - (decl) => - !decl.getSourceFile().isDeclarationFile || - ctx.isExternalTypeFile(decl.getSourceFile()) || - packageTypeFile(decl.getSourceFile(), ctx), - ), + property.declarations.every( + (decl) => + !decl.getSourceFile().isDeclarationFile || + ctx.isExternalTypeFile(decl.getSourceFile()) || + packageTypeFile(decl.getSourceFile(), ctx), + ), ); } return !decls.some((d) => { @@ -5727,7 +5663,7 @@ export function isOverloadedCallableMemberType(t: ts.Type, checker: ts.TypeCheck ) return false; return sigs.every((sig) => { - const decl = checker.signatureDeclaration(sig); + const decl = sig.declaration; if (decl === undefined) return false; if (!ts.isMethodSignatureDeclaration(decl) && !ts.isCallSignatureDeclaration(decl)) return false; @@ -5851,7 +5787,7 @@ function describeOverloadSetBlocker(t: ts.Type, ctx: TypeMapperCtx): string { }; const mapped = mapCallSignature(sig, sigCtx); if (mapped?.kind === "func" && mapped.rest !== true && mapped.argumentsAll !== true) continue; - const decl = checker.signatureDeclaration(sig); + const decl = sig.declaration; const spelled = decl ? decl.getText().replace(/\s+/g, " ").replace(/;$/, "") : ""; const which = spelled ? `overload '${spelled}'` : "one overload"; if (mapped?.kind === "func") @@ -6123,7 +6059,7 @@ function mapRecordTypeInner( // representable. const anonSym = widened.getSymbol(); const anonymousEmpty = - (anonSym === undefined || checker.declarationsOf(anonSym).length === 0) && + (anonSym === undefined || anonSym.declarations.length === 0) && !widened.isIntersectionType() && indexValue === undefined && checker.getPropertiesOfType(widened).length === 0; @@ -6162,10 +6098,7 @@ function mapRecordTypeInner( indexValue === undefined && checker.getCallSignatures(widened).length === 0 && checker.getConstructSignatures(widened).length === 0 && - !( - anonSym !== undefined && - checker.declarationsOf(anonSym).some((d) => ts.isObjectLiteralExpression(d)) - ) + !(anonSym !== undefined && anonSym.declarations.some((d) => ts.isObjectLiteralExpression(d))) ) { return DYN; } @@ -6186,7 +6119,7 @@ function mapRecordTypeInner( // their existing paths (the CJS export-table narrowing and the // checked-dynamic fallback), and .d.ts declarations stay out with // the rest of the lib's type worlds. - const decls = checker.declarationsOf(p); + const decls = p.declarations; const getDecl = decls.find((d) => ts.isGetAccessorDeclaration(d)); const setDecl = decls.find((d) => ts.isSetAccessorDeclaration(d)); const accessorOwned = @@ -6236,7 +6169,7 @@ function mapRecordTypeInner( // declarations and pass. if ( computed && - checker.declarationsOf(p).some((d) => { + p.declarations.some((d) => { const sf = d.getSourceFile(); return sf.isDeclarationFile && !ctx.isExternalTypeFile(sf) && !packageTypeFile(sf, ctx); }) @@ -6267,7 +6200,7 @@ function mapRecordTypeInner( if ( pt === null && (fieldTs.flags & ts.TypeFlags.Any) !== 0 && - constAssertedEmptyArrayProp(p, ctx) + constAssertedEmptyArrayProp(p) ) { pt = arrayOf(unitOnlyUnion(ctx.unions)); } @@ -6436,9 +6369,9 @@ export function describeComponentBlocker(widened: ts.Type, ctx: TypeMapperCtx): const container = psym !== undefined && Object.prototype.hasOwnProperty.call(STDLIB_CONTAINERS, psym.name) && - checker - .declarationsOf(psym) - .some((d) => ts.isInterfaceDeclaration(d) && ctx.isStdlibFile(d.getSourceFile())) + psym.declarations.some( + (d) => ts.isInterfaceDeclaration(d) && ctx.isStdlibFile(d.getSourceFile()), + ) ? psym.name : undefined; if (container !== undefined) { @@ -6569,7 +6502,7 @@ export function describeComponentBlocker(widened: ts.Type, ctx: TypeMapperCtx): if (callSigs.length === 1) { const sig = callSigs[0]!; if (sig.getTypeParameters().length > 0) return null; - const sigDecl = checker.signatureDeclaration(sig); + const sigDecl = sig.declaration; if ( sigDecl !== undefined && ts.isFunctionLike(sigDecl) && @@ -6619,7 +6552,7 @@ export function describeRecordMemberBlocker(widened: ts.Type, ctx: TypeMapperCtx // any one member's type — that story stays with the residual fence. if ( computed && - checker.declarationsOf(p).some((d) => { + p.declarations.some((d) => { const sf = d.getSourceFile(); return sf.isDeclarationFile && !ctx.isExternalTypeFile(sf) && !packageTypeFile(sf, ctx); }) diff --git a/packages/compiler/src/frontend/worker-target.ts b/packages/compiler/src/frontend/worker-target.ts index c395da593..99c97dded 100644 --- a/packages/compiler/src/frontend/worker-target.ts +++ b/packages/compiler/src/frontend/worker-target.ts @@ -25,7 +25,7 @@ function workerNamespace(program: ts.Program, expression: ts.Expression): boolea if (isBuiltinNamespaceImport(program, expression, "worker_threads")) return true; const checker = program.getTypeChecker(), symbol = checker.getSymbolAtLocation(expression); - const declaration = symbol ? checker.declarationsOf(symbol)[0] : undefined; + const declaration = symbol ? symbol.declarations[0] : undefined; return ( declaration !== undefined && ts.isVariableDeclaration(declaration) && @@ -53,7 +53,7 @@ export function isWorkerConstructor(program: ts.Program, expression: ts.Expressi if (isBuiltinMemberImport(program, expression, "worker_threads", "Worker")) return true; const checker = program.getTypeChecker(), symbol = checker.getSymbolAtLocation(expression); - const declaration = symbol ? checker.declarationsOf(symbol)[0] : undefined; + const declaration = symbol ? symbol.declarations[0] : undefined; if ( declaration && ts.isBindingElement(declaration) && diff --git a/packages/compiler/src/index.ts b/packages/compiler/src/index.ts index ca9dc4d18..6ce523611 100644 --- a/packages/compiler/src/index.ts +++ b/packages/compiler/src/index.ts @@ -528,6 +528,8 @@ async function compileExecutableNative( fileHandle: features.fileHandle, fetch: features.fetch, ...(features.eventTarget ? { eventTarget: true } : {}), + ...(features.warningReplace ? { warningReplace: true } : {}), + ...(features.stdioExtended ? { stdioExtended: true } : {}), zlib: features.zlib, assert: features.assert, inspect: features.inspect, @@ -554,6 +556,7 @@ async function compileExecutableNative( tls: features.tls, tlsCa: features.tlsCa, cipher: features.cipher, + webcrypto: features.webcrypto, ...(onArtifactReady === undefined ? {} : { onArtifactReady }), ...(ffi === null ? {} diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index f20d0945e..20dc82555 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -137,6 +137,9 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "fetch.streamNew", "fetch.streamFrom", "num.toFixed", + "num.toPrecision", + "num.toExponentialDigits", + "str.localeCase", "num.toStringRadix", "num.toStringRadixF64", "insp.jsonDyn", @@ -678,6 +681,8 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "crypto.randomIntMax", "crypto.randomFillView", "crypto.randomFillViewRest", + "crypto.getRandomValues", + "crypto.getRandomValuesDyn", "crypto.randomFillSyncDyn", "crypto.randomFillCb", "crypto.randomFillCbDyn", diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index f3ad4db28..4315e5467 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -568,6 +568,8 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ /** os.constants: the per-thread null-prototype constants object (dlopen, * errno, signals, priority, UV_UDP_REUSEADDR). Never throws. */ "os.constants": { argTypes: [], result: DYN }, + /** fs.constants: the target's cached null-prototype constants object. */ + "fs.constants": { argTypes: [], result: DYN }, /** The deprecated node:constants object (scr_os.c): the os groups and * fs.constants merged with the borrowed baked crypto.constants table, * frozen, one per thread. */ @@ -695,6 +697,13 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ /** `a.localeCompare(b)`: -1/0/1 in Node's root-locale collation for * Latin-script text (scr_str_locale_compare). Never throws. */ "str.localeCompare": { argTypes: [STRING, STRING], result: F64 }, + /** Annex B substr(start, length) with start ToNumber'd and length's + * presence; never throws. */ + "str.substr": { argTypes: [STRING, F64, F64, BOOL], result: STRING }, + /** toLocaleLowerCase/toLocaleUpperCase (scr_regex.c): the locales value + * as written, upper=true for toLocaleUpperCase. Throws for null and for + * locale forms with no native rules. */ + "str.localeCase": { argTypes: [STRING, DYN, BOOL], result: STRING }, "str.replaceLiteral": { argTypes: [STRING, STRING, STRING, BOOL], result: STRING }, // (subject, characters, NUL-separated replacement texts): the fused // chain of global one-character replacements. @@ -759,6 +768,11 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "num.toExponential": { argTypes: [F64], result: STRING }, "num.toFixed0": { argTypes: [F64], result: STRING }, "num.toFixed": { argTypes: [F64, F64], result: STRING }, + /** toPrecision(precision) with the argument ToNumber'd and its presence + * (undefined answers ToString); toExponential with a DEFINED + * fractionDigits. Both throw V8's RangeError for out-of-range digits. */ + "num.toPrecision": { argTypes: [F64, F64, BOOL], result: STRING }, + "num.toExponentialDigits": { argTypes: [F64, F64], result: STRING }, "num.toStringRadix": { argTypes: [F64, DYN], result: STRING }, /** toString(radix) with a number radix (scr_lib.c): truncates it as * ToIntegerOrInfinity does, THROWS RangeError outside 2..36. */ @@ -2605,6 +2619,11 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ * whether min was given. */ "crypto.randomFillView": { argTypes: [null, F64, F64], result: BYTES_U8 }, "crypto.randomFillViewRest": { argTypes: [null, F64], result: BYTES_U8 }, + /** crypto.getRandomValues: a view returns itself at its own type (the + * float/size DOMExceptions are thrown at run time); any other value + * takes the checked form. */ + "crypto.getRandomValues": { argTypes: [null], result: BYTES_U8 }, + "crypto.getRandomValuesDyn": { argTypes: [DYN], result: DYN }, "crypto.randomFillSyncDyn": { argTypes: [DYN, DYN, DYN], result: DYN }, "crypto.randomFillCb": { argTypes: [null, F64, F64, F64, null], result: VOID }, "crypto.randomFillCbDyn": { argTypes: [DYN, DYN, DYN, F64, null], result: VOID }, @@ -2617,6 +2636,14 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "crypto.generateKeyCb": { argTypes: [DYN, DYN, null], result: VOID }, "crypto.keyObjectInput": { argTypes: [DYN], result: BYTES_U8 }, "crypto.isKeyObject": { argTypes: [DYN], result: BOOL }, + /** 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.sign": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, + "webcrypto.verify": { argTypes: [DYN], result: { kind: "promise", inner: BOOL } }, + "webcrypto.digest": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, + "webcrypto.isCryptoKey": { argTypes: [DYN], result: BOOL }, "crypto.randomUUIDv7": { argTypes: [DYN], result: STRING }, "crypto.pbkdf2": { argTypes: [BYTES_U8, BYTES_U8, F64, F64, STRING], result: BYTES_U8 }, "crypto.randomBytesCb": { argTypes: [F64, null], result: VOID }, @@ -2655,6 +2682,9 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "buffer.fromStr": { argTypes: [STRING, STRING], result: BYTES_U8 }, "buffer.fromStrChecked": { argTypes: [STRING, STRING], result: BYTES_U8 }, "buffer.brand": { argTypes: [BYTES_U8], result: BYTES_U8 }, + /** Buffer.isBuffer over a typed view: whether the view carries the + * Buffer brand (any element kind; only Buffers do). Never throws. */ + "buffer.isBranded": { argTypes: [null], result: BOOL }, /** Buffer.from on checked-native strings, bytes, arrays and data-only * array-like/Buffer-JSON objects. The encoding is a normalized literal; * non-string inputs ignore it. Copies the input and may throw during @@ -4154,6 +4184,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ * warnings and DeprecationWarning emissions read their truthiness. */ "process.deprecationFlag": { argTypes: [F64], result: DYN }, "process.setDeprecationFlag": { argTypes: [F64, DYN], result: VOID }, + "process.setEmitWarning": { argTypes: [DYN], result: VOID }, /** AsyncLocalStorage (node:async_hooks — scr_async.c): stores are f64 * handles; contexts are immutable fiber-carried snapshots (spawned * fibers inherit the spawner's — Node's init-time capture). run/exit diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts index 6c8df7733..67622719c 100644 --- a/packages/compiler/src/ir/ir.ts +++ b/packages/compiler/src/ir/ir.ts @@ -2591,18 +2591,8 @@ export type IrExpr = | { kind: "logical"; op: "&&" | "||"; left: IrExpr; right: IrExpr; type: IrType; loc: SrcLoc } | { kind: "strConcat"; left: IrExpr; right: IrExpr; type: IrType; loc: SrcLoc } | { kind: "strEq"; negated: boolean; left: IrExpr; right: IrExpr; type: IrType; loc: SrcLoc } - /** String ordering. Ordinary source comparisons omit `utf16` and retain - * scriptc's documented code-point order; the default Array sort comparator - * sets it to request ECMAScript's UTF-16 code-unit order. */ - | { - kind: "strCmp"; - op: IrStrCmpOp; - left: IrExpr; - right: IrExpr; - utf16?: boolean; - type: IrType; - loc: SrcLoc; - } + /** String ordering in ECMAScript's UTF-16 code-unit order. */ + | { kind: "strCmp"; op: IrStrCmpOp; left: IrExpr; right: IrExpr; type: IrType; loc: SrcLoc } /** f64|bool → string, JS-exact (Number::toString / "true"/"false"). * Union operands dispatch through the per-union ToString helper (arms * fenced to unit/string/f64/bool by the frontend); a CAUGHT operand is diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts index 6e71fe91c..d9d32566d 100644 --- a/packages/compiler/src/ir/runtime-features.ts +++ b/packages/compiler/src/ir/runtime-features.ts @@ -17,6 +17,12 @@ export interface RuntimeFeatures { fetch: boolean; eventTarget: boolean; processEvents: boolean; + /** process.emitWarning is assigned (process.setEmitWarning). Declared + * in the literal's order: JSON.stringify of the features record must + * match between the Node-run and self-hosted compilers. */ + warningReplace: boolean; + /** stdin setEncoding / string data listeners / stdio 'resize' events. */ + stdioExtended: boolean; emitter: boolean; stream: boolean; zlib: boolean; @@ -49,6 +55,8 @@ export interface RuntimeFeatures { tls: boolean; tlsCa: boolean; cipher: boolean; + /** crypto.subtle importKey/sign/verify/digest: scr_webcrypto.c. */ + webcrypto: boolean; } const DYN_ASYNC_LIB_FNS: ReadonlySet = new Set([ @@ -75,6 +83,7 @@ const DYN_ASYNC_LIB_FNS: ReadonlySet = new Set([ "process.emitWarningText", "process.deprecationFlag", "process.setDeprecationFlag", + "process.setEmitWarning", // The performance timeline (scr_perf.c, linked with this unit for its // MaxPerformanceEntryBufferExceededWarning). "perf.mark", @@ -350,18 +359,41 @@ function boxesPromise(t: IrType, tables: TypeTables, seen: Set): boolean } case "object": { // A boxed class instance exposes its fields as dyn values (Object.keys, - // spread, inspection), boxing a promise any field holds. + // spread, inspection), boxing a promise any field holds. It + // materializes inherited fields too, and the instance can be any + // subclass. const key = `class:${t.className}`; if (seen.has(key)) return false; seen.add(key); - const def = tables.classes.get(t.className); - return def?.fields.some((f) => boxesPromise(f.type, tables, seen)) ?? false; + const related = (def: IrClassDef): boolean => { + for ( + let c: IrClassDef | undefined = def; + c; + c = c.base ? tables.classes.get(c.base) : undefined + ) + if (c.name === t.className) return true; + return false; + }; + for (const def of tables.classes.values()) { + if (!related(def)) continue; + for ( + let c: IrClassDef | undefined = def; + c; + c = c.base ? tables.classes.get(c.base) : undefined + ) + if (c.fields.some((f) => boxesPromise(f.type, tables, seen))) return true; + } + return false; } default: return false; } } +/** Names that reach the extended process-stream surface (the + * SCR_STDIO_EXTENDED events unit): stdin.setEncoding and 'resize'. */ +const STDIO_EXTENDED_NAMES: ReadonlySet = new Set(["setEncoding", "resize"]); + function scanRuntimeFeatures(mod: IrModule, stopAt?: keyof RuntimeFeatures): RuntimeFeatures { const features: RuntimeFeatures = { regex: false, @@ -375,6 +407,8 @@ function scanRuntimeFeatures(mod: IrModule, stopAt?: keyof RuntimeFeatures): Run (c) => c.name === RUNTIME_EVENT_TARGET_CLASS || c.name === RUNTIME_EVENT_CLASS, ), processEvents: false, + warningReplace: false, + stdioExtended: false, emitter: mod.workers === true || (mod.classes ?? []).some((c) => c.name === RUNTIME_EMITTER_CLASS), stream: (mod.classes ?? []).some((c) => RUNTIME_STREAM_CLASSES.has(c.name)), @@ -407,6 +441,7 @@ function scanRuntimeFeatures(mod: IrModule, stopAt?: keyof RuntimeFeatures): Run tls: false, tlsCa: false, cipher: false, + webcrypto: false, }; const keepGoing = (): boolean => stopAt === undefined || !features[stopAt]; let tables: TypeTables | undefined; @@ -422,6 +457,14 @@ function scanRuntimeFeatures(mod: IrModule, stopAt?: keyof RuntimeFeatures): Run if (REGEX_LIB_FNS.has(fn)) features.regex = true; if (LEGACY_TEXT_DECODER_LIB_FNS.has(fn)) features.legacyTextDecoder = true; if (fn.startsWith("fetch.")) features.fetch = true; + if (fn === "process.setEmitWarning") features.warningReplace = true; + // A typed string 'data' listener takes setEncoding-decoded chunks. + if ( + fn === "stdin.onData" && + node.args[0]?.type.kind === "func" && + node.args[0].type.params[0]?.kind === "string" + ) + features.stdioExtended = true; if (fn.startsWith("event.") || fn === "events.addAbortListener") features.eventTarget = true; if (ABORT_SIGNAL_LIB_FNS.has(fn)) features.eventTarget = true; if (PROCESS_EVENT_LIB_FNS.has(fn)) features.processEvents = true; @@ -502,6 +545,8 @@ function scanRuntimeFeatures(mod: IrModule, stopAt?: keyof RuntimeFeatures): Run } if (fn.startsWith("dgram.") || fn.startsWith("dns.")) features.dgram = true; if (fn.startsWith("tlsca.")) features.tlsCa = true; + // Locale case mapping runs the default conversion of the regex unit. + if (fn === "str.localeCase") features.regex = true; // scr_cipher.c over the mbedTLS archive; DEP0182 reports through the // process-warning unit, and unknown output encodings render through // the inspect unit. @@ -510,6 +555,8 @@ function scanRuntimeFeatures(mod: IrModule, stopAt?: keyof RuntimeFeatures): Run features.dynAsync = true; features.inspect = true; } + // crypto.subtle and CryptoKey: scr_webcrypto.c. + if (fn.startsWith("webcrypto.")) features.webcrypto = true; } else { switch (node.kind) { case "regexLit": @@ -527,6 +574,12 @@ function scanRuntimeFeatures(mod: IrModule, stopAt?: keyof RuntimeFeatures): Run break; case "dynInvoke": features.dynInvoke = true; + if (STDIO_EXTENDED_NAMES.has(node.method)) features.stdioExtended = true; + break; + // The process stream object's setEncoding and 'resize' members are + // reached by name (dynamic member calls and event-name strings). + case "strLit": + if (STDIO_EXTENDED_NAMES.has(node.value)) features.stdioExtended = true; break; case "awaitExpr": if (node.type.kind === "dyn") features.dynAsync = true; diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts index 5cf262e1f..4a961dfca 100644 --- a/packages/compiler/src/ir/validate.ts +++ b/packages/compiler/src/ir/validate.ts @@ -5103,7 +5103,8 @@ function validateFunction( let ok = cbT?.kind === "func" && cbT.ret.kind === "void" && cbT.params.length <= 1; if (ok && cbT?.kind === "func" && cbT.params.length === 1) { const p = cbT.params[0]!; - if (e.fn === "stdin.onData") ok = p.kind === "bytes" && p.elem === "u8"; + if (e.fn === "stdin.onData") + ok = (p.kind === "bytes" && p.elem === "u8") || p.kind === "string"; else if (e.fn === "stdin.onError") ok = p.kind === "object" && p.className === "%Error"; else ok = p.kind === "f64"; } @@ -5242,7 +5243,11 @@ function validateFunction( } function checkLibSpecialCall(e: IrExpr & { kind: "libCall" }): boolean { - if (e.fn === "crypto.randomFillView" || e.fn === "crypto.randomFillViewRest") { + if ( + e.fn === "crypto.randomFillView" || + e.fn === "crypto.randomFillViewRest" || + e.fn === "crypto.getRandomValues" + ) { // The filled view is the result, at its own element type. const view = e.args[0]?.type; if (view?.kind !== "bytes" || !typeEquals(e.type, view)) { diff --git a/packages/compiler/src/library/ir-policy.ts b/packages/compiler/src/library/ir-policy.ts index 3c3855e5c..2d1bc85d4 100644 --- a/packages/compiler/src/library/ir-policy.ts +++ b/packages/compiler/src/library/ir-policy.ts @@ -51,6 +51,7 @@ const LIB_MODE_REFUSED_PREFIXES: readonly [string, string][] = [ ["dns.", "the node:dns surface"], ["tls.", "the node:tls surface"], ["crypto.cipherNew", "crypto.createCipheriv and createDecipheriv"], + ["webcrypto.", "WebCrypto (crypto.subtle)"], ["fs.watch", "fs.watch"], ["watcher.", "fs.watch"], ["test.", "the node:test surface"], diff --git a/packages/compiler/src/native/cli.ts b/packages/compiler/src/native/cli.ts index c0d89bbbf..7d00acbc8 100644 --- a/packages/compiler/src/native/cli.ts +++ b/packages/compiler/src/native/cli.ts @@ -100,6 +100,7 @@ async function main(): Promise { nodeTest: false, tls: false, cipher: false, + webcrypto: false, tlsCa: false, }, options.optimization ?? "release", diff --git a/packages/compiler/src/native/prepare.ts b/packages/compiler/src/native/prepare.ts index bb901a674..54100a413 100644 --- a/packages/compiler/src/native/prepare.ts +++ b/packages/compiler/src/native/prepare.ts @@ -155,6 +155,7 @@ function validFeatures(value: unknown): value is NativeLinkFeatures { "tls", "tlsCa", "cipher", + "webcrypto", ].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 79984d6c5..72cdac31a 100644 --- a/packages/compiler/src/native/sanitizer.test.ts +++ b/packages/compiler/src/native/sanitizer.test.ts @@ -75,6 +75,8 @@ test.skipIf(process.platform !== "linux" && process.platform !== "darwin")( workers: false, youngNursery: false, eventTarget: false, + warningReplace: false, + stdioExtended: false, regex: true, copying: false, textDecoderLegacy: false, @@ -104,6 +106,7 @@ test.skipIf(process.platform !== "linux" && process.platform !== "darwin")( tls: false, tlsCa: false, cipher: false, + webcrypto: false, nativeFetch: false, netEffective: false, httpEffective: false, diff --git a/packages/compiler/surface-manifest.json b/packages/compiler/surface-manifest.json index 5cea78586..01d12dedc 100644 --- a/packages/compiler/surface-manifest.json +++ b/packages/compiler/surface-manifest.json @@ -618,6 +618,12 @@ "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" }, + { + "id": "node-builtin.crypto.getRandomValues", + "kind": "node-builtin", + "name": "crypto.getRandomValues", + "status": "static" + }, { "id": "node-builtin.crypto.hash", "kind": "node-builtin", @@ -868,6 +874,13 @@ "name": "fs.closeSync", "status": "static" }, + { + "id": "node-builtin.fs.constants", + "kind": "node-builtin", + "name": "fs.constants", + "status": "static", + "note": "node_constants.cc's fs group under the target libc's macros; members a platform lacks read as undefined" + }, { "id": "node-builtin.fs.copyFileSync", "kind": "node-builtin", @@ -4849,8 +4862,8 @@ "id": "stdlib.number.toPrecision", "kind": "stdlib", "name": "number.prototype.toPrecision", - "status": "unsupported", - "code": "SC2012" + "status": "static", + "note": "the lowered call form takes 0 to 1 arguments" }, { "id": "stdlib.number.toString", @@ -5663,6 +5676,20 @@ "code": "SC2020", "note": "WebAssembly support is planned as translation of each module whose bytes the build can see (an inline literal, or a file read from a path the compiler resolves) into native code; 'scriptc coverage' lists the modules this program would load (https://scriptc.dev/limitations)" }, + { + "id": "stdlib.webcrypto.CryptoKey", + "kind": "stdlib", + "name": "CryptoKey", + "status": "static", + "note": "secret HMAC keys with type, extractable, algorithm and usages, and instanceof CryptoKey" + }, + { + "id": "stdlib.webcrypto.subtle", + "kind": "stdlib", + "name": "globalThis.crypto.subtle", + "status": "static", + "note": "importKey('raw') for HMAC keys, HMAC sign and verify, and SHA-1/SHA-2 digest; other methods, algorithms and key formats are refused" + }, { "id": "syntax.async-generators", "kind": "syntax", diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/1112-number-methods.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/1112-number-methods.ts.json new file mode 100644 index 000000000..c95861fe3 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/1112-number-methods.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/1112-number-methods.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5639-union-arm-by-discriminant.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5639-union-arm-by-discriminant.ts.json new file mode 100644 index 000000000..58a162c8f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5639-union-arm-by-discriminant.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5639-union-arm-by-discriminant.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5640-generic-structural-class-narrowing.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5640-generic-structural-class-narrowing.ts.json new file mode 100644 index 000000000..091c37141 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5640-generic-structural-class-narrowing.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5640-generic-structural-class-narrowing.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6023-string-compare-utf16.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6023-string-compare-utf16.ts.json new file mode 100644 index 000000000..365709f61 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6023-string-compare-utf16.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6023-string-compare-utf16.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6080-take-then-flatmap.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6080-take-then-flatmap.ts.json new file mode 100644 index 000000000..ff4454b22 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6080-take-then-flatmap.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6080-take-then-flatmap.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6200-string-annexb-locale-methods.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6200-string-annexb-locale-methods.ts.json new file mode 100644 index 000000000..17a5c1e30 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6200-string-annexb-locale-methods.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6200-string-annexb-locale-methods.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6201-number-precision-exponential.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6201-number-precision-exponential.ts.json new file mode 100644 index 000000000..b80384ada --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6201-number-precision-exponential.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6201-number-precision-exponential.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6202-dyn-string-number-methods.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6202-dyn-string-number-methods.js.json new file mode 100644 index 000000000..dbf5146ed --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6202-dyn-string-number-methods.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6202-dyn-string-number-methods.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6203-any-string-number-methods.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6203-any-string-number-methods.ts.json new file mode 100644 index 000000000..42b17b7bb --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6203-any-string-number-methods.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6203-any-string-number-methods.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6204-crypto-get-random-values.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6204-crypto-get-random-values.ts.json new file mode 100644 index 000000000..86fcbcf58 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6204-crypto-get-random-values.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6204-crypto-get-random-values.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6205-crypto-get-random-values.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6205-crypto-get-random-values.js.json new file mode 100644 index 000000000..42d90fd62 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6205-crypto-get-random-values.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6205-crypto-get-random-values.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6206-buffer-is-buffer-views.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6206-buffer-is-buffer-views.ts.json new file mode 100644 index 000000000..7a9c25279 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6206-buffer-is-buffer-views.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6206-buffer-is-buffer-views.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6207-fs-constants-object.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6207-fs-constants-object.ts.json new file mode 100644 index 000000000..6f24442b0 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6207-fs-constants-object.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6207-fs-constants-object.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6208-fs-constants-require.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6208-fs-constants-require.cjs.json new file mode 100644 index 000000000..eac8fa544 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6208-fs-constants-require.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6208-fs-constants-require.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6209-buffer-module-alias.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6209-buffer-module-alias.cjs.json new file mode 100644 index 000000000..3d3c194b2 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6209-buffer-module-alias.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6209-buffer-module-alias.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6210-typeof-global-static-members.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6210-typeof-global-static-members.js.json new file mode 100644 index 000000000..33ccb2715 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6210-typeof-global-static-members.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6210-typeof-global-static-members.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6231-process-emit-warning-assign.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6231-process-emit-warning-assign.ts.json new file mode 100644 index 000000000..ac7be42ac --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6231-process-emit-warning-assign.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6231-process-emit-warning-assign.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6232-process-builtin-module-typed/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6232-process-builtin-module-typed/main.ts.json new file mode 100644 index 000000000..8adab38b7 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6232-process-builtin-module-typed/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6232-process-builtin-module-typed/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6233-for-await-class-async-generator.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6233-for-await-class-async-generator.ts.json new file mode 100644 index 000000000..89f131b0f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6233-for-await-class-async-generator.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6233-for-await-class-async-generator.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6234-weak-collection-class-fields.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6234-weak-collection-class-fields.ts.json new file mode 100644 index 000000000..e7e18883a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6234-weak-collection-class-fields.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6234-weak-collection-class-fields.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6235-bare-builtin-shadowed-package/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6235-bare-builtin-shadowed-package/main.ts.json new file mode 100644 index 000000000..eee3db81a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6235-bare-builtin-shadowed-package/main.ts.json @@ -0,0 +1,7 @@ +{ + "order": [ + "/tests/corpus/6235-bare-builtin-shadowed-package/helper.cjs", + "/tests/corpus/6235-bare-builtin-shadowed-package/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6236-stdin-encoding-tty-probes/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6236-stdin-encoding-tty-probes/main.ts.json new file mode 100644 index 000000000..c7546dd63 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6236-stdin-encoding-tty-probes/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6236-stdin-encoding-tty-probes/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6237-error-subclass-delete-stack/main.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6237-error-subclass-delete-stack/main.js.json new file mode 100644 index 000000000..a48385871 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6237-error-subclass-delete-stack/main.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6237-error-subclass-delete-stack/main.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6250-string-of-string-or-url.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6250-string-of-string-or-url.ts.json new file mode 100644 index 000000000..437bbfcb9 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6250-string-of-string-or-url.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6250-string-of-string-or-url.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6260-js-var-dyn-identity.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6260-js-var-dyn-identity.js.json new file mode 100644 index 000000000..bcd9bcf0f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6260-js-var-dyn-identity.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6260-js-var-dyn-identity.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6261-js-destructure-for-of-identity.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6261-js-destructure-for-of-identity.js.json new file mode 100644 index 000000000..3733ef399 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6261-js-destructure-for-of-identity.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6261-js-destructure-for-of-identity.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6262-js-file-scope-var-identity.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6262-js-file-scope-var-identity.js.json new file mode 100644 index 000000000..2db0c3083 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6262-js-file-scope-var-identity.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6262-js-file-scope-var-identity.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6263-js-factory-return-identity.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6263-js-factory-return-identity.cjs.json new file mode 100644 index 000000000..d0e4d3fcb --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6263-js-factory-return-identity.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6263-js-factory-return-identity.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6291-webcrypto-hmac-sign-verify.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6291-webcrypto-hmac-sign-verify.ts.json new file mode 100644 index 000000000..6b7a65b24 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6291-webcrypto-hmac-sign-verify.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6291-webcrypto-hmac-sign-verify.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6292-webcrypto-hmac-errors.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6292-webcrypto-hmac-errors.ts.json new file mode 100644 index 000000000..a1b096f32 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6292-webcrypto-hmac-errors.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6292-webcrypto-hmac-errors.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6293-webcrypto-digest.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6293-webcrypto-digest.ts.json new file mode 100644 index 000000000..7200349f0 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6293-webcrypto-digest.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6293-webcrypto-digest.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6294-webcrypto-js-values.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6294-webcrypto-js-values.mjs.json new file mode 100644 index 000000000..c68b4b49c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6294-webcrypto-js-values.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6294-webcrypto-js-values.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6295-jose-hs256/main.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6295-jose-hs256/main.mjs.json new file mode 100644 index 000000000..286b2c4bf --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6295-jose-hs256/main.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6295-jose-hs256/main.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6296-array-position-patterns.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6296-array-position-patterns.ts.json new file mode 100644 index 000000000..05d9557e7 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6296-array-position-patterns.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6296-array-position-patterns.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6297-library-runtime-idioms.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6297-library-runtime-idioms.mjs.json new file mode 100644 index 000000000..3c3818490 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6297-library-runtime-idioms.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6297-library-runtime-idioms.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6331-jose-root-entry/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6331-jose-root-entry/main.ts.json new file mode 100644 index 000000000..ad7513645 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6331-jose-root-entry/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6331-jose-root-entry/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6332-fluent-this-declaration/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6332-fluent-this-declaration/main.ts.json new file mode 100644 index 000000000..d848dd493 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6332-fluent-this-declaration/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6332-fluent-this-declaration/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6333-fluent-this-declaration-js/main.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6333-fluent-this-declaration-js/main.mjs.json new file mode 100644 index 000000000..4bde02942 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6333-fluent-this-declaration-js/main.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6333-fluent-this-declaration-js/main.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6334-discarded-logical-statements.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6334-discarded-logical-statements.mjs.json new file mode 100644 index 000000000..e510d9e2c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6334-discarded-logical-statements.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6334-discarded-logical-statements.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6335-discarded-logical-statements.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6335-discarded-logical-statements.ts.json new file mode 100644 index 000000000..fd7b159a5 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6335-discarded-logical-statements.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6335-discarded-logical-statements.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6340-prototype-reflection-optional-view-param.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6340-prototype-reflection-optional-view-param.ts.json new file mode 100644 index 000000000..89aef0f85 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6340-prototype-reflection-optional-view-param.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6340-prototype-reflection-optional-view-param.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6350-globalthis-assertion-state.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6350-globalthis-assertion-state.ts.json new file mode 100644 index 000000000..e4f556b43 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6350-globalthis-assertion-state.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6350-globalthis-assertion-state.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6352-globalthis-state-modules/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6352-globalthis-state-modules/main.ts.json new file mode 100644 index 000000000..2d11062c1 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6352-globalthis-state-modules/main.ts.json @@ -0,0 +1,8 @@ +{ + "order": [ + "/tests/corpus/6352-globalthis-state-modules/boot.ts", + "/tests/corpus/6352-globalthis-state-modules/state.ts", + "/tests/corpus/6352-globalthis-state-modules/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/bound-constructor-inspect-names.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/bound-constructor-inspect-names.cjs.json new file mode 100644 index 000000000..b6046aead --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/bound-constructor-inspect-names.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/bound-constructor-inspect-names.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/builtin-function-record-values/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/builtin-function-record-values/main.ts.json new file mode 100644 index 000000000..eb79fc63a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/builtin-function-record-values/main.ts.json @@ -0,0 +1,7 @@ +{ + "order": [ + "/tests/corpus/builtin-function-record-values/native.js", + "/tests/corpus/builtin-function-record-values/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/builtin-function-values.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/builtin-function-values.mjs.json new file mode 100644 index 000000000..f13bd6642 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/builtin-function-values.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/builtin-function-values.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/constructor-returns-chained-this.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/constructor-returns-chained-this.js.json new file mode 100644 index 000000000..ad1317aa6 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/constructor-returns-chained-this.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/constructor-returns-chained-this.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/constructor-returns-chained-this.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/constructor-returns-chained-this.ts.json new file mode 100644 index 000000000..9fb2595c4 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/constructor-returns-chained-this.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/constructor-returns-chained-this.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/constructor-returns-instance.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/constructor-returns-instance.js.json new file mode 100644 index 000000000..a85c33c1a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/constructor-returns-instance.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/constructor-returns-instance.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/constructor-returns-instance.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/constructor-returns-instance.ts.json new file mode 100644 index 000000000..e01b6a426 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/constructor-returns-instance.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/constructor-returns-instance.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/constructor-returns-this-super.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/constructor-returns-this-super.js.json new file mode 100644 index 000000000..fa81da713 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/constructor-returns-this-super.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/constructor-returns-this-super.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/es5-constructor-inspect-names.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/es5-constructor-inspect-names.cjs.json new file mode 100644 index 000000000..b5e8ba087 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/es5-constructor-inspect-names.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/es5-constructor-inspect-names.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/exponent-unit-base.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/exponent-unit-base.ts.json new file mode 100644 index 000000000..b8de10f6a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/exponent-unit-base.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/exponent-unit-base.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/key-order-late-optional.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/key-order-late-optional.ts.json new file mode 100644 index 000000000..5975cbb40 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/key-order-late-optional.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/key-order-late-optional.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/key-order-literal-flows.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/key-order-literal-flows.ts.json new file mode 100644 index 000000000..7b99fa5f6 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/key-order-literal-flows.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/key-order-literal-flows.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/key-order-spread-copies.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/key-order-spread-copies.ts.json new file mode 100644 index 000000000..43b336a1b --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/key-order-spread-copies.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/key-order-spread-copies.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/let-uninitialized-function-binding.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/let-uninitialized-function-binding.ts.json new file mode 100644 index 000000000..f5706acaa --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/let-uninitialized-function-binding.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/let-uninitialized-function-binding.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/logical-and-truthy-index.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/logical-and-truthy-index.ts.json new file mode 100644 index 000000000..2e7388984 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/logical-and-truthy-index.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/logical-and-truthy-index.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/constructor-returns-instance-super.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/constructor-returns-instance-super.ts.json new file mode 100644 index 000000000..86b15f680 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/constructor-returns-instance-super.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/constructor-returns-instance-super.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/constructor-returns-object.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/constructor-returns-object.ts.json new file mode 100644 index 000000000..f3415a937 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/constructor-returns-object.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/constructor-returns-object.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/retired-engine-1112-number-methods.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/retired-engine-1112-number-methods.ts.json deleted file mode 100644 index 27b24a99a..000000000 --- a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/retired-engine-1112-number-methods.ts.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "order": [ - "/tests/diagnostics/retired-engine-1112-number-methods.ts" - ], - "diags": [] -} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/webcrypto-refusals.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/webcrypto-refusals.ts.json new file mode 100644 index 000000000..5f9d95858 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/webcrypto-refusals.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/webcrypto-refusals.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/navigator.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/navigator.ts.json new file mode 100644 index 000000000..27fe582a8 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/navigator.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/node-types/navigator.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/webcrypto-hmac.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/webcrypto-hmac.ts.json new file mode 100644 index 000000000..af889405d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/webcrypto-hmac.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/node-types/webcrypto-hmac.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/undici5/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/undici5/main.ts.json new file mode 100644 index 000000000..80e48f7f1 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/undici5/main.ts.json @@ -0,0 +1,21 @@ +{ + "order": [ + "/packages/compiler/shims/undici/core/errors.ts", + "/packages/compiler/shims/undici/core/connector.ts", + "/packages/compiler/shims/undici/core/diagnostics.ts", + "/packages/compiler/shims/undici/core/readable.ts", + "/packages/compiler/shims/undici/core/util.ts", + "/packages/compiler/shims/undici/core/dispatcher.ts", + "/packages/compiler/shims/undici/core/request.ts", + "/packages/compiler/shims/undici/core/timers.ts", + "/packages/compiler/shims/undici/core/client.ts", + "/packages/compiler/shims/undici/core/pool.ts", + "/packages/compiler/shims/undici/core/agent.ts", + "/packages/compiler/shims/undici/core/api.ts", + "/packages/compiler/shims/undici/core/fetch.ts", + "/packages/compiler/shims/undici/core/errors-v5.ts", + "/packages/compiler/shims/undici/v5.ts", + "/tests/fixtures/npm/cases/undici5/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/undici7/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/undici7/main.ts.json new file mode 100644 index 000000000..d4b65f4ae --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/undici7/main.ts.json @@ -0,0 +1,21 @@ +{ + "order": [ + "/packages/compiler/shims/undici/core/errors.ts", + "/packages/compiler/shims/undici/core/connector.ts", + "/packages/compiler/shims/undici/core/diagnostics.ts", + "/packages/compiler/shims/undici/core/readable.ts", + "/packages/compiler/shims/undici/core/util.ts", + "/packages/compiler/shims/undici/core/dispatcher.ts", + "/packages/compiler/shims/undici/core/request.ts", + "/packages/compiler/shims/undici/core/timers.ts", + "/packages/compiler/shims/undici/core/client.ts", + "/packages/compiler/shims/undici/core/pool.ts", + "/packages/compiler/shims/undici/core/agent.ts", + "/packages/compiler/shims/undici/core/api.ts", + "/packages/compiler/shims/undici/core/fetch.ts", + "/packages/compiler/shims/undici/core/errors-v7.ts", + "/packages/compiler/shims/undici/v7.ts", + "/tests/fixtures/npm/cases/undici7/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/unloaded-optional-dependency/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/unloaded-optional-dependency/main.ts.json new file mode 100644 index 000000000..2f0712b1d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/unloaded-optional-dependency/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/npm/cases/unloaded-optional-dependency/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/parity.test.ts b/packages/compiler/test/ts7/parity.test.ts index 872676c81..4904f2720 100644 --- a/packages/compiler/test/ts7/parity.test.ts +++ b/packages/compiler/test/ts7/parity.test.ts @@ -488,7 +488,7 @@ test("symbol declarations resolve back into the same client AST", () => { expect(sym7).toBeDefined(); const aliased = w.c7.getAliasedSymbol(sym7!); expect(aliased.name).toBe("answer"); - const declHandle = aliased.declarations[0]; + const declHandle = aliased.declarationHandles[0]; expect(declHandle).toBeDefined(); const decl = declHandle!.resolve(w.p7.project); expect(decl).toBeDefined(); diff --git a/packages/compiler/test/ts7/rpc-api.test.ts b/packages/compiler/test/ts7/rpc-api.test.ts index d323bd901..28b63ec34 100644 --- a/packages/compiler/test/ts7/rpc-api.test.ts +++ b/packages/compiler/test/ts7/rpc-api.test.ts @@ -2,7 +2,7 @@ import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { expect, test } from "vitest"; -import { API, type Snapshot } from "typescript/unstable/sync"; +import { API, type Snapshot, type Symbol as SdkSymbol } from "typescript/unstable/sync"; import { Ts7Api, ts7Executable } from "../../src/frontend/ts7/rpc-api.js"; import { Ts7RpcClient } from "../../src/frontend/ts7/rpc-client.js"; import type { Ts7FileSystem } from "../../src/frontend/ts7/rpc-filesystem.js"; @@ -16,6 +16,15 @@ import { SemanticType, } from "../../src/frontend/ts7/semantic-model.js"; +// scriptc's symbols keep their declarations' node handles in +// `declarationHandles`, and the SDK's in `declarations`. +function declarationHandlesOf(symbol: SdkSymbol): SdkSymbol["declarations"] { + return ( + (symbol as unknown as { declarationHandles?: SdkSymbol["declarations"] }).declarationHandles ?? + symbol.declarations + ); +} + function tsgoPath(path: string): string { return process.platform === "win32" ? path.replaceAll("\\", "/") : path; } @@ -43,7 +52,7 @@ function programFacts(snapshot: Snapshot, config: string, path: string) { text: file.text, type: checker.typeToString(type), symbol: symbol.name, - declaration: symbol.declarations[0]!.resolve()!.getText(file), + declaration: declarationHandlesOf(symbol)[0]!.resolve()!.getText(file), syntactic: project.program.getSyntacticDiagnostics(path), semantic: project.program.getSemanticDiagnostics(path), globals: project.program.getGlobalDiagnostics(), @@ -83,10 +92,10 @@ test("production RPC bridge preserves pinned SDK parser/checker behavior", () => expect(project.checker.getTypeAtPosition(path, source.text.indexOf("answer"))).toBe(first); const symbol = project.checker.getSymbolAtPosition(path, source.text.indexOf("answer")); expect(symbol).toBeInstanceOf(SemanticSymbol); - expect(symbol!.declarations[0]).toBeInstanceOf(SemanticNodeHandle); + expect(symbol!.declarationHandles[0]).toBeInstanceOf(SemanticNodeHandle); expect(project.checker.getSymbolAtPosition(path, source.text.indexOf("answer"))).toBe(symbol); - expect(symbol!.declarations[0]!.resolve()).toBe(symbol!.declarations[0]!.resolve()); - const declaration = symbol!.declarations[0]!.resolve()!; + expect(symbol!.declarationHandles[0]!.resolve()).toBe(symbol!.declarationHandles[0]!.resolve()); + const declaration = symbol!.declarationHandles[0]!.resolve()!; expect(declaration).toBeInstanceOf(AstNode); const variable = source.statements .nodes[0]! as import("../../src/frontend/ts7/ast-types.js").VariableStatement; @@ -205,9 +214,11 @@ test("native semantic registries preserve project scope and snapshot lifetimes", sharedSymbol: symbolA === symbolB, sharedType: typeA === typeB, sharedSignature: signatureA === signatureB, - sameDeclaration: symbolA.declarations[0]!.resolve() === symbolB.declarations[0]!.resolve(), + sameDeclaration: + declarationHandlesOf(symbolA)[0]!.resolve() === + declarationHandlesOf(symbolB)[0]!.resolve(), explicitDeclaration: - symbolB.declarations[0]!.resolve(b) === + declarationHandlesOf(symbolB)[0]!.resolve(b) === nodesOf( (nodesOf(fileB.statements)[1] as import("typescript/unstable/ast").VariableStatement) .declarationList.declarations, @@ -237,7 +248,7 @@ test("native semantic registries preserve project scope and snapshot lifetimes", expect(next.signatureA).not.toBe(actual.signatureA); first.dispose(); expect(() => actual.typeA.getSymbol()).toThrow("disposed"); - expect(() => actual.symbolA.declarations[0]!.resolve()).toThrow("disposed"); + expect(() => declarationHandlesOf(actual.symbolA)[0]!.resolve()).toThrow("disposed"); expect(next.signatureA.getParameters()[0]!.name).toBe("value"); expect(second.getProject(firstConfig)!.checker.typeToString(next.typeA)).toBe("42"); second.dispose(); diff --git a/packages/compiler/test/undici-shim-types.test.ts b/packages/compiler/test/undici-shim-types.test.ts new file mode 100644 index 000000000..c7bc4ccaf --- /dev/null +++ b/packages/compiler/test/undici-shim-types.test.ts @@ -0,0 +1,331 @@ +/* The undici shim's declarations against the real package's (5.12 and 7.30, + * vendored by the npm fixture cases undici5/undici7). + * + * The shim restates the subset of undici's types it implements. Two checks + * keep the restatement honest: + * + * - usage conformance: one program written against undici's API (the + * transport-style Agent subclass, request(), responses, errors, pool + * stats, buildConnector, diagnostics message types, fetch through a + * dispatcher) type-checks without diagnostics against the real + * declarations AND against the shim, so code written for the implemented + * subset keeps compiling when scriptc swaps the module; + * - shape: every value the shim exports, every `errors` class, and every + * member of the response/stats/message records exists in the real + * declarations with an assignable type. + * + * The shim modules themselves must type-check under @types/node too (the + * program the check builds includes them), since real projects compile + * them with their own Node types. + */ +import { join } from "node:path"; +import ts5 from "typescript5"; +import { describe, expect, test } from "vitest"; + +const repoRoot = join(import.meta.dirname, "../../.."); +const shimRoot = join(repoRoot, "packages/compiler/shims/undici"); + +function realEntry(major: number): string { + return join(repoRoot, `tests/fixtures/npm/cases/undici${major}/node_modules/undici`); +} + +/** Drops the shim's implementation members (`__`-prefixed) at every + * level, so instance types compare by their public API. */ +const helpers = ` +type Public = T extends (...args: never[]) => unknown + ? T + : T extends object + ? { [K in keyof T as K extends \`__\${string}\` ? never : K]: T[K] } + : T; +type Assert = T; +type ExpectNever = T; +type Sub = [A] extends [B] ? true : false; +`; + +function usage(major: number): string { + return ` +import type { Socket } from "node:net"; +import { channel } from "node:diagnostics_channel"; +import { + Agent, + Client, + Headers, + Pool, + buildConnector, + errors, + fetch, + getGlobalDispatcher, + request, + setGlobalDispatcher, +} from "UNDICI"; +import type { DiagnosticsChannel, Dispatcher } from "UNDICI"; + +interface TransportOptions { + connections?: number; + keepAliveTimeout?: number; + connect?: buildConnector.BuildOptions; +} + +function wrap(options?: buildConnector.BuildOptions) { + const connect = buildConnector(options ?? {}) as ( + options: buildConnector.Options, + callback: buildConnector.Callback, + ) => Socket; + return (params: buildConnector.Options, callback: buildConnector.Callback) => { + const socket = connect(params, (error, connected) => { + if (error) callback(error, null); + else callback(null, connected); + }); + socket.once("connect", () => {}); + return socket; + }; +} + +class Transport extends Agent { + private readonly pools: Map; + constructor(options: TransportOptions = {}) { + const pools = new Map(); + super({ + factory(origin) { + const pool = new Pool(origin, { + connect: wrap(options.connect), + connections: options.connections, + keepAliveTimeout: options.keepAliveTimeout, + pipelining: 1, + interceptors: { + Client: [ + (dispatch) => (params, handler) => { + const opaque = (params as Dispatcher.RequestOptions).opaque; + return opaque === undefined ? dispatch(params, handler) : dispatch(params, handler); + }, + ], + }, + }); + pools.set(String(origin), pool); + return pool; + }, + }); + this.pools = pools; + } + counts(origin: string): number { + const stats = this.pools.get(origin)?.stats; + return (stats?.connected ?? 0) + (stats?.free ?? 0) + (stats?.pending ?? 0) + + (stats?.queued ?? 0) + (stats?.running ?? 0) + (stats?.size ?? 0); + } +} + +class Counting extends Agent { + count = 0; + dispatch(options: Dispatcher.DispatchOptions, handler: Dispatcher.${major === 7 ? "DispatchHandler" : "DispatchHandlers"}): boolean { + this.count++; + return super.dispatch(options, handler); + } +} + +export async function run(url: string, lookup: import("node:net").LookupFunction): Promise { + const transport = new Transport({ connections: 2 }); + const response: Dispatcher.ResponseData${major === 7 ? "" : ""} = await request(url, { + dispatcher: transport, + opaque: { label: "x" }, + body: "payload", + bodyTimeout: 1000, + headers: { "x-test": "1", authorization: "Ticket t" }, + headersTimeout: 1000, + method: "POST", + signal: AbortSignal.timeout(1000), + }); + const value = response.headers["content-type"]; + const header = Array.isArray(value) ? value.join(", ") : value === undefined ? null : String(value); + const status: number = response.statusCode; + const text: string = await response.body.text(); + const parsed = (await response.body.json()) as { ok: boolean }; + const bytes: ArrayBuffer = await response.body.arrayBuffer(); + response.body.on("error", () => {}); + response.body.destroy(); + const counting = new Counting(); + await counting.close(); + const client = new Client(url, { keepAliveTimeout: 1000, headersTimeout: 1000 }); + await client.destroy(); + const pool = new Pool(url, { connections: 4 }); + const viaPool = await pool.request({ path: "/x", method: "GET", opaque: null }); + const safe = new Agent({ connect: { lookup } }); + const fetched = await fetch(url, { + dispatcher: safe, + method: "GET", + headers: { a: "b" }, + redirect: "manual", + signal: new AbortController().signal, + }); + const fetchedText: string = await fetched.text(); + setGlobalDispatcher(transport); + const global = getGlobalDispatcher(); + let code = ""; + try { + await request(url); + } catch (err) { + if (err instanceof errors.HeadersTimeoutError || err instanceof errors.BodyTimeoutError) code = err.code; + if (err instanceof errors.UndiciError || err instanceof errors.SocketError) code += "?"; + if (err instanceof errors.RequestAbortedError || err instanceof errors.InvalidArgumentError) code += "."; + } + channel("undici:request:headers").subscribe((message) => { + const m = message as DiagnosticsChannel.RequestHeadersMessage; + code += m.response.statusCode; + }); + channel("undici:client:sendHeaders").subscribe((message) => { + const m = message as DiagnosticsChannel.ClientSendHeadersMessage; + code += m.headers; + }); + return [header, status, text, parsed.ok, bytes.byteLength, viaPool.statusCode, fetched.status, + fetchedText, global === transport, code, transport.counts(url)].join("${major}"); +} +`; +} + +/** Names the shim declares beyond a version's published declarations, + * each because undici's runtime has it although its .d.ts does not (5.12's + * declarations predate several runtime exports and options) or because the + * shim shares one declaration between the 5.x and 7.x entries. */ +const tolerated: Record> = { + 5: { + // Exported by lib/core/errors.js 5.12, missing from types/errors.d.ts. + errors: [ + "ConnectTimeoutError", + "HeadersOverflowError", + "InvalidReturnValueError", + "ResponseContentLengthMismatchError", + "HTTPParserError", + "BalancedPoolMissingUpstreamError", + ], + // 7.x request options in the shared DispatchOptions; 5.12 ignores them. + // The RequestOptions members ride in DispatchOptions (see core/types.ts). + dispatch: ["reset", "blocking", "opaque", "signal", "onInfo", "highWaterMark"], + // Accepted by the 5.12 Client/Pool constructors, undeclared in 5.12. + options: ["localAddress", "connectTimeout"], + // 5.12 declares the connector's port as a number; it passes the URL's + // port string (7.x declares string). + connector: ["port"], + }, + 7: { + // lib/core/errors.js 7.30 exports AbortError; types/errors.d.ts omits it. + errors: ["AbortError"], + dispatch: ["opaque", "signal", "onInfo", "highWaterMark"], + options: [], + connector: [], + }, +}; + +const union = (names: string[]): string => + names.length === 0 ? "never" : names.map((n) => JSON.stringify(n)).join(" | "); + +function shape(major: number): string { + const t = tolerated[major]!; + return ` +import * as real from "REAL"; +import * as shim from "SHIM"; +import type { Dispatcher as RealDispatcher, DiagnosticsChannel as RealDC } from "REAL"; +import type { Dispatcher as ShimDispatcher, DiagnosticsChannel as ShimDC } from "SHIM"; + +type _values = ExpectNever>; +type _errors = ExpectNever>; +type SharedErrors = Extract; +type Instance = C extends abstract new (...args: never[]) => infer I ? I : never; +type ErrorFields = Pick>; +// The fields the real class declares (5.12 declares no UndiciError.code). +type _errorInstances = ExpectNever<{ + [K in SharedErrors]: Sub< + ErrorFields>, + Pick, keyof ErrorFields>> + > extends true + ? never + : K; +}[SharedErrors]>; + +// Records the shim hands to programs: each member of the real record is +// assignable to the shim's. +type RealResponse = RealDispatcher.ResponseData; +type ShimResponse = ShimDispatcher.ResponseData; +type ResponseKeys = Exclude; +type _responseKeys = ExpectNever>; +type _response = ExpectNever<{ + [K in Extract]: Sub extends true ? never : K; +}[Extract]>; +// The body mixin (the Readable half is node:stream's on both sides). +type BodyApi = "text" | "json" | "arrayBuffer" | "blob" | "bodyUsed" | "dump"; +type RealBody = RealResponse["body"]; +type ShimBody = ShimResponse["body"]; +type _bodyKeys = ExpectNever>; +type _body = ExpectNever<{ + [K in BodyApi]: Sub extends true ? never : K; +}[BodyApi]>; +type StatsApi = "connected" | "free" | "pending" | "queued" | "running" | "size"; +type _stats = Assert, Pick>>; +type _headersMessage = Assert< + Sub +>; +type ConnectorKeys = Exclude; +type SharedConnectorKeys = Extract; +type _connectorOptions = ExpectNever<{ + [K in SharedConnectorKeys]: Sub extends true + ? never + : K; +}[SharedConnectorKeys]>; +// Option records programs pass in: the shim's accepted options are a subset. +type _dispatchKeys = ExpectNever< + Exclude +>; +type _clientKeys = ExpectNever>; +type _poolKeys = ExpectNever>; +// Class APIs: every public member the shim declares exists on the real class. +type _agentApi = ExpectNever, keyof real.Agent>>; +type _poolApi = ExpectNever, keyof real.Pool>>; +type _clientApi = ExpectNever, keyof real.Client>>; +`; +} + +function check(files: Record): string[] { + const options: ts5.CompilerOptions = { + strict: true, + noEmit: true, + skipLibCheck: true, + allowImportingTsExtensions: true, + module: ts5.ModuleKind.ESNext, + moduleResolution: ts5.ModuleResolutionKind.Bundler, + target: ts5.ScriptTarget.ES2022, + lib: ["lib.es2023.d.ts"], + types: ["node"], + typeRoots: [join(repoRoot, "node_modules/@types")], + }; + const host = ts5.createCompilerHost(options); + const readFile = host.readFile.bind(host); + const fileExists = host.fileExists.bind(host); + host.readFile = (name) => files[name] ?? readFile(name); + host.fileExists = (name) => name in files || fileExists(name); + const program = ts5.createProgram(Object.keys(files), options, host); + return ts5.getPreEmitDiagnostics(program).map((d) => { + const where = d.file + ? `${d.file.fileName}:${d.file.getLineAndCharacterOfPosition(d.start ?? 0).line + 1}` + : ""; + return `${where} ${ts5.flattenDiagnosticMessageText(d.messageText, "\n")}`; + }); +} + +describe.each([5, 7])("undici %i.x shim types", (major) => { + const real = realEntry(major); + const shim = join(shimRoot, `v${major}.ts`); + + test("a program written against undici type-checks against the real declarations", () => { + const file = join(repoRoot, `.undici-usage-real-${major}.ts`); + expect(check({ [file]: helpers + usage(major).replace(/UNDICI/g, real) })).toEqual([]); + }); + + test("the same program type-checks against the shim", () => { + const file = join(repoRoot, `.undici-usage-shim-${major}.ts`); + expect(check({ [file]: helpers + usage(major).replace(/UNDICI/g, shim) })).toEqual([]); + }); + + test("the shim's exports and records are a compatible subset", () => { + const file = join(repoRoot, `.undici-shape-${major}.ts`); + const text = helpers + shape(major).replace(/REAL/g, real).replace(/SHIM/g, shim); + expect(check({ [file]: text })).toEqual([]); + }); +}); diff --git a/packages/runtime-darwin-arm64/runtime-pack-matrix.mjs b/packages/runtime-darwin-arm64/runtime-pack-matrix.mjs index 5eec9b142..97e22c7fa 100644 --- a/packages/runtime-darwin-arm64/runtime-pack-matrix.mjs +++ b/packages/runtime-darwin-arm64/runtime-pack-matrix.mjs @@ -113,6 +113,7 @@ const optional = [ ["scr_tls_ca.c", "tlsCaEffective"], ["scr_tls.c", "tlsEffective"], ["scr_cipher.c", "cipher"], + ["scr_webcrypto.c", "webcrypto"], ["scr_fetch.c", "nativeFetch"], ]; @@ -125,6 +126,23 @@ function variantsFor(source) { defines: ["SCR_TEXT_DECODER_LEGACY"], }); } + // Programs that assign process.emitWarning link the replacement-aware + // warning paths; every other program keeps the default object. + if (source === "scr_async_dyn.c") { + variants.push({ + id: "warning-replace", + when: { warningReplace: true }, + defines: ["SCR_WARNING_REPLACE"], + }); + } + // stdin setEncoding, string data listeners and stdio 'resize' events. + if (source === "scr_events.c") { + variants.push({ + id: "stdio-extended", + when: { stdioExtended: true }, + defines: ["SCR_STDIO_EXTENDED"], + }); + } if (source === "scr_cycle.c") { // The young-in nursery (SCRIPTC_YOUNG_NURSERY=1): the collector with // nursery collections; programs built without it link the default. diff --git a/packages/runtime-darwin-arm64/scripts/build.mjs b/packages/runtime-darwin-arm64/scripts/build.mjs index 18ee441e6..dbde1564f 100644 --- a/packages/runtime-darwin-arm64/scripts/build.mjs +++ b/packages/runtime-darwin-arm64/scripts/build.mjs @@ -343,7 +343,12 @@ async function build() { ...(unitHelper === null ? {} : { speed_codegen: unitHelper.identity }), }, macros: { - executable: ["SCR_TEXT_DECODER_LEGACY", "SCR_YIN"], + executable: [ + "SCR_TEXT_DECODER_LEGACY", + "SCR_WARNING_REPLACE", + "SCR_STDIO_EXTENDED", + "SCR_YIN", + ], excluded: ["SCR_RC_AUDIT", "SCR_ASAN_FIBERS"], sanitizer: "external-toolchain-required", }, diff --git a/packages/runtime-pack-common/runtime-pack-matrix.mjs b/packages/runtime-pack-common/runtime-pack-matrix.mjs index eab680a55..ee498b5b2 100644 --- a/packages/runtime-pack-common/runtime-pack-matrix.mjs +++ b/packages/runtime-pack-common/runtime-pack-matrix.mjs @@ -103,6 +103,7 @@ const OPTIONAL = [ ["scr_tls_ca.c", "tlsCaEffective"], ["scr_tls.c", "tlsEffective"], ["scr_cipher.c", "cipher"], + ["scr_webcrypto.c", "webcrypto"], ["scr_fetch.c", "nativeFetch"], ]; @@ -115,6 +116,23 @@ function variantsFor(source) { defines: ["SCR_TEXT_DECODER_LEGACY"], }); } + // Programs that assign process.emitWarning link the replacement-aware + // warning paths; every other program keeps the default object. + if (source === "scr_async_dyn.c") { + variants.push({ + id: "warning-replace", + when: { warningReplace: true }, + defines: ["SCR_WARNING_REPLACE"], + }); + } + // stdin setEncoding, string data listeners and stdio 'resize' events. + if (source === "scr_events.c") { + variants.push({ + id: "stdio-extended", + when: { stdioExtended: true }, + defines: ["SCR_STDIO_EXTENDED"], + }); + } if (source === "scr_cycle.c") { // The young-in nursery (SCRIPTC_YOUNG_NURSERY=1): the collector with // nursery collections; programs built without it link the default. diff --git a/packages/runtime-pack-common/scripts/build.mjs b/packages/runtime-pack-common/scripts/build.mjs index 1bcd7455d..a0f117cec 100644 --- a/packages/runtime-pack-common/scripts/build.mjs +++ b/packages/runtime-pack-common/scripts/build.mjs @@ -440,7 +440,12 @@ async function build() { ...(unitHelper === null ? {} : { speed_codegen: unitHelper.identity }), }, macros: { - executable: ["SCR_TEXT_DECODER_LEGACY", "SCR_YIN"], + executable: [ + "SCR_TEXT_DECODER_LEGACY", + "SCR_WARNING_REPLACE", + "SCR_STDIO_EXTENDED", + "SCR_YIN", + ], excluded: ["SCR_RC_AUDIT", "SCR_ASAN_FIBERS"], sanitizer: "external-toolchain-required", }, diff --git a/packages/runtime/src/scr_async.h b/packages/runtime/src/scr_async.h index d16fd8b9c..6f56758ce 100644 --- a/packages/runtime/src/scr_async.h +++ b/packages/runtime/src/scr_async.h @@ -216,6 +216,12 @@ void scr_process_on_warning(ScrDyn *fn); void scr_process_off_warning(ScrDyn *fn); void scr_process_emit_warning(ScrDyn *args); void scr_emit_warning(const char *name, const char *code, ScrStr *message); +/* process.emitWarning = fn (borrowed; the runtime keeps its own + * reference). Later process.emitWarning(...) calls apply the replacement + * to their arguments, and runtime-built warnings (Node's own modules call + * process.emitWarning) reach it as (message, type[, code]) instead of the + * report and the 'warning' listeners. */ +void scr_process_set_emit_warning(ScrDyn *fn); /* node:punycode's first load (scr_async_dyn.c): later calls are module * cache hits. The first reports DEP0040 when `report` (false for a require * made from node_modules) — at once for a static ES import, which Node diff --git a/packages/runtime/src/scr_async_dyn.c b/packages/runtime/src/scr_async_dyn.c index dbddc5b18..1cdd466c3 100644 --- a/packages/runtime/src/scr_async_dyn.c +++ b/packages/runtime/src/scr_async_dyn.c @@ -822,10 +822,59 @@ ScrDyn *scr_warning_new(ScrStr *message, const char *type, const ScrStr *code) { return w; } +#ifdef SCR_WARNING_REPLACE +/* The program's process.emitWarning replacement, or NULL while the + * property holds Node's own function. Compiled only into the + * SCR_WARNING_REPLACE variant, which programs assigning process.emitWarning + * link: every other executable keeps the plain warning paths. */ +static SCR_TL ScrDyn *scr_emit_warning_override = NULL; + +static void scr_emit_warning_override_teardown(void) { + scr_dyn_release(scr_emit_warning_override); + scr_emit_warning_override = NULL; +} + +void scr_process_set_emit_warning(ScrDyn *fn) { + if (scr_emit_warning_override == NULL) scr_atexit(scr_emit_warning_override_teardown); + ScrDyn *previous = scr_emit_warning_override; + scr_emit_warning_override = scr_dyn_retain(fn); + scr_dyn_release(previous); + /* Units that report their own warnings without a listener route them + * here once a replacement exists. */ + scr_warning_emit_hook = scr_emit_warning; +} + +/* A runtime-built warning while a replacement is installed: Node's modules + * call process.emitWarning(message, type, code), so the replacement sees + * those arguments; nothing is reported. A throw stays pending. */ +static void scr_emit_warning_replaced(ScrStr *message, ScrStr *type, ScrStr *code) { + ScrDyn *args[3]; + size_t argc = 0; + args[argc++] = scr_dyn_new_str(message); + ScrStr *fallback = type ? NULL : scr_str_new("Warning", 7); + args[argc++] = scr_dyn_new_str(type ? type : fallback); + scr_str_release(fallback); + if (code) args[argc++] = scr_dyn_new_str(code); + ScrDyn *r = scr_dyn_call(scr_emit_warning_override, args, argc, "process.emitWarning"); + scr_dyn_release(r); + for (size_t i = 0; i < argc; i++) scr_dyn_release(args[i]); +} +#endif /* SCR_WARNING_REPLACE */ + /* A warning built from C parts (the runtime deprecation sites): name * defaults to "Warning"; code/detail optional. Reported synchronously. * Borrows message. */ void scr_emit_warning(const char *name, const char *code, ScrStr *message) { +#ifdef SCR_WARNING_REPLACE + if (scr_emit_warning_override != NULL) { + ScrStr *type_text = scr_str_new(name ? name : "Warning", strlen(name ? name : "Warning")); + ScrStr *code_text = code ? scr_str_new(code, strlen(code)) : NULL; + scr_emit_warning_replaced(message, type_text, code_text); + scr_str_release(type_text); + scr_str_release(code_text); + return; + } +#endif ScrStr *code_text = code ? scr_str_new(code, strlen(code)) : NULL; ScrDyn *w = scr_warning_new(message, name ? name : "Warning", code_text); scr_str_release(code_text); @@ -856,6 +905,18 @@ static void scr_warning_defer(ScrDyn *w, void (*fire)(ScrClosure *)) { } void scr_warning_emit_deferred(ScrDyn *w) { +#ifdef SCR_WARNING_REPLACE + if (scr_emit_warning_override != NULL) { + const ScrDyn *em = scr_dyn_obj_get(w, "message", 7); + const ScrDyn *ec = scr_dyn_obj_get(w, "code", 4); + ScrStr *name = scr_warning_name(w); + ScrStr *message = em && em->kind == SCR_DYN_STR ? scr_str_retain(em->v.str) : scr_str_new("", 0); + scr_emit_warning_replaced(message, name, ec && ec->kind == SCR_DYN_STR ? ec->v.str : NULL); + scr_str_release(message); + scr_str_release(name); + return; + } +#endif if (scr_warning_is_deprecation(w)) { if (scr_process_deprecation_flag_on(0)) return; if (scr_process_deprecation_flag_on(1)) { @@ -942,6 +1003,13 @@ static void scr_warn_bad_arg(const char *arg, const char *want) { * ERR_INVALID_ARG_TYPE TypeErrors at the call; non-string details are * ignored. The warning itself is emitted on the next tick. */ void scr_process_emit_warning(ScrDyn *args) { +#ifdef SCR_WARNING_REPLACE + if (scr_emit_warning_override != NULL) { + ScrDyn *r = scr_dyn_apply(scr_emit_warning_override, args, "process.emitWarning"); + scr_dyn_release(r); + return; + } +#endif size_t argc = args->kind == SCR_DYN_ARR ? args->v.arr.len : 0; ScrDyn *const *items = args->kind == SCR_DYN_ARR ? args->v.arr.items : NULL; ScrDyn *warning = argc >= 1 ? items[0] : NULL; diff --git a/packages/runtime/src/scr_bytes.c b/packages/runtime/src/scr_bytes.c index d33f32a97..64fc36484 100644 --- a/packages/runtime/src/scr_bytes.c +++ b/packages/runtime/src/scr_bytes.c @@ -293,6 +293,9 @@ ScrBytes *scr_bytes_as_buffer(ScrBytes *bytes) { return scr_bytes_retain(bytes); } +/* Nothing on an existing path calls this. */ +bool scr_bytes_is_buffer(const ScrBytes *bytes) { return bytes->is_buffer; } + void scr_bytes_release(ScrBytes *b) { if (!b || b->rc == SIZE_MAX) return; /* NULL: an uninitialized `let` local */ if (--b->rc == 0) { diff --git a/packages/runtime/src/scr_bytes.h b/packages/runtime/src/scr_bytes.h index 993921043..b7b913d7f 100644 --- a/packages/runtime/src/scr_bytes.h +++ b/packages/runtime/src/scr_bytes.h @@ -159,6 +159,8 @@ ScrBytes *scr_bytes_new(ScrBytesElem elem, double n); /* +1 */ /* Shared u8 view over exactly the source view's raw bytes. Borrows; +1. */ ScrBytes *scr_bytes_raw_view(ScrBytes *bytes); ScrBytes *scr_bytes_as_buffer(ScrBytes *bytes); /* borrows; +1, Buffer factory only */ +/* Buffer.isBuffer over a typed view: the view's Buffer brand. Borrows. */ +bool scr_bytes_is_buffer(const ScrBytes *bytes); /* Views retain the root allocation; offsets are relative to its complete * byte storage, including when src is itself a subarray. */ ScrBytes *scr_bytes_buffer_view(ScrBytes *src, ScrBytesElem elem, diff --git a/packages/runtime/src/scr_crypto.h b/packages/runtime/src/scr_crypto.h index 70fa94dd2..4b6b6a6a6 100644 --- a/packages/runtime/src/scr_crypto.h +++ b/packages/runtime/src/scr_crypto.h @@ -1,4 +1,5 @@ -/* scriptc runtime — node:crypto (scr_lib.c, scr_crypto_async.c). Included by +/* scriptc runtime — node:crypto (scr_lib.c, scr_crypto_async.c) and the + * WebCrypto HMAC slice (scr_webcrypto.c). Included by * scr_runtime.h, which says where new declarations go. */ #ifndef SCR_CRYPTO_H #define SCR_CRYPTO_H @@ -50,6 +51,10 @@ ScrBytes *scr_crypto_random_fill_rest(ScrBytes *bytes, double offset); double scr_crypto_random_int(double min, double max); double scr_crypto_random_int_max(double max); ScrDyn *scr_crypto_random_fill_sync_dyn(ScrDyn *buf, ScrDyn *offset, ScrDyn *size); +/* crypto.getRandomValues over a view (returned +1) or any value (the + * value itself, +1); NULL with Node's DOMException pending. */ +ScrBytes *scr_crypto_get_random_values(ScrBytes *view); +ScrDyn *scr_crypto_get_random_values_dyn(ScrDyn *value); double scr_crypto_random_fill_args(ScrBytes *view, bool typed_array, double offset, double size, int form); void scr_crypto_random_int_refill(void); @@ -189,6 +194,22 @@ size_t scr_crypto_digest_raw(const char *alg, const unsigned char *data, size_t unsigned char out[64]); size_t scr_crypto_hmac_raw(const char *alg, const unsigned char *key, size_t keylen, const unsigned char *data, size_t len, unsigned char out[64]); +/* ── WebCrypto (scr_webcrypto.c) ───────────────────────────────────── + * crypto.subtle.importKey (raw HMAC keys), sign and verify (HMAC) and + * digest (SHA-1/SHA-2) over node:crypto's digest core. Each takes the + * call's argument vector as one dyn array (borrowed) and answers a +1 + * promise that every argument or validation failure rejects with Node's + * TypeError or DOMException; algorithms and formats outside the slice + * reject with a coded SC2020 Error. importKey settles at once with a + * CryptoKey handle; sign and digest fulfill with an ArrayBuffer and + * verify with a boolean on a later loop turn. scr_webcrypto_is_crypto_key + * answers `value instanceof CryptoKey`. */ +ScrPromise *scr_webcrypto_import_key(ScrDyn *args); +ScrPromise *scr_webcrypto_sign(ScrDyn *args); +ScrPromise *scr_webcrypto_verify(ScrDyn *args); +ScrPromise *scr_webcrypto_digest(ScrDyn *args); +bool scr_webcrypto_is_crypto_key(const ScrDyn *value); + /* The composed `new crypto.X509Certificate(data).fingerprint` read (the * handle never materializes): the SHA-1 of the DER, uppercase * colon-separated — PEM or raw-DER input; anything else throws Node's diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index 17b68712e..68b2af8d6 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -137,6 +137,7 @@ typedef enum { SCR_DYNH_CIPHER = 45, /* node:crypto Cipheriv (scr_cipher.c) */ SCR_DYNH_CONSOLE = 32, /* stored global console value */ SCR_DYNH_CONSOLE_INSTANCE = 38, /* node:console Console instance */ + SCR_DYNH_CRYPTO_KEY = 56, /* WebCrypto CryptoKey (scr_webcrypto.c) */ SCR_DYNH_CUSTOM_EVENT = 40, /* ScrEvent with detail — new CustomEvent() */ SCR_DYNH_DATE = 30, /* JavaScript Date with stable native identity */ SCR_DYNH_DECIPHER = 46, /* node:crypto Decipheriv (scr_cipher.c) */ @@ -926,6 +927,10 @@ void scr_dyn_key_set(ScrDyn *recv, ScrStr *key, ScrDyn *value); void scr_dyn_arr_named_put(ScrDyn *arr, const char *key, size_t key_len, ScrDyn *value); void scr_dyn_key_set_computed(ScrDyn *recv, ScrDyn *key, ScrDyn *value); void scr_dyn_key_delete(ScrDyn *recv, const ScrStr *key, bool strict); +/* This thread's global object box (borrowed). Every spelling of the global + * object answers it; its symbol-keyed properties are the global object's + * (scr_node_builtin.c). */ +ScrDyn *scr_global_object(void); ScrDyn *scr_dyn_global_symbol_get(ScrSym *key); /* borrowed key; +1 value */ void scr_dyn_global_symbol_set(ScrSym *key, ScrDyn *value); /* both borrowed */ bool scr_dyn_global_symbol_has(ScrSym *key); diff --git a/packages/runtime/src/scr_dyn_invoke.c b/packages/runtime/src/scr_dyn_invoke.c index 752c19805..8a1bfa728 100644 --- a/packages/runtime/src/scr_dyn_invoke.c +++ b/packages/runtime/src/scr_dyn_invoke.c @@ -559,10 +559,8 @@ static ScrDyn *scr_dynh_dispatch(ScrDyn *recv, const char *method, ScrDyn *const * reaching the comparator. The ordered list writes back index by index, so * a comparator that mutates the receiver mid-sort never dangles the items * being ordered. The default comparator compares ToString images - * (scr_dyn_display_buf — join's conversion) bytewise: code-POINT order, - * where JS orders UTF-16 code units — identical through the BMP, divergent - * only across the surrogate boundary (SEMANTICS.md). A comparator result - * converts with ToNumber; NaN counts as 0. */ + * (scr_dyn_display_buf — join's conversion) in UTF-16 code-unit order. A + * comparator result converts with ToNumber; NaN counts as 0. */ static ScrStr *dyn_sort_str(const ScrDyn *e) { ScrJsonBuf b; scr_jb_init(&b); @@ -584,7 +582,7 @@ static bool dyn_sort_less(void *ctx, uint64_t xs, uint64_t ys, bool *failed) { } ScrStr *xstr = dyn_sort_str(x); ScrStr *ystr = dyn_sort_str(y); - int c = scr_str_cmp(xstr, ystr); + int c = scr_str_cmp_u16(xstr, ystr); scr_str_release(xstr); scr_str_release(ystr); return c < 0; diff --git a/packages/runtime/src/scr_events.c b/packages/runtime/src/scr_events.c index 54eb87a0d..3391f54b1 100644 --- a/packages/runtime/src/scr_events.c +++ b/packages/runtime/src/scr_events.c @@ -345,9 +345,23 @@ static void scr_signals_drain(void) { * listener it is treated as EOF (piped fd 0 read errors are not * meaningfully reachable). */ +#ifdef SCR_STDIO_EXTENDED +/* What a 'data' listener receives (the SCR_STDIO_EXTENDED variant, linked + * by programs that use setEncoding, string data listeners or 'resize'): + * raw chunks (typed Buffer listeners and the runtime's own consumers), the + * decoded string (typed string listeners, which need setEncoding), or + * whichever Node delivers (checked-dynamic listeners: the string once an + * encoding is set, else the Buffer). */ +enum { SCR_STDIN_DATA_BYTES, SCR_STDIN_DATA_STRING, SCR_STDIN_DATA_DYN }; + +#endif typedef struct { ScrClosure *cb; /* owned */ void (*fn)(ScrClosure *cb, ScrBytes *chunk); /* adapter: chunk borrowed */ +#ifdef SCR_STDIO_EXTENDED + void (*str_fn)(ScrClosure *cb, ScrStr *text); /* STRING/DYN: text borrowed */ + unsigned char kind; +#endif bool once; } ScrStdinDataL; @@ -377,6 +391,57 @@ static SCR_TL bool scr_stdin_read_eof = false; static SCR_TL ScrBytes *scr_stdin_buffer; static SCR_TL size_t scr_stdin_read_demand; +#ifdef SCR_STDIO_EXTENDED +/* process.stdin.setEncoding(enc): the canonical encoding name (NULL until + * set) and the StringDecoder's packed pending partial sequence, so a + * multibyte character split across reads decodes whole, as Node's + * decoder does. */ +static SCR_TL ScrStr *scr_stdin_encoding = NULL; +static SCR_TL double scr_stdin_dec_pending = 0; + +static void scr_stdin_add_data(ScrClosure *cb /*moves*/, void (*fn)(ScrClosure *, ScrBytes *), + void (*str_fn)(ScrClosure *, ScrStr *), unsigned char kind, + bool once) { + if (scr_stdin_eof || scr_stdin_destroyed) { + scr_closure_release(cb); /* never fires again, like Node post-'end' */ + return; + } + if (scr_stdin_ndata == scr_stdin_data_cap) { + scr_stdin_data_cap = scr_stdin_data_cap ? scr_stdin_data_cap * 2 : 2; + scr_stdin_data = realloc(scr_stdin_data, scr_stdin_data_cap * sizeof *scr_stdin_data); + if (!scr_stdin_data) scr_events_oom(); + } + scr_stdin_data[scr_stdin_ndata].cb = cb; + scr_stdin_data[scr_stdin_ndata].fn = fn; + scr_stdin_data[scr_stdin_ndata].str_fn = str_fn; + scr_stdin_data[scr_stdin_ndata].kind = kind; + scr_stdin_data[scr_stdin_ndata].once = once; + scr_stdin_ndata++; +} + +void scr_stdin_on_data(ScrClosure *cb /*moves*/, void (*fn)(ScrClosure *, ScrBytes *), + bool once) { + scr_stdin_add_data(cb, fn, NULL, SCR_STDIN_DATA_BYTES, once); +} + +void scr_stdin_on_data_str(ScrClosure *cb /*moves*/, void (*fn)(ScrClosure *, ScrStr *), + bool once) { + scr_stdin_add_data(cb, NULL, fn, SCR_STDIN_DATA_STRING, once); +} + +static void scr_stdin_encoding_teardown(void) { + scr_str_release(scr_stdin_encoding); + scr_stdin_encoding = NULL; +} + +void scr_stdin_set_encoding(ScrStr *enc) { + if (scr_stdin_encoding == NULL) scr_atexit(scr_stdin_encoding_teardown); + ScrStr *previous = scr_stdin_encoding; + scr_stdin_encoding = scr_str_retain(enc); + scr_str_release(previous); + scr_stdin_dec_pending = 0; +} +#else void scr_stdin_on_data(ScrClosure *cb /*moves*/, void (*fn)(ScrClosure *, ScrBytes *), bool once) { if (scr_stdin_eof || scr_stdin_destroyed) { @@ -393,6 +458,7 @@ void scr_stdin_on_data(ScrClosure *cb /*moves*/, void (*fn)(ScrClosure *, ScrByt scr_stdin_data[scr_stdin_ndata].once = once; scr_stdin_ndata++; } +#endif void scr_stdin_on_end(ScrClosure *cb /*moves*/, bool once) { if (scr_stdin_eof || scr_stdin_destroyed) { @@ -487,7 +553,20 @@ static void scr_stdin_settle_done(void) { /* EOF/teardown shared tail: 'end' fires (snapshot; once semantics are * moot — everything drops right after), the parked promise settles done, * and every listener is released. */ +#ifdef SCR_STDIO_EXTENDED +static void scr_stdin_deliver_parts(ScrBytes *chunk, ScrStr *text); +#endif static void scr_stdin_finish(bool fire_end) { +#ifdef SCR_STDIO_EXTENDED + if (fire_end && scr_stdin_encoding != NULL && scr_stdin_ndata > 0) { + /* The decoder's end() flush reaches the string listeners before 'end'. */ + ScrStr *tail = scr_strdec_end(scr_stdin_encoding, scr_stdin_dec_pending); + scr_stdin_dec_pending = 0; + if (tail->len > 0) scr_stdin_deliver_parts(NULL, tail); + scr_str_release(tail); + if (scr_exc_pending()) fire_end = false; + } +#endif scr_stdin_eof = true; if (fire_end) { size_t n = scr_stdin_nend; @@ -657,7 +736,25 @@ static void scr_stdin_service(void) { scr_stdin_arrived(chunk); } +#ifdef SCR_STDIO_EXTENDED static void scr_stdin_deliver(ScrBytes *chunk) { + if (scr_stdin_ndata > 0 && scr_stdin_encoding != NULL) { + /* Node's readable decodes once, in the stream: every listener sees + * the same string, and a read holding only a partial sequence emits + * nothing. */ + ScrStr *text = scr_strdec_write(scr_stdin_encoding, scr_stdin_dec_pending, chunk); + scr_stdin_dec_pending = scr_strdec_next(scr_stdin_encoding, scr_stdin_dec_pending, chunk); + if (text->len > 0) scr_stdin_deliver_parts(chunk, text); + scr_str_release(text); + return; + } + scr_stdin_deliver_parts(chunk, NULL); +} + +static void scr_stdin_deliver_parts(ScrBytes *chunk, ScrStr *text) { +#else +static void scr_stdin_deliver(ScrBytes *chunk) { +#endif if (scr_stdin_ndata > 0) { size_t nd = scr_stdin_ndata; ScrStdinDataL *snap = malloc(nd * sizeof *snap); @@ -678,11 +775,31 @@ static void scr_stdin_deliver(ScrBytes *chunk) { } } } +#ifdef SCR_STDIO_EXTENDED + if (!scr_exc_pending()) { + if (snap[i].kind == SCR_STDIN_DATA_BYTES) { + if (chunk != NULL) snap[i].fn(snap[i].cb, chunk); + } else if (text != NULL) { + snap[i].str_fn(snap[i].cb, text); + } else if (snap[i].kind == SCR_STDIN_DATA_DYN) { + if (chunk != NULL) snap[i].fn(snap[i].cb, chunk); + } else { + static const char msg[] = + "stdin data listener type does not match setEncoding state"; + scr_throw_error_msg(SCR_ERR_TYPE, msg, sizeof msg - 1); + } + } + scr_closure_release(snap[i].cb); + } + free(snap); + } else if (scr_stdin_waiter && chunk != NULL) { +#else if (!scr_exc_pending()) snap[i].fn(snap[i].cb, chunk); scr_closure_release(snap[i].cb); } free(snap); } else if (scr_stdin_waiter) { +#endif ScrPromise *p = scr_stdin_waiter; scr_stdin_waiter = NULL; scr_promise_fulfill_ref(p, scr_bytes_retain(chunk), scr_bytes_retain_v, @@ -701,14 +818,31 @@ typedef struct { bool *error_once; size_t nerrors; ScrDyn *write_error; +#ifdef SCR_STDIO_EXTENDED + /* 'resize' listeners (stdout/stderr) and the size they last saw. */ + ScrDyn **resizes; + bool *resize_once; + size_t nresizes; + double columns, rows; +#endif } ScrStdio; static SCR_TL ScrStdio scr_stdio_streams[3] = {{.fd = 0}, {.fd = 1}, {.fd = 2}}; +#ifdef SCR_STDIO_EXTENDED +enum { STDIO_WRITE, STDIO_ON, STDIO_ONCE, STDIO_OFF, STDIO_PAUSE, STDIO_RESUME, + STDIO_IS_PAUSED, STDIO_READ, STDIO_RAW, STDIO_DESTROY, STDIO_WINDOW_SIZE, + STDIO_SET_ENCODING, STDIO_METHOD_COUNT }; +static const char *const scr_stdio_names[] = { + "write", "on", "once", "removeListener", "pause", "resume", "isPaused", "read", "setRawMode", "destroy", "getWindowSize", + "setEncoding" +}; +#else enum { STDIO_WRITE, STDIO_ON, STDIO_ONCE, STDIO_OFF, STDIO_PAUSE, STDIO_RESUME, STDIO_IS_PAUSED, STDIO_READ, STDIO_RAW, STDIO_DESTROY, STDIO_WINDOW_SIZE, STDIO_METHOD_COUNT }; static const char *const scr_stdio_names[] = { "write", "on", "once", "removeListener", "pause", "resume", "isPaused", "read", "setRawMode", "destroy", "getWindowSize" }; +#endif static SCR_TL ScrDyn *scr_stdio_methods[STDIO_METHOD_COUNT]; static SCR_TL bool scr_stdio_initialized; static SCR_TL bool scr_stdio_raw; @@ -736,6 +870,13 @@ static void scr_stdio_data(ScrClosure *cb, ScrBytes *bytes) { scr_stdio_fire(cb, chunk, 1); scr_dyn_release(chunk); } +#ifdef SCR_STDIO_EXTENDED +static void scr_stdio_data_text(ScrClosure *cb, ScrStr *text) { + ScrDyn *chunk = scr_dyn_new_str(text); + scr_stdio_fire(cb, chunk, 1); + scr_dyn_release(chunk); +} +#endif static void scr_stdio_end(ScrClosure *cb) { scr_stdio_fire(cb, NULL, 0); } static void scr_stdio_error(ScrClosure *cb, ScrStr *message) { ScrError *error = scr_error_new(SCR_ERR_ERROR, message); @@ -861,6 +1002,95 @@ static ScrDyn *scr_stdio_read(ScrDyn *size) { return result; } +#ifdef SCR_STDIO_EXTENDED +/* 'resize' on a TTY stdout/stderr: Node's tty.WriteStream listens for + * SIGWINCH and emits 'resize' when the window size changed. The signal + * listener does not keep the loop alive, and a non-TTY stream never + * emits. */ +static SCR_TL ScrDyn *scr_stdio_winch = NULL; + +static void scr_stdio_resize_emit(ScrStdio *stream) { + size_t n = stream->nresizes; + if (n == 0) return; + ScrDyn **snap = malloc(n * sizeof *snap); + bool *once = malloc(n * sizeof *once); + if (!snap || !once) scr_events_oom(); + for (size_t i = 0; i < n; i++) { + snap[i] = scr_dyn_retain(stream->resizes[i]); + once[i] = stream->resize_once[i]; + } + for (size_t i = 0; i < n; i++) { + if (once[i]) { + for (size_t j = 0; j < stream->nresizes; j++) { + if (stream->resizes[j] == snap[i]) { + scr_dyn_release(stream->resizes[j]); + memmove(stream->resizes + j, stream->resizes + j + 1, (stream->nresizes - j - 1) * sizeof *stream->resizes); + memmove(stream->resize_once + j, stream->resize_once + j + 1, (stream->nresizes - j - 1) * sizeof *stream->resize_once); + stream->nresizes--; + break; + } + } + } + if (!scr_exc_pending()) { + scr_dyn_this_push(stream, SCR_DYNH_STDIO); + ScrDyn *r = scr_dyn_call(snap[i], NULL, 0, "listener"); + scr_dyn_this_pop(); + scr_dyn_release(r); + } + scr_dyn_release(snap[i]); + } + free(snap); + free(once); +} + +static ScrDyn *scr_stdio_winch_fire(ScrClosure *cb, ScrDyn *const *args, size_t argc) { + (void)cb; (void)args; (void)argc; + for (int fd = 1; fd <= 2 && !scr_exc_pending(); fd++) { + ScrStdio *stream = &scr_stdio_streams[fd]; + if (stream->nresizes == 0 || !scr_process_is_tty(fd)) continue; + double columns = scr_process_columns(fd), rows = scr_process_rows(fd); + if (columns == stream->columns && rows == stream->rows) continue; + stream->columns = columns; + stream->rows = rows; + scr_stdio_resize_emit(stream); + } + return scr_exc_pending() ? NULL : scr_dyn_retain(scr_dyn_undefined()); +} + +static void scr_stdio_resize_listener(ScrStdio *stream, ScrDyn *listener, bool remove, bool once) { + if (remove) { + for (size_t i = stream->nresizes; i > 0; i--) { + if (scr_dyn_strict_eq(stream->resizes[i - 1], listener)) { + scr_dyn_release(stream->resizes[i - 1]); + memmove(stream->resizes + i - 1, stream->resizes + i, (stream->nresizes - i) * sizeof *stream->resizes); + memmove(stream->resize_once + i - 1, stream->resize_once + i, (stream->nresizes - i) * sizeof *stream->resize_once); + stream->nresizes--; + break; + } + } + return; + } + if (stream->nresizes == 0) { + stream->columns = scr_process_columns(stream->fd); + stream->rows = scr_process_rows(stream->fd); + } + stream->resizes = realloc(stream->resizes, (stream->nresizes + 1) * sizeof *stream->resizes); + stream->resize_once = realloc(stream->resize_once, (stream->nresizes + 1) * sizeof *stream->resize_once); + if (!stream->resizes || !stream->resize_once) scr_events_oom(); + stream->resizes[stream->nresizes] = scr_dyn_retain(listener); + stream->resize_once[stream->nresizes++] = once; +#if !defined(_WIN32) && !defined(__wasi__) + if (scr_stdio_winch == NULL && scr_context_is_main()) { + ScrClosure *cb = scr_closure_new(NULL, 0); + scr_stdio_winch = scr_dyn_new_func(cb, scr_stdio_winch_fire, 0, "", "refreshSize"); + ScrStr *name = scr_str_new("SIGWINCH", 8); + scr_signal_on(name, scr_stdio_winch, false); + scr_str_release(name); + } +#endif +} + +#endif static ScrDyn *scr_stdio_method(ScrClosure *cb, ScrDyn *const *args, size_t argc) { int method = (int)scr_box_get_f64(cb->caps[0]); ScrDyn *self = scr_dyn_this_get(); @@ -920,14 +1150,30 @@ static ScrDyn *scr_stdio_method(ScrClosure *cb, ScrDyn *const *args, size_t argc bool data = arg->kind == SCR_DYN_STR && arg->v.str->len == 4 && memcmp(event, "data", 4) == 0; bool end = arg->kind == SCR_DYN_STR && arg->v.str->len == 3 && memcmp(event, "end", 3) == 0; bool error = arg->kind == SCR_DYN_STR && arg->v.str->len == 5 && memcmp(event, "error", 5) == 0; +#ifdef SCR_STDIO_EXTENDED + bool resize = stream->fd != 0 && arg->kind == SCR_DYN_STR && arg->v.str->len == 6 && + memcmp(event, "resize", 6) == 0; + if ((!data && !end && !error && !resize) || (stream->fd != 0 && !error && !resize)) { +#else if ((!data && !end && !error) || (stream->fd != 0 && !error)) { +#endif result = scr_stdio_refusal("event"); goto done; } bool remove = method == STDIO_OFF, once = method == STDIO_ONCE; +#ifdef SCR_STDIO_EXTENDED + if (resize) { + scr_stdio_resize_listener(stream, listener, remove, once); + } else if (stream->fd == 0) { + if (!remove) { + if (data) + scr_stdin_add_data(scr_stdio_callback(listener, (void *)scr_stdio_data), scr_stdio_data, + scr_stdio_data_text, SCR_STDIN_DATA_DYN, once); +#else if (stream->fd == 0) { if (!remove) { if (data) scr_stdin_on_data(scr_stdio_callback(listener, (void *)scr_stdio_data), scr_stdio_data, once); +#endif if (end) scr_stdin_on_end(scr_stdio_callback(listener, (void *)scr_stdio_end), once); if (error) scr_stdin_on_error(scr_stdio_callback(listener, (void *)scr_stdio_error), scr_stdio_error, once); } else { @@ -972,6 +1218,15 @@ static ScrDyn *scr_stdio_method(ScrClosure *cb, ScrDyn *const *args, size_t argc } else if (stream->fd == 0 && method == STDIO_RAW) { scr_process_stdin_set_raw_mode(scr_dyn_truthy(arg)); if (!scr_exc_pending()) { scr_stdio_raw = scr_dyn_truthy(arg); result = scr_dyn_retain(self); } +#ifdef SCR_STDIO_EXTENDED + } else if (stream->fd == 0 && method == STDIO_SET_ENCODING) { + ScrStr *enc = scr_strdec_encoding(arg); + if (enc) { + scr_stdin_set_encoding(enc); + scr_str_release(enc); + result = scr_dyn_retain(self); + } +#endif } else if (stream->fd == 0 && method == STDIO_DESTROY && argc == 0) { scr_stdin_destroy(); result = scr_dyn_retain(self); @@ -1069,7 +1324,16 @@ static void scr_stdio_cleanup(void) { for (size_t j = 0; j < stream->nerrors; j++) scr_dyn_release(stream->errors[j]); free(stream->errors); free(stream->error_once); stream->errors = NULL; stream->error_once = NULL; stream->nerrors = 0; +#ifdef SCR_STDIO_EXTENDED + for (size_t j = 0; j < stream->nresizes; j++) scr_dyn_release(stream->resizes[j]); + free(stream->resizes); free(stream->resize_once); + stream->resizes = NULL; stream->resize_once = NULL; stream->nresizes = 0; +#endif } +#ifdef SCR_STDIO_EXTENDED + scr_dyn_release(scr_stdio_winch); + scr_stdio_winch = NULL; +#endif for (int i = 0; i < STDIO_METHOD_COUNT; i++) { scr_dyn_release(scr_stdio_methods[i]); scr_stdio_methods[i] = NULL; @@ -1213,6 +1477,11 @@ void scr_stdin_data_thunk_bytes(ScrClosure *cb, ScrBytes *chunk) { /* The listener owns its +1 param per the universal convention. */ ((void (*)(ScrClosure *, ScrBytes *))cb->fn)(cb, scr_bytes_retain(chunk)); } +#ifdef SCR_STDIO_EXTENDED +void scr_stdin_data_thunk_str(ScrClosure *cb, ScrStr *text) { + ((void (*)(ScrClosure *, ScrStr *))cb->fn)(cb, scr_str_retain(text)); +} +#endif /* ── the loop hooks (see scr_async.c) ─────────────────────────────────── */ diff --git a/packages/runtime/src/scr_events.h b/packages/runtime/src/scr_events.h index 7dcf9cda0..a0025783e 100644 --- a/packages/runtime/src/scr_events.h +++ b/packages/runtime/src/scr_events.h @@ -42,6 +42,13 @@ void scr_run_exit_listeners(double code); void scr_exit_thunk0(ScrClosure *cb, double code); void scr_exit_thunk_code(ScrClosure *cb, double code); void scr_stdin_on_data(ScrClosure *cb, void (*fn)(ScrClosure *, ScrBytes *), bool once); +/* A string 'data' listener (moves cb): receives each chunk decoded by the + * stdin's setEncoding decoder; with no encoding set, a delivery throws + * the setEncoding-state TypeError instead (Node would pass a Buffer). */ +void scr_stdin_on_data_str(ScrClosure *cb, void (*fn)(ScrClosure *, ScrStr *), bool once); +/* process.stdin.setEncoding: the canonical encoding (borrowed). */ +void scr_stdin_set_encoding(ScrStr *enc); +void scr_stdin_data_thunk_str(ScrClosure *cb, ScrStr *text); void scr_stdin_on_end(ScrClosure *cb, bool once); void scr_stdin_on_error(ScrClosure *cb, ScrChildErrFn fn, bool once); ScrPromise *scr_stdin_next_chunk(void); /* +1 */ diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 6174913c0..83b56bea4 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -4356,6 +4356,60 @@ bool scr_dyn_data_view_is(const ScrDyn *value) { return value->kind == SCR_DYN_BYTES && value->v.bytes->is_data_view; } +/* util.inspect names an object after the function held by the first own + * `constructor` data property on its prototype chain (Node's + * getConstructorName). The dyn printer reads runtime class names + * (v.obj.class_name, set only on prototypes that stay alive), so a + * construction through a plain function stamps that prototype with a copy + * of the function's name and keeps it alive. The copy is the stamp's own: a + * function's name can live in storage the function owns (a bound + * function's "bound "), and the function is released when the + * writable `constructor` property is replaced. The stamps are cleared and + * their copies freed before release at exit, so freelist reuse never sees + * them. */ +typedef struct { + ScrDyn *prototype; + char *name; +} ScrDynNamedPrototype; +static SCR_TL ScrDynNamedPrototype *scr_dyn_named_prototypes; +static SCR_TL size_t scr_dyn_named_count, scr_dyn_named_cap; + +static void scr_dyn_named_prototypes_cleanup(void) { + for (size_t i = 0; i < scr_dyn_named_count; i++) { + scr_dyn_named_prototypes[i].prototype->v.obj.class_name = NULL; + scr_dyn_release(scr_dyn_named_prototypes[i].prototype); + free(scr_dyn_named_prototypes[i].name); + } + free(scr_dyn_named_prototypes); + scr_dyn_named_prototypes = NULL; + scr_dyn_named_count = scr_dyn_named_cap = 0; +} + +static void scr_dyn_name_prototype_chain(const ScrDyn *object) { + for (ScrDyn *p = object->prototype; p; p = p->prototype) { + if (p->kind != SCR_DYN_OBJ || p->v.obj.class_name) return; + ScrDynEntry *entry = scr_dyn_find_entry(p, "constructor", 11); + if (!entry || entry->accessor || !entry->value || entry->value->kind != SCR_DYN_FUNC) continue; + const char *name = entry->value->v.fn.name; + if (!name || !name[0]) continue; + if (scr_dyn_named_count == scr_dyn_named_cap) { + if (scr_dyn_named_cap == 0) scr_atexit(scr_dyn_named_prototypes_cleanup); + size_t cap = scr_dyn_named_cap ? scr_dyn_named_cap * 2 : 8; + ScrDynNamedPrototype *grown = realloc(scr_dyn_named_prototypes, cap * sizeof *grown); + if (!grown) scr_json_oom(); + scr_dyn_named_prototypes = grown; + scr_dyn_named_cap = cap; + } + size_t len = strlen(name); + char *copy = malloc(len + 1); + if (!copy) scr_json_oom(); + memcpy(copy, name, len + 1); + p->v.obj.class_name = copy; + scr_dyn_named_prototypes[scr_dyn_named_count++] = (ScrDynNamedPrototype){ scr_dyn_retain(p), copy }; + return; + } +} + /* Ordinary native function constructors retain their live prototype table. * A constructor may replace the fresh receiver with another object. */ ScrDyn *scr_dyn_construct(const ScrDyn *callee, const ScrDyn *args, const ScrStr *what) { @@ -4366,6 +4420,7 @@ ScrDyn *scr_dyn_construct(const ScrDyn *callee, const ScrDyn *args, const ScrStr ScrDyn *receiver = scr_iterator_object(prototype) ? scr_dyn_obj_create(prototype) : scr_dyn_new_obj(); scr_dyn_release(prototype); if (!receiver) return NULL; + scr_dyn_name_prototype_chain(receiver); scr_dyn_this_push_dyn(receiver); ScrDyn *result = scr_dyn_call(callee, args->v.arr.items, args->v.arr.len, what->data); scr_dyn_this_pop(); diff --git a/packages/runtime/src/scr_lib.c b/packages/runtime/src/scr_lib.c index ae9c5c2b1..930d44818 100644 --- a/packages/runtime/src/scr_lib.c +++ b/packages/runtime/src/scr_lib.c @@ -11408,6 +11408,137 @@ ScrStr *scr_num_to_fixed(double x, double fraction_digits) { return scr_str_new(out, len); } +/* String.prototype.substr (Annex B.2.2.1) over substring's UTF-16 + * clamp: -Infinity and other starts below -size clamp to 0, an Infinity + * length to the end. Comparisons only (no floating-point literal other + * than zero, which would join the constant pool every binary links). + * Nothing on an existing path calls this. */ +ScrStr *scr_str_substr(ScrStr *s, double start, double length, bool has_length) { + double size = scr_str_utf16_len(s); + double first = isnan(start) ? 0 : trunc(start); + if (first < 0) { + first = size + first; + if (first < 0) first = 0; + } else if (first > size) { + first = size; + } + double count = has_length ? (isnan(length) ? 0 : trunc(length)) : size; + if (count < 0) count = 0; + double end = first + count; + if (end > size) end = size; + return scr_str_substring(s, first, end); +} + +/* The exponent suffix of toPrecision/toExponential: "e+5", "e-7". */ +static size_t scr_num_exp_suffix(char *o, int e) { + size_t n = 0; + o[n++] = 'e'; + o[n++] = e < 0 ? '-' : '+'; + int a = e < 0 ? -e : e; + char rev[8]; + int k = 0; + do { + rev[k++] = (char)('0' + a % 10); + a /= 10; + } while (a != 0); + while (k > 0) o[n++] = rev[--k]; + return n; +} + +/* Number.prototype.toPrecision(precision) (ECMA-262 §21.1.3.5): undefined + * precision is ToString(x); ToIntegerOrInfinity runs (in the caller's + * coercion) before the non-finite arm, which precedes the 1..100 range + * check. Digits are the exact-value, ties-up n of + * scr_f64_precision_digits; exponential form when e < -6 or e >= p. + * Nothing on an existing path calls this. */ +ScrStr *scr_num_to_precision(double x, double precision, bool has_precision) { + if (!has_precision || !isfinite(x)) return scr_f64_to_scrstr(x); + double p = isnan(precision) ? 0 : trunc(precision); + if (!(p >= 1 && p <= 100)) { + static const char msg[] = "toPrecision() argument must be between 1 and 100"; + scr_throw_error_msg(SCR_ERR_RANGE, msg, sizeof msg - 1); + return NULL; + } + int digits_count = (int)p; + char out[160]; + size_t o = 0; + double a = x; + if (x < 0) { + out[o++] = '-'; + a = -x; + } + char d[104]; + int e = 0; + if (a == 0) { + memset(d, '0', (size_t)digits_count); + d[digits_count] = '\0'; + } else { + scr_f64_precision_digits(a, digits_count, d, &e); + } + if (e < -6 || e >= digits_count) { + out[o++] = d[0]; + if (digits_count != 1) { + out[o++] = '.'; + memcpy(out + o, d + 1, (size_t)(digits_count - 1)); + o += (size_t)(digits_count - 1); + } + o += scr_num_exp_suffix(out + o, e); + } else if (e >= 0) { + memcpy(out + o, d, (size_t)(e + 1)); + o += (size_t)(e + 1); + if (e + 1 < digits_count) { + out[o++] = '.'; + memcpy(out + o, d + e + 1, (size_t)(digits_count - e - 1)); + o += (size_t)(digits_count - e - 1); + } + } else { + out[o++] = '0'; + out[o++] = '.'; + for (int i = 0; i < -(e + 1); i++) out[o++] = '0'; + memcpy(out + o, d, (size_t)digits_count); + o += (size_t)digits_count; + } + return scr_str_new(out, o); +} + +/* Number.prototype.toExponential(fractionDigits) with a DEFINED argument + * (the undefined form is scr_num_to_exponential's shortest mantissa): + * f + 1 exact-value, ties-up digits, "d.ddde±X". Nothing on an existing + * path calls this. */ +ScrStr *scr_num_to_exponential_digits(double x, double fraction_digits) { + if (!isfinite(x)) return scr_f64_to_scrstr(x); + double f = isnan(fraction_digits) ? 0 : trunc(fraction_digits); + if (!(f >= 0 && f <= 100)) { + static const char msg[] = "toExponential() argument must be between 0 and 100"; + scr_throw_error_msg(SCR_ERR_RANGE, msg, sizeof msg - 1); + return NULL; + } + int count = (int)f + 1; + char out[160]; + size_t o = 0; + double a = x; + if (x < 0) { + out[o++] = '-'; + a = -x; + } + char d[104]; + int e = 0; + if (a == 0) { + memset(d, '0', (size_t)count); + d[count] = '\0'; + } else { + scr_f64_precision_digits(a, count, d, &e); + } + out[o++] = d[0]; + if (count != 1) { + out[o++] = '.'; + memcpy(out + o, d + 1, (size_t)(count - 1)); + o += (size_t)(count - 1); + } + o += scr_num_exp_suffix(out + o, e); + return scr_str_new(out, o); +} + /* Increment a decimal digit string in place. Returns true on overflow — * the value becomes 1 followed by len zeros (the caller folds the zeros * into its scale); an EMPTY string increments to "1" the same way (the @@ -11739,3 +11870,52 @@ ScrDyn *scr_dyn_native_date_from_ms(double ms) { scr_native_date_release(date); return result; } + +/* crypto.getRandomValues(typedArray) (WebCrypto, also node:crypto's + * export): integer typed arrays only (Node's TypeMismatchError DOMException + * for float arrays, DataViews and every other value), at most 65536 bytes + * (QuotaExceededError), filled in place and returned. Nothing on an + * existing path calls these. They stay at the end of the unit: clang emits + * a static helper (scr_random_write) where its first emitted caller + * appears, and an earlier caller would move it in every binary that links + * the random fills. */ +ScrBytes *scr_crypto_get_random_values(ScrBytes *view) { + static const char mismatch[] = "TypeMismatchError"; + static const char mismatch_msg[] = "The data argument must be an integer-type TypedArray"; + static const char quota[] = "QuotaExceededError"; + static const char quota_msg[] = "The requested length exceeds 65,536 bytes"; + if (view->is_data_view || view->elem == SCR_BYTES_F32 || view->elem == SCR_BYTES_F64 || + view->elem == SCR_BYTES_F16) { + scr_throw_domex(mismatch, mismatch_msg); + return NULL; + } + /* Integer arithmetic: a new floating-point literal would land in the + * merged constant pool every binary links. */ + size_t length = view->len * (size_t)scr_bytes_elem_size(view->elem); + if (length > 65536u) { + scr_throw_domex(quota, quota_msg); + return NULL; + } + scr_random_write(view, 0, length); + return scr_bytes_retain(view); +} + +ScrDyn *scr_crypto_get_random_values_dyn(ScrDyn *value) { + if (value->kind == SCR_DYN_TYPED_REF) { + ScrDyn *materialized = scr_dyn_typed_ref_materialize(value); + if (!materialized) return NULL; + ScrDyn *result = scr_crypto_get_random_values_dyn(materialized); + scr_dyn_release(materialized); + return result ? scr_dyn_retain(value) : NULL; + } + if (value->kind != SCR_DYN_BYTES) { + static const char mismatch[] = "TypeMismatchError"; + static const char mismatch_msg[] = "The data argument must be an integer-type TypedArray"; + scr_throw_domex(mismatch, mismatch_msg); + return NULL; + } + ScrBytes *filled = scr_crypto_get_random_values(value->v.bytes); + if (!filled) return NULL; + scr_bytes_release(filled); + return scr_dyn_retain(value); +} diff --git a/packages/runtime/src/scr_node_builtin.c b/packages/runtime/src/scr_node_builtin.c index 34c700350..233617092 100644 --- a/packages/runtime/src/scr_node_builtin.c +++ b/packages/runtime/src/scr_node_builtin.c @@ -86,6 +86,10 @@ ScrDyn *scr_util_system_error_entries(void) { static SCR_TL ScrDyn *scr_global_known; static SCR_TL ScrDyn *scr_global_own; +/* The one box denoting this thread's global object. Symbol-keyed properties + * live on the box (string-keyed ones in scr_global_own), so every spelling + * of the global object answers this box. */ +static SCR_TL ScrDyn *scr_global_box; static SCR_TL bool scr_global_cleanup_registered; static SCR_TL ScrDyn *(*scr_global_console_get)(void); static SCR_TL ScrDyn *(*scr_global_fetch_get)(void); @@ -180,6 +184,8 @@ ScrDyn *scr_process_hrtime_value(void) { } static void scr_global_cleanup(void) { + scr_dyn_release(scr_global_box); + scr_global_box = NULL; scr_dyn_release(scr_global_known); scr_dyn_release(scr_global_own); scr_global_known = NULL; @@ -195,7 +201,7 @@ static ScrDyn *scr_global_get(void *handle, const char *key, size_t length) { if (own) return scr_dyn_retain(own); if ((length == 10 && memcmp(key, "globalThis", length) == 0) || (length == 6 && memcmp(key, "global", length) == 0)) - return scr_dyn_new_handle(&scr_global_known, SCR_DYNH_GLOBAL); + return scr_dyn_retain(scr_global_box); if (length == 7 && memcmp(key, "console", length) == 0 && scr_global_console_get) return scr_global_console_get(); if (length == 5 && memcmp(key, "fetch", length) == 0 && scr_global_fetch_get) return scr_global_fetch_get(); @@ -223,26 +229,32 @@ static bool scr_global_set(void *handle, const char *key, size_t length, const S return true; } -ScrDyn *scr_global_native_init(ScrArr *known, ScrDyn *(*console_get)(void)) { +ScrDyn *scr_global_object(void) { static const ScrDynHandleOps ops = { .cls = "Object", .retain = scr_global_retain, .release = scr_global_release, .get = scr_global_get, .set = scr_global_set, }; scr_dyn_handle_install(SCR_DYNH_GLOBAL, &ops); - scr_global_console_get = console_get; if (!scr_global_known) scr_global_known = scr_dyn_new_obj(); if (!scr_global_own) scr_global_own = scr_dyn_new_obj(); + if (!scr_global_box) scr_global_box = scr_dyn_new_handle(&scr_global_known, SCR_DYNH_GLOBAL); if (!scr_global_cleanup_registered) { scr_atexit(scr_global_cleanup); scr_global_cleanup_registered = true; } + return scr_global_box; +} + +ScrDyn *scr_global_native_init(ScrArr *known, ScrDyn *(*console_get)(void)) { + ScrDyn *global = scr_global_object(); + scr_global_console_get = console_get; for (size_t i = 0; i < known->len; i++) { ScrStr *key = (ScrStr *)scr_arr_get_ref(known, (double)i); if (!scr_dyn_obj_get(scr_global_known, key->data, key->len)) scr_dyn_obj_set(scr_global_known, key->data, key->len, scr_dyn_new_bool(true)); scr_str_release(key); } - return scr_dyn_new_handle(&scr_global_known, SCR_DYNH_GLOBAL); + return scr_dyn_retain(global); } typedef struct ScrBuiltinModule { diff --git a/packages/runtime/src/scr_number.c b/packages/runtime/src/scr_number.c index 1ccc414b7..dc8743e14 100644 --- a/packages/runtime/src/scr_number.c +++ b/packages/runtime/src/scr_number.c @@ -654,3 +654,163 @@ size_t scr_f64_to_fixed(double x, int f, char *buf) { *o = '\0'; return (size_t)(o - buf); } + +/* ── toPrecision / toExponential(fractionDigits) digit engine ────────── + * The spec's n and e for p significant digits: the integer n with + * 10^(p-1) <= n < 10^p for which n × 10^(e-p+1) - x is closest to zero, + * ties picking the LARGER n (half-up on the exact binary value). Exact + * big-integer arithmetic over the double's mantissa: the value scaled by + * 10^(p-1-e) is mantissa × 2^(binary_exp + f) × 5^f, and + * round_half_up(v) = floor((floor(2v) + 1) / 2) composes from floor + * divisions alone. Values reach ~1100 bits (5e-324 at 100 digits and + * 1.8e308), so the limbs are wider than toFixed's. Nothing on an existing + * path calls this. */ +#define SCR_PREC_LIMBS 40 +typedef struct { + uint32_t limb[SCR_PREC_LIMBS]; /* little-endian */ + int len; +} ScrPrecInt; + +static void scr_prec_normalize(ScrPrecInt *v) { + while (v->len > 1 && v->limb[v->len - 1] == 0) v->len--; +} + +static void scr_prec_mul_small(ScrPrecInt *v, uint32_t m) { + uint64_t carry = 0; + for (int i = 0; i < v->len; i++) { + uint64_t p = (uint64_t)v->limb[i] * m + carry; + v->limb[i] = (uint32_t)p; + carry = p >> 32; + } + if (carry != 0) v->limb[v->len++] = (uint32_t)carry; +} + +static uint32_t scr_prec_div_small(ScrPrecInt *v, uint32_t d) { + uint64_t rem = 0; + for (int i = v->len - 1; i >= 0; i--) { + uint64_t cur = (rem << 32) | v->limb[i]; + v->limb[i] = (uint32_t)(cur / d); + rem = cur % d; + } + scr_prec_normalize(v); + return (uint32_t)rem; +} + +static void scr_prec_shl(ScrPrecInt *v, int bits) { + uint32_t out[SCR_PREC_LIMBS] = {0}; + int words = bits / 32; + int rem = bits % 32; + for (int i = 0; i < v->len; i++) { + int at = i + words; + out[at] |= v->limb[i] << rem; + if (rem != 0 && at + 1 < SCR_PREC_LIMBS) out[at + 1] |= v->limb[i] >> (32 - rem); + } + int n = v->len + words + 1; + if (n > SCR_PREC_LIMBS) n = SCR_PREC_LIMBS; + memcpy(v->limb, out, sizeof out); + v->len = n; + scr_prec_normalize(v); +} + +static void scr_prec_shr(ScrPrecInt *v, int bits) { + int words = bits / 32; + int rem = bits % 32; + if (words >= v->len) { + v->limb[0] = 0; + v->len = 1; + return; + } + int n = v->len - words; + for (int i = 0; i < n; i++) { + uint32_t lo = v->limb[i + words] >> rem; + uint32_t hi = rem != 0 && i + words + 1 < v->len ? v->limb[i + words + 1] << (32 - rem) : 0; + v->limb[i] = lo | hi; + } + v->len = n; + scr_prec_normalize(v); +} + +/* round_half_up(a × 10^f) for positive finite a, as a decimal digit + * string written right-aligned ending at `end`; returns the first digit. */ +static char *scr_prec_scaled_digits(double a, int f, char *end) { + uint64_t bits; + memcpy(&bits, &a, sizeof bits); + uint64_t mantissa = bits & ((UINT64_C(1) << 52) - 1); + int ieee_exp = (int)((bits >> 52) & 0x7ffu); + int binary_exp; + if (ieee_exp == 0) { + binary_exp = -1074; + } else { + mantissa |= UINT64_C(1) << 52; + binary_exp = ieee_exp - 1023 - 52; + } + ScrPrecInt v = {{(uint32_t)mantissa, (uint32_t)(mantissa >> 32)}, 2}; + scr_prec_normalize(&v); + for (int i = 0; i < f; i++) scr_prec_mul_small(&v, 5); + int twos = binary_exp + f + 1; /* the +1 doubles: floor(2v) */ + if (twos > 0) scr_prec_shl(&v, twos); + for (int i = 0; i < -f; i++) scr_prec_div_small(&v, 5); + if (twos < 0) scr_prec_shr(&v, -twos); + /* (floor(2v) + 1) / 2 */ + uint64_t carry = 1; + for (int i = 0; i < v.len && carry != 0; i++) { + uint64_t s = (uint64_t)v.limb[i] + carry; + v.limb[i] = (uint32_t)s; + carry = s >> 32; + } + if (carry != 0) v.limb[v.len++] = (uint32_t)carry; + scr_prec_shr(&v, 1); + char *p = end; + for (;;) { + uint32_t chunk = scr_prec_div_small(&v, 1000000000u); + bool last = v.len == 1 && v.limb[0] == 0; + for (int i = 0; i < 9 && (chunk != 0 || !last || p == end); i++) { + *--p = (char)('0' + chunk % 10); + chunk /= 10; + } + if (last) break; + } + return p; +} + +int scr_f64_precision_digits(double a, int p, char *digits, int *e_out) { + char shortest[18]; + int n; + scr_f64_digits(a, shortest, &n); + int e = n - 1; + char buf[400]; + char *end = buf + sizeof buf; + for (int attempt = 0; attempt < 3; attempt++) { + char *first = scr_prec_scaled_digits(a, p - 1 - e, end); + int len = (int)(end - first); + if (len == p) { + memcpy(digits, first, (size_t)p); + digits[p] = '\0'; + *e_out = e; + return p; + } + if (len > p) { + /* Rounding carried into a new digit (or the estimate was low). */ + if (len == p + 1 && first[0] == '1') { + bool zeros = true; + for (int i = 1; i < len; i++) zeros = zeros && first[i] == '0'; + if (zeros) { + digits[0] = '1'; + memset(digits + 1, '0', (size_t)(p - 1)); + digits[p] = '\0'; + *e_out = e + 1; + return p; + } + } + e++; + } else { + e--; + } + } + /* Unreachable for finite positive doubles: the estimate is off by at + * most one decade. */ + memset(digits, '0', (size_t)p); + digits[p] = '\0'; + *e_out = e; + return p; +} diff --git a/packages/runtime/src/scr_number.h b/packages/runtime/src/scr_number.h index 2d95d95f5..f43195116 100644 --- a/packages/runtime/src/scr_number.h +++ b/packages/runtime/src/scr_number.h @@ -37,6 +37,12 @@ int scr_decimal_scan(const char *p, size_t n, double *out); * the length. */ size_t scr_f64_to_fixed(double x, int f, char *buf); +/* toPrecision / toExponential(fractionDigits) digits for POSITIVE finite + * a and 1 <= p <= 101: the spec's p-digit n (ties to the larger n on the + * exact binary value) into digits (NUL-terminated, >= p + 1 bytes) and its + * decimal exponent e (value ~ d.ddd x 10^e) into *e_out. Returns p. */ +int scr_f64_precision_digits(double a, int p, char *digits, int *e_out); + /* ToString for template literals / string coercion. Returns +1. */ ScrStr *scr_f64_to_scrstr(double x); ScrStr *scr_bool_to_scrstr(bool b); /* interned "true"/"false" */ @@ -73,6 +79,11 @@ bool scr_num_is_safe_integer(double x); ScrStr *scr_num_to_exponential(double x); ScrStr *scr_num_to_fixed0(double x); ScrStr *scr_num_to_fixed(double x, double fraction_digits); +/* Number.prototype.toPrecision(precision) and toExponential(fractionDigits) + * with the argument already coerced (has_precision false = undefined). + * Invalid digit counts throw V8's RangeError (NULL). Result +1. */ +ScrStr *scr_num_to_precision(double x, double precision, bool has_precision); +ScrStr *scr_num_to_exponential_digits(double x, double fraction_digits); ScrStr *scr_num_to_string_radix_f64(double value, double radix); /* Object.is over two numbers — the spec's SameValue on doubles: NaN diff --git a/packages/runtime/src/scr_os.c b/packages/runtime/src/scr_os.c index 2e70d0d85..500283995 100644 --- a/packages/runtime/src/scr_os.c +++ b/packages/runtime/src/scr_os.c @@ -1526,3 +1526,178 @@ ScrDyn *scr_node_constants(const ScrDyn *crypto_table) { scr_atexit(scr_node_constants_clear); return scr_dyn_retain(o); } + +/* fs.constants as a value: node_constants.cc's fs group (the set + * node:constants merges above), in its order and under its #ifdef guards, + * on its own cached null-prototype object with read-only members. It keeps + * its own table instead of calling the scr_fs_constants fill: a second + * caller would stop that static from inlining into scr_node_constants and + * change every binary that links node:constants. The names live inside + * the table entries, so the table is one data object section GC drops. + * Nothing on an existing path calls this. */ +typedef struct { + char name[32]; + double value; +} ScrFsConstant; +#define SCR_FS_C(n) {#n, (double)(n)} +static const ScrFsConstant scr_fs_constant_table[] = { + {"UV_FS_SYMLINK_DIR", 1}, + {"UV_FS_SYMLINK_JUNCTION", 2}, +#ifdef O_RDONLY + SCR_FS_C(O_RDONLY), +#endif +#ifdef O_WRONLY + SCR_FS_C(O_WRONLY), +#endif +#ifdef O_RDWR + SCR_FS_C(O_RDWR), +#endif + {"UV_DIRENT_UNKNOWN", 0}, + {"UV_DIRENT_FILE", 1}, + {"UV_DIRENT_DIR", 2}, + {"UV_DIRENT_LINK", 3}, + {"UV_DIRENT_FIFO", 4}, + {"UV_DIRENT_SOCKET", 5}, + {"UV_DIRENT_CHAR", 6}, + {"UV_DIRENT_BLOCK", 7}, +#ifdef S_IFMT + SCR_FS_C(S_IFMT), +#endif +#ifdef S_IFREG + SCR_FS_C(S_IFREG), +#endif +#ifdef S_IFDIR + SCR_FS_C(S_IFDIR), +#endif +#ifdef S_IFCHR + SCR_FS_C(S_IFCHR), +#endif +#ifdef S_IFBLK + SCR_FS_C(S_IFBLK), +#endif +#ifdef S_IFIFO + SCR_FS_C(S_IFIFO), +#endif +#ifdef S_IFLNK + SCR_FS_C(S_IFLNK), +#endif +#ifdef S_IFSOCK + SCR_FS_C(S_IFSOCK), +#endif +#ifdef O_CREAT + SCR_FS_C(O_CREAT), +#endif +#ifdef O_EXCL + SCR_FS_C(O_EXCL), +#endif + {"UV_FS_O_FILEMAP", 0}, + {"UV_FS_O_TEMPORARY", 0}, + {"UV_FS_O_SHORT_LIVED", 0}, + {"UV_FS_O_SEQUENTIAL", 0}, + {"UV_FS_O_RANDOM", 0}, +#ifdef O_NOCTTY + SCR_FS_C(O_NOCTTY), +#endif +#ifdef O_TRUNC + SCR_FS_C(O_TRUNC), +#endif +#ifdef O_APPEND + SCR_FS_C(O_APPEND), +#endif +#ifdef O_DIRECTORY + SCR_FS_C(O_DIRECTORY), +#endif +#ifdef O_NOATIME + SCR_FS_C(O_NOATIME), +#endif +#ifdef O_NOFOLLOW + SCR_FS_C(O_NOFOLLOW), +#endif +#ifdef O_SYNC + SCR_FS_C(O_SYNC), +#endif +#ifdef O_DSYNC + SCR_FS_C(O_DSYNC), +#endif +#ifdef O_SYMLINK + SCR_FS_C(O_SYMLINK), +#endif +#ifdef O_DIRECT + SCR_FS_C(O_DIRECT), +#endif +#ifdef O_NONBLOCK + SCR_FS_C(O_NONBLOCK), +#endif +#ifdef S_IRWXU + SCR_FS_C(S_IRWXU), +#endif +#ifdef S_IRUSR + SCR_FS_C(S_IRUSR), +#endif +#ifdef S_IWUSR + SCR_FS_C(S_IWUSR), +#endif +#ifdef S_IXUSR + SCR_FS_C(S_IXUSR), +#endif +#ifdef S_IRWXG + SCR_FS_C(S_IRWXG), +#endif +#ifdef S_IRGRP + SCR_FS_C(S_IRGRP), +#endif +#ifdef S_IWGRP + SCR_FS_C(S_IWGRP), +#endif +#ifdef S_IXGRP + SCR_FS_C(S_IXGRP), +#endif +#ifdef S_IRWXO + SCR_FS_C(S_IRWXO), +#endif +#ifdef S_IROTH + SCR_FS_C(S_IROTH), +#endif +#ifdef S_IWOTH + SCR_FS_C(S_IWOTH), +#endif +#ifdef S_IXOTH + SCR_FS_C(S_IXOTH), +#endif +#ifdef F_OK + SCR_FS_C(F_OK), +#endif +#ifdef R_OK + SCR_FS_C(R_OK), +#endif +#ifdef W_OK + SCR_FS_C(W_OK), +#endif +#ifdef X_OK + SCR_FS_C(X_OK), +#endif + {"UV_FS_COPYFILE_EXCL", 1}, + {"COPYFILE_EXCL", 1}, + {"UV_FS_COPYFILE_FICLONE", 2}, + {"COPYFILE_FICLONE", 2}, + {"UV_FS_COPYFILE_FICLONE_FORCE", 4}, + {"COPYFILE_FICLONE_FORCE", 4}, +}; +#undef SCR_FS_C + +static SCR_TL ScrDyn *scr_fs_constants_value; +static void scr_fs_constants_clear(void) { + ScrDyn *value = scr_fs_constants_value; + scr_fs_constants_value = NULL; + scr_dyn_release(value); +} + +ScrDyn *scr_fs_constants_object(void) { + if (scr_fs_constants_value) return scr_dyn_retain(scr_fs_constants_value); + ScrDyn *o = scr_dyn_new_obj_null_proto(); + for (size_t i = 0; i < sizeof scr_fs_constant_table / sizeof scr_fs_constant_table[0]; i++) + scr_os_constant(o, scr_fs_constant_table[i].name, scr_fs_constant_table[i].value); + scr_fs_constants_value = o; + scr_atexit(scr_fs_constants_clear); + return scr_dyn_retain(o); +} diff --git a/packages/runtime/src/scr_os.h b/packages/runtime/src/scr_os.h index 219726660..57f219ad1 100644 --- a/packages/runtime/src/scr_os.h +++ b/packages/runtime/src/scr_os.h @@ -38,6 +38,8 @@ ScrStr *scr_os_cpus_model(double i); double scr_os_cpus_field(double i, double field); /* os.constants: the per-thread null-prototype constants object (+1). */ ScrDyn *scr_os_constants(void); +/* fs.constants: the target's null-prototype constants object (cached, +1). */ +ScrDyn *scr_fs_constants_object(void); /* The deprecated node:constants module object (scr_os.c): the os groups, * fs.constants and the crypto table the borrowed function answers, merged * and frozen (+1). */ diff --git a/packages/runtime/src/scr_regex.c b/packages/runtime/src/scr_regex.c index 44a91410b..fa104058f 100644 --- a/packages/runtime/src/scr_regex.c +++ b/packages/runtime/src/scr_regex.c @@ -2020,6 +2020,222 @@ static ScrStr *scr_str_case_conv(const ScrStr *s, int to_lower) { ScrStr *scr_str_to_lower(const ScrStr *s) { return scr_str_case_conv(s, 1); } ScrStr *scr_str_to_upper(const ScrStr *s) { return scr_str_case_conv(s, 0); } +/* ── String.prototype.toLocaleLowerCase / toLocaleUpperCase ──────────── + * V8 decides the case mapping from the FIRST requested locale only + * (probed: ["en", 5] maps like "en", [5] throws, a non-object primitive + * requests nothing). ICU tailors case mapping for Turkic (tr, az: dotted + * and dotless I), Lithuanian (dot above retention) and Greek uppercasing + * (accent removal); every other language uses the default conversion. + * The Turkic rules are SpecialCasing's: lowercase I → ı unless a + * U+0307 follows (then I + U+0307 → i), İ → i; uppercase i → İ. Tags are + * checked against the unicode_locale_id structure; anything this unit + * cannot decide exactly (malformed tags, whose V8 RangeError texts vary, + * objects, lt, el uppercasing, I before other combining marks) throws a + * 'not supported yet' Error rather than guess. Nothing on an existing + * path calls this. */ + +static bool scr_tag_alpha(const char *p, size_t n) { + for (size_t i = 0; i < n; i++) + if (!((p[i] >= 'a' && p[i] <= 'z') || (p[i] >= 'A' && p[i] <= 'Z'))) return false; + return true; +} + +static bool scr_tag_digit(const char *p, size_t n) { + for (size_t i = 0; i < n; i++) + if (!(p[i] >= '0' && p[i] <= '9')) return false; + return true; +} + +static bool scr_tag_alnum(const char *p, size_t n) { + for (size_t i = 0; i < n; i++) + if (!scr_tag_alpha(p + i, 1) && !scr_tag_digit(p + i, 1)) return false; + return true; +} + +static bool scr_tag_same(const char *a, size_t an, const char *b, size_t bn) { + if (an != bn) return false; + for (size_t i = 0; i < an; i++) { + char x = a[i] >= 'A' && a[i] <= 'Z' ? (char)(a[i] + 32) : a[i]; + char y = b[i] >= 'A' && b[i] <= 'Z' ? (char)(b[i] + 32) : b[i]; + if (x != y) return false; + } + return true; +} + +/* 0 default, 1 Turkic, 2 Lithuanian, 3 Greek; -1 when the tag is outside + * the structural grammar handled here. */ +static int scr_locale_case_rules(const char *tag, size_t len) { + const char *subtags[64]; + size_t lens[64]; + size_t count = 0; + size_t start = 0; + for (size_t i = 0; i <= len; i++) { + if (i < len && tag[i] != '-') continue; + if (i == start || count == 64) return -1; + subtags[count] = tag + start; + lens[count++] = i - start; + start = i + 1; + } + size_t k = 0; + size_t lang = lens[0]; + if (!scr_tag_alpha(subtags[0], lang) || !((lang >= 2 && lang <= 3) || (lang >= 5 && lang <= 8))) + return -1; + k = 1; + if (k < count && lens[k] == 4 && scr_tag_alpha(subtags[k], 4)) k++; + if (k < count && ((lens[k] == 2 && scr_tag_alpha(subtags[k], 2)) || + (lens[k] == 3 && scr_tag_digit(subtags[k], 3)))) + k++; + size_t variants = k; + while (k < count && scr_tag_alnum(subtags[k], lens[k]) && + ((lens[k] >= 5 && lens[k] <= 8) || (lens[k] == 4 && scr_tag_digit(subtags[k], 1)))) { + for (size_t j = variants; j < k; j++) + if (scr_tag_same(subtags[j], lens[j], subtags[k], lens[k])) return -1; + k++; + } + size_t singletons = k; + while (k < count) { + if (lens[k] != 1 || !scr_tag_alnum(subtags[k], 1)) return -1; + for (size_t j = singletons; j < k; j++) + if (lens[j] == 1 && scr_tag_same(subtags[j], 1, subtags[k], 1)) return -1; + bool private_use = subtags[k][0] == 'x' || subtags[k][0] == 'X'; + size_t min = private_use ? 1 : 2; + size_t first = ++k; + while (k < count && lens[k] >= min && lens[k] <= 8 && scr_tag_alnum(subtags[k], lens[k]) && + (private_use || lens[k] > 1)) + k++; + if (k == first) return -1; + if (private_use && k != count) return -1; + } + if (lang > 3) return 0; + char l[3] = {0, 0, 0}; + for (size_t i = 0; i < lang; i++) + l[i] = subtags[0][i] >= 'A' && subtags[0][i] <= 'Z' ? (char)(subtags[0][i] + 32) : subtags[0][i]; + bool two = lang == 2; + if ((two && l[0] == 't' && l[1] == 'r') || (two && l[0] == 'a' && l[1] == 'z') || + (!two && l[0] == 't' && l[1] == 'u' && l[2] == 'r') || + (!two && l[0] == 'a' && l[1] == 'z' && l[2] == 'e')) + return 1; + if ((two && l[0] == 'l' && l[1] == 't') || (!two && l[0] == 'l' && l[1] == 'i' && l[2] == 't')) + return 2; + if ((two && l[0] == 'e' && l[1] == 'l') || (!two && l[0] == 'e' && l[1] == 'l' && l[2] == 'l') || + (!two && l[0] == 'g' && l[1] == 'r' && l[2] == 'e')) + return 3; + return 0; +} + +static ScrStr *scr_locale_case_unsupported(bool upper) { + static const char lower_msg[] = + "String.prototype.toLocaleLowerCase with this locale argument is not supported yet"; + static const char upper_msg[] = + "String.prototype.toLocaleUpperCase with this locale argument is not supported yet"; + /* The unit's existing "SC2020" literal: a separate constant array would + * absorb it (LLVM merges identical constants) and move it out of the + * string section every regex-linking binary keeps. */ + if (upper) scr_throw_error_msg_code(SCR_ERR_ERROR, upper_msg, sizeof upper_msg - 1, "SC2020"); + else scr_throw_error_msg_code(SCR_ERR_ERROR, lower_msg, sizeof lower_msg - 1, "SC2020"); + return NULL; +} + +static ScrStr *scr_turkic_case(const ScrStr *s, bool upper) { + ScrJsonBuf b; + scr_jb_init(&b); + for (size_t i = 0; i < s->len; i++) { + unsigned char c = (unsigned char)s->data[i]; + if (upper && c == 'i') { + scr_jb_putc(&b, (char)0xC4); /* İ */ + scr_jb_putc(&b, (char)0xB0); + continue; + } + if (!upper && c == 'I') { + unsigned char n0 = i + 1 < s->len ? (unsigned char)s->data[i + 1] : 0; + unsigned char n1 = i + 2 < s->len ? (unsigned char)s->data[i + 2] : 0; + if (n0 == 0xCC && n1 == 0x87) { /* I + U+0307 → i */ + scr_jb_putc(&b, 'i'); + i += 2; + continue; + } + /* Another combining mark (U+0300..U+036F) may precede a U+0307. */ + if ((n0 == 0xCC && n1 >= 0x80) || (n0 == 0xCD && n1 >= 0x80 && n1 <= 0xAF)) { + scr_str_release(scr_jb_finish(&b)); + return scr_locale_case_unsupported(upper); + } + scr_jb_putc(&b, (char)0xC4); /* ı */ + scr_jb_putc(&b, (char)0xB1); + continue; + } + if (!upper && c == 0xC4 && i + 1 < s->len && (unsigned char)s->data[i + 1] == 0xB0) { + scr_jb_putc(&b, 'i'); /* İ → i */ + i++; + continue; + } + scr_jb_putc(&b, (char)c); + } + ScrStr *mapped = scr_jb_finish(&b); + ScrStr *result = scr_str_case_conv(mapped, upper ? 0 : 1); + scr_str_release(mapped); + return result; +} + +ScrStr *scr_str_locale_case(const ScrStr *s, ScrDyn *locales, bool upper) { + ScrDyn *owned = NULL; + if (locales->kind == SCR_DYN_TYPED_REF) { + owned = scr_dyn_typed_ref_materialize(locales); + if (!owned) return NULL; + locales = owned; + } + const ScrDyn *tag = NULL; + int rules = 0; + switch (locales->kind) { + case SCR_DYN_NULL: { + static const char msg[] = "Cannot convert undefined or null to object"; + scr_throw_error_msg(SCR_ERR_TYPE, msg, sizeof msg - 1); + return NULL; + } + case SCR_DYN_UNDEF: + case SCR_DYN_NUM: + case SCR_DYN_BOOL: + case SCR_DYN_SYMBOL: + case SCR_DYN_BIGINT: + break; + case SCR_DYN_STR: + tag = locales; + break; + case SCR_DYN_ARR: + if (locales->v.arr.len == 0) break; + if (locales->v.arr.presence && !locales->v.arr.presence[0]) { + static const char msg[] = "Language ID should be string or object."; + scr_throw_error_msg(SCR_ERR_TYPE, msg, sizeof msg - 1); + scr_dyn_release(owned); + return NULL; + } + tag = locales->v.arr.items[0]; + if (tag->kind != SCR_DYN_STR) { + bool primitive = tag->kind == SCR_DYN_UNDEF || tag->kind == SCR_DYN_NULL || + tag->kind == SCR_DYN_NUM || tag->kind == SCR_DYN_BOOL || + tag->kind == SCR_DYN_BIGINT || tag->kind == SCR_DYN_SYMBOL; + scr_dyn_release(owned); + if (!primitive) return scr_locale_case_unsupported(upper); + static const char msg[] = "Language ID should be string or object."; + scr_throw_error_msg(SCR_ERR_TYPE, msg, sizeof msg - 1); + return NULL; + } + break; + default: + scr_dyn_release(owned); + return scr_locale_case_unsupported(upper); + } + if (tag) { + rules = scr_locale_case_rules(tag->v.str->data, tag->v.str->len); + if (rules < 0 || rules == 2 || (rules == 3 && upper)) { + scr_dyn_release(owned); + return scr_locale_case_unsupported(upper); + } + } + scr_dyn_release(owned); + if (rules == 1) return scr_turkic_case(s, upper); + return scr_str_case_conv(s, upper ? 0 : 1); +} + static void *scr_normalize_realloc(void *opaque, void *ptr, size_t size) { (void)opaque; if (!size) { free(ptr); return NULL; } diff --git a/packages/runtime/src/scr_string.c b/packages/runtime/src/scr_string.c index 43cf21b0c..8358658b5 100644 --- a/packages/runtime/src/scr_string.c +++ b/packages/runtime/src/scr_string.c @@ -6,6 +6,11 @@ #include #include #include +#if defined(__SSE2__) +#include +#elif defined(__ARM_NEON) && defined(__aarch64__) +#include +#endif /* Live heap-string count for the RC audit lane (-DSCR_RC_AUDIT): the test * harness builds with it to prove the emitted retain/release discipline @@ -836,24 +841,93 @@ static __attribute__((noinline)) int scr_str_cmp_u16_units(const ScrStr *a, cons } } +/* The memory index of the first differing byte of two words; x is their + * xor and is not zero. */ +static inline size_t scr_word_mismatch(uint64_t x) { +#if defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__ + return (size_t)__builtin_clzll(x) >> 3; +#else + return (size_t)__builtin_ctzll(x) >> 3; +#endif +} + +static inline size_t scr_word32_mismatch(uint32_t x) { +#if defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__ + return (size_t)__builtin_clz(x) >> 3; +#else + return (size_t)__builtin_ctz(x) >> 3; +#endif +} + +/* The index of the first byte where the n-byte spans a and b differ, or n + * when they are equal. Spans of at least 8 bytes compare words up to byte + * 32, then 32-byte blocks with SSE2 or NEON where the target has them, + * then words; the last word overlaps bytes already compared. Shorter + * spans compare two overlapping 4-byte halves, or bytes. */ +static inline size_t scr_span_mismatch(const char *a, const char *b, size_t n) { + size_t i = 0; + uint64_t l, r; + if (n < sizeof(uint64_t)) { + if (n >= sizeof(uint32_t)) { + uint32_t l4, r4; + memcpy(&l4, a, 4); + memcpy(&r4, b, 4); + if (l4 != r4) return scr_word32_mismatch(l4 ^ r4); + memcpy(&l4, a + n - 4, 4); + memcpy(&r4, b + n - 4, 4); + return l4 != r4 ? n - 4 + scr_word32_mismatch(l4 ^ r4) : n; + } + while (i < n && a[i] == b[i]) i++; + return i; + } + memcpy(&l, a, 8); + memcpy(&r, b, 8); + if (l != r) return scr_word_mismatch(l ^ r); + size_t head = n < 32 ? n & ~(size_t)7 : 32; + for (i = sizeof(uint64_t); i < head; i += sizeof(uint64_t)) { + memcpy(&l, a + i, 8); + memcpy(&r, b + i, 8); + if (l != r) return i + scr_word_mismatch(l ^ r); + } +#if defined(__SSE2__) + for (; n - i >= 32; i += 32) { + __m128i e0 = _mm_cmpeq_epi8(_mm_loadu_si128((const __m128i *)(a + i)), + _mm_loadu_si128((const __m128i *)(b + i))); + __m128i e1 = _mm_cmpeq_epi8(_mm_loadu_si128((const __m128i *)(a + i + 16)), + _mm_loadu_si128((const __m128i *)(b + i + 16))); + uint32_t equal = (uint32_t)_mm_movemask_epi8(e0) | (uint32_t)_mm_movemask_epi8(e1) << 16; + if (equal != UINT32_MAX) return i + (size_t)__builtin_ctz(~equal); + } +#elif defined(__ARM_NEON) && defined(__aarch64__) + /* A differing block leaves the loop; the word loop below finds the byte. */ + for (; n - i >= 32; i += 32) { + uint8x16_t e0 = vceqq_u8(vld1q_u8((const uint8_t *)a + i), vld1q_u8((const uint8_t *)b + i)); + uint8x16_t e1 = + vceqq_u8(vld1q_u8((const uint8_t *)a + i + 16), vld1q_u8((const uint8_t *)b + i + 16)); + if (vminvq_u8(vandq_u8(e0, e1)) != 0xff) break; + } +#endif + for (; n - i >= sizeof(uint64_t); i += sizeof(uint64_t)) { + memcpy(&l, a + i, 8); + memcpy(&r, b + i, 8); + if (l != r) return i + scr_word_mismatch(l ^ r); + } + if (i == n) return n; + size_t j = n - sizeof(uint64_t); + memcpy(&l, a + j, 8); + memcpy(&r, b + j, 8); + return l != r ? j + scr_word_mismatch(l ^ r) : n; +} + /* UTF-16 ordering over canonical WTF-8. Equal bytes can be skipped even * inside a multibyte character: unequal continuation bytes preserve code * point order. Differing lead bytes for U+E000..U+FFFF versus a * supplementary character reverse byte order, because the latter starts * with a UTF-16 high surrogate below U+E000. A lone surrogate against a * supplementary character compares their units. */ -int scr_str_cmp_u16(ScrStr *a, ScrStr *b) { - if (a == b) return 0; +__attribute__((noinline)) int scr_str_cmp_u16_bytes(const ScrStr *a, const ScrStr *b) { size_t common = a->len < b->len ? a->len : b->len; - size_t i = 0; - while (common - i >= sizeof(uint64_t)) { - uint64_t left, right; - memcpy(&left, a->data + i, sizeof(left)); - memcpy(&right, b->data + i, sizeof(right)); - if (left != right) break; - i += sizeof(left); - } - while (i < common && a->data[i] == b->data[i]) i++; + size_t i = scr_span_mismatch(a->data, b->data, common); if (i == common) return (a->len > b->len) - (a->len < b->len); unsigned char left = (unsigned char)a->data[i]; unsigned char right = (unsigned char)b->data[i]; @@ -866,6 +940,16 @@ int scr_str_cmp_u16(ScrStr *a, ScrStr *b) { return left < right ? -1 : 1; } +/* Byte order differs from UTF-16 order only where both differing bytes are + * at least 0xED, which a proven-ASCII string never has: such pairs compare + * with memcmp. The scan stays out of line in a global function, so program + * builds can import this body. */ +int scr_str_cmp_u16(ScrStr *a, ScrStr *b) { + if (a == b) return 0; + if (scr_str_known_ascii(a) || scr_str_known_ascii(b)) return scr_str_cmp(a, b); + return scr_str_cmp_u16_bytes(a, b); +} + /* ── interned strings ───────────────────────────────────────────────── * The empty string and every single-character ASCII string are immortal * statics (same layout the emitter uses for literals): charAt/slice churn diff --git a/packages/runtime/src/scr_string.h b/packages/runtime/src/scr_string.h index 425dfaabf..fc2f15562 100644 --- a/packages/runtime/src/scr_string.h +++ b/packages/runtime/src/scr_string.h @@ -290,16 +290,19 @@ ScrStr *scr_str_concat_f64(ScrStr *a, double x); bool scr_str_eq(ScrStr *a, ScrStr *b); -/* memcmp byte order == code-point order (see SEMANTICS.md: diverges from - * JS UTF-16 code-unit order only for non-BMP vs U+E000..U+FFFF). Returns - * <0, 0, >0. */ +/* memcmp byte order == code-point order, which differs from JS UTF-16 + * code-unit order for non-BMP characters against U+E000..U+FFFF and for + * lone surrogates. Returns <0, 0, >0. Relational string operators use + * scr_str_cmp_u16. */ int scr_str_cmp(ScrStr *a, ScrStr *b); /* a.localeCompare(b) with no locales/options: Node's root-locale order for * Latin-script text, code-point order outside it (scr_string.c). */ double scr_str_locale_compare(ScrStr *a, ScrStr *b); -/* ECMAScript string-list ordering: compare UTF-16 code units even though - * ScrStr stores WTF-8. Returns <0, 0, >0. */ +/* ECMAScript string ordering: compare UTF-16 code units even though ScrStr + * stores WTF-8. Returns <0, 0, >0. */ int scr_str_cmp_u16(ScrStr *a, ScrStr *b); +/* scr_str_cmp_u16 for two distinct strings that are not proven ASCII. */ +int scr_str_cmp_u16_bytes(const ScrStr *a, const ScrStr *b); #endif /* SCR_STRING_H */ diff --git a/packages/runtime/src/scr_string_methods.h b/packages/runtime/src/scr_string_methods.h index 62354142e..6ed2e09df 100644 --- a/packages/runtime/src/scr_string_methods.h +++ b/packages/runtime/src/scr_string_methods.h @@ -73,6 +73,10 @@ bool scr_str_ends_with_from(ScrStr *s, ScrStr *needle, double end_position); ScrStr *scr_str_slice(ScrStr *s, double start, double end); /* substring: clamp-and-swap (negatives clamp to 0, start > end swaps). */ ScrStr *scr_str_substring(ScrStr *s, double start, double end); +/* Annex B substr(start, length): start already ToNumber'd (NaN → 0), a + * negative start counts from the end, length absent (has_length false) + * runs to the end. Returns +1. */ +ScrStr *scr_str_substr(ScrStr *s, double start, double length, bool has_length); /* Scalar-only local slices retain their original owner without copying. * Bounds and split-surrogate replacements are snapshotted at construction. */ @@ -111,6 +115,13 @@ ScrStr *scr_str_trim(ScrStr *s); ScrStr *scr_str_to_lower(const ScrStr *s); ScrStr *scr_str_to_upper(const ScrStr *s); ScrStr *scr_str_normalize(const ScrStr *s, const ScrStr *form); +/* toLocaleLowerCase/toLocaleUpperCase(locales) (scr_regex.c): undefined, + * empty lists, and primitives other than strings use the default case + * conversion; the first requested tag selects the Turkic (tr, az) rules or + * the default ones. Lithuanian, Greek uppercasing and tags outside the + * structural grammar throw a 'not supported yet' Error; null throws Node's + * TypeError. Borrows both; +1 or NULL with an exception pending. */ +ScrStr *scr_str_locale_case(const ScrStr *s, struct ScrDyn *locales, bool upper); /* NFC over a code point buffer (scr_regex.c, beside normalize): a fresh * malloc'd buffer the caller frees, its length in *out_len. Borrows input. */ uint32_t *scr_nfc_code_points(const uint32_t *input, size_t length, size_t *out_len); diff --git a/packages/runtime/src/scr_symbol.c b/packages/runtime/src/scr_symbol.c index 383b95986..304f2544f 100644 --- a/packages/runtime/src/scr_symbol.c +++ b/packages/runtime/src/scr_symbol.c @@ -181,72 +181,33 @@ void scr_assert_eq_sym(ScrSym *a, ScrSym *b, bool negated, bool deep, scr_str_release(ib); } -/* Symbol-keyed globals share the symbol unit's link gate. The always-linked - * JSON unit must not depend on optional symbol retain/release functions. */ -typedef struct ScrGlobalSymbolProperty { - ScrSym *key; - ScrDyn *value; - struct ScrGlobalSymbolProperty *next; -} ScrGlobalSymbolProperty; - -static SCR_TL ScrGlobalSymbolProperty *scr_global_symbols = NULL; -static SCR_TL bool scr_global_symbols_cleanup_registered = false; - -static void scr_global_symbols_cleanup(void) { - while (scr_global_symbols) { - ScrGlobalSymbolProperty *entry = scr_global_symbols; - scr_global_symbols = entry->next; - scr_sym_release(entry->key); - scr_dyn_release(entry->value); - free(entry); - } -} - +/* Direct `globalThis[symbol]` accesses use the symbol-keyed properties of + * the global object's box (scr_global_object), the storage every other + * spelling of the global object reaches, so all of them see one property. */ ScrDyn *scr_dyn_global_symbol_get(ScrSym *key) { - for (ScrGlobalSymbolProperty *entry = scr_global_symbols; entry; entry = entry->next) { - if (entry->key == key) return scr_dyn_retain(entry->value); - } - return scr_dyn_retain(scr_dyn_undefined()); + ScrDyn *k = scr_dyn_new_symbol(key); + ScrDyn *value = scr_dyn_symbol_key_get(scr_global_object(), k, false); + scr_dyn_release(k); + return value; } void scr_dyn_global_symbol_set(ScrSym *key, ScrDyn *value) { - for (ScrGlobalSymbolProperty *entry = scr_global_symbols; entry; entry = entry->next) { - if (entry->key != key) continue; - ScrDyn *previous = entry->value; - entry->value = scr_dyn_retain(value); - scr_dyn_release(previous); - return; - } - if (!scr_global_symbols_cleanup_registered) { - scr_atexit(scr_global_symbols_cleanup); - scr_global_symbols_cleanup_registered = true; - } - ScrGlobalSymbolProperty *entry = malloc(sizeof *entry); - if (!entry) scr_trap("scriptc: out of memory\n"); - entry->key = scr_sym_retain(key); - entry->value = scr_dyn_retain(value); - entry->next = scr_global_symbols; - scr_global_symbols = entry; + ScrDyn *k = scr_dyn_new_symbol(key); + scr_dyn_symbol_key_set(scr_global_object(), k, value); + scr_dyn_release(k); } bool scr_dyn_global_symbol_has(ScrSym *key) { - for (ScrGlobalSymbolProperty *entry = scr_global_symbols; entry; entry = entry->next) { - if (entry->key == key) return true; - } - return false; + ScrDyn *k = scr_dyn_new_symbol(key); + bool present = scr_dyn_has_own_computed(scr_global_object(), k); + scr_dyn_release(k); + return present; } void scr_dyn_global_symbol_delete(ScrSym *key) { - ScrGlobalSymbolProperty **slot = &scr_global_symbols; - while (*slot) { - ScrGlobalSymbolProperty *entry = *slot; - if (entry->key != key) { slot = &entry->next; continue; } - *slot = entry->next; - scr_sym_release(entry->key); - scr_dyn_release(entry->value); - free(entry); - return; - } + ScrDyn *k = scr_dyn_new_symbol(key); + scr_dyn_key_delete_computed(scr_global_object(), k, false); + scr_dyn_release(k); } /* ── fs.Dirent as a dynamic value ──────────────────────────────────── diff --git a/packages/runtime/src/scr_webcrypto.c b/packages/runtime/src/scr_webcrypto.c new file mode 100644 index 000000000..546b24151 --- /dev/null +++ b/packages/runtime/src/scr_webcrypto.c @@ -0,0 +1,829 @@ +/* WebCrypto's HMAC slice: crypto.subtle.importKey (raw HMAC keys), + * sign, verify and the SHA-1/SHA-2 digests, plus the CryptoKey values + * importKey answers. + * + * This unit is only the WebCrypto layer, modeled on Node 24's + * lib/internal/crypto/webcrypto.js and mac.js: Web IDL argument + * conversion, algorithm normalization, the CryptoKey handle, promise + * wrapping and Node's error shapes. The MACs and digests themselves come + * from node:crypto's digest core (scr_crypto_hmac_raw and + * scr_crypto_digest_raw in scr_lib.c), and results settle through the + * deferred crypto queue that node:crypto's callback forms use, so sign, + * verify and digest settle on a later loop turn like Node's thread-pool + * jobs while importKey settles at once. + * + * Every argument and validation failure rejects the returned promise. + * Algorithms Node recognizes but this slice does not implement (RSA, + * ECDSA, AES, SHA-3, ...) and the 'jwk' key format reject with a coded + * SC2020 Error instead of silently diverging. The compiler links the unit + * only into programs that call these methods (the webcrypto feature). */ +#include "scr_runtime.h" + +#include +#include +#include +#include +#include + +/* ── algorithm registry ─────────────────────────────────────────────── + * The names Node 24 registers per operation (util.js's + * kAlgorithmDefinitions with the conditional algorithms its OpenSSL + * build enables), matched case-insensitively. Only HMAC and the SHA-1/ + * SHA-2 digests are implemented. */ +static const char *const scr_wc_import_names[] = { + "AES-CBC", "AES-CTR", "AES-GCM", "AES-KW", "AES-OCB", + "Argon2d", "Argon2i", "Argon2id", "ChaCha20-Poly1305", + "ECDH", "ECDSA", "Ed25519", "Ed448", "HKDF", + "HMAC", "KMAC128", "KMAC256", "ML-DSA-44", "ML-DSA-65", + "ML-DSA-87", "ML-KEM-512", "ML-KEM-768", "ML-KEM-1024", "MLKEM768-P256", + "MLKEM768-X25519", "MLKEM1024-P384", "PBKDF2", "RSA-OAEP", "RSA-PSS", + "RSASSA-PKCS1-v1_5", "X25519", "X448", +}; +static const char *const scr_wc_sign_names[] = { + "ECDSA", "Ed25519", "Ed448", "HMAC", "KMAC128", "KMAC256", + "ML-DSA-44", "ML-DSA-65", "ML-DSA-87", "RSA-PSS", "RSASSA-PKCS1-v1_5", +}; +static 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 { + const char *name; + const char *native; +} scr_wc_hashes[] = { + {"SHA-1", "sha1"}, + {"SHA-256", "sha256"}, + {"SHA-384", "sha384"}, + {"SHA-512", "sha512"}, +}; + +static bool scr_wc_ascii_ieq(const ScrStr *s, const char *name) { + size_t n = strlen(name); + if (s->len != n) return false; + for (size_t i = 0; i < n; i++) { + unsigned char a = (unsigned char)s->data[i], b = (unsigned char)name[i]; + if (a >= 'a' && a <= 'z') a = (unsigned char)(a - 32); + if (b >= 'a' && b <= 'z') b = (unsigned char)(b - 32); + if (a != b) return false; + } + return true; +} + +/* The canonical registered spelling of `name`, or NULL. */ +static 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; +} + +static const char *scr_wc_native_hash(const char *name) { + for (size_t i = 0; i < SCR_WC_COUNT(scr_wc_hashes); i++) + if (strcmp(scr_wc_hashes[i].name, name) == 0) return scr_wc_hashes[i].native; + return NULL; +} + +/* ── errors ────────────────────────────────────────────────────────── */ + +static 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, ...) { + char message[512]; + va_list ap; + va_start(ap, fmt); + int n = vsnprintf(message, sizeof message, fmt, ap); + va_end(ap); + if (n < 0) n = 0; + if ((size_t)n >= sizeof message) n = (int)sizeof message - 1; + scr_throw_error_msg_code(SCR_ERR_TYPE, message, (size_t)n, code); +} + +/* An operation or algorithm Node supports that this slice does not. */ +static 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); + if ((size_t)n >= sizeof message) n = (int)sizeof message - 1; + 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) { + char what[96]; + snprintf(what, sizeof what, "the %s algorithm", name); + scr_wc_unsupported(method, what); +} + +/* ── Web IDL conversions ────────────────────────────────────────────── + * Each answers NULL/false with the TypeError pending. `prefix` is + * "Failed to execute '' on 'SubtleCrypto'". */ + +static bool scr_wc_is_object(const ScrDyn *v) { + switch (v->kind) { + case SCR_DYN_UNDEF: + case SCR_DYN_NULL: + case SCR_DYN_BOOL: + case SCR_DYN_NUM: + case SCR_DYN_STR: + case SCR_DYN_BIGINT: + case SCR_DYN_SYMBOL: + return false; + default: + return true; + } +} + +/* A typed reference capsule as its plain view; other values retained. */ +static 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) { + ScrDyn *plain = scr_wc_plain(value); + if (!plain) return NULL; + ScrBytes *view = NULL; + if (plain->kind == SCR_DYN_BYTES || scr_array_buffer_is(plain)) { + view = scr_bytes_buffer_source(plain); + } else { + 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]); + } + 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); } + +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) { + 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++) { + if (s->len == strlen(scr_wc_key_formats[i]) && + memcmp(s->data, scr_wc_key_formats[i], s->len) == 0) { + scr_str_release(s); + return scr_wc_key_formats[i]; + } + } + scr_wc_type_error("ERR_INVALID_ARG_VALUE", + "%s: 1st argument '%.*s' is not a valid enum value of type KeyFormat.", prefix, + (int)(s->len > 200 ? 200 : s->len), s->data); + scr_str_release(s); + return NULL; +} + +/* Node's canonical usage order; the bit is the index. */ +static const char *const scr_wc_usages[] = { + "encrypt", "decrypt", "sign", "verify", + "deriveKey", "deriveBits", "wrapKey", "unwrapKey", + "encapsulateKey", "encapsulateBits", "decapsulateKey", "decapsulateBits", +}; +enum { SCR_WC_USAGE_SIGN = 1u << 2, SCR_WC_USAGE_VERIFY = 1u << 3 }; + +/* sequence over an array: the usage mask (-1 on failure). */ +static 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]); + scr_dyn_release(plain); + return -1; + } + long mask = 0; + double n = scr_dyn_arr_len(plain); + for (double i = 0; i < n; i++) { + ScrDyn *item = scr_dyn_arr_at(plain, i); + ScrStr *s = scr_wc_dom_string(item); + scr_dyn_release(item); + if (!s) { + scr_dyn_release(plain); + return -1; + } + size_t k = 0; + while (k < SCR_WC_COUNT(scr_wc_usages) && + !(s->len == strlen(scr_wc_usages[k]) && memcmp(s->data, scr_wc_usages[k], s->len) == 0)) + k++; + if (k == SCR_WC_COUNT(scr_wc_usages)) { + 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), + s->data); + scr_str_release(s); + scr_dyn_release(plain); + return -1; + } + scr_str_release(s); + mask |= 1L << k; + } + scr_dyn_release(plain); + return mask; +} + +/* ── algorithm normalization ────────────────────────────────────────── + * normalizeAlgorithm(algorithm, op) for the members this slice reads: + * the canonical name, and for HMAC imports the hash and length. */ +typedef struct { + const char *name; /* canonical registered name */ + const char *hash; /* HMAC import: the normalized hash name */ + bool has_length; + double length; +} ScrWcAlgorithm; + +static const char scr_wc_normalize_prefix[] = "Failed to normalize algorithm"; + +static 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'.", + scr_wc_normalize_prefix, dict, member, dict); +} + +/* Reads `key` off an algorithm dictionary (+1; undefined when absent). */ +static 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()); +} + +/* 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) { + if (!scr_wc_is_object(identifier)) return scr_wc_dom_string(identifier); + ScrDyn *plain = scr_wc_plain(identifier); + if (!plain) return NULL; + ScrDyn *name = scr_wc_member(plain, "name"); + scr_dyn_release(plain); + ScrStr *out = NULL; + if (name->kind == SCR_DYN_UNDEF) scr_wc_missing_member(dict, "name"); + else out = scr_wc_dom_string(name); + scr_dyn_release(name); + return out; +} + +/* 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) { + 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); + scr_str_release(name); + if (!canonical) { + scr_throw_domex("NotSupportedError", "Unrecognized algorithm name"); + return NULL; + } + if (!scr_wc_native_hash(canonical)) { + scr_wc_unsupported_algorithm(method, canonical); + return NULL; + } + return canonical; +} + +/* HmacImportParams over an object: hash (required, a SHA name), then the + * optional [EnforceRange] unsigned long length. */ +static bool scr_wc_hmac_import_params(ScrDyn *identifier, ScrWcAlgorithm *out) { + if (!scr_wc_is_object(identifier)) { + scr_wc_missing_member("HmacImportParams", "hash"); + return false; + } + ScrDyn *plain = scr_wc_plain(identifier); + if (!plain) return false; + ScrDyn *hash = scr_wc_member(plain, "hash"); + bool ok = true; + if (hash->kind == SCR_DYN_UNDEF) { + scr_wc_missing_member("HmacImportParams", "hash"); + ok = false; + } + ScrDyn *hash_value = NULL; + if (ok) { + /* HashAlgorithmIdentifier converts to an object or a DOMString, then + * ensureSHA reads the name: a string, or the object's name member. */ + ScrStr *hash_name = NULL; + if (scr_wc_is_object(hash)) { + hash_value = scr_dyn_retain(hash); + ScrDyn *hp = scr_wc_plain(hash); + ScrDyn *member = hp ? scr_wc_member(hp, "name") : NULL; + if (hp) scr_dyn_release(hp); + if (!member) ok = false; + else { + if (member->kind == SCR_DYN_STR) hash_name = scr_str_retain(member->v.str); + scr_dyn_release(member); + } + } else { + hash_name = scr_wc_dom_string(hash); + if (!hash_name) ok = false; + else hash_value = scr_dyn_new_str(hash_name); + } + bool sha = hash_name && hash_name->len >= 3 && + (hash_name->data[0] | 32) == 's' && (hash_name->data[1] | 32) == 'h' && + (hash_name->data[2] | 32) == 'a'; + if (hash_name) scr_str_release(hash_name); + if (ok && !sha) { + scr_throw_domex("NotSupportedError", "Only SHA hashes are supported in HmacImportParams"); + ok = false; + } + } + if (ok) { + ScrDyn *length = scr_wc_member(plain, "length"); + if (length->kind != SCR_DYN_UNDEF) { + double x; + if (!scr_dyn_number_coerce_js(length, &x)) ok = false; + else if (x != x || x == 1.0 / 0.0 || x == -1.0 / 0.0) { + scr_wc_type_error("ERR_INVALID_ARG_TYPE", + "%s: length in passed algorithm is not a finite number.", + scr_wc_normalize_prefix); + ok = false; + } else { + x = trunc(x); + if (x == 0) x = 0; + if (x < 0 || x > 4294967295.0) { + scr_wc_type_error("ERR_OUT_OF_RANGE", + "%s: length in passed algorithm is outside the expected range of 0 to " + "4294967295.", + scr_wc_normalize_prefix); + ok = false; + } else if (x == 0) { + scr_throw_domex("DataError", "HmacImportParams.length cannot be 0"); + ok = false; + } else { + out->has_length = true; + out->length = x; + } + } + } + scr_dyn_release(length); + } + if (ok) { + out->hash = scr_wc_digest_algorithm(hash_value, "importKey"); + if (!out->hash) ok = false; + } + if (hash_value) scr_dyn_release(hash_value); + scr_dyn_release(hash); + scr_dyn_release(plain); + return ok; +} + +/* normalizeAlgorithm for importKey, sign/verify (`names`) — false with + * the error pending. Only HMAC parameters are read; any other recognized + * algorithm is refused by the caller. */ +static bool scr_wc_normalize(ScrDyn *identifier, const char *const *names, size_t count, + bool import, ScrWcAlgorithm *out) { + memset(out, 0, sizeof *out); + ScrStr *name = scr_wc_algorithm_name(identifier, "Algorithm"); + if (!name) return false; + out->name = scr_wc_lookup(names, count, name); + scr_str_release(name); + if (!out->name) { + scr_throw_domex("NotSupportedError", "Unrecognized algorithm name"); + return false; + } + if (import && strcmp(out->name, "HMAC") == 0) return scr_wc_hmac_import_params(identifier, out); + return true; +} + +/* ── CryptoKey ───────────────────────────────────────────────────────── + * A secret HMAC key: the bytes, the hash, the bit length, the usage mask + * and the extractable flag. The algorithm and usages getters answer the + * same object on every read (Node caches its clones), built on first + * read. The bytes are cleared when the last reference goes. */ +typedef struct ScrCryptoKey { + size_t rc; + const char *hash; /* WebCrypto hash name */ + double length; /* bits */ + unsigned usages; + bool extractable; + ScrDyn *algorithm; /* cached getter results, or NULL */ + ScrDyn *usage_list; + size_t len; + unsigned char data[]; +} ScrCryptoKey; + +static void *scr_crypto_key_retain(void *p) { + ((ScrCryptoKey *)p)->rc++; + return p; +} + +static void scr_crypto_key_release(void *p) { + ScrCryptoKey *key = p; + if (!key || --key->rc != 0) return; + volatile unsigned char *bytes = key->data; + for (size_t i = 0; i < key->len; i++) bytes[i] = 0; + if (key->algorithm) scr_dyn_release(key->algorithm); + if (key->usage_list) scr_dyn_release(key->usage_list); + free(key); +} + +static ScrCryptoKey *scr_crypto_key_of(const ScrDyn *value) { + if (value->kind != SCR_DYN_HANDLE || value->v.handle.tag != SCR_DYNH_CRYPTO_KEY) return NULL; + return value->v.handle.ptr; +} + +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; +} + +static 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); + return out; +} + +static ScrDyn *scr_crypto_key_algorithm(ScrCryptoKey *key) { + if (!key->algorithm) { + ScrDyn *hash = scr_dyn_new_obj(); + scr_dyn_obj_set(hash, "name", 4, scr_wc_cstr(key->hash)); + ScrDyn *algorithm = scr_dyn_new_obj(); + scr_dyn_obj_set(algorithm, "name", 4, scr_wc_cstr("HMAC")); + scr_dyn_obj_set(algorithm, "length", 6, scr_dyn_new_num(key->length)); + scr_dyn_obj_set(algorithm, "hash", 4, hash); + key->algorithm = algorithm; + } + return scr_dyn_retain(key->algorithm); +} + +static ScrDyn *scr_crypto_key_usages(ScrCryptoKey *key) { + if (!key->usage_list) { + ScrDyn *list = scr_dyn_new_arr(); + for (size_t i = 0; i < SCR_WC_COUNT(scr_wc_usages); i++) + if (key->usages & (1u << i)) scr_dyn_arr_push(list, scr_wc_cstr(scr_wc_usages[i])); + key->usage_list = list; + } + return scr_dyn_retain(key->usage_list); +} + +static bool scr_wc_name_is(const char *name, size_t len, const char *literal) { + return len == strlen(literal) && memcmp(name, literal, len) == 0; +} + +static ScrDyn *scr_crypto_key_get(void *h, const char *name, size_t len) { + ScrCryptoKey *key = h; + if (scr_wc_name_is(name, len, "type")) return scr_wc_cstr("secret"); + if (scr_wc_name_is(name, len, "extractable")) return scr_dyn_new_bool(key->extractable); + if (scr_wc_name_is(name, len, "algorithm")) return scr_crypto_key_algorithm(key); + if (scr_wc_name_is(name, len, "usages")) return scr_crypto_key_usages(key); + return NULL; +} + +/* The four members are prototype getters without setters: an assignment + * in strict code throws Node's TypeError. */ +static bool scr_crypto_key_set(void *h, const char *name, size_t len, const ScrDyn *value) { + (void)h; + (void)value; + if (!scr_wc_name_is(name, len, "type") && !scr_wc_name_is(name, len, "extractable") && + !scr_wc_name_is(name, len, "algorithm") && !scr_wc_name_is(name, len, "usages")) + return false; + char message[128]; + int n = snprintf(message, sizeof message, + "Cannot set property %.*s of # which has only a getter", (int)len, + name); + scr_throw_error_msg(SCR_ERR_TYPE, message, (size_t)n); + return true; +} + +static ScrDyn *scr_crypto_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]"); + 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; +} + +/* key[Symbol.toStringTag] answers "CryptoKey". */ +static ScrDyn *scr_crypto_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; +} + +/* No inspect_view: Node's custom inspector prints the members one level + * deeper than a plain object would, which the shared handle inspection + * cannot reproduce, so util.inspect refuses a CryptoKey ('not supported + * yet') rather than printing a different shape. */ +static const ScrDynHandleOps scr_crypto_key_ops = { + .cls = "CryptoKey", + .retain = scr_crypto_key_retain, + .release = scr_crypto_key_release, + .invoke = scr_crypto_key_invoke, + .get = scr_crypto_key_get, + .set = scr_crypto_key_set, + .json_empty_object = true, + .to_string_tag = "CryptoKey", + .symbol_get = scr_crypto_key_symbol_get, +}; + +static ScrDyn *scr_crypto_key_new(const unsigned char *data, size_t len, const char *hash, + double length, unsigned usages, bool extractable) { + static SCR_TL bool installed; + if (!installed) { + installed = true; + scr_dyn_handle_install(SCR_DYNH_CRYPTO_KEY, &scr_crypto_key_ops); + } + ScrCryptoKey *key = malloc(sizeof *key + (len ? len : 1)); + if (!key) scr_trap("scriptc: out of memory\n"); + key->rc = 1; + key->hash = hash; + key->length = length; + key->usages = usages; + key->extractable = extractable; + key->algorithm = NULL; + key->usage_list = NULL; + key->len = len; + if (len) memcpy(key->data, data, len); + ScrDyn *box = scr_dyn_new_handle(key, SCR_DYNH_CRYPTO_KEY); + scr_crypto_key_release(key); + return box; +} + +/* ── the operations ─────────────────────────────────────────────────── + * Each takes its argument vector as one dyn array (borrowed) and answers + * a +1 promise. */ + +static ScrPromise *scr_wc_rejected(void) { + ScrPromise *p = scr_promise_new(); + scr_promise_reject_pending(p); + return p; +} + +/* 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) { + 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, + required, argc); + return false; +} + +typedef struct { + ScrDyn *v[5]; +} ScrWcArgs; + +static 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) { + for (size_t i = 0; i < n; i++) scr_dyn_release(a->v[i]); +} + +/* importKey(format, keyData, algorithm, extractable, keyUsages): Node's + * conversion order, then macImportKey's checks and importKeySync's empty + * usage check. */ +static ScrDyn *scr_wc_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; + const char *format = scr_wc_key_format(prefix, a.v[0]); + if (!format) goto done; + bool jwk = strcmp(format, "jwk") == 0; + /* The JsonWebKey dictionary is not implemented; that format is refused + * after the remaining arguments convert. */ + if (!jwk) { + 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; + ScrWcAlgorithm algorithm; + if (!scr_wc_normalize(a.v[2], scr_wc_import_names, SCR_WC_COUNT(scr_wc_import_names), true, + &algorithm)) + goto done; + if (strcmp(algorithm.name, "HMAC") != 0) { + scr_wc_unsupported_algorithm("importKey", algorithm.name); + goto done; + } + if (usages & ~(long)(SCR_WC_USAGE_SIGN | SCR_WC_USAGE_VERIFY)) { + scr_throw_domex("SyntaxError", "Unsupported key usage for HMAC key"); + goto done; + } + if (jwk) { + scr_wc_unsupported("importKey", "the 'jwk' key format"); + goto done; + } + if (strcmp(format, "raw") != 0 && strcmp(format, "raw-secret") != 0) { + char message[96]; + snprintf(message, sizeof message, "Unable to import HMAC using %s format", format); + scr_throw_domex("NotSupportedError", message); + goto done; + } + { + ScrBytes *view = data; + SCR_BYTES_SNAPSHOT(view); + size_t len = (size_t)scr_bytes_byte_len(view); + if (len == 0) { + scr_throw_domex("DataError", "Zero-length key is not supported"); + goto done; + } + double length = (double)len * 8; + unsigned char last = view->data[len - 1]; + if (algorithm.has_length) { + double bytes = floor(algorithm.length / 8) + floor((7 + fmod(algorithm.length, 8)) / 8); + if (bytes != (double)len) { + scr_throw_domex("DataError", "Invalid key length"); + goto done; + } + /* A length that is not a whole number of bytes clears the unused + * low bits of the final byte (truncateToBitLength). */ + int extra = (int)fmod(algorithm.length, 8); + if (extra) last = (unsigned char)(last & (0xff << (8 - extra))); + length = algorithm.length; + } + if (usages == 0) { + scr_throw_domex("SyntaxError", "Usages cannot be empty when importing a secret key."); + goto done; + } + result = scr_crypto_key_new(view->data, len, algorithm.hash, length, (unsigned)usages, + extractable); + scr_crypto_key_of(result)->data[len - 1] = last; + } +done: + if (data) scr_bytes_release(data); + scr_wc_args_drop(&a, 5); + return result; +} + +ScrPromise *scr_webcrypto_import_key(ScrDyn *args) { + ScrDyn *key = scr_wc_import(args); + if (!key) return scr_wc_rejected(); + return scr_promise_settled_ref(key, &scr_dyn_retain_v, &scr_dyn_release_v, NULL); +} + +/* sign(algorithm, key, data) / verify(algorithm, key, signature, data): + * the HMAC over `data`, or NULL with the error pending. *mac_len is the + * digest length. */ +static bool scr_wc_sign_verify(const ScrDyn *args, bool verify, unsigned char mac[64], + size_t *mac_len, ScrBytes **signature) { + 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 false; + ScrWcArgs a; + scr_wc_args_take(&a, args, n); + bool ok = false; + ScrBytes *data = 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); + } + ScrCryptoKey *key = scr_crypto_key_of(a.v[1]); + if (!key) { + 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; + ScrWcAlgorithm algorithm; + if (!scr_wc_normalize(a.v[0], scr_wc_sign_names, SCR_WC_COUNT(scr_wc_sign_names), false, + &algorithm)) + goto done; + if (strcmp(algorithm.name, "HMAC") != 0) { + scr_wc_unsupported_algorithm(method, algorithm.name); + goto done; + } + if (!(key->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; + } + { + ScrBytes *view = data; + SCR_BYTES_SNAPSHOT(view); + *mac_len = scr_crypto_hmac_raw(scr_wc_native_hash(key->hash), key->data, key->len, view->data, + (size_t)scr_bytes_byte_len(view), mac); + } + ok = true; +done: + if (data) scr_bytes_release(data); + scr_wc_args_drop(&a, n); + return ok; +} + +ScrPromise *scr_webcrypto_sign(ScrDyn *args) { + unsigned char mac[64]; + size_t mac_len = 0; + if (!scr_wc_sign_verify(args, false, mac, &mac_len, NULL)) return scr_wc_rejected(); + ScrBytes *out = scr_bytes_new(SCR_BYTES_U8, (double)mac_len); + memcpy(out->data, mac, mac_len); + ScrPromise *p = scr_promise_new(); + scr_crypto_defer_bytes(out, NULL, scr_crypto_promise_callback(p), + scr_crypto_promise_settle_array_buffer); + return p; +} + +/* The verify result's adapter: the deferred value is a dyn boolean. */ +static void scr_wc_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); +} + +ScrPromise *scr_webcrypto_verify(ScrDyn *args) { + unsigned char mac[64]; + size_t mac_len = 0; + ScrBytes *signature = NULL; + if (!scr_wc_sign_verify(args, true, mac, &mac_len, &signature)) { + if (signature) scr_bytes_release(signature); + return scr_wc_rejected(); + } + bool equal; + { + ScrBytes *view = signature; + SCR_BYTES_SNAPSHOT(view); + /* A signature of another length is simply not valid; equal lengths + * compare in constant time. */ + size_t sig_len = (size_t)scr_bytes_byte_len(view); + equal = sig_len == mac_len; + unsigned char diff = 0; + for (size_t i = 0; equal && i < mac_len; i++) diff |= (unsigned char)(view->data[i] ^ mac[i]); + equal = equal && diff == 0; + } + scr_bytes_release(signature); + ScrPromise *p = scr_promise_new(); + scr_crypto_defer_dyn(scr_dyn_new_bool(equal), NULL, scr_crypto_promise_callback(p), + scr_wc_settle_verified); + return p; +} + +/* digest(algorithm, data). */ +ScrPromise *scr_webcrypto_digest(ScrDyn *args) { + static const char prefix[] = "Failed to execute 'digest' on 'SubtleCrypto'"; + if (!scr_wc_required(prefix, args, 2)) return scr_wc_rejected(); + ScrWcArgs a; + scr_wc_args_take(&a, args, 2); + ScrBytes *data = NULL; + ScrBytes *out = 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); + } + data = scr_wc_buffer_source(prefix, 1, a.v[1]); + if (!data) goto done; + const char *hash = scr_wc_digest_algorithm(a.v[0], "digest"); + if (!hash) goto done; + { + ScrBytes *view = data; + SCR_BYTES_SNAPSHOT(view); + unsigned char digest[64]; + size_t len = scr_crypto_digest_raw(scr_wc_native_hash(hash), view->data, + (size_t)scr_bytes_byte_len(view), digest); + out = scr_bytes_new(SCR_BYTES_U8, (double)len); + memcpy(out->data, digest, len); + } +done: + if (data) scr_bytes_release(data); + scr_wc_args_drop(&a, 2); + if (!out) return scr_wc_rejected(); + ScrPromise *p = scr_promise_new(); + scr_crypto_defer_bytes(out, NULL, scr_crypto_promise_callback(p), + scr_crypto_promise_settle_array_buffer); + return p; +} diff --git a/scripts/native-cli-assets.mts b/scripts/native-cli-assets.mts index 614fe52d5..59beb3baa 100644 --- a/scripts/native-cli-assets.mts +++ b/scripts/native-cli-assets.mts @@ -61,6 +61,10 @@ export function stageNativeCliAssets(options: { for (const name of ["scriptc.d.ts", "scriptc-overrides.d.ts", "scriptc-node-fallback.d.ts"]) { cpSync(join(repository, "packages/compiler/ambient", name), join(declarations, name)); } + // Package shims (packageShimRoot): the built-in npm package implementations. + cpSync(join(repository, "packages/compiler/shims"), join(declarations, "shims"), { + recursive: true, + }); // This optional Node host runs the built WASI module, never the compiler. const wasi = join(lib, "wasi"); mkdirSync(join(wasi, "cli"), { recursive: true }); diff --git a/tests/diagnostics/retired-engine-1112-number-methods.ts b/tests/corpus/1112-number-methods.ts similarity index 73% rename from tests/diagnostics/retired-engine-1112-number-methods.ts rename to tests/corpus/1112-number-methods.ts index c96f9df00..510850dca 100644 --- a/tests/diagnostics/retired-engine-1112-number-methods.ts +++ b/tests/corpus/1112-number-methods.ts @@ -1,7 +1,5 @@ -// Explicit toFixed is static; toPrecision/toString(radix) execute in the -// island. The receiver marshals by value there and the engine auto-boxes -// it, so `this` binds the number exactly as in JS. All stay byte-exact -// differential. +// Number.prototype.toFixed, toPrecision and toString(radix) on typed +// numbers all compile statically and stay byte-exact differential. const n = 1234.5678; console.log(n.toFixed(0), n.toFixed(2), n.toFixed(6)); console.log((0.1 + 0.2).toFixed(17), (-3.7).toFixed(1), (0).toFixed(2)); diff --git a/tests/corpus/1536-string-array-sweep.ts b/tests/corpus/1536-string-array-sweep.ts index 0e5c12ebc..e3d5259ff 100644 --- a/tests/corpus/1536-string-array-sweep.ts +++ b/tests/corpus/1536-string-array-sweep.ts @@ -1,8 +1,7 @@ // The surface-tail sweep trio: string.substring (clamp-and-swap, the // port-suffix idiom), array findIndex (first match or -1, index-aware // callbacks), and the comparator-less sort() on string[] (JS's default -// UTF-16 ordering — a dedicated comparator because relational operators use -// scriptc's documented code-point order). +// UTF-16 code-unit ordering). const localAddr = "127.0.0.1.51823"; const lastColon = localAddr.lastIndexOf("."); console.log(localAddr.substring(lastColon + 1)); diff --git a/tests/corpus/2195-require-missing-package/lib.cjs b/tests/corpus/2195-require-missing-package/lib.cjs index 541bae658..f8d0bd73a 100644 --- a/tests/corpus/2195-require-missing-package/lib.cjs +++ b/tests/corpus/2195-require-missing-package/lib.cjs @@ -4,6 +4,6 @@ try { require("scriptc-test-definitely-not-installed"); status = "installed"; } catch (e) { - status = "missing: " + String(e.message).split("\n")[0]; + status = "missing: " + e.code + " " + String(e.message).split("\n")[0]; } exports.probe = function probe() { return status; }; diff --git a/tests/corpus/5639-union-arm-by-discriminant.ts b/tests/corpus/5639-union-arm-by-discriminant.ts new file mode 100644 index 000000000..3c29273c5 --- /dev/null +++ b/tests/corpus/5639-union-arm-by-discriminant.ts @@ -0,0 +1,48 @@ +// A record that converts into several arms of a discriminated union takes +// the arm its discriminant names. The `map` callback below builds statement +// records whose `value` is an expression literal; that literal also fits the +// shorter `ref` expression, and the statement also fits `brk` (by dropping +// fields), but the `kind` values pick `get` and `set`, as in Node. +type Expr = + | { kind: "get"; obj: Expr | null; field: string } + | { kind: "ref"; obj: Expr | null } + | { kind: "num"; n: number }; +type Stmt = + | { kind: "set"; obj: Expr; field: string; value: Expr } + | { kind: "brk"; label?: string }; + +function fill(target: Expr, names: string[]): Stmt[] { + return names.map((name) => ({ + kind: "set", + obj: target, + field: name, + value: { kind: "get", obj: target, field: name }, + })); +} + +function describe(s: Stmt): string { + switch (s.kind) { + case "set": + return `set ${s.field} = ${s.value.kind}${s.value.kind === "get" ? `.${s.value.field}` : ""} on ${s.obj.kind}`; + case "brk": + return `brk ${s.label ?? "-"}`; + } +} + +for (const s of fill({ kind: "num", n: 1 }, ["a", "b"])) console.log(describe(s)); + +// The same records with the other discriminant value take the other arm. +function pick(kind: "set" | "brk", label: string): Stmt[] { + return [label].map((l) => + kind === "set" + ? { kind: "set" as const, obj: { kind: "num" as const, n: 2 }, field: l, value: { kind: "ref" as const, obj: null } } + : { kind: "brk" as const, label: l }, + ); +} +console.log(pick("set", "x").map(describe).join("; ")); +console.log(pick("brk", "y").map(describe).join("; ")); + +// A record that fits one arm only, by dropping a field, still takes it. +const extra = { kind: "brk" as const, label: "z", note: 1 }; +const stmts: Stmt[] = [extra]; +console.log(stmts.map(describe).join("; ")); diff --git a/tests/corpus/5640-generic-structural-class-narrowing.ts b/tests/corpus/5640-generic-structural-class-narrowing.ts new file mode 100644 index 000000000..a4144c5df --- /dev/null +++ b/tests/corpus/5640-generic-structural-class-narrowing.ts @@ -0,0 +1,78 @@ +// In a generic function, `node instanceof Identifier || node instanceof +// PrivateIdentifier` narrows `node` to `T & PrivateIdentifier`: Identifier +// has every member PrivateIdentifier has, so TypeScript reduces the union of +// the two intersections to one. The narrowed read still holds either class, +// as in Node, when it is stored, read or called. +abstract class Node { + kind: number; + constructor(kind: number) { + this.kind = kind; + } +} +class Identifier extends Node { + text: string; + flow: number | undefined; + constructor(text: string) { + super(1); + this.text = text; + this.flow = undefined; + } + describe(): string { + return `identifier ${this.text}`; + } +} +class PrivateIdentifier extends Node { + text: string; + constructor(text: string) { + super(2); + this.text = text; + } + describe(): string { + return `private ${this.text}`; + } +} +class Keyword extends Node { + constructor() { + super(3); + } +} + +const names = new Map(); + +function clone(node: T, copy: T): T { + if (node instanceof Identifier || node instanceof PrivateIdentifier) { + const name = names.get(node); + if (name !== undefined) names.set(copy, name + "'"); + } + return copy; +} + +function label(node: T): string { + if (node instanceof Identifier || node instanceof PrivateIdentifier) + return `${node.kind} ${node.text} (${node.describe()})`; + return `${node.kind} -`; +} + +// One instanceof test names one class. +function privateText(node: T): string { + if (node instanceof PrivateIdentifier) return node.text; + return "-"; +} + +const id = new Identifier("a"); +const hash = new PrivateIdentifier("#b"); +const keyword = new Keyword(); +const asNode: Node = id; +names.set(id, "A"); +names.set(hash, "B"); + +console.log(names.get(clone(id, new Identifier("a2")))); +console.log(names.get(clone(hash, new PrivateIdentifier("#b2")))); +console.log(names.get(clone(asNode, new Identifier("a3")))); +console.log(names.get(clone(hash, new PrivateIdentifier("#b3")))); +console.log(names.get(clone(keyword, new Keyword()))); +console.log(names.size); + +for (const node of [id, hash, keyword] as Node[]) console.log(label(node)); +console.log(label(id), "|", label(hash)); +console.log(privateText(id), privateText(hash), privateText(hash)); diff --git a/tests/corpus/6023-string-compare-utf16.ts b/tests/corpus/6023-string-compare-utf16.ts new file mode 100644 index 000000000..c4b9ccf91 --- /dev/null +++ b/tests/corpus/6023-string-compare-utf16.ts @@ -0,0 +1,72 @@ +// Relational string comparisons order strings by UTF-16 code units, as in +// Node: characters above U+FFFF (surrogate pairs) sort before U+E000..U+FFFF, +// lone surrogates sort by their own code unit, and ASCII and other BMP text +// keep their ordinary order. Typed comparisons, sort comparators and +// comparisons of untyped values agree. +const astral = "😀"; // U+1F600: D83D DE00 +const privateUse = "\ue000"; +const last = "\uffff"; +const highSurrogate = "\ud83d"; +const lowSurrogate = "\ude00"; +const hex = (s: string): string => { + const units: string[] = []; + for (let i = 0; i < s.length; i++) units.push(s.charCodeAt(i).toString(16)); + return units.join(" "); +}; + +const pairs: [string, string][] = [ + [astral, last], + [astral, privateUse], + ["\ud7ff", astral], + [highSurrogate, astral], + [lowSurrogate, astral], + [lowSurrogate, last], + ["é", "e"], + ["abc😀", "abc\uffff"], + ["same", "same"], + ["prefix", "prefix😀"], +]; +for (const [a, b] of pairs) + console.log(hex(a), "|", hex(b), a < b, a <= b, a > b, a >= b); + +const words = ["\uffff", "😀", "é", "e", "\ue000", "z", "\ud83d", "A"]; +const byOperator = [...words].sort((x, y) => (x < y ? -1 : x > y ? 1 : 0)); +const byDefault = [...words].sort(); +console.log(byOperator.map(hex).join(", ")); +console.log(byDefault.map(hex).join(", ")); +console.log(byOperator.join("") === byDefault.join("")); + +const untyped: unknown[] = JSON.parse('["\\uffff", "\\ud83d\\ude00", "a"]'); +const [u0, u1] = untyped as string[]; +console.log(u1! < u0!, [...untyped].sort().map((v) => hex(String(v))).join(", ")); + +let smallest = words[0]!; +for (const word of words) if (word < smallest) smallest = word; +console.log(hex(smallest), words.filter((w) => w > "\ud7ff").length); + +// Every pair of these characters after shared prefixes of 0 to 24 bytes, +// ASCII and multibyte, with and without a suffix: the first differing byte +// falls in the first word, the tail word, the first byte or past 16 bytes. +const chars = ["a", "z", "é", "한", "中", "\ud7ff", "\ue000", "\uffff"]; +chars.push(astral, highSurrogate, lowSurrogate, "\u{10ffff}"); +const hashOf = (text: string): number => { + let h = 0; + for (let i = 0; i < text.length; i++) h = (Math.imul(h, 31) + text.charCodeAt(i)) | 0; + return h >>> 0; +}; +for (const unit of ["p", "é", "中"]) { + const rows: string[] = []; + for (let n = 0; n <= 24; n += 3) { + const prefix = unit.repeat(n); + let marks = ""; + for (const x of chars) + for (const y of chars) + for (const suffix of ["", "q", "tail-of-text"]) { + const a = prefix + x + suffix; + const b = prefix + y; + marks += a < b ? "<" : a > b ? ">" : "="; + } + rows.push(`${n}:${marks.split("<").length - 1}:${hashOf(marks)}`); + } + console.log(hex(unit), rows.join(" ")); +} diff --git a/tests/corpus/6080-take-then-flatmap.ts b/tests/corpus/6080-take-then-flatmap.ts new file mode 100644 index 000000000..1db6edbb4 --- /dev/null +++ b/tests/corpus/6080-take-then-flatmap.ts @@ -0,0 +1,19 @@ +// take(n) followed by flatMap counts n source values: the last value +// taken still flattens in full, and nothing is pulled after it. +const log: string[] = []; +function* gen(): Generator { for (let i = 0; i < 4; i++) { log.push("y" + i); yield i; } } +const flush = (): string => log.splice(0).join(" "); +console.log(gen().take(2).flatMap((x) => [x, x + 10]).toArray(), flush()); +console.log([0, 1, 2, 3].values().take(2).flatMap((x) => [x, x + 10]).toArray()); +console.log(gen().take(3).flatMap((x) => new Set([x, -x])).toArray(), flush()); +console.log(gen().take(2).flatMap((x) => [x, x + 10]).take(3).toArray(), flush()); +console.log(gen().take(2).flatMap((x) => [x, x + 10]).find((v) => v === 10), flush()); +console.log(gen().take(2).flatMap((x) => [x, x + 10]).some((v) => v > 10), flush()); +console.log(gen().filter((x) => x > 0).take(2).flatMap((x) => [x, x]).map((v) => v * 2).toArray(), flush()); +const out: number[] = []; +for (const v of gen().take(2).flatMap((x) => [x, x + 10])) out.push(v); +console.log(out, flush()); +const h = gen().take(1).flatMap((x) => [x, x + 1, x + 2]); +console.log(h.next(), h.next(), h.next(), h.next(), flush()); +console.log(gen().take(0).flatMap((x) => [x]).toArray(), flush()); +console.log(gen().take(2).flatMap((x) => [x]).flatMap((x) => [x, -x]).toArray(), flush()); diff --git a/tests/corpus/6200-string-annexb-locale-methods.ts b/tests/corpus/6200-string-annexb-locale-methods.ts new file mode 100644 index 000000000..8c0c2f66b --- /dev/null +++ b/tests/corpus/6200-string-annexb-locale-methods.ts @@ -0,0 +1,33 @@ +// String.prototype.substr, the locale case pair and the Annex B HTML +// methods on typed strings. substr counts a negative start from the end +// and clamps its length; toLocaleLowerCase/toLocaleUpperCase use the +// default case conversion except for the Turkic languages, decided by the +// first requested locale only; the HTML methods escape '"' in attribute +// values. +const s = "Hello, World"; +console.log(s.substr(0), s.substr(7), s.substr(-5), s.substr(-50, 3), s.substr(3, 4)); +console.log(JSON.stringify([s.substr(20), s.substr(2, -1), s.substr(NaN, 2), s.substr(1, Infinity)])); +console.log(JSON.stringify([s.substr(-Infinity, 2), s.substr(1.9, 2.9), "😀ab".substr(1, 2)])); +function tail(from: number, length?: number): string { + return s.substr(from, length); +} +console.log(tail(2), tail(2, 3), tail(-3, undefined)); + +function lower(text: string, locale?: string | string[]): string { + return text.toLocaleLowerCase(locale); +} +function upper(text: string, locale?: string | string[]): string { + return text.toLocaleUpperCase(locale); +} +const sample = "IİiıÍ ΣΑΣ Straße"; +for (const locale of [undefined, "en", "en-US", "de-DE", "TR", "tr-TR", "az", "tur", "zh-Hant-TW", "und", "en-u-co-phonebk", "en-x-tr"]) { + console.log(String(locale), lower(sample, locale), upper(sample, locale)); +} +console.log(lower(sample, ["tr", "en"]), lower(sample, ["en", "tr"]), lower(sample, [])); +console.log(upper("istanbul", ["az"]), lower("DİYARBAKIR"), lower("İx", "tr").length); +console.log("Dž".toLocaleUpperCase(), "ß".toLocaleUpperCase("de"), "ffi".toLocaleUpperCase("en")); + + +console.log(s.anchor('a"b'), s.big(), s.blink(), s.bold(), s.fixed()); +console.log(s.fontcolor("red"), s.fontsize(7), s.italics(), s.link("https://x/?q=\"1\"")); +console.log(s.small(), s.strike(), s.sub(), s.sup(), "".anchor("")); diff --git a/tests/corpus/6201-number-precision-exponential.ts b/tests/corpus/6201-number-precision-exponential.ts new file mode 100644 index 000000000..7c3d4eff8 --- /dev/null +++ b/tests/corpus/6201-number-precision-exponential.ts @@ -0,0 +1,34 @@ +// Number.prototype.toPrecision and toExponential(fractionDigits): the +// exact binary value rounds half up at the requested digit, exponential +// form takes over when the exponent is below -6 or reaches the precision, +// and the non-finite answers come before the digit-range RangeError. +const values = [0, -0, 1, -1, 2.5, -2.5, 0.5, 1.005, 1.45, 123.456, 0.000001234, 0.0000001234, 1e21, 1e-7, 5e-324, 1.7976931348623157e308, 123456789012345680000, 0.1, 99.99, 9.995, 1e23, -1234.5678]; +for (const v of values) { + const p = [1, 2, 3, 5, 10, 21].map((d) => v.toPrecision(d)); + const e = [0, 1, 2, 6, 20].map((d) => v.toExponential(d)); + console.log(String(v), p.join(" "), e.join(" ")); +} +console.log((123.456).toPrecision(), (123.456).toPrecision(undefined), (1 / 3).toPrecision(100)); +console.log((1 / 3).toExponential(100), (2 ** 70).toPrecision(25), (2 ** -30).toExponential(30)); +console.log(NaN.toPrecision(200), Infinity.toPrecision(0), (-Infinity).toExponential(-1), NaN.toExponential(101)); +console.log((15).toPrecision(1.9), (15).toExponential(1.9), (0.00001).toPrecision(1), (1e-6).toPrecision(2)); +for (const bad of [0, 101, -1, Infinity]) { + try { + console.log((1).toPrecision(bad)); + } catch (e) { + console.log((e as Error).name, (e as Error).message); + } +} +for (const bad of [-1, 101, Infinity]) { + try { + console.log((1).toExponential(bad)); + } catch (e) { + console.log((e as Error).name, (e as Error).message); + } +} +let n = 0; +for (let i = 1; i < 2000; i++) { + const x = i / 7 + i * 1e-9; + if (x.toPrecision(7) !== Number(x.toPrecision(7)).toPrecision(7)) n++; +} +console.log("stable", n); diff --git a/tests/corpus/6202-dyn-string-number-methods.js b/tests/corpus/6202-dyn-string-number-methods.js new file mode 100644 index 000000000..3f50a4636 --- /dev/null +++ b/tests/corpus/6202-dyn-string-number-methods.js @@ -0,0 +1,92 @@ +// String and Number methods on JS-inferred receivers: string receivers run +// the real method (string-width's codePointAt, camelcase's +// toLocaleLowerCase, toml's substr, pretty-bytes' toPrecision); other +// receivers keep JavaScript's answer for the name, an object's own method +// or Node's TypeError; arrays keep the runtime dispatch of shared names. +function width(text) { + let total = 0; + for (const character of text) { + const code = character.codePointAt(0); + total += code >= 0x1100 ? 2 : 1; + } + return total; +} +console.log(width("abc"), width("古"), width("👍x")); + +function describe(input) { + return [ + input.codePointAt(0), + input.codePointAt(-1), + input.codePointAt("1"), + input.substr(1), + input.substr(-2, 1), + input.toLocaleLowerCase(), + input.toLocaleUpperCase("en-US"), + input.toLocaleLowerCase("tr"), + input.trimLeft(), + input.trimRight(), + input.isWellFormed(), + input.toWellFormed().length, + ]; +} +console.log(JSON.stringify(describe(" Iİ😀 "))); +console.log(JSON.stringify(describe("\ud800Ab"))); +const parsed = JSON.parse('[" I\\u0130\\ud83d\\ude00 ", 1337, 0.000123456, [1, 2, 3]]'); +console.log(JSON.stringify(describe(parsed[0]))); + +function number(value, digits) { + return [value.toPrecision(digits), value.toExponential(digits), value.toPrecision()]; +} +console.log(JSON.stringify(number(1337, 3)), JSON.stringify(number(0.000123456, 2))); +console.log(JSON.stringify(number(-1.5, "4")), JSON.stringify(number(2.5, undefined))); +console.log(JSON.stringify(number(parsed[1], 3)), JSON.stringify(number(parsed[2], parsed[1] - 1335))); + +function prettyBytes(n) { + const units = ["B", "kB", "MB", "GB"]; + const exponent = Math.min(Math.floor(Math.log10(n) / 3), units.length - 1); + return Number((n / 1000 ** exponent).toPrecision(3)) + " " + units[exponent]; +} +console.log(prettyBytes(1337), prettyBytes(100), prettyBytes(12345678)); + +const own = { codePointAt(i) { return "own " + i; }, substr() { return "own substr"; }, toPrecision(d) { return "own " + d; } }; +console.log(describeOwn(own)); +function describeOwn(value) { + return [value.codePointAt(3), value.substr(), value.toPrecision(2)].join("|"); +} +function arrays(list) { + return JSON.stringify([list.at(-1), list.concat([4], 5)]); +} +console.log(arrays([1, 2, 3]), arrays(parsed[3])); + +function attempt(f) { + try { + return String(f()); + } catch (e) { + return e.name + ": " + e.message; + } +} +function fail(value) { + return [ + attempt(() => value.codePointAt(0)), + attempt(() => value.substr(0)), + attempt(() => value.toPrecision(2)), + attempt(() => value.toLocaleLowerCase()), + ].join(" / "); +} +for (const value of [42, "str", undefined, null, {}, parsed[3]]) console.log(fail(value)); +for (const value of JSON.parse('[42, "str", null, {}, true]')) console.log(fail(value)); +console.log(attempt(() => number(1, 200))); +console.log(attempt(() => "x".toLocaleUpperCase(null))); + +// A helper whose parameter the checker leaves untyped answers `string | +// undefined` natively; the guarded receiver still runs String methods. +function afterX(segment) { + const index = segment.indexOf("x"); + return index === -1 ? undefined : segment.slice(index + 1); +} +function tailCode(text) { + const rest = afterX(text); + if (rest === undefined) return -1; + return [rest.codePointAt(0), rest.substr(1), rest.toUpperCase(), rest.slice(1)].join(","); +} +console.log(tailCode("axé😀"), tailCode("abc")); diff --git a/tests/corpus/6203-any-string-number-methods.ts b/tests/corpus/6203-any-string-number-methods.ts new file mode 100644 index 000000000..202365e5e --- /dev/null +++ b/tests/corpus/6203-any-string-number-methods.ts @@ -0,0 +1,15 @@ +// String and Number methods through 'any'-typed values: string and number +// receivers run the real methods, other values keep JavaScript's answer. +const values: any[] = JSON.parse('["Straße İx", 1234.5678, {"substr": 1}, [1, 2]]'); +const [text, num, obj, list] = values; +console.log(text.codePointAt(7), text.substr(-2), text.toLocaleUpperCase(), text.toLocaleLowerCase("az")); +console.log(text.trimRight(), text.isWellFormed(), text.toLocaleLowerCase(["TR"])); +console.log(num.toPrecision(6), num.toExponential(3), num.toPrecision(), num.toExponential()); +console.log(list.at(-1), JSON.stringify(list.concat([3]))); +for (const call of [() => obj.substr(0), () => num.codePointAt(0), () => text.toPrecision(2)]) { + try { + console.log(call()); + } catch (e) { + console.log((e as Error).name + ": " + (e as Error).message); + } +} diff --git a/tests/corpus/6204-crypto-get-random-values.ts b/tests/corpus/6204-crypto-get-random-values.ts new file mode 100644 index 000000000..fae3c4049 --- /dev/null +++ b/tests/corpus/6204-crypto-get-random-values.ts @@ -0,0 +1,36 @@ +// crypto.getRandomValues through node:crypto's export and webcrypto: +// integer typed arrays fill in place and come back as the same object; +// views over 65536 bytes throw QuotaExceededError. (The global spelling and +// the float/DataView TypeMismatchError are 6205's.) +import { getRandomValues, webcrypto } from "node:crypto"; +import * as nodeCrypto from "node:crypto"; + +const bytes = new Uint8Array(4096); +const same = webcrypto.getRandomValues(bytes); +function nonzero(view: Uint8Array): boolean { + for (let i = 0; i < view.length; i++) if (view[i] !== 0) return true; + return false; +} +function nonzeroWords(view: Uint32Array): boolean { + for (let i = 0; i < view.length; i++) if (view[i] !== 0) return true; + return false; +} +console.log(same === bytes, bytes.length, nonzero(bytes)); +const words = new Uint32Array(256); +console.log(getRandomValues(words) === words, nonzeroWords(words)); +const shorts = new Int16Array(8); +console.log(webcrypto.getRandomValues(shorts) === shorts, nodeCrypto.getRandomValues(new Uint8ClampedArray(3)).length); +console.log(nodeCrypto.webcrypto.getRandomValues(new Int8Array(65536)).length, getRandomValues(new Uint8Array(0)).length); +const buf = Buffer.alloc(16); +console.log(Buffer.isBuffer(getRandomValues(buf)), buf.subarray(4, 8).length); + +function attempt(label: string, f: () => unknown): void { + try { + f(); + console.log(label, "ok"); + } catch (e) { + console.log(label, (e as DOMException).name, (e as DOMException).code, (e as Error).message, e instanceof DOMException); + } +} +attempt("big", () => getRandomValues(new Uint8Array(65537))); +attempt("big32", () => webcrypto.getRandomValues(new Uint32Array(16385))); diff --git a/tests/corpus/6205-crypto-get-random-values.js b/tests/corpus/6205-crypto-get-random-values.js new file mode 100644 index 000000000..97d3e67d5 --- /dev/null +++ b/tests/corpus/6205-crypto-get-random-values.js @@ -0,0 +1,38 @@ +// nanoid's fillRandom: crypto.getRandomValues over Buffer windows of an +// untyped helper, plus non-view values that throw TypeMismatchError. +import { getRandomValues } from "node:crypto"; +const GET_RANDOM_LIMIT = 65536; +function fillRandom(buffer) { + let from = 0; + while (from < buffer.length) { + const to = Math.min(from + GET_RANDOM_LIMIT, buffer.length); + crypto.getRandomValues(buffer.subarray(from, to)); + from = to; + } +} +const alphabet = "useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict"; +function nanoid(size = 21) { + const bytes = Buffer.allocUnsafe(size); + fillRandom(bytes); + let id = ""; + for (let i = 0; i < size; i++) id += alphabet[bytes[i] & 63]; + return id; +} +const id = nanoid(); +console.log(id.length, /^[A-Za-z0-9_-]+$/.test(id), nanoid(10).length, nanoid(70000).length); +const filled = globalThis.crypto.getRandomValues(new Uint16Array(3)); +console.log(filled.length); +for (const value of [new Float64Array(1), new Float32Array(2), new DataView(new ArrayBuffer(2)), new Uint8Array(65537), [1, 2], "x", 5, {}]) { + try { + crypto.getRandomValues(value); + console.log("ok"); + } catch (e) { + console.log(e.name, e.message); + } +} +const untyped = JSON.parse("[1]"); +try { + getRandomValues(untyped); +} catch (e) { + console.log(e.name, e.code, e.message); +} diff --git a/tests/corpus/6206-buffer-is-buffer-views.ts b/tests/corpus/6206-buffer-is-buffer-views.ts new file mode 100644 index 000000000..55e9fd49c --- /dev/null +++ b/tests/corpus/6206-buffer-is-buffer-views.ts @@ -0,0 +1,16 @@ +// Buffer.isBuffer over Uint8Array-typed values reads the view's Buffer +// brand: Buffer factories, Buffer subarrays and slices are Buffers, plain +// typed arrays and copies into them are not. +function check(view: Uint8Array): boolean { + return Buffer.isBuffer(view); +} +const plain = new Uint8Array([1, 2, 3]); +const buffer = Buffer.from(plain); +console.log(check(plain), check(buffer), check(Buffer.alloc(2)), check(Buffer.from("hey").subarray(1))); +console.log(check(plain.subarray(1)), check(new Uint8Array(buffer)), check(buffer.slice(1)), check(Buffer.concat([plain]))); +const views: Uint8Array[] = [plain, buffer, new TextEncoder().encode("x"), Buffer.allocUnsafe(1)]; +console.log(views.map((v) => Buffer.isBuffer(v)).join(",")); +function words(view: Uint32Array): boolean { + return Buffer.isBuffer(view); +} +console.log(words(new Uint32Array(2)), Buffer.from(plain).toString("hex"), Buffer.from(views[2]!).toString()); diff --git a/tests/corpus/6207-fs-constants-object.ts b/tests/corpus/6207-fs-constants-object.ts new file mode 100644 index 000000000..860cc4708 --- /dev/null +++ b/tests/corpus/6207-fs-constants-object.ts @@ -0,0 +1,19 @@ +// fs.constants as a value and every member: the target platform's flags, +// file-type bits and permission bits, in Node's order, on a null-prototype +// object whose members are read-only. The access-mode bits keep their +// baked literals. +import * as fs from "node:fs"; +import { constants } from "node:fs"; + +// libuv's Windows-only open flags (0 elsewhere) joined the object in later +// Node 24 releases; the rest is the same in every one. +const windowsOnly = ["UV_FS_O_TEMPORARY", "UV_FS_O_SHORT_LIVED", "UV_FS_O_SEQUENTIAL", "UV_FS_O_RANDOM"]; +console.log(JSON.stringify(Object.entries(fs.constants).filter(([k]) => !windowsOnly.includes(k)))); +console.log(fs.constants.O_RDONLY, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_TRUNC, constants.S_IFMT, constants.S_IFDIR); +console.log(constants.W_OK, fs.constants.F_OK, constants.COPYFILE_EXCL, fs.constants.UV_FS_O_FILEMAP); +const c: unknown = fs.constants; +console.log(typeof c, c === (fs.constants as unknown), Object.getPrototypeOf(c as object) === null, Object.keys(fs.constants).length > 40); +const mode = fs.statSync(".").mode; +console.log((mode & constants.S_IFMT) === constants.S_IFDIR, (mode & fs.constants.S_IRUSR) !== 0); +const descriptor = Object.getOwnPropertyDescriptor(c as object, "O_RDONLY"); +console.log(JSON.stringify(descriptor)); diff --git a/tests/corpus/6208-fs-constants-require.cjs b/tests/corpus/6208-fs-constants-require.cjs new file mode 100644 index 000000000..c8488a8bf --- /dev/null +++ b/tests/corpus/6208-fs-constants-require.cjs @@ -0,0 +1,8 @@ +// graceful-fs's reads of require('fs').constants: own-property probes for +// platform-specific flags and member reads through a stored binding. +var fs = require("fs"); +var constants = fs.constants; +console.log(typeof constants.O_RDONLY, constants.O_RDWR, Object.prototype.hasOwnProperty.call(constants, "O_SYMLINK") === ("O_SYMLINK" in constants), typeof constants.hasOwnProperty); +console.log(Object.prototype.hasOwnProperty.call(constants, "S_IFREG"), constants.O_EXCL > 0, typeof require("fs").constants.S_IFLNK); +var names = Object.keys(constants).filter(function (k) { return k.indexOf("UV_DIRENT_") === 0; }); +console.log(names.join(","), constants.NOT_A_CONSTANT); diff --git a/tests/corpus/6209-buffer-module-alias.cjs b/tests/corpus/6209-buffer-module-alias.cjs new file mode 100644 index 000000000..cec2d06d8 --- /dev/null +++ b/tests/corpus/6209-buffer-module-alias.cjs @@ -0,0 +1,25 @@ +// The buffer module's Buffer through CommonJS bindings — `var Buffer = +// require('buffer').Buffer` and a member of a stored require — is the +// global Buffer constructor, with its statics. +var Buffer = require("buffer").Buffer; +var buffer = require("buffer"); +var B2 = buffer.Buffer; +const B3 = require("node:buffer").Buffer; +console.log(Buffer.from("hi").toString("hex"), Buffer.isBuffer(Buffer.alloc(1)), B2.byteLength("héllo"), B2.isBuffer("x")); +console.log(B3.concat([B2.from("a"), Buffer.from([98])]).toString(), B3.alloc(2, 1).toString("base64")); +function toBuffer(chunk) { + return Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk); +} +console.log(toBuffer("abc").length, toBuffer(Buffer.from("xyz")).toString()); + +// Buffer.from over a JS-inferred `string | Uint8Array | undefined` result +// dispatches on the runtime kind, Node's TypeError for undefined included. +function make(flag) { + return flag ? "ab" : flag === false ? new Uint8Array([99, 100]) : undefined; +} +console.log(Buffer.from(make(true)).toString(), Buffer.from(make(false)).toString("hex")); +try { + Buffer.from(make(null)); +} catch (e) { + console.log(e.name, e.code); +} diff --git a/tests/corpus/6210-typeof-global-static-members.js b/tests/corpus/6210-typeof-global-static-members.js new file mode 100644 index 000000000..88c3c40a1 --- /dev/null +++ b/tests/corpus/6210-typeof-global-static-members.js @@ -0,0 +1,11 @@ +// ky's feature probes: typeof Node's Web-platform constructor globals and +// their static methods answers "function" without materializing the value. +export const supportsAbortController = typeof globalThis.AbortController === "function"; +export const supportsAbortSignal = + typeof globalThis.AbortSignal === "function" && typeof globalThis.AbortSignal.any === "function"; +console.log(supportsAbortController, supportsAbortSignal); +console.log(typeof AbortSignal.timeout, typeof AbortSignal.abort, typeof globalThis.AbortSignal.any); +console.log(typeof Response.json, typeof Response.error, typeof URL.canParse, typeof Promise.withResolvers); +console.log(typeof AbortSignal, typeof globalThis.URL, typeof Headers, typeof globalThis.TextEncoder, typeof URLSearchParams); +const signal = AbortSignal.abort("stop"); +console.log(signal.aborted, signal.reason); diff --git a/tests/corpus/6231-process-emit-warning-assign.ts b/tests/corpus/6231-process-emit-warning-assign.ts new file mode 100644 index 000000000..1cae44f82 --- /dev/null +++ b/tests/corpus/6231-process-emit-warning-assign.ts @@ -0,0 +1,35 @@ +// process.emitWarning and process.title are ordinary writable properties +// (pi's CLI setup assigns both). After the replacement, process.emitWarning +// calls reach it with their own arguments, no report reaches stderr, and +// the warnings Node's own modules emit through process.emitWarning (a +// util.deprecate wrapper's first call) reach it too. +import { deprecate } from "node:util"; + +process.title = "pi"; +console.log("title", process.title); +process.env.PI_CODING_AGENT = "true"; +console.log("env", process.env.PI_CODING_AGENT); + +const seen: string[] = []; +process.on("warning", (w) => seen.push(`listener ${w.name}`)); +function show(a: unknown): string { + if (a instanceof Error) return `Error(${a.message})`; + return typeof a === "object" ? JSON.stringify(a) : String(a); +} +process.emitWarning = ((warning: unknown, type: unknown, code: unknown) => { + seen.push(`${show(warning)}|${show(type)}|${show(code)}`); +}) as typeof process.emitWarning; + +process.emitWarning("plain"); +process.emitWarning("typed", "CustomWarning", "CW_1"); +process.emitWarning("options", { type: "OptWarning", code: "OW_1", detail: "d" }); +process.emitWarning(new Error("as error")); +const old = deprecate(() => "result", "old() is deprecated", "DEP_SCRIPTC_1"); +console.log("deprecated call", old()); +console.log("second call", old()); +console.log("sync", seen.join(" ; ")); + +// A replacement that does nothing silences warnings entirely. +process.emitWarning = (() => {}) as typeof process.emitWarning; +process.emitWarning("silenced"); +setTimeout(() => console.log("after tick", seen.length), 0); diff --git a/tests/corpus/6232-process-builtin-module-typed/main.ts b/tests/corpus/6232-process-builtin-module-typed/main.ts new file mode 100644 index 000000000..7765bd228 --- /dev/null +++ b/tests/corpus/6232-process-builtin-module-typed/main.ts @@ -0,0 +1,28 @@ +// process.getBuiltinModule and process.features under @types/node: a +// literal id types as the module namespace, so the result is the same +// object `import * as os` binds (pi's user-agent probe casts process and +// calls the member optionally), and process.features is an object a +// feature probe can test with `in` (pi's single-executable check). +import * as os from "node:os"; +import type * as NodeOs from "node:os"; + +const direct = process.getBuiltinModule("node:os"); +console.log("direct", direct.platform() === os.platform(), direct.EOL === os.EOL); +console.log("bare id", process.getBuiltinModule("os").arch() === os.arch()); + +type ProcessWithOsBuiltinModule = typeof process & { + getBuiltinModule?: (id: "node:os") => typeof NodeOs; +}; +function loadNodeOs(): typeof NodeOs | null { + if (typeof process === "undefined" || !(process.versions?.node || process.versions?.bun)) { + return null; + } + return (process as ProcessWithOsBuiltinModule).getBuiltinModule?.("node:os") ?? null; +} +const nodeOs = loadNodeOs(); +const agent = nodeOs ? `pi (${nodeOs.platform()} ${nodeOs.release()}; ${nodeOs.arch()})` : "pi (browser)"; +console.log("agent", agent === `pi (${os.platform()} ${os.release()}; ${os.arch()})`); +console.log("present", nodeOs !== null, nodeOs ? typeof nodeOs.homedir() : "none"); + +const isNodeSeaBinary = "sea" in process.features && process.features.sea === true; +console.log("sea", isNodeSeaBinary, "sea" in process.features); diff --git a/tests/corpus/6232-process-builtin-module-typed/package.json b/tests/corpus/6232-process-builtin-module-typed/package.json new file mode 100644 index 000000000..5ffd9800b --- /dev/null +++ b/tests/corpus/6232-process-builtin-module-typed/package.json @@ -0,0 +1 @@ +{ "type": "module" } diff --git a/tests/corpus/6232-process-builtin-module-typed/tsconfig.json b/tests/corpus/6232-process-builtin-module-typed/tsconfig.json new file mode 100644 index 000000000..ae4eda194 --- /dev/null +++ b/tests/corpus/6232-process-builtin-module-typed/tsconfig.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true + } +} diff --git a/tests/corpus/6233-for-await-class-async-generator.ts b/tests/corpus/6233-for-await-class-async-generator.ts new file mode 100644 index 000000000..1a88df7e8 --- /dev/null +++ b/tests/corpus/6233-for-await-class-async-generator.ts @@ -0,0 +1,168 @@ +// `for await` over a class instance whose [Symbol.asyncIterator] is an +// async generator method — pi-ai's EventStream, which is how every +// provider stream is consumed: a generic push/end queue, a subclass, the +// protocol-interface return annotations, producers that run between +// iterations, early exit (the generator's finally runs), and errors. +class FifoQueue { + private incoming: T[] = []; + private outgoing: T[] = []; + + get length(): number { + return this.incoming.length + this.outgoing.length; + } + + enqueue(value: T): void { + this.incoming.push(value); + } + + dequeue(): T | undefined { + if (this.outgoing.length === 0) { + while (this.incoming.length > 0) { + this.outgoing.push(this.incoming.pop()!); + } + } + return this.outgoing.pop(); + } +} + +class EventStream implements AsyncIterable { + private queue = new FifoQueue(); + private waiting = new FifoQueue<(value: IteratorResult) => void>(); + protected done = false; + private finalResultPromise: Promise; + private resolveFinalResult!: (result: R) => void; + private isComplete: (event: T) => boolean; + private extractResult: (event: T) => R; + + constructor(isComplete: (event: T) => boolean, extractResult: (event: T) => R) { + this.isComplete = isComplete; + this.extractResult = extractResult; + this.finalResultPromise = new Promise((resolve) => { + this.resolveFinalResult = resolve; + }); + } + + push(event: T): void { + if (this.done) return; + if (this.isComplete(event)) { + this.done = true; + this.resolveFinalResult(this.extractResult(event)); + } + const waiter = this.waiting.dequeue(); + if (waiter) { + waiter({ value: event, done: false }); + } else { + this.queue.enqueue(event); + } + } + + end(result?: R): void { + this.done = true; + if (result !== undefined) { + this.resolveFinalResult(result); + } + while (this.waiting.length > 0) { + const waiter = this.waiting.dequeue()!; + waiter({ value: undefined as any, done: true }); + } + } + + async *[Symbol.asyncIterator](): AsyncIterator { + while (true) { + if (this.queue.length > 0) { + yield this.queue.dequeue()!; + } else if (this.done) { + return; + } else { + const result = await new Promise>((resolve) => this.waiting.enqueue(resolve)); + if (result.done) return; + yield result.value; + } + } + } + + result(): Promise { + return this.finalResultPromise; + } +} + +type Event = { type: "text"; delta: string } | { type: "done"; text: string }; + +class TextStream extends EventStream { + constructor() { + super( + (event) => event.type === "done", + (event) => (event.type === "done" ? event.text : ""), + ); + } +} + +function produce(stream: TextStream, words: string[]): void { + let i = 0; + const step = (): void => { + if (i < words.length) { + stream.push({ type: "text", delta: words[i]! }); + i++; + setTimeout(step, 1); + } else { + stream.push({ type: "done", text: words.join("") }); + } + }; + step(); +} + +class Ticker { + log: string[] = []; + async *[Symbol.asyncIterator](): AsyncIterableIterator { + try { + for (let n = 1; n <= 5; n++) { + this.log.push(`yield ${n}`); + yield n; + } + } finally { + this.log.push("closed"); + } + } +} + +class Failing { + async *[Symbol.asyncIterator](): AsyncGenerator { + yield "first"; + throw new Error("stream failed"); + } +} + +async function main(): Promise { + const stream = new TextStream(); + produce(stream, ["Hello", " from", " the", " stub"]); + let text = ""; + for await (const event of stream) { + if (event.type === "text") text += event.delta; + else console.log("done event", event.text); + } + console.log("streamed", JSON.stringify(text), "result", await stream.result()); + + // Events queued before iteration starts, then end() with no consumer waiting. + const early = new TextStream(); + early.push({ type: "text", delta: "a" }); + early.push({ type: "text", delta: "b" }); + early.end("ab"); + const seen: string[] = []; + for await (const event of early) seen.push(event.type === "text" ? event.delta : "?"); + console.log("queued", seen.join(","), await early.result()); + + // break closes the generator: its finally runs before the loop exits. + const ticker = new Ticker(); + for await (const n of ticker) { + if (n === 3) break; + } + console.log("ticker", ticker.log.join(" | ")); + + // A throw from the generator rejects the loop. + try { + for await (const s of new Failing()) console.log("got", s); + } catch (e) { + console.log("caught", (e as Error).message); + } +} +main(); diff --git a/tests/corpus/6234-weak-collection-class-fields.ts b/tests/corpus/6234-weak-collection-class-fields.ts new file mode 100644 index 000000000..d7a085921 --- /dev/null +++ b/tests/corpus/6234-weak-collection-class-fields.ts @@ -0,0 +1,39 @@ +// WeakMap- and WeakSet-typed class fields (pi's AgentSession keys entry ids by +// message object): initialized, optional, and constructor-assigned fields hold +// the native weak collections like locals of those types. +class Session { + private readonly _entryIdsByMessage = new WeakMap(); + private seen = new WeakSet(); + cache: WeakMap | undefined; + record(message: object, id: string): void { + this._entryIdsByMessage.set(message, id); + this.seen.add(message); + } + idOf(message: object): string | undefined { + return this._entryIdsByMessage.get(message); + } + has(message: object): boolean { + return this.seen.has(message) && this._entryIdsByMessage.has(message); + } +} +const s = new Session(); +const a = { role: "user" }; +const b = { role: "assistant" }; +s.record(a, "e1"); +console.log(s.idOf(a), s.idOf(b), s.has(a), s.has(b)); +s.cache = new WeakMap(); +s.cache.set(a, 1); +console.log(s.cache.get(a), s.cache.has(b), s.cache instanceof WeakMap); +class Registry { + private readonly names: WeakMap; + constructor(names: WeakMap) { + this.names = names; + } + name(o: object): string { + return this.names.get(o) ?? "(unnamed)"; + } +} +const names = new WeakMap(); +names.set(a, "alice"); +const registry = new Registry(names); +console.log(registry.name(a), registry.name(b)); diff --git a/tests/corpus/6235-bare-builtin-shadowed-package/helper.cjs b/tests/corpus/6235-bare-builtin-shadowed-package/helper.cjs new file mode 100644 index 000000000..8e0a0457c --- /dev/null +++ b/tests/corpus/6235-bare-builtin-shadowed-package/helper.cjs @@ -0,0 +1,6 @@ +// A CommonJS require of the bare names resolves to the builtins too. +const { StringDecoder } = require("string_decoder"); +exports.decodeEuro = () => { + const d = new StringDecoder("utf8"); + return d.write(Buffer.from([0xe2, 0x82])) + d.end(Buffer.from([0xac])); +}; diff --git a/tests/corpus/6235-bare-builtin-shadowed-package/main.ts b/tests/corpus/6235-bare-builtin-shadowed-package/main.ts new file mode 100644 index 000000000..e9e83ea87 --- /dev/null +++ b/tests/corpus/6235-bare-builtin-shadowed-package/main.ts @@ -0,0 +1,17 @@ +// A bare core-module specifier names the Node builtin even when an npm +// package of the same name is installed (pi depends on string_decoder +// transitively): Node checks its builtins before node_modules. +import { StringDecoder } from "string_decoder"; +import { EventEmitter } from "events"; +import { createRequire } from "node:module"; + +const decoder = new StringDecoder("utf8"); +console.log(decoder.write(Buffer.from([0xe2, 0x82])) + decoder.end(Buffer.from([0xac]))); + +const emitter = new EventEmitter(); +emitter.on("ping", (n: number) => console.log("ping", n)); +emitter.emit("ping", 1); + +const require = createRequire(import.meta.url); +const helper = require("./helper.cjs") as { decodeEuro(): string }; +console.log("cjs", helper.decodeEuro()); diff --git a/tests/corpus/6235-bare-builtin-shadowed-package/node_modules/events/events.js b/tests/corpus/6235-bare-builtin-shadowed-package/node_modules/events/events.js new file mode 100644 index 000000000..f1a344ef1 --- /dev/null +++ b/tests/corpus/6235-bare-builtin-shadowed-package/node_modules/events/events.js @@ -0,0 +1 @@ +throw new Error("the npm events package was loaded"); diff --git a/tests/corpus/6235-bare-builtin-shadowed-package/node_modules/events/package.json b/tests/corpus/6235-bare-builtin-shadowed-package/node_modules/events/package.json new file mode 100644 index 000000000..0fb1a3c8a --- /dev/null +++ b/tests/corpus/6235-bare-builtin-shadowed-package/node_modules/events/package.json @@ -0,0 +1 @@ +{ "name": "events", "version": "3.3.0", "main": "events.js" } diff --git a/tests/corpus/6235-bare-builtin-shadowed-package/node_modules/string_decoder/index.js b/tests/corpus/6235-bare-builtin-shadowed-package/node_modules/string_decoder/index.js new file mode 100644 index 000000000..02af46d5a --- /dev/null +++ b/tests/corpus/6235-bare-builtin-shadowed-package/node_modules/string_decoder/index.js @@ -0,0 +1,3 @@ +// An installed package named like the core module: Node never loads it +// for the bare specifier. +throw new Error("the npm string_decoder package was loaded"); diff --git a/tests/corpus/6235-bare-builtin-shadowed-package/node_modules/string_decoder/package.json b/tests/corpus/6235-bare-builtin-shadowed-package/node_modules/string_decoder/package.json new file mode 100644 index 000000000..ad07adde8 --- /dev/null +++ b/tests/corpus/6235-bare-builtin-shadowed-package/node_modules/string_decoder/package.json @@ -0,0 +1 @@ +{ "name": "string_decoder", "version": "1.3.0", "main": "index.js" } diff --git a/tests/corpus/6235-bare-builtin-shadowed-package/package.json b/tests/corpus/6235-bare-builtin-shadowed-package/package.json new file mode 100644 index 000000000..5ffd9800b --- /dev/null +++ b/tests/corpus/6235-bare-builtin-shadowed-package/package.json @@ -0,0 +1 @@ +{ "type": "module" } diff --git a/tests/corpus/6236-stdin-encoding-tty-probes/main.ts b/tests/corpus/6236-stdin-encoding-tty-probes/main.ts new file mode 100644 index 000000000..0fbe7eb99 --- /dev/null +++ b/tests/corpus/6236-stdin-encoding-tty-probes/main.ts @@ -0,0 +1,62 @@ +// pi-tui's terminal setup on a piped stdin: the `if (process.stdin.setRawMode)` +// presence check (the method exists only on a TTY), setEncoding with a +// string 'data' listener whose chunks keep multibyte characters split +// across writes whole, and stdout 'resize' listeners (registered, +// removed, never fired off a TTY). The program re-runs itself with a +// pipe so the parent controls the write boundaries. +import { spawn } from "node:child_process"; + +if (process.argv[2] === "child") { + console.log("raw present", typeof process.stdin.setRawMode, Boolean(process.stdin.setRawMode)); + if (process.stdin.setRawMode) process.stdin.setRawMode(true); + process.stdin.setEncoding("utf8"); + const onResize = (): void => console.log("resize", process.stdout.columns); + process.stdout.on("resize", onResize); + process.stdout.once("resize", onResize); + process.stdout.removeListener("resize", onResize); + let total = ""; + let replacement = false; + process.stdin.on("data", (data: string) => { + if (typeof data !== "string") console.log("not a string"); + if (data.includes("�")) replacement = true; + total += data; + }); + process.stdin.on("end", () => { + console.log("end", JSON.stringify(total), "replacement", replacement); + process.stdout.removeListener("resize", onResize); + }); + process.stdin.resume(); + // Off a TTY no stream listens for SIGWINCH and its default is to ignore. + process.kill(process.pid, "SIGWINCH"); +} else { + const child = spawn(process.execPath, [process.argv[1]!, "child"], { + stdio: ["pipe", "pipe", "inherit"], + }); + let out = ""; + child.stdout!.setEncoding("utf8"); + child.stdout!.on("data", (chunk: string) => { + out += chunk; + }); + child.on("close", (code: number | null) => { + process.stdout.write(out); + console.log("child exit", code); + }); + const parts = [ + Buffer.from([0x68, 0xc3]), + Buffer.from([0xa9, 0x6c, 0x6c, 0x6f, 0x20, 0xe2, 0x82]), + Buffer.from([0xac, 0x20, 0xf0, 0x9f]), + Buffer.from([0x98, 0x80, 0x21]), + ]; + let i = 0; + const next = (): void => { + if (i < parts.length) { + const part: Buffer = parts[i]!; + i++; + child.stdin!.write(part); + setTimeout(next, 30); + } else { + child.stdin!.end(); + } + }; + next(); +} diff --git a/tests/corpus/6236-stdin-encoding-tty-probes/package.json b/tests/corpus/6236-stdin-encoding-tty-probes/package.json new file mode 100644 index 000000000..5ffd9800b --- /dev/null +++ b/tests/corpus/6236-stdin-encoding-tty-probes/package.json @@ -0,0 +1 @@ +{ "type": "module" } diff --git a/tests/corpus/6236-stdin-encoding-tty-probes/tsconfig.json b/tests/corpus/6236-stdin-encoding-tty-probes/tsconfig.json new file mode 100644 index 000000000..ae4eda194 --- /dev/null +++ b/tests/corpus/6236-stdin-encoding-tty-probes/tsconfig.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true + } +} diff --git a/tests/corpus/6237-error-subclass-delete-stack/main.js b/tests/corpus/6237-error-subclass-delete-stack/main.js new file mode 100644 index 000000000..96ca7593d --- /dev/null +++ b/tests/corpus/6237-error-subclass-delete-stack/main.js @@ -0,0 +1,12 @@ +// Deleting `stack` on an Error subclass that declares a stack accessor +// lets reads reach the accessor (the package's guest exceptions). +import { GuestError } from "guest-error"; + +try { + throw new GuestError(7); +} catch (e) { + console.log(e instanceof GuestError, e instanceof Error, e.handle, e.message); + console.log(e.stack); + e.stack = "rewritten"; + console.log(e.stack, Object.keys(e)); +} diff --git a/tests/corpus/6237-error-subclass-delete-stack/node_modules/guest-error/index.d.ts b/tests/corpus/6237-error-subclass-delete-stack/node_modules/guest-error/index.d.ts new file mode 100644 index 000000000..7f3b83f94 --- /dev/null +++ b/tests/corpus/6237-error-subclass-delete-stack/node_modules/guest-error/index.d.ts @@ -0,0 +1,6 @@ +export declare class GuestError extends Error { + handle: number; + constructor(handle: number); + get stack(): string; + set stack(v: string); +} diff --git a/tests/corpus/6237-error-subclass-delete-stack/node_modules/guest-error/index.js b/tests/corpus/6237-error-subclass-delete-stack/node_modules/guest-error/index.js new file mode 100644 index 000000000..693c06b51 --- /dev/null +++ b/tests/corpus/6237-error-subclass-delete-stack/node_modules/guest-error/index.js @@ -0,0 +1,18 @@ +// quickjs-wasi's JSException shape: an Error subclass whose stack +// accessor pair the constructor exposes by deleting V8's own `stack`. +export class GuestError extends Error { + handle; + #stack; + constructor(handle) { + super("guest error " + handle); + this.handle = handle; + delete this.stack; + this.#stack = "guest stack " + handle; + } + get stack() { + return this.#stack; + } + set stack(v) { + this.#stack = v; + } +} diff --git a/tests/corpus/6237-error-subclass-delete-stack/node_modules/guest-error/package.json b/tests/corpus/6237-error-subclass-delete-stack/node_modules/guest-error/package.json new file mode 100644 index 000000000..99f59d07b --- /dev/null +++ b/tests/corpus/6237-error-subclass-delete-stack/node_modules/guest-error/package.json @@ -0,0 +1 @@ +{"name": "guest-error", "version": "1.0.0", "type": "module", "main": "index.js", "types": "index.d.ts"} diff --git a/tests/corpus/6237-error-subclass-delete-stack/package.json b/tests/corpus/6237-error-subclass-delete-stack/package.json new file mode 100644 index 000000000..5ffd9800b --- /dev/null +++ b/tests/corpus/6237-error-subclass-delete-stack/package.json @@ -0,0 +1 @@ +{ "type": "module" } diff --git a/tests/corpus/6250-string-of-string-or-url.ts b/tests/corpus/6250-string-of-string-or-url.ts new file mode 100644 index 000000000..3241d79e8 --- /dev/null +++ b/tests/corpus/6250-string-of-string-or-url.ts @@ -0,0 +1,22 @@ +// String(), template literals and concatenation over a `string | URL` +// union: the string arm passes through and the URL arm converts through its +// href, exactly as each would alone (an origin URL keeps its trailing +// slash, the origin string does not). +function key(origin: string | URL): string { + return String(origin); +} + +function label(origin: string | URL | undefined): string { + return `[${origin}]`; +} + +const url = new URL("http://127.0.0.1:8080"); +console.log(key("http://127.0.0.1:8080")); +console.log(key(url)); +console.log(key(new URL("https://example.com/a/b?x=1#h"))); +console.log(label(url), label("plain"), label(undefined)); +const both: (string | URL)[] = ["a", url]; +console.log(both.map((o) => "" + o).join(" ")); +const map = new Map(); +map.set(key(url.origin), 1); +console.log(map.get(String(url.origin)), map.get(String(url))); diff --git a/tests/corpus/6260-js-var-dyn-identity.js b/tests/corpus/6260-js-var-dyn-identity.js new file mode 100644 index 000000000..f22ce2888 --- /dev/null +++ b/tests/corpus/6260-js-var-dyn-identity.js @@ -0,0 +1,103 @@ +// JavaScript: a function-scoped `var` initialized with an object that +// already exists (an element of an array built with push, a find result, +// a member of another object) holds that object, not a copy: writes +// through the binding are visible through the array, and `===` holds. +function make(i) { + return { id: i, x: i % 17 }; +} + +function updateInPlace() { + var all = []; + for (var i = 0; i < 3; i++) all.push(make(i)); + var same = 0; + for (var j = 0; j < all.length; j++) { + var o = all[j]; + o.x += 1; + if (o === all[j]) same++; + } + console.log(JSON.stringify(all), same); +} +updateInPlace(); + +function literalElements() { + var all = []; + for (var i = 0; i < 3; i++) all.push({ id: i, x: i }); + for (var j = 0; j < all.length; j++) { + var o = all[j]; + o.x *= 10; + } + console.log(JSON.stringify(all)); +} +literalElements(); + +function initializerForms(flag) { + var all = []; + all.push({ n: 0, inner: { v: 1 } }); + all.push({ n: 1, inner: { v: 2 } }); + var found = all.find(function (e) { + return e.n === 1; + }); + found.n = 11; + var picked = flag ? all[0] : null; + picked.n = 10; + var either = all[5] || all[0]; + either.inner.v += 100; + var inner = all[1].inner; + inner.v = 20; + var last = all.pop(); + console.log(JSON.stringify(all), last === found, picked === either); + console.log(JSON.stringify(last)); +} +initializerForms(true); + +function arraysAndClosures() { + var grid = []; + grid.push([1, 2]); + var row = grid[0]; + row[0] = 9; + row.push(3); + var list = []; + list.push({ count: 0 }); + var target = list[0]; + var bump = function () { + target.count++; + }; + bump(); + bump(); + console.log(JSON.stringify(grid), JSON.stringify(list), target === list[0]); +} +arraysAndClosures(); + +function redeclaredAndReassigned() { + var all = []; + all.push({ x: 1 }); + all.push({ x: 2 }); + var o = all[0]; + o.x = 10; + var o = all[1]; + o.x = 20; + o = { x: 30 }; + all.push(o); + o.x = 40; + console.log(JSON.stringify(all), o === all[2]); +} +redeclaredAndReassigned(); + +const arrow = () => { + var all = []; + all.push({ x: 1 }); + var o = all[0]; + o.x = 5; + console.log(JSON.stringify(all)); +}; +arrow(); + +// An annotated binding keeps its declared type. +function annotated() { + var all = []; + all.push({ x: 1 }); + /** @type {{ x: number }} */ + var o = all[0]; + console.log(o.x); +} +annotated(); diff --git a/tests/corpus/6261-js-destructure-for-of-identity.js b/tests/corpus/6261-js-destructure-for-of-identity.js new file mode 100644 index 000000000..a8821b3b4 --- /dev/null +++ b/tests/corpus/6261-js-destructure-for-of-identity.js @@ -0,0 +1,46 @@ +// JavaScript: array destructuring (var, let and const) and `for (var ... +// of ...)` bind the existing element objects of an array built with push, +// so writes through the bindings reach the array's objects. +function destructure() { + var all = []; + all.push({ x: 1 }); + all.push({ x: 2 }); + var [first] = all; + first.x = 10; + let [, second] = all; + second.x = 20; + const [again] = all; + again.x += 1; + console.log(JSON.stringify(all), first === all[0], again === first, second === all[1]); +} +destructure(); + +function forVarOf() { + var all = []; + all.push({ x: 1 }); + all.push({ x: 2 }); + var seen = []; + for (var o of all) { + o.x *= 10; + seen.push(o === all[seen.length]); + } + console.log(JSON.stringify(all), seen.join()); + for (let p of all) p.x += 1; + console.log(JSON.stringify(all)); +} +forVarOf(); + +function* tagged(list) { + for (var o of list) { + o.seen = true; + yield o; + } +} +function generators() { + var items = []; + items.push({ seen: false }); + items.push({ seen: false }); + for (var it of tagged(items)) it.extra = items.indexOf(it); + console.log(JSON.stringify(items)); +} +generators(); diff --git a/tests/corpus/6262-js-file-scope-var-identity.js b/tests/corpus/6262-js-file-scope-var-identity.js new file mode 100644 index 000000000..cb8678b5f --- /dev/null +++ b/tests/corpus/6262-js-file-scope-var-identity.js @@ -0,0 +1,40 @@ +// JavaScript: file-scope `var` and `let` bindings initialized with an +// existing object (an element of an array built with push) hold that +// object, including bindings declared in a loop body, by destructuring or +// by for-of, and functions declared above them read the same object. +function show() { + return typeof o === "undefined" ? "unset" : o.x; +} +console.log(show()); + +var all = []; +all.push({ x: 1 }); +all.push({ x: 2 }); + +var o = all[0]; +o.x = 10; +console.log(show(), JSON.stringify(all), o === all[0]); + +var same = 0; +for (var j = 0; j < all.length; j++) { + var item = all[j]; + item.x += 5; + if (item === all[j]) same++; +} +console.log(JSON.stringify(all), same); + +var [head] = all; +head.x = 100; +for (var each of all) { + each.x += 1; + if (each === all[1]) console.log("last", each.x); +} +console.log(JSON.stringify(all), head === o); + +let current = all[1]; +current.x = 7; +function touch() { + current.x++; +} +touch(); +console.log(JSON.stringify(all), current === all[1]); diff --git a/tests/corpus/6263-js-factory-return-identity.cjs b/tests/corpus/6263-js-factory-return-identity.cjs new file mode 100644 index 000000000..eb3f77557 --- /dev/null +++ b/tests/corpus/6263-js-factory-return-identity.cjs @@ -0,0 +1,56 @@ +// JavaScript: an object a function returns is the object the function +// holds elsewhere (a cache entry, a registry element). Bindings that +// receive it and array or object literals built from those bindings keep +// that object, so writes through them reach the cache and registry. +'use strict'; +const cache = { n: 0 }; +function get() { + return cache; +} + +const registry = []; +function make(name) { + const o = { name: name, hits: 0 }; + registry.push(o); + return o; +} + +function identity(v) { + return v; +} +function wrapped(i) { + return identity({ id: i, x: i % 17 }); +} + +function main() { + var a = get(); + a.n = 5; + console.log(cache.n, a === cache); + var list = [a]; + list[0].n = 6; + var holder = { item: a }; + holder.item.n += 1; + console.log(cache.n, list[0] === cache, holder.item === cache); + + var x = make("x"); + let y = make("y"); + const z = make("z"); + x.hits++; + y.hits += 2; + z.hits += 3; + var group = [x, y, z]; + for (var j = 0; j < group.length; j++) group[j].hits *= 10; + console.log(JSON.stringify(registry), group[0] === registry[0]); + + var all = []; + for (var i = 0; i < 4; i++) all.push(wrapped(i)); + var sum = 0; + for (var r = 0; r < 3; r++) + for (var k = 0; k < all.length; k++) { + var e = all[k]; + e.x += 1; + sum += e.x; + } + console.log(all.length, sum, JSON.stringify(all.slice(0, 2))); +} +main(); diff --git a/tests/corpus/6291-webcrypto-hmac-sign-verify.ts b/tests/corpus/6291-webcrypto-hmac-sign-verify.ts new file mode 100644 index 000000000..ddb480a4d --- /dev/null +++ b/tests/corpus/6291-webcrypto-hmac-sign-verify.ts @@ -0,0 +1,94 @@ +// WebCrypto HMAC through the crypto global: importKey('raw') for SHA-1 and +// SHA-2 keys, sign and verify (a wrong or short signature answers false), +// the CryptoKey members, instanceof CryptoKey, the key sources importKey +// and sign accept, and that results settle asynchronously. +const encoder = new TextEncoder(); +const secret = encoder.encode("scriptc-webcrypto-secret"); +const data = encoder.encode("header.payload"); + +function hex(buffer: ArrayBuffer): string { + return Buffer.from(buffer).toString("hex"); +} + +const key = await crypto.subtle.importKey( + "raw", + secret, + { name: "HMAC", hash: "SHA-256" }, + false, + ["sign", "verify"], +); +console.log(key.type, key.extractable, key.usages.join(",")); +const algorithm = key.algorithm as HmacKeyAlgorithm; +console.log(algorithm.name, algorithm.length, algorithm.hash.name); +console.log(key instanceof CryptoKey, key.algorithm === key.algorithm, key.usages === key.usages); + +const signature = await crypto.subtle.sign("HMAC", key, data); +console.log(signature.byteLength, hex(signature)); +console.log(await crypto.subtle.verify("HMAC", key, signature, data)); +console.log(await crypto.subtle.verify({ name: "HMAC" }, key, signature, encoder.encode("header.payloaD"))); +const tampered = new Uint8Array(signature); +tampered[0] ^= 1; +console.log(await crypto.subtle.verify("HMAC", key, tampered, data)); +console.log(await crypto.subtle.verify("HMAC", key, signature.slice(0, 16), data)); +console.log(await crypto.subtle.verify("HMAC", key, new Uint8Array(0), data)); + +for (const hash of ["SHA-1", "SHA-384", "SHA-512"]) { + const k = await crypto.subtle.importKey("raw", secret, { name: "HMAC", hash }, true, ["sign"]); + const a = k.algorithm as HmacKeyAlgorithm; + const mac = await crypto.subtle.sign({ name: "hmac" }, k, data); + console.log(hash, a.hash.name, a.length, k.extractable, k.usages.join(","), mac.byteLength, hex(mac)); +} + +// Names normalize case-insensitively; usages come back in Node's order. +const lower = await crypto.subtle.importKey( + "raw", + secret, + { name: "hmac", hash: { name: "sha-256" } }, + true, + ["verify", "sign"], +); +const lowerAlgorithm = lower.algorithm as HmacKeyAlgorithm; +console.log(lowerAlgorithm.name, lowerAlgorithm.hash.name, lower.usages.join(",")); + +// An explicit length in bits must match the key bytes. +const sized = await crypto.subtle.importKey( + "raw", + secret, + { name: "HMAC", hash: "SHA-256", length: secret.length * 8 }, + false, + ["verify"], +); +console.log((sized.algorithm as HmacKeyAlgorithm).length, sized.usages.join(",")); + +// Key and data sources: ArrayBuffer, Buffer, DataView and subarray views. +const fromBuffer = await crypto.subtle.importKey( + "raw", + secret.slice().buffer, + { name: "HMAC", hash: "SHA-256" }, + false, + ["sign"], +); +console.log(hex(await crypto.subtle.sign("HMAC", fromBuffer, Buffer.from("header.payload"))) === hex(signature)); +const padded = encoder.encode("xxheader.payloadyy"); +console.log(hex(await crypto.subtle.sign("HMAC", key, padded.subarray(2, 16))) === hex(signature)); +console.log(hex(await crypto.subtle.sign("HMAC", key, new DataView(data.buffer))) === hex(signature)); + +// The data is read when sign is called. +const mutable = encoder.encode("header.payload"); +const pending = crypto.subtle.sign("HMAC", key, mutable); +mutable[0] = 0; +console.log(hex(await pending) === hex(signature)); + +// Results settle after the synchronous code that started them. +const order: string[] = []; +const signed = crypto.subtle.sign("HMAC", key, data).then(() => { + order.push("sign"); +}); +const imported = crypto.subtle + .importKey("raw", secret, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]) + .then(() => { + order.push("importKey"); + }); +order.push("sync"); +await Promise.all([signed, imported]); +console.log(order.join(",")); diff --git a/tests/corpus/6292-webcrypto-hmac-errors.ts b/tests/corpus/6292-webcrypto-hmac-errors.ts new file mode 100644 index 000000000..95e33ae84 --- /dev/null +++ b/tests/corpus/6292-webcrypto-hmac-errors.ts @@ -0,0 +1,83 @@ +// WebCrypto HMAC failures reject with Node's TypeErrors (argument count, +// enum values, buffer sources, CryptoKey arguments, required members) and +// DOMExceptions (usages, key length, algorithm names, key access). +const encoder = new TextEncoder(); +const secret = encoder.encode("scriptc-webcrypto-secret"); +const data = encoder.encode("payload"); + +function report(label: string, error: Error): void { + if (error instanceof DOMException) { + const dom = error as DOMException; + console.log(label, dom.name, dom.code, dom.message, "DOMException"); + } else { + const coded = error as Error & { code?: string }; + console.log(label, coded.name, coded.code, coded.message); + } +} + +async function attempt(label: string, run: () => Promise): Promise { + try { + await run(); + console.log(label, "resolved"); + } catch (e) { + report(label, e as Error); + } +} + +async function attemptVerify(label: string, run: () => Promise): Promise { + try { + console.log(label, "resolved", await run()); + } catch (e) { + report(label, e as Error); + } +} + +const signOnly = await crypto.subtle.importKey("raw", secret, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]); +const verifyOnly = await crypto.subtle.importKey("raw", secret, { name: "HMAC", hash: "SHA-384" }, false, ["verify"]); + +await attempt("usage", () => + crypto.subtle.importKey("raw", secret, { name: "HMAC", hash: "SHA-256" }, false, ["sign", "encrypt"]), +); +await attempt("empty usages", () => + crypto.subtle.importKey("raw", secret, { name: "HMAC", hash: "SHA-256" }, false, []), +); +await attempt("zero-length key", () => + crypto.subtle.importKey("raw", new Uint8Array(0), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]), +); +await attempt("length mismatch", () => + crypto.subtle.importKey("raw", secret, { name: "HMAC", hash: "SHA-256", length: 64 }, false, ["sign"]), +); +await attempt("length zero", () => + crypto.subtle.importKey("raw", secret, { name: "HMAC", hash: "SHA-256", length: 0 }, false, ["sign"]), +); +await attempt("missing hash", () => + crypto.subtle.importKey("raw", secret, "HMAC", false, ["sign"]), +); +await attempt("md5 hash", () => + crypto.subtle.importKey("raw", secret, { name: "HMAC", hash: "MD5" }, false, ["sign"]), +); +await attempt("unknown sha", () => + crypto.subtle.importKey("raw", secret, { name: "HMAC", hash: "SHA-999" }, false, ["sign"]), +); +await attempt("unknown algorithm", () => + crypto.subtle.importKey("raw", secret, { name: "NOPE", hash: "SHA-256" }, false, ["sign"]), +); +await attempt("pkcs8 format", () => + crypto.subtle.importKey("pkcs8", secret, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]), +); +await attempt("string key", () => + crypto.subtle.importKey("raw", "secret" as unknown as BufferSource, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]), +); + +await attempt("sign with verify key", () => crypto.subtle.sign("HMAC", verifyOnly, data)); +await attemptVerify("verify with sign key", () => crypto.subtle.verify("HMAC", signOnly, new Uint8Array(32), data)); +await attempt("sign without key", () => + crypto.subtle.sign("HMAC", secret as unknown as CryptoKey, data), +); +await attempt("sign unknown algorithm", () => crypto.subtle.sign("HMAC-SHA256", signOnly, data)); +await attempt("sign string data", () => + crypto.subtle.sign("HMAC", signOnly, "payload" as unknown as BufferSource), +); +await attemptVerify("verify string signature", () => + crypto.subtle.verify("HMAC", verifyOnly, "sig" as unknown as BufferSource, data), +); diff --git a/tests/corpus/6293-webcrypto-digest.ts b/tests/corpus/6293-webcrypto-digest.ts new file mode 100644 index 000000000..ae107c6f0 --- /dev/null +++ b/tests/corpus/6293-webcrypto-digest.ts @@ -0,0 +1,35 @@ +// crypto.subtle.digest over the SHA-1 and SHA-2 hashes: string and object +// algorithm identifiers, typed-array, Buffer and ArrayBuffer input, and +// Node's rejections for unknown names and non-buffer data. +const encoder = new TextEncoder(); +const input = encoder.encode("scriptc digest input"); + +function hex(buffer: ArrayBuffer): string { + return Buffer.from(buffer).toString("hex"); +} + +for (const name of ["SHA-1", "SHA-256", "SHA-384", "SHA-512"]) { + const digest = await crypto.subtle.digest(name, input); + console.log(name, digest.byteLength, hex(digest)); +} +console.log(hex(await crypto.subtle.digest({ name: "sha-256" }, Buffer.from("scriptc digest input")))); +console.log(hex(await crypto.subtle.digest("SHA-256", input.slice().buffer))); +console.log(hex(await crypto.subtle.digest("SHA-256", new Uint8Array(0)))); + +async function attempt(label: string, run: () => Promise): Promise { + try { + await run(); + console.log(label, "resolved"); + } catch (e) { + if (e instanceof DOMException) { + const error = e as DOMException; + console.log(label, error.name, error.code, error.message, "DOMException"); + } else { + const error = e as Error & { code?: string }; + console.log(label, error.name, error.code, error.message); + } + } +} + +await attempt("md5", () => crypto.subtle.digest("MD5", input)); +await attempt("string data", () => crypto.subtle.digest("SHA-256", "text" as unknown as BufferSource)); diff --git a/tests/corpus/6294-webcrypto-js-values.mjs b/tests/corpus/6294-webcrypto-js-values.mjs new file mode 100644 index 000000000..93071b39d --- /dev/null +++ b/tests/corpus/6294-webcrypto-js-values.mjs @@ -0,0 +1,52 @@ +// WebCrypto from JavaScript: keys and algorithms carried as dynamic +// values, the CryptoKey brand checks libraries use (Symbol.toStringTag, +// Object.prototype.toString and instanceof), JSON.stringify of a key, and +// rejections for missing arguments and non-key values. +const encoder = new TextEncoder(); +const secret = encoder.encode("dynamic-secret"); +const table = { + HS256: { name: "HMAC", hash: "SHA-256" }, + HS512: { name: "HMAC", hash: "SHA-512" }, +}; + +async function importFor(alg, usage) { + const params = table[alg]; + return crypto.subtle.importKey("raw", secret, params, false, [usage]); +} + +const key = await importFor("HS256", "sign"); +console.log(key[Symbol.toStringTag], Object.prototype.toString.call(key), String(key)); +console.log(key instanceof CryptoKey, secret instanceof CryptoKey, {} instanceof CryptoKey); +console.log(JSON.stringify(key), typeof key, key.type, key.algorithm.hash.name, key.usages.includes("sign")); +const sig = new Uint8Array(await crypto.subtle.sign(table.HS256, key, encoder.encode("a.b"))); +console.log(sig.length, Buffer.from(sig).toString("base64url")); +const verifier = await importFor("HS256", "verify"); +let verified = false; +try { + verified = await crypto.subtle.verify(table.HS256, verifier, sig, encoder.encode("a.b")); +} catch {} +console.log(verified); +const other = await importFor("HS512", "verify"); +console.log(await crypto.subtle.verify(table.HS512, other, sig, encoder.encode("a.b"))); + +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("too few", () => crypto.subtle.sign("HMAC", key)); +await attempt("import too few", () => crypto.subtle.importKey("raw", secret, table.HS256, false)); +await attempt("verify too few", () => crypto.subtle.verify("HMAC", key, sig)); +await attempt("digest too few", () => crypto.subtle.digest("SHA-256")); +const lookalike = JSON.parse('{"type":"secret","usages":["sign"]}'); +await attempt("not a key", () => crypto.subtle.sign("HMAC", lookalike, secret)); +await attempt("bad format", () => crypto.subtle.importKey("nope", secret, table.HS256, false, ["sign"])); +await attempt("bad usage", () => crypto.subtle.importKey("raw", secret, table.HS256, false, ["bogus"])); +await attempt("usages not a sequence", () => crypto.subtle.importKey("raw", secret, table.HS256, false, "sign")); +await attempt("algorithm without name", () => crypto.subtle.sign({ hash: "SHA-256" }, key, secret)); +await attempt("hash object without name", () => + crypto.subtle.importKey("raw", secret, { name: "HMAC", hash: {} }, false, ["sign"]), +); diff --git a/tests/corpus/6295-jose-hs256/main.mjs b/tests/corpus/6295-jose-hs256/main.mjs new file mode 100644 index 000000000..a8a669a57 --- /dev/null +++ b/tests/corpus/6295-jose-hs256/main.mjs @@ -0,0 +1,103 @@ +// The real jose@6.2.12 HS256 path: SignJWT and jwtVerify from jose's +// public jose/jwt/sign, jose/jwt/verify and jose/errors entry points, over +// crypto.subtle importKey, sign and verify. 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. +import { SignJWT } from "jose/jwt/sign"; +import { jwtVerify } from "jose/jwt/verify"; +import * as errors from "jose/errors"; + +const encoder = new TextEncoder(); +const secret = encoder.encode("scriptc-jose-hs256-shared-secret"); +const now = new Date(1_800_000_000_000); +const iat = 1_799_999_000; + +async function sign(claims, alg = "HS256", key = secret) { + const jwt = new SignJWT(claims); + jwt.setProtectedHeader({ alg, typ: "JWT" }); + jwt.setIssuedAt(iat); + jwt.setIssuer("urn:scriptc:issuer"); + jwt.setAudience("urn:scriptc:audience"); + jwt.setSubject("alice"); + return jwt.sign(key); +} + +async function verify(label, token, options = {}, key = secret) { + try { + const { payload, protectedHeader } = await jwtVerify(token, key, { + algorithms: ["HS256"], + currentDate: now, + ...options, + }); + console.log(label, "ok", JSON.stringify(protectedHeader), JSON.stringify(payload)); + } catch (e) { + console.log(label, e.code, e.message, e instanceof errors.JOSEError, e.name, e.claim, e.reason); + } +} + +const token = await sign({ role: "admin", exp: iat + 3600 }); +console.log(token); +await verify("valid", token, { issuer: "urn:scriptc:issuer", audience: "urn:scriptc:audience" }); +await verify("bytes token", encoder.encode(token)); +await verify("wrong secret", token, {}, encoder.encode("another-secret")); +const [header, body, signature] = token.split("."); +const forged = Buffer.from(JSON.stringify({ role: "root", sub: "mallory" })).toString("base64url"); +await verify("tampered payload", `${header}.${forged}.${signature}`); +await verify("tampered signature", `${header}.${body}.${signature.slice(0, -2)}AA`); +await verify("malformed", "not-a-jwt"); +await verify("bad signature encoding", `${header}.${body}.***`); +await verify("expired", token, { currentDate: new Date((iat + 7200) * 1000) }); +await verify("clock tolerance", token, { currentDate: new Date((iat + 3610) * 1000), clockTolerance: 30 }); +await verify("not yet valid", await sign({ nbf: iat + 600 })); +await verify("issuer", token, { issuer: "urn:other" }); +await verify("audience", token, { audience: "urn:other" }); +await verify("required claim", token, { requiredClaims: ["jti"] }); +await verify("alg not allowed", token, { algorithms: ["HS512"] }); +await verify("max token age", token, { maxTokenAge: 600 }); + +const hs384 = await sign({ scope: "read" }, "HS384"); +await verify("hs384", hs384, { algorithms: ["HS384"] }); +const hs512 = await sign({ scope: "write" }, "HS512"); +await verify("hs512", hs512, { algorithms: ["HS256", "HS512"] }); +await verify("hs512 as hs256 only", hs512); +await verify("none", `${Buffer.from('{"alg":"none"}').toString("base64url")}.${body}.`, { algorithms: ["none"] }); + +// The same key imported once and reused as a CryptoKey. +const cryptoKey = await crypto.subtle.importKey("raw", secret, { name: "HMAC", hash: "SHA-256" }, false, ["verify"]); +await verify("crypto key", token, {}, cryptoKey); +const signingKey = await crypto.subtle.importKey("raw", secret, { name: "HMAC", hash: "SHA-512" }, false, ["sign"]); +await verify("wrong key hash", token, {}, signingKey); + +// Chained builder calls: jose's declarations type every setter's result as +// `this`, implemented by the JWTClaimsBuilder base SignJWT extends. +const chained = await new SignJWT({ role: "reader" }) + .setProtectedHeader({ alg: "HS256", typ: "JWT" }) + .setIssuedAt(iat) + .setIssuer("urn:scriptc:issuer") + .setAudience(["urn:scriptc:audience", "urn:other"]) + .setSubject("bob") + .setJti("jti-2") + .setNotBefore(iat - 60) + .setExpirationTime(iat + 60) + .sign(secret); +console.log(chained); +await verify("chained", chained, { audience: "urn:scriptc:audience" }); + +// String durations: jose's secs() parses them. +for (const maxTokenAge of ["10 minutes", "1 hour", "1.5h", "2 days", "+20m", "1 week", "1y", "900 secs", "3 HOURS", "2h ago", "-2h ago", "10 fortnights", ""]) { + await verify(`max token age ${JSON.stringify(maxTokenAge)}`, token, { maxTokenAge }); +} +for (const clockTolerance of ["30s", "5 sec", "1 min from now", "1 min ago"]) { + await verify(`clock tolerance ${clockTolerance}`, token, { currentDate: new Date((iat + 3620) * 1000), clockTolerance }); +} +const relative = new SignJWT({ role: "reader" }); +relative.setProtectedHeader({ alg: "HS256" }).setIssuedAt("-1 minute").setExpirationTime("2h"); +const claims = JSON.parse(Buffer.from((await relative.sign(secret)).split(".")[1], "base64url").toString()); +const clock = Math.floor(Date.now() / 1000); +console.log(Math.abs(claims.exp - clock - 7200) <= 2, Math.abs(claims.iat - clock + 60) <= 2); +try { + new SignJWT({}).setExpirationTime("soon"); +} catch (e) { + console.log(e.name, e.message); +} diff --git a/tests/corpus/6295-jose-hs256/node_modules/jose/LICENSE.md b/tests/corpus/6295-jose-hs256/node_modules/jose/LICENSE.md new file mode 100644 index 000000000..d0ec038a8 --- /dev/null +++ b/tests/corpus/6295-jose-hs256/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/6295-jose-hs256/node_modules/jose/dist/types/jwt/sign.d.ts b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/types/jwt/sign.d.ts new file mode 100644 index 000000000..67ce10434 --- /dev/null +++ b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/types/jwt/sign.d.ts @@ -0,0 +1,21 @@ +import type * as t from '../types.d.ts'; +/** @param payload Initial JWT Claims Set. Defaults to an empty object. */ +declare const SignJWT_base: new (payload?: t.JWTPayload) => t.ProduceJWT; +/** Builds and signs Compact JWS-formatted JSON Web Tokens. */ +export declare class SignJWT extends SignJWT_base { + #private; + /** + * Sets the JWS Protected Header. May only be called once. + * + * @param protectedHeader JWS Protected Header. Must contain an "alg" (JWS Algorithm) property. + */ + setProtectedHeader(protectedHeader: t.JWTHeaderParameters): this; + /** + * Signs and returns the JWT. + * + * @param key Private key or shared secret to sign the JWT with. See + * {@link https://github.com/panva/jose/issues/210#jws-alg Algorithm Key Requirements}. + */ + sign(key: t.KeyInput, options?: t.SignOptions): Promise; +} +export {}; diff --git a/tests/corpus/6295-jose-hs256/node_modules/jose/dist/types/jwt/verify.d.ts b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/types/jwt/verify.d.ts new file mode 100644 index 000000000..6c8b2f32b --- /dev/null +++ b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/types/jwt/verify.d.ts @@ -0,0 +1,35 @@ +import type * as t from '../types.d.ts'; +/** JWS verification and JWT Claims Set validation options. */ +export interface JWTVerifyOptions extends t.VerifyOptions, t.JWTClaimVerificationOptions { +} +/** + * Resolves a JWT verification key. No token components have been authenticated when this function + * is called. + */ +export interface JWTVerifyGetKey extends t.GetKeyFunction { +} +/** + * Verifies a Compact JWS-formatted JWT and validates its Claims Set. + * + * @param key Public key or shared secret to verify the JWT with. See + * {@link https://github.com/panva/jose/issues/210#jws-alg Algorithm Key Requirements}. + * @param options JWT Verification and JWT Claims Set validation options. + */ +export declare function jwtVerify(jwt: string | Uint8Array, key: t.KeyInput, options?: JWTVerifyOptions): Promise>; +/** + * Verifies the JWT signature and claims, returning the resolved key. + * + * @param getKey Function resolving a public key or shared secret to verify the JWT with. See + * {@link https://github.com/panva/jose/issues/210#jws-alg Algorithm Key Requirements}. + * @param options JWT Verification and JWT Claims Set validation options. + */ +export declare function jwtVerify(jwt: string | Uint8Array, getKey: JWTVerifyGetKey, options?: JWTVerifyOptions): 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 Key, or function resolving a key, to verify the JWT with. See + * {@link https://github.com/panva/jose/issues/210#jws-alg Algorithm Key Requirements}. + * @param options JWT Verification and JWT Claims Set validation options. + */ +export declare function jwtVerify(jwt: string | Uint8Array, key: t.KeyInput | JWTVerifyGetKey, options?: JWTVerifyOptions): Promise & Partial>; diff --git a/tests/corpus/6295-jose-hs256/node_modules/jose/dist/types/types.d.ts b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/types/types.d.ts new file mode 100644 index 000000000..9815c018d --- /dev/null +++ b/tests/corpus/6295-jose-hs256/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/6295-jose-hs256/node_modules/jose/dist/types/util/errors.d.ts b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/types/util/errors.d.ts new file mode 100644 index 000000000..930a29b9f --- /dev/null +++ b/tests/corpus/6295-jose-hs256/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/6295-jose-hs256/node_modules/jose/dist/webapi/jwt/sign.js b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/jwt/sign.js new file mode 100644 index 000000000..4d5e51c99 --- /dev/null +++ b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/jwt/sign.js @@ -0,0 +1,19 @@ +import { createCompactSignature } from "../lib/jws_sign.js"; +import { JWTInvalid } from "../util/errors.js"; +import { JWTClaimsBuilder, jwtData } from "../lib/jwt_claims_set.js"; +import { assertNotSet } from "../lib/validate.js"; +const SignJWT_base = JWTClaimsBuilder; +class SignJWT extends SignJWT_base { + #protectedHeader; + setProtectedHeader(protectedHeader) { + return assertNotSet(this.#protectedHeader, "setProtectedHeader"), this.#protectedHeader = protectedHeader, this; + } + async sign(key, options) { + return createCompactSignature(jwtData(this), this.#protectedHeader, options?.crit, key, () => { + throw new JWTInvalid("JWTs MUST NOT use unencoded payload"); + }); + } +} +export { + SignJWT +}; diff --git a/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/jwt/verify.js b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/jwt/verify.js new file mode 100644 index 000000000..8e02f3a8d --- /dev/null +++ b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/jwt/verify.js @@ -0,0 +1,13 @@ +import { prepareVerify, verifyCompact } from "../lib/jws_verify.js"; +import { validateClaimsSet } from "../lib/jwt_claims_set.js"; +import { JWTInvalid } from "../util/errors.js"; +async function jwtVerify(jwt, key, options) { + const [verified, b64] = await verifyCompact(jwt, prepareVerify(options), key); + if (!b64) + throw new JWTInvalid("JWTs MUST NOT use unencoded payload"); + const payload = validateClaimsSet(verified.protectedHeader, verified.payload, options); + return { ...verified, payload }; +} +export { + jwtVerify +}; diff --git a/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/buffer_utils.js b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/buffer_utils.js new file mode 100644 index 000000000..b3cad85a9 --- /dev/null +++ b/tests/corpus/6295-jose-hs256/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/6295-jose-hs256/node_modules/jose/dist/webapi/lib/jws_algorithms.js b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/jws_algorithms.js new file mode 100644 index 000000000..08e2e22ac --- /dev/null +++ b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/jws_algorithms.js @@ -0,0 +1,74 @@ +import { JOSENotSupported } from "../util/errors.js"; +import { table } from "./key_descriptor.js"; +const sig = [["verify"], ["sign"]]; +function hmac(bits) { + const subtle = { name: "HMAC", hash: `SHA-${bits}` }; + return { kty: ["oct"], secret: !0, subtle, signing: subtle, usages: sig }; +} +function rsa(bits, saltLength) { + const subtle = { name: saltLength ? "RSA-PSS" : "RSASSA-PKCS1-v1_5", hash: `SHA-${bits}` }; + return { + kty: ["RSA"], + subtle, + signing: saltLength ? { ...subtle, saltLength } : subtle, + usages: sig, + minRsaBits: 2048 + }; +} +function ecdsa(crv, bits) { + return { + kty: ["EC"], + crv, + subtle: { name: "ECDSA", namedCurve: crv }, + signing: { name: "ECDSA", hash: `SHA-${bits}` }, + usages: sig + }; +} +function eddsa() { + const subtle = { name: "Ed25519" }; + return { + kty: ["OKP"], + crv: "Ed25519", + subtle, + signing: subtle, + usages: sig + }; +} +function mldsa(bits) { + const subtle = { name: `ML-DSA-${bits}` }; + return { + kty: ["AKP"], + subtle, + signing: subtle, + usages: sig + }; +} +const JWS = table({ + HS256: hmac(256), + HS384: hmac(384), + HS512: hmac(512), + RS256: rsa(256), + RS384: rsa(384), + RS512: rsa(512), + PS256: rsa(256, 32), + PS384: rsa(384, 48), + PS512: rsa(512, 64), + ES256: ecdsa("P-256", 256), + ES384: ecdsa("P-384", 384), + ES512: ecdsa("P-521", 512), + EdDSA: eddsa(), + Ed25519: eddsa(), + "ML-DSA-44": mldsa(44), + "ML-DSA-65": mldsa(65), + "ML-DSA-87": mldsa(87) +}); +function jwsAlgorithm(alg) { + const entry = typeof alg == "string" ? JWS[alg] : void 0; + if (!entry) + throw new JOSENotSupported(`alg ${alg} is not supported either by JOSE or your javascript runtime`); + return entry; +} +export { + JWS, + jwsAlgorithm +}; diff --git a/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/jws_sign.js b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/jws_sign.js new file mode 100644 index 000000000..76e6e02dd --- /dev/null +++ b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/jws_sign.js @@ -0,0 +1,46 @@ +import { encode as b64u } from "../util/base64url.js"; +import { jwsAlgorithm } from "./jws_algorithms.js"; +import { isDisjoint, serializeJoseHeader, validateB64, validateCrit, validateCritDuplicates, JWS_RECOGNIZED } from "./validate.js"; +import { JWSInvalid } from "../util/errors.js"; +import { concat, encode, encoder } from "./buffer_utils.js"; +import { prepareKey, rawKey, checkModulusLength } from "./key.js"; +async function createSignature(input, key, rejectUnencoded) { + let [payload, protectedHeader, unprotectedHeader, crit] = input, protectedHeaderString = ""; + if (protectedHeader !== void 0) { + const normalized = serializeJoseHeader(JWSInvalid, protectedHeader); + protectedHeader = normalized[0], protectedHeaderString = b64u(normalized[1]); + } + if (unprotectedHeader !== void 0 && (unprotectedHeader = serializeJoseHeader(JWSInvalid, unprotectedHeader)[0]), !protectedHeader && !unprotectedHeader) + throw new JWSInvalid("either setProtectedHeader or setUnprotectedHeader must be called before #sign()"); + if (!isDisjoint(protectedHeader, unprotectedHeader)) + throw new JWSInvalid("JWS Protected and JWS Unprotected Header Parameter names must be disjoint"); + const joseHeader = { ...protectedHeader, ...unprotectedHeader }; + validateCritDuplicates(JWSInvalid, protectedHeader); + const b64 = validateB64(protectedHeader, validateCrit(JWSInvalid, JWS_RECOGNIZED, crit, protectedHeader, joseHeader)); + b64 || rejectUnencoded?.(); + const { alg } = joseHeader; + if (typeof alg != "string" || !alg) + throw new JWSInvalid('JWS "alg" (Algorithm) Header Parameter missing or invalid'); + const entry = jwsAlgorithm(alg); + let payloadS = "", payloadB = payload, data; + if (b64) { + const encoded = input[4]; + encoded ? (payloadS = encoded[0] ??= b64u(payload), payloadB = encoded[1] ??= encode(payloadS)) : (payloadS = b64u(payload), data = encoder.encode(`${protectedHeaderString}.${payloadS}`)); + } + data ??= concat(encode(protectedHeaderString), encode("."), payloadB); + const k = await rawKey(await prepareKey(entry, key, "sign"), entry.subtle, "sign"); + entry.minRsaBits && checkModulusLength(entry.alg, k); + const jws = { + signature: b64u(new Uint8Array(await crypto.subtle.sign(entry.signing, k, data))), + payload: payloadS + }; + return protectedHeader && (jws.protected = protectedHeaderString), unprotectedHeader && (jws.header = unprotectedHeader), [jws, b64]; +} +async function createCompactSignature(payload, protectedHeader, crit, key, rejectUnencoded) { + const [jws] = await createSignature([payload, protectedHeader, void 0, crit], key, rejectUnencoded); + return `${jws.protected}.${jws.payload}.${jws.signature}`; +} +export { + createCompactSignature, + createSignature +}; diff --git a/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/jws_verify.js b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/jws_verify.js new file mode 100644 index 000000000..7337b9c8e --- /dev/null +++ b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/jws_verify.js @@ -0,0 +1,86 @@ +import { jwsAlgorithm } from "./jws_algorithms.js"; +import { JOSEAlgNotAllowed, JWSInvalid, JWSSignatureVerificationFailed } from "../util/errors.js"; +import { concat, decoder, encoder, encode } from "./buffer_utils.js"; +import { decodeBase64url, encodeBase64url, parseJoseHeader, isDisjoint, isObject, validateB64, validateCrit, validateAlgorithms, JWS_RECOGNIZED } from "./validate.js"; +import { prepareKey, rawKey, checkModulusLength } from "./key.js"; +function snapshotJws(jws, sharedPayload) { + const encodedProtected = jws.protected, inputHeader = jws.header, header = isObject(inputHeader) ? { ...inputHeader } : inputHeader; + let payload = sharedPayload ? sharedPayload[0] : jws.payload; + !sharedPayload && payload instanceof Uint8Array && (payload = new Uint8Array(payload)); + const signature = jws.signature, snapshot = { payload, signature }; + if (encodedProtected !== void 0 && (snapshot.protected = encodedProtected), inputHeader !== void 0 && (snapshot.header = header), encodedProtected === void 0 && header === void 0) + throw new JWSInvalid('Flattened JWS must have either of the "protected" or "header" members'); + if (encodedProtected !== void 0 && typeof encodedProtected != "string") + throw new JWSInvalid("JWS Protected Header incorrect type"); + if (payload === void 0) + throw new JWSInvalid("JWS Payload missing"); + if (typeof signature != "string") + throw new JWSInvalid("JWS Signature missing or incorrect type"); + if (header !== void 0 && !isObject(header)) + throw new JWSInvalid("JWS Unprotected Header incorrect type"); + return snapshot; +} +function prepareVerify(options) { + return [options && validateAlgorithms("algorithms", options.algorithms), options?.crit]; +} +function parseProtectedHeader(encodedProtected) { + return encodedProtected === void 0 ? {} : parseJoseHeader(encodedProtected, JWSInvalid, "JWS Protected Header is invalid"); +} +function encodeJsonUnencodedPayload(payload) { + const invalid = /[\p{Cs}\p{Cn}]/u.exec(payload)?.[0]; + if (invalid !== void 0) + throw new JWSInvalid(/\p{Cs}/u.test(invalid) ? "JWS Payload must be a well-formed Unicode string" : "JWS Payload must not contain unassigned Unicode code points"); + return encoder.encode(payload); +} +function encodeCompactUnencodedPayload(payload) { + try { + return encode(payload); + } catch { + throw new JWSInvalid("JWS Compact Serialization payload must use only ASCII characters"); + } +} +async function verifySignature(jws, shared, key, encodeUnencodedPayload, parsedProtected) { + const { protected: encodedProtected, header, payload: inputPayload } = jws, parsedProt = parsedProtected ?? parseProtectedHeader(encodedProtected); + if (!isDisjoint(parsedProt, header)) + throw new JWSInvalid("JWS Protected and JWS Unprotected Header Parameter names must be disjoint"); + const joseHeader = { ...parsedProt, ...header }, b64 = validateB64(parsedProt, validateCrit(JWSInvalid, JWS_RECOGNIZED, shared[1], parsedProt, joseHeader)), { alg } = joseHeader; + if (typeof alg != "string" || !alg) + throw new JWSInvalid('JWS "alg" (Algorithm) Header Parameter missing or invalid'); + if (shared[0] && !shared[0].has(alg)) + throw new JOSEAlgNotAllowed('"alg" (Algorithm) Header Parameter value not allowed'); + if (b64) { + if (typeof inputPayload != "string") + throw new JWSInvalid("JWS Payload must be a string"); + } else if (typeof inputPayload != "string" && !(inputPayload instanceof Uint8Array)) + throw new JWSInvalid("JWS Payload must be a string or an Uint8Array instance"); + const signingPayload = b64 || typeof inputPayload != "string" ? inputPayload : encodeUnencodedPayload(inputPayload); + let resolvedKey = !1; + typeof key == "function" && (key = await key(parsedProt, jws), resolvedKey = !0); + const entry = jwsAlgorithm(alg), data = concat(encodedProtected !== void 0 ? encode(encodedProtected) : new Uint8Array(), encode("."), typeof signingPayload == "string" ? shared[2] ??= encodeBase64url(signingPayload, "payload", JWSInvalid) : signingPayload), signature = decodeBase64url(jws.signature, "signature", JWSInvalid), k = await prepareKey(entry, key, "verify"), cryptoKey = await rawKey(k, entry.subtle, "verify"); + entry.minRsaBits && checkModulusLength(entry.alg, cryptoKey); + let verified = !1; + try { + verified = await crypto.subtle.verify(entry.signing, cryptoKey, signature, data); + } catch { + } + if (!verified) + throw new JWSSignatureVerificationFailed(); + const result = { payload: typeof signingPayload == "string" ? decodeBase64url(signingPayload, "payload", JWSInvalid) : signingPayload }; + return encodedProtected !== void 0 && (result.protectedHeader = parsedProt), header !== void 0 && (result.unprotectedHeader = header), resolvedKey ? [{ ...result, key: k }, b64] : [result, b64]; +} +async function verifyCompact(jws, shared, key) { + if (jws instanceof Uint8Array && (jws = decoder.decode(jws)), typeof jws != "string") + throw new JWSInvalid("Compact JWS must be a string or Uint8Array"); + const { 0: protectedHeader, 1: payload, 2: signature, length } = jws.split("."); + if (length !== 3) + throw new JWSInvalid("Invalid Compact JWS"); + return verifySignature({ payload, protected: protectedHeader, signature }, shared, key, encodeCompactUnencodedPayload); +} +export { + encodeJsonUnencodedPayload, + parseProtectedHeader, + prepareVerify, + snapshotJws, + verifyCompact, + verifySignature +}; diff --git a/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/jwt_claims_set.js b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/jwt_claims_set.js new file mode 100644 index 000000000..b99943f9c --- /dev/null +++ b/tests/corpus/6295-jose-hs256/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/6295-jose-hs256/node_modules/jose/dist/webapi/lib/key.js b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/key.js new file mode 100644 index 000000000..7dc5602f1 --- /dev/null +++ b/tests/corpus/6295-jose-hs256/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/6295-jose-hs256/node_modules/jose/dist/webapi/lib/key_descriptor.js b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/key_descriptor.js new file mode 100644 index 000000000..cf0afbd90 --- /dev/null +++ b/tests/corpus/6295-jose-hs256/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/6295-jose-hs256/node_modules/jose/dist/webapi/lib/validate.js b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/validate.js new file mode 100644 index 000000000..51b7dfe89 --- /dev/null +++ b/tests/corpus/6295-jose-hs256/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/6295-jose-hs256/node_modules/jose/dist/webapi/util/base64url.js b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/util/base64url.js new file mode 100644 index 000000000..4447baa6d --- /dev/null +++ b/tests/corpus/6295-jose-hs256/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/6295-jose-hs256/node_modules/jose/dist/webapi/util/errors.js b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/util/errors.js new file mode 100644 index 000000000..b7d579f51 --- /dev/null +++ b/tests/corpus/6295-jose-hs256/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/6295-jose-hs256/node_modules/jose/package.json b/tests/corpus/6295-jose-hs256/node_modules/jose/package.json new file mode 100644 index 000000000..bcb353430 --- /dev/null +++ b/tests/corpus/6295-jose-hs256/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/6296-array-position-patterns.ts b/tests/corpus/6296-array-position-patterns.ts new file mode 100644 index 000000000..8c55b6152 --- /dev/null +++ b/tests/corpus/6296-array-position-patterns.ts @@ -0,0 +1,23 @@ +// Object patterns over arrays that name positions and `length` (jose's +// `const { 0: header, 1: payload, 2: signature, length } = jws.split(".")`): +// each position is the element or undefined past the end, defaults apply +// to missing positions, and `length` is the array's length. +const { 0: header, 1: payload, 2: signature, length } = "aaa.bbb.ccc".split("."); +console.log(header, payload, signature, length); + +const short = "aaa.bbb".split("."); +const { 0: first, 2: third, length: count } = short; +console.log(first, third === undefined, count); + +const { 1: second = "fallback", 3: fourth = "missing" } = short; +console.log(second, fourth); + +function parts(values: number[]): string { + const { 0: a, 1: b, length: n } = values; + return `${a}:${b}:${n}`; +} +console.log(parts([1, 2, 3]), parts([7])); + +let renamed = ""; +for (const { 0: key, 1: value } of [["x", "1"], ["y", "2"]]) renamed += `${key}=${value};`; +console.log(renamed); diff --git a/tests/corpus/6297-library-runtime-idioms.mjs b/tests/corpus/6297-library-runtime-idioms.mjs new file mode 100644 index 000000000..11d5d5e67 --- /dev/null +++ b/tests/corpus/6297-library-runtime-idioms.mjs @@ -0,0 +1,34 @@ +// Runtime idioms from jose's dist build: the Uint8Array base64/hex +// feature probes (absent in Node 24), String.fromCharCode.apply over byte +// chunks and number lists (the btoa fallback), and structuredClone of a +// parameter whose default is `{}` called with differently shaped objects. +console.log(typeof Uint8Array.prototype.toBase64, typeof Uint8Array.prototype.toHex); +console.log(typeof Uint8Array.prototype.setFromBase64, typeof Uint8Array.prototype.setFromHex); + +function encodeBase64(input) { + if (Uint8Array.prototype.toBase64) return input.toBase64(); + const CHUNK_SIZE = 4, arr = []; + for (let i = 0; i < input.length; i += CHUNK_SIZE) + arr.push(String.fromCharCode.apply(null, input.subarray(i, i + CHUNK_SIZE))); + return btoa(arr.join("")); +} +console.log(encodeBase64(new TextEncoder().encode("hello, base64 chunks"))); +console.log(encodeBase64(new Uint8Array([0, 255, 128, 1, 2]))); + +const bytes = new Uint8Array([104, 105]); +console.log(String.fromCharCode.apply(null, bytes), String.fromCharCode.apply(undefined, [72, 73])); +const untyped = JSON.parse("[79, 75]"); +console.log(String.fromCharCode.apply(null, untyped), String.fromCharCode.apply(null, []).length); + +let store; +class Builder { + constructor(payload = {}) { + (store ||= new WeakMap()).set(this, structuredClone(payload)); + } + claims() { + return JSON.stringify(store.get(this)); + } +} +console.log(new Builder({ role: "admin", exp: 5 }).claims()); +console.log(new Builder({ scope: "read" }).claims()); +console.log(new Builder().claims()); diff --git a/tests/corpus/6331-jose-root-entry/main.ts b/tests/corpus/6331-jose-root-entry/main.ts new file mode 100644 index 000000000..598154040 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/main.ts @@ -0,0 +1,110 @@ +// The real jose@6.2.12 through its root entry, from TypeScript, the way a +// Next.js proxy uses it: `import { jwtVerify, errors } from "jose"`. +// node_modules/jose vendors the published package's dist/ (every webapi +// module the root entry loads, and its declarations), package.json and +// LICENSE.md unmodified (MIT). Loading the root runs jwks/remote.js, which +// reads Node's navigator global at module load. Covers chained SignJWT +// builders, string durations (jose's secs()), destructured jwtVerify +// results and `instanceof errors.JOSEError` brand checks. +import { SignJWT, jwtVerify, errors, cryptoRuntime, decodeJwt } from "jose"; + +const encoder = new TextEncoder(); +const secret = encoder.encode("scriptc-jose-root-shared-secret"); +const now = 1_800_000_000; +const iat = 1_799_999_000; + +interface Options { + issuer?: string; + audience?: string; + algorithms?: string[]; + at?: number; + maxTokenAge?: string; + clockTolerance?: string; +} + +async function check(label: string, token: string, options: Options = {}): Promise { + try { + const { payload, protectedHeader } = await jwtVerify(token, secret, { + algorithms: options.algorithms ?? ["HS256"], + currentDate: new Date((options.at ?? now) * 1000), + issuer: options.issuer, + audience: options.audience, + maxTokenAge: options.maxTokenAge, + clockTolerance: options.clockTolerance, + }); + const role = payload.role; + console.log(label, "ok", protectedHeader.alg, payload.sub ?? null, role, payload.exp ?? null); + } catch (error) { + if (error instanceof errors.JOSEError) { + console.log(label, error.code, error.message, error instanceof errors.JWTExpired); + } else if (error instanceof TypeError) { + console.log(label, "TypeError", error.message); + } else { + throw error; + } + } +} + +console.log(cryptoRuntime, errors.JWTExpired.code, errors.JWSSignatureVerificationFailed.code); + +const token = await new SignJWT({ role: "admin" }) + .setProtectedHeader({ alg: "HS256", typ: "JWT" }) + .setIssuedAt(iat) + .setIssuer("urn:scriptc:issuer") + .setAudience("urn:scriptc:audience") + .setSubject("alice") + .setJti("jti-1") + .setNotBefore(iat - 60) + .setExpirationTime(iat + 3600) + .sign(secret); +console.log(token); +console.log(JSON.stringify(decodeJwt(token))); + +await check("valid", token, { issuer: "urn:scriptc:issuer", audience: "urn:scriptc:audience" }); +await check("issuer", token, { issuer: "urn:other" }); +await check("audience", token, { audience: "urn:other" }); +await check("expired", token, { at: iat + 7200 }); +await check("wrong secret", await new SignJWT({ role: "admin" }).setProtectedHeader({ alg: "HS256" }).sign(encoder.encode("x"))); +await check("malformed", "not.a.jwt"); +await check("alg", token, { algorithms: ["HS512"] }); + +// String durations: maxTokenAge and clockTolerance against the fixed date. +await check("max age 10 minutes", token, { maxTokenAge: "10 minutes" }); +await check("max age 1 hour", token, { maxTokenAge: "1 hour" }); +await check("max age 2 days", token, { maxTokenAge: "2 days" }); +await check("max age 1.5h", token, { maxTokenAge: "1.5h" }); +await check("max age +20m", token, { maxTokenAge: "+20m" }); +await check("max age 1 week", token, { maxTokenAge: "1 week" }); +await check("max age 1y", token, { maxTokenAge: "1y" }); +await check("max age 900 secs", token, { maxTokenAge: "900 secs" }); +await check("max age 3 HOURS", token, { maxTokenAge: "3 HOURS" }); +await check("tolerance 30s", token, { at: iat + 3620, clockTolerance: "30s" }); +await check("tolerance 5 sec", token, { at: iat + 3620, clockTolerance: "5 sec" }); +await check("tolerance 1 min from now", token, { at: iat + 3620, clockTolerance: "1 min from now" }); +await check("tolerance 1 min ago", token, { at: iat + 3620, clockTolerance: "1 min ago" }); +await check("bad duration", token, { maxTokenAge: "10 fortnights" }); +await check("signed ago", token, { maxTokenAge: "-2h ago" }); +await check("empty duration", token, { maxTokenAge: "" }); + +// String durations on the builder are relative to the clock: check the +// distance from now instead of printing it. +const relative = await new SignJWT({ role: "reader" }) + .setProtectedHeader({ alg: "HS256" }) + .setIssuedAt("-1 minute") + .setNotBefore("0s") + .setExpirationTime("2h") + .sign(secret); +const claims = decodeJwt(relative); +const clock = Math.floor(Date.now() / 1000); +console.log( + Math.abs((claims.exp ?? 0) - clock - 7200) <= 2, + Math.abs((claims.iat ?? 0) - clock + 60) <= 2, + Math.abs((claims.nbf ?? 0) - clock) <= 2, +); +const { payload } = await jwtVerify(relative, secret, { maxTokenAge: "5 minutes" }); +console.log(payload.role); +try { + new SignJWT({}).setExpirationTime("soon"); +} catch (error) { + console.log((error as Error).name, (error as Error).message); +} diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/LICENSE.md b/tests/corpus/6331-jose-root-entry/node_modules/jose/LICENSE.md new file mode 100644 index 000000000..d0ec038a8 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/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/6331-jose-root-entry/node_modules/jose/dist/types/index.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/index.d.ts new file mode 100644 index 000000000..730247dd1 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/index.d.ts @@ -0,0 +1,55 @@ +export { compactDecrypt } from './jwe/compact/decrypt.js'; +export type { CompactDecryptGetKey } from './jwe/compact/decrypt.js'; +export { flattenedDecrypt } from './jwe/flattened/decrypt.js'; +export type { FlattenedDecryptGetKey } from './jwe/flattened/decrypt.js'; +export { generalDecrypt } from './jwe/general/decrypt.js'; +export type { GeneralDecryptGetKey } from './jwe/general/decrypt.js'; +export { GeneralEncrypt } from './jwe/general/encrypt.js'; +export type { Recipient } from './jwe/general/encrypt.js'; +export { compactVerify } from './jws/compact/verify.js'; +export type { CompactVerifyGetKey } from './jws/compact/verify.js'; +export { flattenedVerify } from './jws/flattened/verify.js'; +export type { FlattenedVerifyGetKey } from './jws/flattened/verify.js'; +export { generalVerify } from './jws/general/verify.js'; +export type { GeneralVerifyGetKey } from './jws/general/verify.js'; +export { jwtVerify } from './jwt/verify.js'; +export type { JWTVerifyOptions, JWTVerifyGetKey } from './jwt/verify.js'; +export { jwtDecrypt } from './jwt/decrypt.js'; +export type { JWTDecryptOptions, JWTDecryptGetKey } from './jwt/decrypt.js'; +export { CompactEncrypt } from './jwe/compact/encrypt.js'; +export { FlattenedEncrypt } from './jwe/flattened/encrypt.js'; +export { CompactSign } from './jws/compact/sign.js'; +export { FlattenedSign } from './jws/flattened/sign.js'; +export { GeneralSign } from './jws/general/sign.js'; +export type { Signature } from './jws/general/sign.js'; +export { SignJWT } from './jwt/sign.js'; +export { EncryptJWT } from './jwt/encrypt.js'; +export { calculateJwkThumbprint, calculateJwkThumbprintUri } from './jwk/thumbprint.js'; +export { EmbeddedJWK } from './jwk/embedded.js'; +export { createLocalJWKSet } from './jwks/local.js'; +export type { LocalJWKSet } from './jwks/local.js'; +export { createRemoteJWKSet, jwksCache, customFetch } from './jwks/remote.js'; +export type { RemoteJWKSet, RemoteJWKSetOptions, JWKSCacheInput, ExportedJWKSCache, FetchImplementation, } from './jwks/remote.js'; +export { UnsecuredJWT } from './jwt/unsecured.js'; +export type { UnsecuredResult } from './jwt/unsecured.js'; +export { exportPKCS8, exportSPKI, exportJWK } from './key/export.js'; +export { importSPKI, importPKCS8, importX509, importJWK } from './key/import.js'; +export type { KeyImportOptions } from './key/import.js'; +export { decodeProtectedHeader } from './util/decode_protected_header.js'; +export { decodeJwt } from './util/decode_jwt.js'; +export type { ProtectedHeaderParameters } from './util/decode_protected_header.js'; +import * as errors from './util/errors.js'; +export { errors }; +export { generateKeyPair } from './key/generate_key_pair.js'; +export type { GenerateKeyPairAlgorithm, GenerateKeyPairResult, GenerateKeyPairOptions, } from './key/generate_key_pair.js'; +export { generateSecret } from './key/generate_secret.js'; +export type { GenerateSecretAlgorithm, GenerateSecretOptions } from './key/generate_secret.js'; +import * as base64url from './util/base64url.js'; +export { base64url }; +export type { AnyJWK, CompactDecryptResult, CompactJWEHeaderParameters, CompactJWSHeaderParameters, CompactVerifyResult, CritOption, CryptoKey, DecryptOptions, EncryptOptions, FlattenedDecryptResult, FlattenedJWE, FlattenedJWS, FlattenedJWSInput, FlattenedVerifyResult, GeneralDecryptResult, GeneralJWE, GeneralJWS, GeneralJWSInput, GeneralVerifyResult, GetKeyFunction, JoseHeaderParameters, JSONWebKeySet, JWEContentEncryptionAlgorithm, JWEHeaderParameters, JWEKeyManagementAlgorithm, JWEKeyManagementHeaderParameters, JWK_AKP_Private, JWK_AKP_Public, JWK_EC_Private, JWK_EC_Public, JWK_oct, JWK_OKP_Private, JWK_OKP_Public, JWK_RSA_Private, JWK_RSA_Public, JWK, JWKKeyType, JWKParameters, JWSAlgorithm, JWSHeaderParameters, JWTClaimVerificationOptions, JWTDecryptResult, JWTHeaderParameters, JWTPayload, JWTVerifyResult, KeyInput, KeyObject, ProduceJWT, ResolvedKey, SignOptions, VerifyOptions, } from './types.d.ts'; +/** + * Identified the loaded runtime-specific build in prior releases; now always `"WebCryptoAPI"`. + * + * @deprecated Remove any runtime branching on this value; it is always `"WebCryptoAPI"`. + */ +export declare const cryptoRuntime = "WebCryptoAPI"; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwe/compact/decrypt.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwe/compact/decrypt.d.ts new file mode 100644 index 000000000..abbac7ac2 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwe/compact/decrypt.d.ts @@ -0,0 +1,24 @@ +import type * as t from '../../types.d.ts'; +/** Resolves a key for Compact JWE decryption from unverified headers and token data. */ +export interface CompactDecryptGetKey extends t.GetKeyFunction { +} +/** + * Decrypts a Compact JWE. + * + * @param key Private key or shared secret. See + * {@link https://github.com/panva/jose/issues/210#jwe-alg Algorithm Key Requirements}. + */ +export declare function compactDecrypt(jwe: string | Uint8Array, key: t.KeyInput, options?: t.DecryptOptions): Promise; +/** + * Decrypts a Compact JWE with a dynamically resolved key, included in the result. + * + * @param getKey Resolves a private key or shared secret from unverified token data. + */ +export declare function compactDecrypt(jwe: string | Uint8Array, getKey: CompactDecryptGetKey, options?: t.DecryptOptions): Promise>; +/** + * Decrypts a Compact JWE with a key or key resolver. The result includes `key` only when a resolver + * is used. + * + * @param key Private key or shared secret, or a function resolving one. + */ +export declare function compactDecrypt(jwe: string | Uint8Array, key: t.KeyInput | CompactDecryptGetKey, options?: t.DecryptOptions): Promise>; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwe/compact/encrypt.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwe/compact/encrypt.d.ts new file mode 100644 index 000000000..966f65c3f --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwe/compact/encrypt.d.ts @@ -0,0 +1,40 @@ +import type * as t from '../../types.d.ts'; +/** Builds and encrypts Compact JWE strings. */ +export declare class CompactEncrypt { + #private; + /** + * Creates a Compact JWE encryptor. + * + * @param plaintext Binary representation of the plaintext to encrypt. + */ + constructor(plaintext: Uint8Array); + /** + * Sets a content encryption key instead of generating a random one for the JWE "enc" algorithm. + * May only be called once. + * + * @deprecated Use only for testing and vector validation. + */ + setContentEncryptionKey(cek: Uint8Array): this; + /** + * Sets the content encryption IV instead of generating a random one for the JWE "enc" algorithm. + * May only be called once. + * + * @deprecated Use only for testing and vector validation. + */ + setInitializationVector(iv: Uint8Array): this; + /** Sets the JWE Protected Header. May only be called once. */ + setProtectedHeader(protectedHeader: t.CompactJWEHeaderParameters): this; + /** + * Sets key management inputs such as ECDH-ES "apu"/"apv" or PBES2 "p2c". Use this method instead + * of header setters; the resulting parameters are added to the JOSE header. May only be called + * once. + */ + setKeyManagementParameters(parameters: t.JWEKeyManagementHeaderParameters): this; + /** + * Encrypts the plaintext as a Compact JWE. + * + * @param key Public key or shared secret. See + * {@link https://github.com/panva/jose/issues/210#jwe-alg Algorithm Key Requirements}. + */ + encrypt(key: t.KeyInput, options?: t.EncryptOptions): Promise; +} diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwe/flattened/decrypt.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwe/flattened/decrypt.d.ts new file mode 100644 index 000000000..b166f8f40 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwe/flattened/decrypt.d.ts @@ -0,0 +1,24 @@ +import type * as t from '../../types.d.ts'; +/** Resolves a key for Flattened JWE decryption from unverified headers and token data. */ +export interface FlattenedDecryptGetKey extends t.GetKeyFunction { +} +/** + * Decrypts a Flattened JWE. + * + * @param key Private key or shared secret. See + * {@link https://github.com/panva/jose/issues/210#jwe-alg Algorithm Key Requirements}. + */ +export declare function flattenedDecrypt(jwe: t.FlattenedJWE, key: t.KeyInput, options?: t.DecryptOptions): Promise; +/** + * Decrypts a Flattened JWE with a dynamically resolved key, included in the result. + * + * @param getKey Resolves a private key or shared secret from unverified token data. + */ +export declare function flattenedDecrypt(jwe: t.FlattenedJWE, getKey: FlattenedDecryptGetKey, options?: t.DecryptOptions): Promise>; +/** + * Decrypts a Flattened JWE with a key or key resolver. The result includes `key` only when a + * resolver is used. + * + * @param key Private key or shared secret, or a function resolving one. + */ +export declare function flattenedDecrypt(jwe: t.FlattenedJWE, key: t.KeyInput | FlattenedDecryptGetKey, options?: t.DecryptOptions): Promise>; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwe/flattened/encrypt.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwe/flattened/encrypt.d.ts new file mode 100644 index 000000000..9d1d3e3fa --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwe/flattened/encrypt.d.ts @@ -0,0 +1,46 @@ +import type * as t from '../../types.d.ts'; +/** Builds and encrypts Flattened JWE objects. */ +export declare class FlattenedEncrypt { + #private; + /** + * Creates a Flattened JWE encryptor. + * + * @param plaintext Binary representation of the plaintext to encrypt. + */ + constructor(plaintext: Uint8Array); + /** + * Sets key management inputs such as ECDH-ES "apu"/"apv" or PBES2 "p2c". Use this method instead + * of header setters; the resulting parameters are added to the JOSE header. May only be called + * once. + */ + setKeyManagementParameters(parameters: t.JWEKeyManagementHeaderParameters): this; + /** Sets the JWE Protected Header. May only be called once. */ + setProtectedHeader(protectedHeader: t.JWEHeaderParameters): this; + /** Sets the JWE Shared Unprotected Header. May only be called once. */ + setSharedUnprotectedHeader(sharedUnprotectedHeader: t.JWEHeaderParameters): this; + /** Sets the JWE Per-Recipient Unprotected Header. May only be called once. */ + setUnprotectedHeader(unprotectedHeader: t.JWEHeaderParameters): this; + /** Sets additional data to authenticate without encrypting it. */ + setAdditionalAuthenticatedData(aad: Uint8Array): this; + /** + * Sets a content encryption key instead of generating a random one for the JWE "enc" algorithm. + * May only be called once. + * + * @deprecated Use only for testing and vector validation. + */ + setContentEncryptionKey(cek: Uint8Array): this; + /** + * Sets the content encryption IV instead of generating a random one for the JWE "enc" algorithm. + * May only be called once. + * + * @deprecated Use only for testing and vector validation. + */ + setInitializationVector(iv: Uint8Array): this; + /** + * Encrypts the plaintext as a Flattened JWE. + * + * @param key Public key or shared secret. See + * {@link https://github.com/panva/jose/issues/210#jwe-alg Algorithm Key Requirements}. + */ + encrypt(key: t.KeyInput, options?: t.EncryptOptions): Promise; +} diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwe/general/decrypt.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwe/general/decrypt.d.ts new file mode 100644 index 000000000..67a396c16 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwe/general/decrypt.d.ts @@ -0,0 +1,28 @@ +import type * as t from '../../types.d.ts'; +/** Resolves a key for General JWE decryption from unverified headers and token data. */ +export interface GeneralDecryptGetKey extends t.GetKeyFunction { +} +/** + * Decrypts a General JWE. + * + * > Note: Returns plaintext and headers from the first recipient that decrypts successfully. Other + * > recipients may be inspected to enforce serialization rules, but their headers are not included in + * > the result. Rely only on the returned data. + * + * @param key Private key or shared secret. See + * {@link https://github.com/panva/jose/issues/210#jwe-alg Algorithm Key Requirements}. + */ +export declare function generalDecrypt(jwe: t.GeneralJWE, key: t.KeyInput, options?: t.DecryptOptions): Promise; +/** + * Decrypts a General JWE with a dynamically resolved key, included in the result. + * + * @param getKey Resolves a private key or shared secret from unverified token data. + */ +export declare function generalDecrypt(jwe: t.GeneralJWE, getKey: GeneralDecryptGetKey, options?: t.DecryptOptions): Promise>; +/** + * Decrypts a General JWE with a key or key resolver. The result includes `key` only when a resolver + * is used. + * + * @param key Private key or shared secret, or a function resolving one. + */ +export declare function generalDecrypt(jwe: t.GeneralJWE, key: t.KeyInput | GeneralDecryptGetKey, options?: t.DecryptOptions): Promise>; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwe/general/encrypt.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwe/general/encrypt.d.ts new file mode 100644 index 000000000..e738a3a14 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwe/general/encrypt.d.ts @@ -0,0 +1,51 @@ +import type * as t from '../../types.d.ts'; +/** Configures an individual recipient in a General JWE. */ +export interface Recipient { + /** Sets the JWE Per-Recipient Unprotected Header. May only be called once. */ + setUnprotectedHeader(unprotectedHeader: t.JWEHeaderParameters): Recipient; + /** + * Sets key management inputs such as ECDH-ES "apu"/"apv" or PBES2 "p2c". Use this method instead + * of header setters; the resulting parameters are added to the JOSE header. May only be called + * once. + */ + setKeyManagementParameters(parameters: t.JWEKeyManagementHeaderParameters): Recipient; + /** + * Adds another recipient to the enclosing {@link GeneralEncrypt} and returns its configuration. + * + * @param key Public key or shared secret. See + * {@link https://github.com/panva/jose/issues/210#jwe-alg Algorithm Key Requirements}. + */ + addRecipient(key: t.KeyInput, options?: t.CritOption): Recipient; + /** + * Encrypts for all recipients on the enclosing {@link GeneralEncrypt}, using their configured + * keys. + */ + encrypt(): Promise; + /** Returns the enclosing {@link GeneralEncrypt} instance. */ + done(): GeneralEncrypt; +} +/** Builds and encrypts General JWE objects. */ +export declare class GeneralEncrypt { + #private; + /** + * Creates a General JWE encryptor. + * + * @param plaintext Binary representation of the plaintext to encrypt. + */ + constructor(plaintext: Uint8Array); + /** + * Adds a recipient and returns its configuration. + * + * @param key Public key or shared secret. See + * {@link https://github.com/panva/jose/issues/210#jwe-alg Algorithm Key Requirements}. + */ + addRecipient(key: t.KeyInput, options?: t.CritOption): Recipient; + /** Sets the JWE Protected Header. May only be called once. */ + setProtectedHeader(protectedHeader: t.JWEHeaderParameters): this; + /** Sets the JWE Shared Unprotected Header. May only be called once. */ + setSharedUnprotectedHeader(sharedUnprotectedHeader: t.JWEHeaderParameters): this; + /** Sets additional data to authenticate without encrypting it. */ + setAdditionalAuthenticatedData(aad: Uint8Array): this; + /** Encrypts the plaintext as a General JWE. */ + encrypt(): Promise; +} diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwk/embedded.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwk/embedded.d.ts new file mode 100644 index 000000000..d02ffcd49 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwk/embedded.d.ts @@ -0,0 +1,10 @@ +import type * as t from '../types.d.ts'; +/** + * Resolves a verification key from an embedded "jwk" (JSON Web Key) Header Parameter. This key + * resolver opts JWS and JWT verification into trusting a public key supplied by the token. Use the + * verification function's `algorithms` option to restrict accepted algorithms. + * + * @param token The consumed JWS token. + * @returns The public key from the JWS "jwk" (JSON Web Key) Header Parameter. + */ +export declare function EmbeddedJWK(protectedHeader?: t.JWSHeaderParameters, token?: t.FlattenedJWSInput): Promise; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwk/thumbprint.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwk/thumbprint.d.ts new file mode 100644 index 000000000..674d7cbe6 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwk/thumbprint.d.ts @@ -0,0 +1,15 @@ +import type * as t from '../types.d.ts'; +/** + * Calculates a base64url-encoded JWK Thumbprint. CryptoKey inputs must be extractable. + * + * @param key Key to calculate the thumbprint for. + * @param digestAlgorithm Digest algorithm. Defaults to "sha256". + */ +export declare function calculateJwkThumbprint(key: t.JWK | t.CryptoKey | t.KeyObject, digestAlgorithm?: 'sha256' | 'sha384' | 'sha512'): Promise; +/** + * Calculates a JWK Thumbprint URI. CryptoKey inputs must be extractable. + * + * @param key Key to calculate the thumbprint for. + * @param digestAlgorithm Digest algorithm. Defaults to "sha256". + */ +export declare function calculateJwkThumbprintUri(key: t.CryptoKey | t.KeyObject | t.JWK, digestAlgorithm?: 'sha256' | 'sha384' | 'sha512'): Promise; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwks/local.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwks/local.d.ts new file mode 100644 index 000000000..57057c105 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwks/local.d.ts @@ -0,0 +1,17 @@ +import type * as t from '../types.d.ts'; +/** A key resolver created by {@link createLocalJWKSet}. */ +export interface LocalJWKSet { + (protectedHeader?: t.JWSHeaderParameters, token?: t.FlattenedJWSInput): Promise; + /** Returns a structured clone of the original JSON Web Key Set. */ + jwks: () => t.JSONWebKeySet; +} +/** + * Creates a resolver for a locally available JSON Web Key Set. Selection uses the header's "alg" + * (Algorithm) and "kid" (Key ID), and respects the JWK's "use" (Public Key Use) and "key_ops" (Key + * Operations). Exactly one key must match. + * + * > Note: Only public signature verification keys are supported, not public encryption keys. + * + * @param jwks JSON Web Key Set formatted object. + */ +export declare function createLocalJWKSet(jwks: t.JSONWebKeySet): LocalJWKSet; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwks/remote.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwks/remote.d.ts new file mode 100644 index 000000000..0b7055e84 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwks/remote.d.ts @@ -0,0 +1,94 @@ +import type * as t from '../types.d.ts'; +/** + * Configures a custom fetch implementation for remote JWKS retrieval. + * + * > Note: Fetch-like libraries may have incompatible TypeScript signatures even when they accept the + * > supplied arguments at runtime. + */ +export declare const customFetch: unique symbol; +/** Custom fetch function. Must return HTTP 200 with a JSON JWKS. See {@link customFetch}. */ +export type FetchImplementation = ( +/** JWKS URL, passed as the fetch resource. */ +url: string, +/** Options passed to fetch. */ +options: { + headers: Headers; + method: 'GET'; + /** See {@link !Request.redirect} */ + redirect: 'manual'; + signal: AbortSignal; +}) => Promise; +/** + * Symbol used to configure an externally persisted remote JWKS cache. + * + * > Warning: Only trusted application code must be allowed to write this cache; its keys are trusted for + * > signature verification. + */ +export declare const jwksCache: unique symbol; +/** Remote JWKS resolver options. */ +export interface RemoteJWKSetOptions { + /** + * Timeout (in milliseconds) for the HTTP request. When reached the request will be aborted and + * the verification will fail. Must be a non-negative integer. Default is 5000 (5 seconds). + */ + timeoutDuration?: number; + /** + * Time in milliseconds after a successful fetch before a missing key can trigger another fetch. + * Must not be `NaN`. Defaults to 30000 (30 seconds). + */ + cooldownDuration?: number; + /** + * Maximum age of cached keys in milliseconds. Defaults to 600000 (10 minutes); `Infinity` + * disables expiry. Must not be `NaN`. + */ + cacheMaxAge?: number | typeof Infinity; + /** Headers to be sent with the HTTP request. */ + headers?: Record; + /** See {@link jwksCache}. */ + [jwksCache]?: JWKSCacheInput; + /** See {@link customFetch}. */ + [customFetch]?: FetchImplementation; +} +/** Shape of an externally persisted remote JWKS cache. */ +export interface ExportedJWKSCache { + /** Current cached JSON Web Key Set */ + jwks: t.JSONWebKeySet; + /** Last successful fetch, in milliseconds since Unix epoch. */ + uat: number; +} +/** Values accepted by the {@link jwksCache} option. */ +export type JWKSCacheInput = ExportedJWKSCache | Record; +/** A key resolver created by {@link createRemoteJWKSet}. */ +export interface RemoteJWKSet { + (protectedHeader?: t.JWSHeaderParameters, token?: t.FlattenedJWSInput): Promise; + /** Whether the cooldown window following the last successful fetch is still in effect. */ + readonly coolingDown: boolean; + /** + * Whether the currently cached JSON Web Key Set is within its + * {@link RemoteJWKSetOptions.cacheMaxAge}. + */ + readonly fresh: boolean; + /** Whether a JSON Web Key Set fetch is currently in flight. */ + readonly reloading: boolean; + /** + * Triggers a JSON Web Key Set fetch, bypassing + * {@link RemoteJWKSetOptions.cooldownDuration the cooldown}. + */ + reload: () => Promise; + /** + * Returns a structured clone of the cached JSON Web Key Set, or `undefined` before keys have been + * fetched or seeded via {@link jwksCache}. + */ + jwks: () => t.JSONWebKeySet | undefined; +} +/** + * Creates a resolver for a JSON Web Key Set available at an HTTP(S) URL. Fetches the JSON Web Key + * Set when the cache is missing or stale. An unmatched key triggers another fetch only when + * `cooldownDuration` has elapsed since the last successful fetch. Selection uses the header's "alg" + * and "kid" and respects the JWK's "use" and "key_ops". Exactly one key must match. + * + * > Note: Only public signature verification keys are supported, not public encryption keys. + * + * @param url URL to fetch the JSON Web Key Set from. + */ +export declare function createRemoteJWKSet(url: URL, options?: RemoteJWKSetOptions): RemoteJWKSet; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jws/compact/sign.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jws/compact/sign.d.ts new file mode 100644 index 000000000..15d7e062b --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jws/compact/sign.d.ts @@ -0,0 +1,20 @@ +import type * as t from '../../types.d.ts'; +/** Builds and signs Compact JWS strings. */ +export declare class CompactSign { + #private; + /** + * Creates a Compact JWS signer. + * + * @param payload Binary representation of the payload to sign. + */ + constructor(payload: Uint8Array); + /** Sets the JWS Protected Header. May only be called once. */ + setProtectedHeader(protectedHeader: t.CompactJWSHeaderParameters): this; + /** + * Signs the payload as a Compact JWS. + * + * @param key Private key or shared secret. See + * {@link https://github.com/panva/jose/issues/210#jws-alg Algorithm Key Requirements}. + */ + sign(key: t.KeyInput, options?: t.SignOptions): Promise; +} diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jws/compact/verify.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jws/compact/verify.d.ts new file mode 100644 index 000000000..0f21744cb --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jws/compact/verify.d.ts @@ -0,0 +1,25 @@ +import type * as t from '../../types.d.ts'; +/** Resolves a key for Compact JWS verification from unverified headers and token data. */ +export interface CompactVerifyGetKey extends t.GetKeyFunction { +} +/** + * Verifies a Compact JWS signature and decodes its payload. + * + * @param key Public key or shared secret. See + * {@link https://github.com/panva/jose/issues/210#jws-alg Algorithm Key Requirements}. + */ +export declare function compactVerify(jws: string | Uint8Array, key: t.KeyInput, options?: t.VerifyOptions): Promise; +/** + * Verifies a Compact JWS signature and decodes its payload with a dynamically resolved key, + * included in the result. + * + * @param getKey Resolves a public key or shared secret from unverified token data. + */ +export declare function compactVerify(jws: string | Uint8Array, getKey: CompactVerifyGetKey, options?: t.VerifyOptions): Promise>; +/** + * Verifies a Compact JWS and decodes its payload using a key or key resolver. The result includes + * `key` only when a resolver is used. + * + * @param key Public key or shared secret, or a function resolving one. + */ +export declare function compactVerify(jws: string | Uint8Array, key: t.KeyInput | CompactVerifyGetKey, options?: t.VerifyOptions): Promise>; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jws/flattened/sign.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jws/flattened/sign.d.ts new file mode 100644 index 000000000..3bc861803 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jws/flattened/sign.d.ts @@ -0,0 +1,22 @@ +import type * as t from '../../types.d.ts'; +/** Builds and signs Flattened JWS objects. */ +export declare class FlattenedSign { + #private; + /** + * Creates a Flattened JWS signer. + * + * @param payload Binary representation of the payload to sign. + */ + constructor(payload: Uint8Array); + /** Sets the JWS Protected Header. May only be called once. */ + setProtectedHeader(protectedHeader: t.JWSHeaderParameters): this; + /** Sets the JWS Unprotected Header. May only be called once. */ + setUnprotectedHeader(unprotectedHeader: t.JWSHeaderParameters): this; + /** + * Signs the payload as a Flattened JWS. + * + * @param key Private key or shared secret. See + * {@link https://github.com/panva/jose/issues/210#jws-alg Algorithm Key Requirements}. + */ + sign(key: t.KeyInput, options?: t.SignOptions): Promise; +} diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jws/flattened/verify.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jws/flattened/verify.d.ts new file mode 100644 index 000000000..151c59b9a --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jws/flattened/verify.d.ts @@ -0,0 +1,25 @@ +import type * as t from '../../types.d.ts'; +/** Resolves a key for Flattened JWS verification from unverified headers and token data. */ +export interface FlattenedVerifyGetKey extends t.GetKeyFunction { +} +/** + * Verifies a Flattened JWS signature and decodes its payload. + * + * @param key Public key or shared secret. See + * {@link https://github.com/panva/jose/issues/210#jws-alg Algorithm Key Requirements}. + */ +export declare function flattenedVerify(jws: t.FlattenedJWSInput, key: t.KeyInput, options?: t.VerifyOptions): Promise; +/** + * Verifies a Flattened JWS signature and decodes its payload with a dynamically resolved key, + * included in the result. + * + * @param getKey Resolves a public key or shared secret from unverified token data. + */ +export declare function flattenedVerify(jws: t.FlattenedJWSInput, getKey: FlattenedVerifyGetKey, options?: t.VerifyOptions): Promise>; +/** + * Verifies a Flattened JWS and decodes its payload using a key or key resolver. The result includes + * `key` only when a resolver is used. + * + * @param key Public key or shared secret, or a function resolving one. + */ +export declare function flattenedVerify(jws: t.FlattenedJWSInput, key: t.KeyInput | FlattenedVerifyGetKey, options?: t.VerifyOptions): Promise>; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jws/general/sign.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jws/general/sign.d.ts new file mode 100644 index 000000000..ad1cd73dd --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jws/general/sign.d.ts @@ -0,0 +1,38 @@ +import type * as t from '../../types.d.ts'; +/** Configures an individual signature in a General JWS. */ +export interface Signature { + /** Sets the JWS Protected Header. May only be called once. */ + setProtectedHeader(protectedHeader: t.JWSHeaderParameters): Signature; + /** Sets the JWS Unprotected Header. May only be called once. */ + setUnprotectedHeader(unprotectedHeader: t.JWSHeaderParameters): Signature; + /** + * Adds another signature to the enclosing {@link GeneralSign} and returns its configuration. + * + * @param key Private key or shared secret. See + * {@link https://github.com/panva/jose/issues/210#jws-alg Algorithm Key Requirements}. + */ + addSignature(key: t.KeyInput, options?: t.SignOptions): Signature; + /** Creates all signatures on the enclosing {@link GeneralSign}, using their configured keys. */ + sign(): Promise; + /** Returns the enclosing {@link GeneralSign} instance. */ + done(): GeneralSign; +} +/** Builds and signs General JWS objects. */ +export declare class GeneralSign { + #private; + /** + * Creates a General JWS signer. + * + * @param payload Binary representation of the payload to sign. + */ + constructor(payload: Uint8Array); + /** + * Adds a signature and returns its configuration. + * + * @param key Private key or shared secret. See + * {@link https://github.com/panva/jose/issues/210#jws-alg Algorithm Key Requirements}. + */ + addSignature(key: t.KeyInput, options?: t.SignOptions): Signature; + /** Signs the payload as a General JWS. */ + sign(): Promise; +} diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jws/general/verify.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jws/general/verify.d.ts new file mode 100644 index 000000000..620325d00 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jws/general/verify.d.ts @@ -0,0 +1,29 @@ +import type * as t from '../../types.d.ts'; +/** Resolves a key for General JWS verification from unverified headers and token data. */ +export interface GeneralVerifyGetKey extends t.GetKeyFunction { +} +/** + * Verifies a General JWS signature and decodes its payload. + * + * > Note: Returns payload and headers from the first signature that verifies successfully. Other entries' + * > headers may be inspected to reject inconsistent use of the JWS Unencoded Payload Option, but are + * > not included in the result. Rely only on the returned data. + * + * @param key Public key or shared secret. See + * {@link https://github.com/panva/jose/issues/210#jws-alg Algorithm Key Requirements}. + */ +export declare function generalVerify(jws: t.GeneralJWSInput, key: t.KeyInput, options?: t.VerifyOptions): Promise; +/** + * Verifies a General JWS signature and decodes its payload with a dynamically resolved key, + * included in the result. + * + * @param getKey Resolves a public key or shared secret from unverified token data. + */ +export declare function generalVerify(jws: t.GeneralJWSInput, getKey: GeneralVerifyGetKey, options?: t.VerifyOptions): Promise>; +/** + * Verifies a General JWS and decodes its payload using a key or key resolver. The result includes + * `key` only when a resolver is used. + * + * @param key Public key or shared secret, or a function resolving one. + */ +export declare function generalVerify(jws: t.GeneralJWSInput, key: t.KeyInput | GeneralVerifyGetKey, options?: t.VerifyOptions): Promise>; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwt/decrypt.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwt/decrypt.d.ts new file mode 100644 index 000000000..cdf6a372a --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/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/6331-jose-root-entry/node_modules/jose/dist/types/jwt/encrypt.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwt/encrypt.d.ts new file mode 100644 index 000000000..c39e4c994 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/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/6331-jose-root-entry/node_modules/jose/dist/types/jwt/sign.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwt/sign.d.ts new file mode 100644 index 000000000..67ce10434 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwt/sign.d.ts @@ -0,0 +1,21 @@ +import type * as t from '../types.d.ts'; +/** @param payload Initial JWT Claims Set. Defaults to an empty object. */ +declare const SignJWT_base: new (payload?: t.JWTPayload) => t.ProduceJWT; +/** Builds and signs Compact JWS-formatted JSON Web Tokens. */ +export declare class SignJWT extends SignJWT_base { + #private; + /** + * Sets the JWS Protected Header. May only be called once. + * + * @param protectedHeader JWS Protected Header. Must contain an "alg" (JWS Algorithm) property. + */ + setProtectedHeader(protectedHeader: t.JWTHeaderParameters): this; + /** + * Signs and returns the JWT. + * + * @param key Private key or shared secret to sign the JWT with. See + * {@link https://github.com/panva/jose/issues/210#jws-alg Algorithm Key Requirements}. + */ + sign(key: t.KeyInput, options?: t.SignOptions): Promise; +} +export {}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwt/unsecured.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwt/unsecured.d.ts new file mode 100644 index 000000000..6c3454eef --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwt/unsecured.d.ts @@ -0,0 +1,19 @@ +import type * as t from '../types.d.ts'; +/** Decoded Unsecured JWT. */ +export interface UnsecuredResult { + /** JWT Claims Set. */ + payload: PayloadType & t.JWTPayload & ([PayloadType] extends [object] ? unknown : unknown extends PayloadType ? unknown : never); + /** The decoded JOSE Header; always `{ "alg": "none" }` for an Unsecured JWT. */ + header: t.JWSHeaderParameters; +} +/** @param payload Initial JWT Claims Set. Defaults to an empty object. */ +declare const UnsecuredJWT_base: new (payload?: t.JWTPayload) => t.ProduceJWT; +/** Encodes and decodes `{ "alg": "none" }` Unsecured JWTs. */ +export declare class UnsecuredJWT extends UnsecuredJWT_base { + private jwt; + /** Encodes the Unsecured JWT. */ + encode(): string; + /** Decodes an unsecured JWT and validates its claims without authenticating the token. */ + static decode(jwt: string, options?: t.JWTClaimVerificationOptions): UnsecuredResult; +} +export {}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwt/verify.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwt/verify.d.ts new file mode 100644 index 000000000..6c8b2f32b --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwt/verify.d.ts @@ -0,0 +1,35 @@ +import type * as t from '../types.d.ts'; +/** JWS verification and JWT Claims Set validation options. */ +export interface JWTVerifyOptions extends t.VerifyOptions, t.JWTClaimVerificationOptions { +} +/** + * Resolves a JWT verification key. No token components have been authenticated when this function + * is called. + */ +export interface JWTVerifyGetKey extends t.GetKeyFunction { +} +/** + * Verifies a Compact JWS-formatted JWT and validates its Claims Set. + * + * @param key Public key or shared secret to verify the JWT with. See + * {@link https://github.com/panva/jose/issues/210#jws-alg Algorithm Key Requirements}. + * @param options JWT Verification and JWT Claims Set validation options. + */ +export declare function jwtVerify(jwt: string | Uint8Array, key: t.KeyInput, options?: JWTVerifyOptions): Promise>; +/** + * Verifies the JWT signature and claims, returning the resolved key. + * + * @param getKey Function resolving a public key or shared secret to verify the JWT with. See + * {@link https://github.com/panva/jose/issues/210#jws-alg Algorithm Key Requirements}. + * @param options JWT Verification and JWT Claims Set validation options. + */ +export declare function jwtVerify(jwt: string | Uint8Array, getKey: JWTVerifyGetKey, options?: JWTVerifyOptions): 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 Key, or function resolving a key, to verify the JWT with. See + * {@link https://github.com/panva/jose/issues/210#jws-alg Algorithm Key Requirements}. + * @param options JWT Verification and JWT Claims Set validation options. + */ +export declare function jwtVerify(jwt: string | Uint8Array, key: t.KeyInput | JWTVerifyGetKey, options?: JWTVerifyOptions): Promise & Partial>; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/key/export.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/key/export.d.ts new file mode 100644 index 000000000..bbc7ef7d4 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/key/export.d.ts @@ -0,0 +1,19 @@ +import type * as t from '../types.d.ts'; +/** + * Exports a public key to PEM-encoded SPKI. CryptoKey inputs must be extractable. + * + * @param key Key to export. + */ +export declare function exportSPKI(key: t.CryptoKey | t.KeyObject): Promise; +/** + * Exports a private key to PEM-encoded PKCS#8. CryptoKey inputs must be extractable. + * + * @param key Key to export. + */ +export declare function exportPKCS8(key: t.CryptoKey | t.KeyObject): Promise; +/** + * Exports a key to JWK. CryptoKey inputs must be extractable. + * + * @param key Key to export. + */ +export declare function exportJWK(key: t.CryptoKey | t.KeyObject | Uint8Array): Promise; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/key/generate_key_pair.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/key/generate_key_pair.d.ts new file mode 100644 index 000000000..ecc53339c --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/key/generate_key_pair.d.ts @@ -0,0 +1,33 @@ +import type * as t from '../types.d.ts'; +/** Algorithms supported by {@link generateKeyPair}, subject to runtime support. */ +export type GenerateKeyPairAlgorithm = 'PS256' | 'PS384' | 'PS512' | 'RS256' | 'RS384' | 'RS512' | 'RSA-OAEP' | 'RSA-OAEP-256' | 'RSA-OAEP-384' | 'RSA-OAEP-512' | 'ES256' | 'ES384' | 'ES512' | 'Ed25519' | 'EdDSA' | 'ML-DSA-44' | 'ML-DSA-65' | 'ML-DSA-87' | 'ECDH-ES' | 'ECDH-ES+A128KW' | 'ECDH-ES+A192KW' | 'ECDH-ES+A256KW' | (string & {}); +/** Generated asymmetric key pair. */ +export interface GenerateKeyPairResult { + privateKey: t.CryptoKey; + publicKey: t.CryptoKey; +} +/** Asymmetric key pair generation options. */ +export interface GenerateKeyPairOptions { + /** + * EC curve or OKP key subtype. Must be supported by the algorithm and runtime. ECDH-ES defaults + * to P-256. + */ + crv?: string; + /** RSA modulus length in bits. Must be an integer of at least 2048; defaults to 2048. */ + modulusLength?: number; + /** + * Whether the private key is extractable. Defaults to false; the public key is always + * extractable. + */ + extractable?: boolean; +} +/** + * Generates an asymmetric key pair for a JWA algorithm identifier. + * + * > Note: Private keys are not extractable by default. Set {@link GenerateKeyPairOptions.extractable} to + * > export them; public keys are always extractable. + * + * @param alg JWA Algorithm Identifier to be used with the generated key pair. See + * {@link https://github.com/panva/jose/issues/210 Algorithm Key Requirements}. + */ +export declare function generateKeyPair(alg: GenerateKeyPairAlgorithm, options?: GenerateKeyPairOptions): Promise; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/key/generate_secret.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/key/generate_secret.d.ts new file mode 100644 index 000000000..da1db236e --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/key/generate_secret.d.ts @@ -0,0 +1,30 @@ +import type * as t from '../types.d.ts'; +/** Algorithms supported by {@link generateSecret}, subject to runtime support. */ +export type GenerateSecretAlgorithm = 'HS256' | 'HS384' | 'HS512' | 'A128CBC-HS256' | 'A192CBC-HS384' | 'A256CBC-HS512' | 'A128KW' | 'A192KW' | 'A256KW' | 'A128GCMKW' | 'A192GCMKW' | 'A256GCMKW' | 'A128GCM' | 'A192GCM' | 'A256GCM' | (string & {}); +/** + * Maps a JWA algorithm identifier to the value returned by {@link generateSecret}. AES-CBC-HMAC + * algorithms return {@link !Uint8Array}; other supported algorithms return + * {@link t.CryptoKey CryptoKey}. When the algorithm is not statically known, the result is their + * union. + */ +export type GeneratedSecret = Alg extends 'A128CBC-HS256' | 'A192CBC-HS384' | 'A256CBC-HS512' ? Uint8Array : string extends Alg ? t.CryptoKey | Uint8Array : t.CryptoKey; +/** Secret generation options. */ +export interface GenerateSecretOptions { + /** + * Whether the generated CryptoKey is extractable. Defaults to false; has no effect for + * A128CBC-HS256, A192CBC-HS384, and A256CBC-HS512, which return raw bytes. + */ + extractable?: boolean; +} +/** + * Generates a symmetric secret key for a given JWA algorithm identifier. + * + * > Note: The secret key is generated with `extractable` set to `false` by default. + * + * > Note: A128CBC-HS256, A192CBC-HS384, and A256CBC-HS512 return {@link !Uint8Array} because these secrets + * > have no CryptoKey representation. + * + * @param alg JWA Algorithm Identifier to be used with the generated secret. See + * {@link https://github.com/panva/jose/issues/210 Algorithm Key Requirements}. + */ +export declare function generateSecret(alg: Alg, options?: GenerateSecretOptions): Promise>; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/key/import.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/key/import.d.ts new file mode 100644 index 000000000..88ba06a28 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/key/import.d.ts @@ -0,0 +1,67 @@ +import type * as t from '../types.d.ts'; +/** + * Maps a JWK key type to the value returned by {@link importJWK}. An "oct" JWK returns + * {@link !Uint8Array}; other supported key types return {@link t.CryptoKey CryptoKey}. A JWK + * whose "kty" is not statically known resolves to their union. + */ +export type ImportedJWK = JWKType extends { + kty: 'oct'; +} ? Uint8Array : JWKType extends { + kty: 'AKP' | 'EC' | 'OKP' | 'RSA'; +} ? t.CryptoKey : t.CryptoKey | Uint8Array; +/** Key import options. */ +export interface KeyImportOptions { + /** + * Whether the imported CryptoKey is extractable. Overrides JWK "ext" when set. Without either, + * defaults to false for private keys, true otherwise. + */ + extractable?: boolean; +} +/** + * Imports a PEM-encoded SPKI string as a {@link !CryptoKey}. + * + * > Note: For RSA keys, use the rsaEncryption OID (1.2.840.113549.1.1.1). The id-RSASSA-PSS OID + * > (1.2.840.113549.1.1.10) is not supported by the Web Cryptography API. + * + * @param spki PEM-encoded SPKI string + * @param alg JSON Web Algorithm identifier to be used with the imported key. See + * {@link https://github.com/panva/jose/issues/210 Algorithm Key Requirements}. + */ +export declare function importSPKI(spki: string, alg: string, options?: KeyImportOptions): Promise; +/** + * Imports a PEM-encoded X.509 certificate's public key as a {@link !CryptoKey}. + * + * > Note: For RSA keys, use the rsaEncryption OID (1.2.840.113549.1.1.1). The id-RSASSA-PSS OID + * > (1.2.840.113549.1.1.10) is not supported by the Web Cryptography API. + * + * @param x509 X.509 certificate string + * @param alg JSON Web Algorithm identifier to be used with the imported key. See + * {@link https://github.com/panva/jose/issues/210 Algorithm Key Requirements}. + */ +export declare function importX509(x509: string, alg: string, options?: KeyImportOptions): Promise; +/** + * Imports a PEM-encoded PKCS#8 string as a {@link !CryptoKey}. + * + * > Note: For RSA keys, use the rsaEncryption OID (1.2.840.113549.1.1.1). The id-RSASSA-PSS OID + * > (1.2.840.113549.1.1.10) is not supported by the Web Cryptography API. + * + * @param pkcs8 PEM-encoded PKCS#8 string + * @param alg JSON Web Algorithm identifier to be used with the imported key. See + * {@link https://github.com/panva/jose/issues/210 Algorithm Key Requirements}. + */ +export declare function importPKCS8(pkcs8: string, alg: string, options?: KeyImportOptions): Promise; +/** + * Imports a JWK as a {@link !CryptoKey} or {@link !Uint8Array}. Asymmetric imports require either the + * "alg" argument or JWK "alg" parameter. For AKP keys, the JWK "alg" parameter is required and must + * match the argument when provided. + * + * > Note: CryptoKey imports honor the JWK "key_ops" and "ext" parameters. + * + * > Note: Symmetric JWKs (`kty: "oct"`) return {@link !Uint8Array} instead of CryptoKey. + * + * @param jwk JSON Web Key. + * @param alg JSON Web Algorithm identifier to be used with the imported key. Default is the "alg" + * property on the JWK. See + * {@link https://github.com/panva/jose/issues/210 Algorithm Key Requirements}. + */ +export declare function importJWK(jwk: JWKType, alg?: string, options?: KeyImportOptions): Promise>; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/types.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/types.d.ts new file mode 100644 index 000000000..9815c018d --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/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/6331-jose-root-entry/node_modules/jose/dist/types/util/base64url.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/util/base64url.d.ts new file mode 100644 index 000000000..6d8a665fa --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/util/base64url.d.ts @@ -0,0 +1,16 @@ +/** + * Decodes base64url text or its UTF-8 bytes. + * + * @param input Base64url-encoded string or its UTF-8 bytes. + * @returns The decoded bytes. + * @throws {TypeError} If the input is not valid base64url. Standard Base64 `+` and `/` are + * rejected. + */ +export declare function decode(input: Uint8Array | string): Uint8Array; +/** + * Encodes unpadded base64url; strings are first encoded as UTF-8. + * + * @param input Bytes or a string to encode. Strings are first encoded as UTF-8. + * @returns The unpadded base64url representation of the input. + */ +export declare function encode(input: Uint8Array | string): string; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/util/decode_jwt.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/util/decode_jwt.d.ts new file mode 100644 index 000000000..63ce31f0e --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/util/decode_jwt.d.ts @@ -0,0 +1,8 @@ +import type * as t from '../types.d.ts'; +/** + * Decodes the Claims Set of a JWT in Compact JWS serialization without checking its signature or + * validating claim types and values. + * + * @returns The parsed JWT Claims Set. + */ +export declare function decodeJwt(jwt: string): PayloadType & t.JWTPayload & ([PayloadType] extends [object] ? unknown : unknown extends PayloadType ? unknown : never); diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/util/decode_protected_header.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/util/decode_protected_header.d.ts new file mode 100644 index 000000000..2d74643d3 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/util/decode_protected_header.d.ts @@ -0,0 +1,10 @@ +import type * as t from '../types.d.ts'; +/** JWE and JWS Header Parameters returned by {@link decodeProtectedHeader}. */ +export type ProtectedHeaderParameters = t.JWSHeaderParameters & t.JWEHeaderParameters; +/** + * Decodes the Protected Header of a JWE, JWS, or JWT without authenticating the token. + * + * @param token Compact token or JSON serialization object with a `protected` member. + * @returns The parsed Protected Header. + */ +export declare function decodeProtectedHeader(token: string | object): ProtectedHeaderParameters; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/util/errors.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/util/errors.d.ts new file mode 100644 index 000000000..930a29b9f --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/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/6331-jose-root-entry/node_modules/jose/dist/webapi/index.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/index.js new file mode 100644 index 000000000..a41eeb626 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/index.js @@ -0,0 +1,70 @@ +import { compactDecrypt } from "./jwe/compact/decrypt.js"; +import { flattenedDecrypt } from "./jwe/flattened/decrypt.js"; +import { generalDecrypt } from "./jwe/general/decrypt.js"; +import { GeneralEncrypt } from "./jwe/general/encrypt.js"; +import { compactVerify } from "./jws/compact/verify.js"; +import { flattenedVerify } from "./jws/flattened/verify.js"; +import { generalVerify } from "./jws/general/verify.js"; +import { jwtVerify } from "./jwt/verify.js"; +import { jwtDecrypt } from "./jwt/decrypt.js"; +import { CompactEncrypt } from "./jwe/compact/encrypt.js"; +import { FlattenedEncrypt } from "./jwe/flattened/encrypt.js"; +import { CompactSign } from "./jws/compact/sign.js"; +import { FlattenedSign } from "./jws/flattened/sign.js"; +import { GeneralSign } from "./jws/general/sign.js"; +import { SignJWT } from "./jwt/sign.js"; +import { EncryptJWT } from "./jwt/encrypt.js"; +import { calculateJwkThumbprint, calculateJwkThumbprintUri } from "./jwk/thumbprint.js"; +import { EmbeddedJWK } from "./jwk/embedded.js"; +import { createLocalJWKSet } from "./jwks/local.js"; +import { createRemoteJWKSet, jwksCache, customFetch } from "./jwks/remote.js"; +import { UnsecuredJWT } from "./jwt/unsecured.js"; +import { exportPKCS8, exportSPKI, exportJWK } from "./key/export.js"; +import { importSPKI, importPKCS8, importX509, importJWK } from "./key/import.js"; +import { decodeProtectedHeader } from "./util/decode_protected_header.js"; +import { decodeJwt } from "./util/decode_jwt.js"; +import * as errors from "./util/errors.js"; +import { generateKeyPair } from "./key/generate_key_pair.js"; +import { generateSecret } from "./key/generate_secret.js"; +import * as base64url from "./util/base64url.js"; +const cryptoRuntime = "WebCryptoAPI"; +export { + CompactEncrypt, + CompactSign, + EmbeddedJWK, + EncryptJWT, + FlattenedEncrypt, + FlattenedSign, + GeneralEncrypt, + GeneralSign, + SignJWT, + UnsecuredJWT, + base64url, + calculateJwkThumbprint, + calculateJwkThumbprintUri, + compactDecrypt, + compactVerify, + createLocalJWKSet, + createRemoteJWKSet, + cryptoRuntime, + customFetch, + decodeJwt, + decodeProtectedHeader, + errors, + exportJWK, + exportPKCS8, + exportSPKI, + flattenedDecrypt, + flattenedVerify, + generalDecrypt, + generalVerify, + generateKeyPair, + generateSecret, + importJWK, + importPKCS8, + importSPKI, + importX509, + jwksCache, + jwtDecrypt, + jwtVerify +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwe/compact/decrypt.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwe/compact/decrypt.js new file mode 100644 index 000000000..3c20fe5df --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwe/compact/decrypt.js @@ -0,0 +1,7 @@ +import { prepareDecrypt, decryptCompact } from "../../lib/jwe_decrypt.js"; +async function compactDecrypt(jwe, key, options) { + return decryptCompact(jwe, prepareDecrypt(options), key); +} +export { + compactDecrypt +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwe/compact/encrypt.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwe/compact/encrypt.js new file mode 100644 index 000000000..a500eccd6 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwe/compact/encrypt.js @@ -0,0 +1,26 @@ +import { assertNotSet, assertUint8Array } from "../../lib/validate.js"; +import { compactJWE, createJWE } from "../../lib/jwe_encrypt.js"; +class CompactEncrypt { + #input; + constructor(plaintext) { + assertUint8Array(plaintext, "plaintext"), this.#input = [plaintext]; + } + 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; + } + 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; + } + async encrypt(key, options) { + return compactJWE(await createJWE([...this.#input], key, options)); + } +} +export { + CompactEncrypt +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwe/flattened/decrypt.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwe/flattened/decrypt.js new file mode 100644 index 000000000..cba368907 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwe/flattened/decrypt.js @@ -0,0 +1,15 @@ +import { JWEInvalid } from "../../util/errors.js"; +import { isObject } from "../../lib/validate.js"; +import { prepareDecrypt, decryptJWE, checkRecipient, snapshotSharedJWE, snapshotRecipientJWE } from "../../lib/jwe_decrypt.js"; +async function flattenedDecrypt(jwe, key, options) { + if (!isObject(jwe)) + throw new JWEInvalid("Flattened JWE must be an object"); + const shared = snapshotSharedJWE(jwe), [recipient, , error] = snapshotRecipientJWE(jwe); + if (!recipient) + throw error; + const snapshot = { ...shared, ...recipient }; + return checkRecipient(snapshot), decryptJWE(snapshot, prepareDecrypt(options), key); +} +export { + flattenedDecrypt +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwe/flattened/encrypt.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwe/flattened/encrypt.js new file mode 100644 index 000000000..7ee1b91ed --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwe/flattened/encrypt.js @@ -0,0 +1,35 @@ +import { assertNotSet, assertUint8Array } from "../../lib/validate.js"; +import { createJWE } from "../../lib/jwe_encrypt.js"; +class FlattenedEncrypt { + #input; + constructor(plaintext) { + assertUint8Array(plaintext, "plaintext"), this.#input = [plaintext]; + } + setKeyManagementParameters(parameters) { + return assertNotSet(this.#input[7], "setKeyManagementParameters"), this.#input[7] = parameters, this; + } + setProtectedHeader(protectedHeader) { + return assertNotSet(this.#input[1], "setProtectedHeader"), this.#input[1] = protectedHeader, this; + } + setSharedUnprotectedHeader(sharedUnprotectedHeader) { + return assertNotSet(this.#input[3], "setSharedUnprotectedHeader"), this.#input[3] = sharedUnprotectedHeader, this; + } + setUnprotectedHeader(unprotectedHeader) { + return assertNotSet(this.#input[2], "setUnprotectedHeader"), this.#input[2] = unprotectedHeader, this; + } + setAdditionalAuthenticatedData(aad) { + return this.#input[4] = aad, 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; + } + async encrypt(key, options) { + return createJWE([...this.#input], key, options); + } +} +export { + FlattenedEncrypt +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwe/general/decrypt.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwe/general/decrypt.js new file mode 100644 index 000000000..dfdb66003 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwe/general/decrypt.js @@ -0,0 +1,40 @@ +import { prepareDecrypt, shareJWE, decryptJWE, checkRecipient, snapshotSharedJWE, snapshotRecipientJWE } from "../../lib/jwe_decrypt.js"; +import { JWEDecryptionFailed, JWEInvalid } from "../../util/errors.js"; +import { isObject } from "../../lib/validate.js"; +import { JWE, isJWECEKTransport } from "../../lib/jwe_algorithms.js"; +async function generalDecrypt(jwe, key, options) { + if (!isObject(jwe)) + throw new JWEInvalid("General JWE must be an object"); + const inputRecipients = jwe.recipients; + if (!Array.isArray(inputRecipients)) + throw new JWEInvalid("JWE Recipients missing or incorrect type"); + const recipients = Array.from(inputRecipients); + if (!recipients.every(isObject)) + throw new JWEInvalid("JWE Recipients missing or incorrect type"); + if (!recipients.length) + throw new JWEInvalid("JWE Recipients has no members"); + let shared, sharedJwe, token; + try { + shared = prepareDecrypt(options), sharedJwe = snapshotSharedJWE(jwe), token = shareJWE(sharedJwe); + } catch { + throw new JWEDecryptionFailed(); + } + const recipientSnapshots = recipients.map((recipient) => snapshotRecipientJWE(recipient)); + if (recipients.length > 1) + for (const [, headerAlg] of recipientSnapshots) { + const alg = token[0]?.alg ?? headerAlg ?? sharedJwe.unprotected?.alg, algEntry = typeof alg == "string" ? JWE[alg] : void 0; + if (algEntry && !isJWECEKTransport(algEntry)) + throw new JWEInvalid(`"${alg}" alg may only have a single recipient`); + } + for (const [recipient] of recipientSnapshots) + if (recipient) + try { + const flattened = { ...sharedJwe, ...recipient }; + return checkRecipient(flattened), await decryptJWE(flattened, shared, key, token); + } catch { + } + throw new JWEDecryptionFailed(); +} +export { + generalDecrypt +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwe/general/encrypt.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwe/general/encrypt.js new file mode 100644 index 000000000..9fe068a8a --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwe/general/encrypt.js @@ -0,0 +1,105 @@ +import { assertNotSet, assertUint8Array } from "../../lib/validate.js"; +import { JWEInvalid } from "../../util/errors.js"; +import { generateCek } from "../../lib/content_encryption.js"; +import { encode as b64u } from "../../util/base64url.js"; +import { checkDisjoint, checkEncryptHeaders, encryptJWE } from "../../lib/jwe_encrypt.js"; +import { encryptKeyManagement } from "../../lib/key_management.js"; +import { isJWECEKTransport, jweAlgorithm } from "../../lib/jwe_algorithms.js"; +class IndividualRecipient { + #parent; + state; + constructor(enc, key, crit) { + this.#parent = enc, this.state = [void 0, void 0, key, crit]; + } + setUnprotectedHeader(unprotectedHeader) { + return assertNotSet(this.state[0], "setUnprotectedHeader"), this.state[0] = unprotectedHeader, this; + } + setKeyManagementParameters(parameters) { + return assertNotSet(this.state[1], "setKeyManagementParameters"), this.state[1] = parameters, this; + } + addRecipient(...args) { + return this.#parent.addRecipient(...args); + } + encrypt(...args) { + return this.#parent.encrypt(...args); + } + done() { + return this.#parent; + } +} +class GeneralEncrypt { + #plaintext; + #recipients = []; + #protectedHeader; + #unprotectedHeader; + #aad; + constructor(plaintext) { + this.#plaintext = plaintext; + } + addRecipient(key, options) { + const recipient = new IndividualRecipient(this, key, options?.crit); + return this.#recipients.push(recipient), recipient; + } + setProtectedHeader(protectedHeader) { + return assertNotSet(this.#protectedHeader, "setProtectedHeader"), this.#protectedHeader = protectedHeader, this; + } + setSharedUnprotectedHeader(sharedUnprotectedHeader) { + return assertNotSet(this.#unprotectedHeader, "setSharedUnprotectedHeader"), this.#unprotectedHeader = sharedUnprotectedHeader, this; + } + setAdditionalAuthenticatedData(aad) { + return this.#aad = aad, this; + } + async encrypt() { + if (!this.#recipients.length) + throw new JWEInvalid("at least one recipient must be added"); + assertUint8Array(this.#plaintext, "plaintext"); + const multiple = this.#recipients.length > 1; + let enc, protectedHeader = this.#protectedHeader, sharedUnprotectedHeader = this.#unprotectedHeader; + const recipients = []; + for (const recipient of this.#recipients) { + const [unprotectedHeader, keyManagementParameters, key, crit] = recipient.state, input = [ + this.#plaintext, + protectedHeader, + unprotectedHeader, + sharedUnprotectedHeader, + this.#aad, + void 0, + void 0, + keyManagementParameters, + crit, + multiple + ], headers = checkEncryptHeaders(input, void 0, recipients.length > 0); + recipients.length || (protectedHeader = input[1], sharedUnprotectedHeader = input[3]), recipients.push([input, headers, key]); + const [{ alg, enc: recipientEnc }, , algEntry] = headers; + if (multiple && algEntry && !isJWECEKTransport(algEntry)) + throw new JWEInvalid(`"${alg}" alg may only have a single recipient`); + if (!enc) + enc = recipientEnc; + else if (enc !== recipientEnc) + throw new JWEInvalid('JWE "enc" (Encryption Algorithm) Header Parameter must be the same for all recipients'); + } + for (const [, headers] of recipients) + headers[2] ??= jweAlgorithm(headers[0].alg); + const [firstInput, firstHeaders, firstKey] = recipients[0], cek = multiple ? generateCek(firstHeaders[1]) : void 0; + firstInput[5] = cek; + const { encrypted_key, header, ...shared } = await encryptJWE(firstInput, firstHeaders, firstKey), jwe = { + ...shared, + recipients: [{}] + }; + encrypted_key && (jwe.recipients[0].encrypted_key = encrypted_key), header && (jwe.recipients[0].header = header); + for (let i = 1; i < recipients.length; i++) { + const [input, [joseHeader, encEntry, algEntry], key] = recipients[i], unprotectedHeader = input[2], [, encryptedKey, parameters] = await encryptKeyManagement(algEntry, encEntry, key, joseHeader, cek, input[7]), target = { + encrypted_key: b64u(encryptedKey) + }; + if (unprotectedHeader || parameters) { + const header2 = { ...unprotectedHeader, ...parameters }; + parameters && checkDisjoint(input[1], header2, input[3]), target.header = header2; + } + jwe.recipients.push(target); + } + return jwe; + } +} +export { + GeneralEncrypt +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwk/embedded.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwk/embedded.js new file mode 100644 index 000000000..1e1dd89c4 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwk/embedded.js @@ -0,0 +1,30 @@ +import { jwkToKey, normalizeJwk } from "../lib/key.js"; +import { jwsAlgorithm } from "../lib/jws_algorithms.js"; +import { isObject } from "../lib/validate.js"; +import { JWSInvalid } from "../util/errors.js"; +async function EmbeddedJWK(protectedHeader, token) { + const joseHeader = { + ...protectedHeader, + ...token?.header + }; + if (!isObject(joseHeader.jwk)) + throw new JWSInvalid('"jwk" (JSON Web Key) Header Parameter must be a JSON object'); + let jwk; + try { + jwk = normalizeJwk(joseHeader.jwk); + } catch (cause) { + throw new JWSInvalid("Invalid Embedded JWK", { cause }); + } + const entry = jwsAlgorithm(joseHeader.alg); + if (jwk.use !== void 0 && jwk.use !== "sig") + throw new JWSInvalid('Invalid Embedded JWK, its "use" must be "sig" when present'); + if (jwk.alg !== void 0 && jwk.alg !== entry.alg) + throw new JWSInvalid(`Invalid Embedded JWK, its "alg" must be "${entry.alg}" when present`); + const key = await jwkToKey(entry, jwk, !0); + if (key.type !== "public") + throw new JWSInvalid('"jwk" (JSON Web Key) Header Parameter must be a public key'); + return key; +} +export { + EmbeddedJWK +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwk/thumbprint.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwk/thumbprint.js new file mode 100644 index 000000000..ec93e222e --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwk/thumbprint.js @@ -0,0 +1,55 @@ +import { digest, encode } from "../lib/buffer_utils.js"; +import { encode as b64u } from "../util/base64url.js"; +import { JOSENotSupported, JWKInvalid } from "../util/errors.js"; +import { isKeyLike, invalidKeyInput, snapshotJwk } from "../lib/key.js"; +import { isObject } from "../lib/validate.js"; +import { exportJWK } from "../key/export.js"; +const check = (value, description) => { + if (typeof value != "string" || !value) + throw new JWKInvalid(`${description} missing or invalid`); +}; +async function calculateJwkThumbprint(key, digestAlgorithm) { + let jwk; + if (isObject(key)) { + if (jwk = snapshotJwk(key), typeof jwk.kty != "string") + throw new TypeError(invalidKeyInput(key, "CryptoKey", "KeyObject", "JSON Web Key")); + } else if (isKeyLike(key)) + jwk = snapshotJwk(await exportJWK(key)); + else + throw new TypeError(invalidKeyInput(key, "CryptoKey", "KeyObject", "JSON Web Key")); + if (digestAlgorithm ??= "sha256", digestAlgorithm !== "sha256" && digestAlgorithm !== "sha384" && digestAlgorithm !== "sha512") + throw new TypeError('digestAlgorithm must one of "sha256", "sha384", or "sha512"'); + let components; + switch (jwk.kty) { + case "AKP": + check(jwk.alg, '"alg" (Algorithm) Parameter'), check(jwk.pub, '"pub" (Public key) Parameter'), components = { alg: jwk.alg, kty: jwk.kty, pub: jwk.pub }; + break; + case "EC": + check(jwk.crv, '"crv" (Curve) Parameter'), check(jwk.x, '"x" (X Coordinate) Parameter'), check(jwk.y, '"y" (Y Coordinate) Parameter'), components = { crv: jwk.crv, kty: jwk.kty, x: jwk.x, y: jwk.y }; + break; + case "OKP": + check(jwk.crv, '"crv" (Subtype of Key Pair) Parameter'), check(jwk.x, '"x" (Public Key) Parameter'), components = { crv: jwk.crv, kty: jwk.kty, x: jwk.x }; + break; + case "RSA": + check(jwk.e, '"e" (Exponent) Parameter'), check(jwk.n, '"n" (Modulus) Parameter'), components = { e: jwk.e, kty: jwk.kty, n: jwk.n }; + break; + case "oct": + if (typeof jwk.k != "string") + throw new JWKInvalid('"k" (Key Value) Parameter missing or invalid'); + components = { k: jwk.k, kty: jwk.kty }; + break; + default: + throw new JOSENotSupported('"kty" (Key Type) Parameter missing or unsupported'); + } + const data = encode(JSON.stringify(components)); + return b64u(await digest(digestAlgorithm, data)); +} +async function calculateJwkThumbprintUri(key, digestAlgorithm) { + digestAlgorithm ??= "sha256"; + const thumbprint = await calculateJwkThumbprint(key, digestAlgorithm); + return `urn:ietf:params:oauth:jwk-thumbprint:sha-${digestAlgorithm.slice(-3)}:${thumbprint}`; +} +export { + calculateJwkThumbprint, + calculateJwkThumbprintUri +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwks/local.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwks/local.js new file mode 100644 index 000000000..448757265 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwks/local.js @@ -0,0 +1,59 @@ +import { jwkToKey, snapshotJwk } from "../lib/key.js"; +import { JWS } from "../lib/jws_algorithms.js"; +import { JWKSInvalid, JOSENotSupported, JWKSNoMatchingKey, JWKSMultipleMatchingKeys } from "../util/errors.js"; +import { isJwkSet } from "../lib/validate.js"; +function isUsableJWK(jwk, entry, alg, kid) { + const { kty, key_ops: keyOps, ext, kid: jwkKid, alg: jwkAlg, use, crv } = jwk; + return (ext === void 0 || typeof ext == "boolean") && (keyOps === void 0 || Array.isArray(keyOps) && keyOps.every((operation, index) => typeof operation == "string" && keyOps.indexOf(operation) === index) && keyOps.includes("verify")) && entry.kty.includes(kty) && (kid === void 0 || typeof kid == "string" && kid === jwkKid) && (jwkAlg === void 0 ? kty !== "AKP" : alg === jwkAlg) && (use === void 0 || use === "sig") && (!entry.crv || crv === entry.crv); +} +async function importWithAlgCache(cache, jwk, entry) { + const cached = cache.get(jwk) || cache.set(jwk, {}).get(jwk), { alg } = entry; + if (cached[alg] === void 0) { + const pending = jwkToKey(entry, jwk, !0).then((key) => { + if (key.type !== "public") + throw new JWKSInvalid("JSON Web Key Set members must be public keys"); + return cached[alg] = key, key; + }).catch((error) => { + throw cached[alg] === pending && delete cached[alg], error; + }); + cached[alg] = pending; + } + return cached[alg]; +} +function createLocalJWKSet(jwks) { + let snapshot; + try { + snapshot = structuredClone(jwks); + } catch { + } + if (!isJwkSet(snapshot)) + throw new JWKSInvalid("JSON Web Key Set malformed"); + const metadata = snapshot.keys.map((jwk) => { + const normalized = snapshotJwk(jwk); + return Array.isArray(normalized.key_ops) && (normalized.key_ops = [...normalized.key_ops]), normalized; + }), cached = /* @__PURE__ */ new WeakMap(); + return Object.defineProperty(async (protectedHeader, token) => { + const { alg, kid } = { ...protectedHeader, ...token?.header }, entry = typeof alg == "string" ? JWS[alg] : void 0; + if (!entry || entry.secret) + throw new JOSENotSupported('Unsupported "alg" value for a JSON Web Key Set'); + const candidates = snapshot.keys.filter((_, index) => isUsableJWK(metadata[index], entry, alg, kid)), { 0: jwk, length } = candidates; + if (!length) + throw new JWKSNoMatchingKey(); + if (length !== 1) { + const error = new JWKSMultipleMatchingKeys(); + throw error[Symbol.asyncIterator] = async function* () { + for (const jwk2 of candidates) + try { + yield await importWithAlgCache(cached, jwk2, entry); + } catch { + } + }, error; + } + return importWithAlgCache(cached, jwk, entry); + }, "jwks", { + value: () => structuredClone(snapshot) + }); +} +export { + createLocalJWKSet +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwks/remote.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwks/remote.js new file mode 100644 index 000000000..39519f27b --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwks/remote.js @@ -0,0 +1,101 @@ +import { JOSEError, JWKSNoMatchingKey, JWKSTimeout } from "../util/errors.js"; +import { createLocalJWKSet } from "./local.js"; +import { isJwkSet } from "../lib/validate.js"; +function isCloudflareWorkers() { + return typeof WebSocketPair < "u" || typeof navigator < "u" && navigator.userAgent === "Cloudflare-Workers" || typeof EdgeRuntime < "u" && EdgeRuntime === "vercel"; +} +let USER_AGENT; +(typeof navigator > "u" || !navigator.userAgent?.startsWith?.("Mozilla/5.0 ")) && (USER_AGENT = "jose/v6.2.12"); +const customFetch = /* @__PURE__ */ Symbol(); +async function fetchJwks(url, headers, signal, fetchImpl = fetch) { + const response = await fetchImpl(url, { + method: "GET", + signal, + redirect: "manual", + headers + }).catch((err) => { + throw err.name === "TimeoutError" ? new JWKSTimeout() : err; + }); + if (response.status !== 200) + throw new JOSEError("Expected 200 OK from the JSON Web Key Set HTTP response"); + try { + return await response.json(); + } catch { + throw new JOSEError("Failed to parse the JSON Web Key Set HTTP response as JSON"); + } +} +const jwksCache = /* @__PURE__ */ Symbol(); +function isFreshFor(timestamp, duration) { + return Number.isFinite(timestamp) && Date.now() < timestamp + duration; +} +function validateDuration(value, fallback, option) { + if (Number.isNaN(value)) + throw new TypeError(`"${option}" option must not be NaN`); + return typeof value == "number" ? value : fallback; +} +function createRemoteJWKSet(url, options) { + if (!(url instanceof URL)) + throw new TypeError("url must be an instance of URL"); + const href = new URL(url.href).href, opts = options ?? {}, timeoutOption = opts.timeoutDuration; + if (typeof timeoutOption == "number" && (!Number.isInteger(timeoutOption) || timeoutOption < 0)) + throw new TypeError('"timeoutDuration" option must be a non-negative integer'); + const timeoutDuration = typeof timeoutOption == "number" ? timeoutOption : 5e3, cooldownDuration = validateDuration(opts.cooldownDuration, 3e4, "cooldownDuration"), cacheMaxAge = validateDuration(opts.cacheMaxAge, 6e5, "cacheMaxAge"), headers = new Headers(opts.headers); + USER_AGENT && !headers.has("User-Agent") && headers.set("User-Agent", USER_AGENT), headers.has("accept") || headers.set("accept", "application/json, application/jwk-set+json"); + const fetchImpl = opts[customFetch], cache = opts[jwksCache]; + let jwksTimestamp, pendingFetch, reloadSequence = 0, appliedSequence = 0, local; + if (cache && typeof cache == "object") { + const { uat, jwks } = cache; + isFreshFor(uat, cacheMaxAge) && isJwkSet(jwks) && (jwksTimestamp = uat, local = createLocalJWKSet(jwks)); + } + const reload = async () => { + if (pendingFetch && isCloudflareWorkers() && (pendingFetch = void 0), !pendingFetch) { + const sequence = ++reloadSequence, current = pendingFetch = fetchJwks(href, headers, AbortSignal.timeout(timeoutDuration), fetchImpl).then((json) => { + const next = createLocalJWKSet(json); + if (sequence <= appliedSequence) + return; + local = next; + const updatedAt = Date.now(); + cache && (cache.uat = updatedAt, cache.jwks = json), jwksTimestamp = updatedAt, appliedSequence = sequence; + }).finally(() => { + pendingFetch === current && (pendingFetch = void 0); + }); + } + await pendingFetch; + }; + return Object.defineProperties(async (protectedHeader, token) => { + (!local || !isFreshFor(jwksTimestamp, cacheMaxAge)) && await reload(); + try { + return await local(protectedHeader, token); + } catch (err) { + if (err instanceof JWKSNoMatchingKey && !isFreshFor(jwksTimestamp, cooldownDuration)) + return await reload(), local(protectedHeader, token); + throw err; + } + }, { + coolingDown: { + get: () => isFreshFor(jwksTimestamp, cooldownDuration), + enumerable: !0 + }, + fresh: { + get: () => isFreshFor(jwksTimestamp, cacheMaxAge), + enumerable: !0 + }, + reload: { + value: reload, + enumerable: !0 + }, + reloading: { + get: () => !!pendingFetch, + enumerable: !0 + }, + jwks: { + value: () => local?.jwks(), + enumerable: !0 + } + }); +} +export { + createRemoteJWKSet, + customFetch, + jwksCache +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jws/compact/sign.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jws/compact/sign.js new file mode 100644 index 000000000..f3288d2f0 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jws/compact/sign.js @@ -0,0 +1,20 @@ +import { createCompactSignature } from "../../lib/jws_sign.js"; +import { assertNotSet, assertUint8Array } from "../../lib/validate.js"; +class CompactSign { + #payload; + #protectedHeader; + constructor(payload) { + assertUint8Array(payload, "payload"), this.#payload = payload; + } + setProtectedHeader(protectedHeader) { + return assertNotSet(this.#protectedHeader, "setProtectedHeader"), this.#protectedHeader = protectedHeader, this; + } + async sign(key, options) { + return createCompactSignature(this.#payload, this.#protectedHeader, options?.crit, key, () => { + throw new TypeError("use the flattened module for creating JWS with b64: false"); + }); + } +} +export { + CompactSign +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jws/compact/verify.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jws/compact/verify.js new file mode 100644 index 000000000..49625ee85 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jws/compact/verify.js @@ -0,0 +1,8 @@ +import { prepareVerify, verifyCompact } from "../../lib/jws_verify.js"; +async function compactVerify(jws, key, options) { + const [result] = await verifyCompact(jws, prepareVerify(options), key); + return result; +} +export { + compactVerify +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jws/flattened/sign.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jws/flattened/sign.js new file mode 100644 index 000000000..7e321402f --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jws/flattened/sign.js @@ -0,0 +1,23 @@ +import { createSignature } from "../../lib/jws_sign.js"; +import { assertNotSet, assertUint8Array } from "../../lib/validate.js"; +class FlattenedSign { + #input; + constructor(payload) { + assertUint8Array(payload, "payload"), this.#input = [payload]; + } + setProtectedHeader(protectedHeader) { + return assertNotSet(this.#input[1], "setProtectedHeader"), this.#input[1] = protectedHeader, this; + } + setUnprotectedHeader(unprotectedHeader) { + return assertNotSet(this.#input[2], "setUnprotectedHeader"), this.#input[2] = unprotectedHeader, this; + } + async sign(key, options) { + const input = [...this.#input]; + input[3] = options?.crit; + const [jws] = await createSignature(input, key); + return jws; + } +} +export { + FlattenedSign +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jws/flattened/verify.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jws/flattened/verify.js new file mode 100644 index 000000000..f4a7c9541 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jws/flattened/verify.js @@ -0,0 +1,12 @@ +import { JWSInvalid } from "../../util/errors.js"; +import { isObject } from "../../lib/validate.js"; +import { encodeJsonUnencodedPayload, prepareVerify, snapshotJws, verifySignature } from "../../lib/jws_verify.js"; +async function flattenedVerify(jws, key, options) { + if (!isObject(jws)) + throw new JWSInvalid("Flattened JWS must be an object"); + const snapshot = snapshotJws(jws), [result] = await verifySignature(snapshot, prepareVerify(options), key, encodeJsonUnencodedPayload); + return result; +} +export { + flattenedVerify +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jws/general/sign.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jws/general/sign.js new file mode 100644 index 000000000..8f76dd00b --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jws/general/sign.js @@ -0,0 +1,58 @@ +import { createSignature } from "../../lib/jws_sign.js"; +import { JWSInvalid } from "../../util/errors.js"; +import { assertNotSet, assertUint8Array } from "../../lib/validate.js"; +class IndividualSignature { + #parent; + state; + constructor(sig, key, options) { + this.#parent = sig, this.state = [void 0, void 0, key, options?.crit]; + } + setProtectedHeader(protectedHeader) { + return assertNotSet(this.state[0], "setProtectedHeader"), this.state[0] = protectedHeader, this; + } + setUnprotectedHeader(unprotectedHeader) { + return assertNotSet(this.state[1], "setUnprotectedHeader"), this.state[1] = unprotectedHeader, this; + } + addSignature(...args) { + return this.#parent.addSignature(...args); + } + sign(...args) { + return this.#parent.sign(...args); + } + done() { + return this.#parent; + } +} +class GeneralSign { + #payload; + #signatures = []; + constructor(payload) { + this.#payload = payload; + } + addSignature(key, options) { + const signature = new IndividualSignature(this, key, options); + return this.#signatures.push(signature), signature; + } + async sign() { + if (!this.#signatures.length) + throw new JWSInvalid("at least one signature must be added"); + assertUint8Array(this.#payload, "payload"); + const jws = { + signatures: [], + payload: "" + }, encoded = []; + let b64; + for (const signature of this.#signatures) { + const [protectedHeader, unprotectedHeader, key, crit] = signature.state, [{ payload, ...rest }, signatureB64] = await createSignature([this.#payload, protectedHeader, unprotectedHeader, crit, encoded], key); + if (b64 === void 0) + b64 = signatureB64, jws.payload = payload; + else if (b64 !== signatureB64) + throw new JWSInvalid("inconsistent use of JWS Unencoded Payload (RFC7797)"); + jws.signatures.push(rest); + } + return jws; + } +} +export { + GeneralSign +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jws/general/verify.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jws/general/verify.js new file mode 100644 index 000000000..3243408b8 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jws/general/verify.js @@ -0,0 +1,48 @@ +import { encodeJsonUnencodedPayload, parseProtectedHeader, prepareVerify, snapshotJws, verifySignature } from "../../lib/jws_verify.js"; +import { JWSInvalid, JWSSignatureVerificationFailed } from "../../util/errors.js"; +import { isObject } from "../../lib/validate.js"; +function snapshotSignature(signature, payload) { + try { + const jws = snapshotJws(signature, [payload]), protectedHeader = parseProtectedHeader(jws.protected), { b64, crit } = protectedHeader; + return [ + jws, + protectedHeader, + Array.isArray(crit) && crit.includes("b64") ? typeof b64 == "boolean" ? b64 ? 1 : 2 : 0 : 1 + ]; + } catch { + return; + } +} +async function generalVerify(jws, key, options) { + if (!isObject(jws)) + throw new JWSInvalid("General JWS must be an object"); + const { signatures, payload: inputPayload } = jws; + if (!Array.isArray(signatures)) + throw new JWSInvalid("JWS Signatures missing or incorrect type"); + const signatureEntries = Array.from(signatures); + if (!signatureEntries.every(isObject)) + throw new JWSInvalid("JWS Signatures missing or incorrect type"); + let shared; + try { + if (inputPayload === void 0) + throw new Error(); + shared = prepareVerify(options); + } catch { + throw new JWSSignatureVerificationFailed(); + } + const payload = inputPayload instanceof Uint8Array ? new Uint8Array(inputPayload) : inputPayload, candidates = signatureEntries.map((signature) => snapshotSignature(signature, payload)).filter((candidate) => candidate !== void 0); + let modes = 0; + for (const [, , mode] of candidates) + if (modes |= mode, modes === 3) + throw new JWSInvalid("inconsistent use of JWS Unencoded Payload (RFC7797)"); + for (const candidate of candidates) + try { + const [result] = await verifySignature(candidate[0], shared, key, encodeJsonUnencodedPayload, candidate[1]); + return result; + } catch { + } + throw new JWSSignatureVerificationFailed(); +} +export { + generalVerify +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwt/decrypt.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwt/decrypt.js new file mode 100644 index 000000000..cfbe4730c --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/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/6331-jose-root-entry/node_modules/jose/dist/webapi/jwt/encrypt.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwt/encrypt.js new file mode 100644 index 000000000..c26908857 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/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/6331-jose-root-entry/node_modules/jose/dist/webapi/jwt/sign.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwt/sign.js new file mode 100644 index 000000000..4d5e51c99 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwt/sign.js @@ -0,0 +1,19 @@ +import { createCompactSignature } from "../lib/jws_sign.js"; +import { JWTInvalid } from "../util/errors.js"; +import { JWTClaimsBuilder, jwtData } from "../lib/jwt_claims_set.js"; +import { assertNotSet } from "../lib/validate.js"; +const SignJWT_base = JWTClaimsBuilder; +class SignJWT extends SignJWT_base { + #protectedHeader; + setProtectedHeader(protectedHeader) { + return assertNotSet(this.#protectedHeader, "setProtectedHeader"), this.#protectedHeader = protectedHeader, this; + } + async sign(key, options) { + return createCompactSignature(jwtData(this), this.#protectedHeader, options?.crit, key, () => { + throw new JWTInvalid("JWTs MUST NOT use unencoded payload"); + }); + } +} +export { + SignJWT +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwt/unsecured.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwt/unsecured.js new file mode 100644 index 000000000..0f01cc7a6 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwt/unsecured.js @@ -0,0 +1,34 @@ +import * as b64u from "../util/base64url.js"; +import { decodeBase64url, parseJoseHeader, JWS_RECOGNIZED, validateB64, validateCrit } from "../lib/validate.js"; +import { JWSInvalid, JWTInvalid } from "../util/errors.js"; +import { validateClaimsSet, JWTClaimsBuilder, jwtData } from "../lib/jwt_claims_set.js"; +const UnsecuredJWT_base = JWTClaimsBuilder; +class UnsecuredJWT extends UnsecuredJWT_base { + encode() { + const header = b64u.encode(JSON.stringify({ alg: "none" })), payload = b64u.encode(jwtData(this)); + return `${header}.${payload}.`; + } + static decode(jwt, options) { + if (typeof jwt != "string") + throw new JWTInvalid("Unsecured JWT must be a string"); + const { 0: encodedHeader, 1: encodedPayload, 2: signature, length } = jwt.split("."); + if (length !== 3 || signature !== "") + throw new JWTInvalid("Invalid Unsecured JWT"); + let header, b64; + try { + header = parseJoseHeader(encodedHeader, JWSInvalid, "JWS Protected Header is invalid"); + const extensions = validateCrit(JWSInvalid, JWS_RECOGNIZED, void 0, header, header); + b64 = validateB64(header, extensions); + } catch (cause) { + throw cause instanceof JWSInvalid ? new JWTInvalid("Invalid Unsecured JWT", { cause }) : cause; + } + if (header.alg !== "none") + throw new JWTInvalid("Invalid Unsecured JWT"); + if (!b64) + throw new JWTInvalid("JWTs MUST NOT use unencoded payload"); + return { payload: validateClaimsSet(header, decodeBase64url(encodedPayload, "payload", JWTInvalid), options), header }; + } +} +export { + UnsecuredJWT +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwt/verify.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwt/verify.js new file mode 100644 index 000000000..8e02f3a8d --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwt/verify.js @@ -0,0 +1,13 @@ +import { prepareVerify, verifyCompact } from "../lib/jws_verify.js"; +import { validateClaimsSet } from "../lib/jwt_claims_set.js"; +import { JWTInvalid } from "../util/errors.js"; +async function jwtVerify(jwt, key, options) { + const [verified, b64] = await verifyCompact(jwt, prepareVerify(options), key); + if (!b64) + throw new JWTInvalid("JWTs MUST NOT use unencoded payload"); + const payload = validateClaimsSet(verified.protectedHeader, verified.payload, options); + return { ...verified, payload }; +} +export { + jwtVerify +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/key/export.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/key/export.js new file mode 100644 index 000000000..49016c5a6 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/key/export.js @@ -0,0 +1,35 @@ +import { toSPKI as exportPublic, toPKCS8 as exportPrivate } from "../lib/asn1.js"; +import { invalidKeyInput, isCryptoKey, isKeyObject } from "../lib/key.js"; +import { encode as b64u } from "../util/base64url.js"; +function exportSPKI(key) { + return exportPublic(key); +} +function exportPKCS8(key) { + return exportPrivate(key); +} +async function exportJWK(key) { + if (isKeyObject(key)) + if (key.type === "secret") + key = key.export(); + else + return key.export({ format: "jwk" }); + if (key instanceof Uint8Array) + return { + kty: "oct", + k: b64u(key) + }; + if (!isCryptoKey(key)) + throw new TypeError(invalidKeyInput(key, "CryptoKey", "KeyObject", "Uint8Array")); + if (!key.extractable) + throw new TypeError("non-extractable CryptoKey cannot be exported as a JWK"); + const jwk = await crypto.subtle.exportKey("jwk", key); + delete jwk.ext, delete jwk.key_ops, delete jwk.use, jwk.kty !== "AKP" && delete jwk.alg; + for (const parameter of Object.keys(jwk)) + jwk[parameter] === void 0 && delete jwk[parameter]; + return jwk; +} +export { + exportJWK, + exportPKCS8, + exportSPKI +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/key/generate_key_pair.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/key/generate_key_pair.js new file mode 100644 index 000000000..c687f420d --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/key/generate_key_pair.js @@ -0,0 +1,35 @@ +import { JOSENotSupported } from "../util/errors.js"; +import { validateExtractableOption } from "../lib/key.js"; +import { keyAlgorithm, unsupportedAlg, algArgument } from "../lib/key_algorithm.js"; +function getModulusLengthOption(options) { + const modulusLength = options?.modulusLength ?? 2048; + if (typeof modulusLength != "number" || !Number.isInteger(modulusLength) || modulusLength < 2048) + throw new JOSENotSupported("Invalid or unsupported modulusLength option provided, 2048 bits or larger keys must be used"); + return modulusLength; +} +async function generateKeyPair(alg, options) { + const extractable = validateExtractableOption(options?.extractable), entry = keyAlgorithm(alg, algArgument); + entry.secret && unsupportedAlg(algArgument); + let algorithm; + if (entry.resolve) { + const crv = options?.crv ?? "P-256"; + if (!["P-256", "P-384", "P-521", "X25519"].includes(crv)) + throw new JOSENotSupported("Invalid or unsupported crv option provided, supported values are P-256, P-384, P-521, and X25519"); + algorithm = entry.resolve({ crv }); + } else { + if (entry.crv !== void 0 && options?.crv !== void 0 && options.crv !== entry.crv) + throw new JOSENotSupported(`Invalid or unsupported crv option provided, the only supported value for ${alg} is ${entry.crv}`); + algorithm = entry.kty[0] === "RSA" ? { + ...entry.subtle, + publicExponent: Uint8Array.of(1, 0, 1), + modulusLength: getModulusLengthOption(options) + } : entry.subtle; + } + return crypto.subtle.generateKey(algorithm, extractable ?? !1, [ + ...entry.usages[1], + ...entry.usages[0] + ]); +} +export { + generateKeyPair +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/key/generate_secret.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/key/generate_secret.js new file mode 100644 index 000000000..b085b4919 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/key/generate_secret.js @@ -0,0 +1,36 @@ +import { unsupportedAlg, algArgument } from "../lib/key_algorithm.js"; +import { validateExtractableOption } from "../lib/key.js"; +async function generateSecret(alg, options) { + const extractable = validateExtractableOption(options?.extractable); + let length, algorithm, keyUsages; + switch (alg) { + case "HS256": + case "HS384": + case "HS512": + length = +alg.slice(-3), algorithm = { name: "HMAC", hash: `SHA-${length}`, length }, keyUsages = ["sign", "verify"]; + break; + case "A128CBC-HS256": + case "A192CBC-HS384": + case "A256CBC-HS512": + return crypto.getRandomValues(new Uint8Array(+alg.slice(-3) >> 3)); + case "A128KW": + case "A192KW": + case "A256KW": + length = +alg.slice(1, 4), algorithm = { name: "AES-KW", length }, keyUsages = ["wrapKey", "unwrapKey"]; + break; + case "A128GCMKW": + case "A192GCMKW": + case "A256GCMKW": + case "A128GCM": + case "A192GCM": + case "A256GCM": + length = +alg.slice(1, 4), algorithm = { name: "AES-GCM", length }, keyUsages = ["encrypt", "decrypt"]; + break; + default: + unsupportedAlg(algArgument); + } + return crypto.subtle.generateKey(algorithm, extractable ?? !1, keyUsages); +} +export { + generateSecret +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/key/import.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/key/import.js new file mode 100644 index 000000000..b171ae5de --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/key/import.js @@ -0,0 +1,53 @@ +import { decode as decodeBase64URL } from "../util/base64url.js"; +import { fromSPKI, fromPKCS8, fromX509 } from "../lib/asn1.js"; +import { jwkToKey, validateExtractableOption, normalizeJwk } from "../lib/key.js"; +import { keyAlgorithm } from "../lib/key_algorithm.js"; +import { JOSENotSupported } from "../util/errors.js"; +import { isObject } from "../lib/validate.js"; +async function importSPKI(spki, alg, options) { + if (typeof spki != "string" || spki.indexOf("-----BEGIN PUBLIC KEY-----") !== 0) + throw new TypeError('"spki" must be SPKI formatted string'); + return fromSPKI(spki, alg, options); +} +async function importX509(x509, alg, options) { + if (typeof x509 != "string" || x509.indexOf("-----BEGIN CERTIFICATE-----") !== 0) + throw new TypeError('"x509" must be X.509 formatted string'); + return fromX509(x509, alg, options); +} +async function importPKCS8(pkcs8, alg, options) { + if (typeof pkcs8 != "string" || pkcs8.indexOf("-----BEGIN PRIVATE KEY-----") !== 0) + throw new TypeError('"pkcs8" must be PKCS#8 formatted string'); + return fromPKCS8(pkcs8, alg, options); +} +async function importJWK(jwk, alg, options) { + if (!isObject(jwk)) + throw new TypeError("JWK must be an object"); + const normalized = normalizeJwk(jwk), extractable = validateExtractableOption(options?.extractable), { alg: jwkAlg } = normalized; + if (alg ??= jwkAlg, normalized.kty !== "oct" && !alg) + throw new TypeError('"alg" argument is required when "jwk.alg" is not present'); + switch (normalized.kty) { + case "oct": + if (typeof normalized.k != "string") + throw new TypeError('missing "k" (Key Value) Parameter value'); + return decodeBase64URL(normalized.k); + case "AKP": { + if (typeof jwkAlg != "string" || !jwkAlg) + throw new TypeError('missing "alg" (Algorithm) Parameter value'); + if (alg !== jwkAlg) + throw new TypeError("JWK alg and alg option value mismatch"); + return jwkToKey(keyAlgorithm(alg), normalized, extractable); + } + case "RSA": + case "EC": + case "OKP": + return jwkToKey(keyAlgorithm(alg), normalized, extractable); + default: + throw new JOSENotSupported('Unsupported "kty" (Key Type) Parameter value'); + } +} +export { + importJWK, + importPKCS8, + importSPKI, + importX509 +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/asn1.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/asn1.js new file mode 100644 index 000000000..51f781b9f --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/asn1.js @@ -0,0 +1,138 @@ +import { invalidKeyInput, isCryptoKey, isKeyObject, validateExtractableOption } from "./key.js"; +import { encodeBase64, decodeBase64 } from "./buffer_utils.js"; +import { JOSENotSupported } from "../util/errors.js"; +import { keyAlgorithm, unsupportedAlg, algArgument } from "./key_algorithm.js"; +const formatPEM = (b64, descriptor) => { + const newlined = (b64.match(/.{1,64}/g) || []).join(` +`); + return `-----BEGIN ${descriptor}----- +${newlined} +-----END ${descriptor}-----`; +}, genericExport = async (keyType, keyFormat, key) => { + if (isKeyObject(key)) { + if (key.type !== keyType) + throw new TypeError(`key is not a ${keyType} key`); + return key.export({ format: "pem", type: keyFormat }); + } + if (!isCryptoKey(key)) + throw new TypeError(invalidKeyInput(key, "CryptoKey", "KeyObject")); + if (!key.extractable) + throw new TypeError("CryptoKey is not extractable"); + if (key.type !== keyType) + throw new TypeError(`key is not a ${keyType} key`); + return formatPEM(encodeBase64(new Uint8Array(await crypto.subtle.exportKey(keyFormat, key))), `${keyType.toUpperCase()} KEY`); +}, toSPKI = (key) => genericExport("public", "spki", key), toPKCS8 = (key) => genericExport("private", "pkcs8", key), bytesEqual = (a, b) => { + if (a.byteLength !== b.length) + return !1; + for (let i = 0; i < a.byteLength; i++) + if (a[i] !== b[i]) + return !1; + return !0; +}, createASN1State = (data) => ({ data, pos: 0 }), readByte = (state) => { + const byte = state.data[state.pos++]; + if (byte === void 0) + throw new Error("Unexpected end of ASN.1 input"); + return byte; +}, parseLength = (state) => { + const first = readByte(state); + if (first & 128) { + const lengthOfLen = first & 127; + let length = 0; + for (let i = 0; i < lengthOfLen; i++) + length = length << 8 | readByte(state); + return length; + } + return first; +}, skipElement = (state, count = 1) => { + for (; count-- > 0; ) { + state.pos++; + const length = parseLength(state); + state.pos += length; + } +}, expectTag = (state, expectedTag, errorMessage) => { + if (readByte(state) !== expectedTag) + throw new Error(errorMessage); +}, getSubarray = (state, length) => { + if (length < 0 || state.pos + length > state.data.length) + throw new Error("Unexpected end of ASN.1 input"); + const result = state.data.subarray(state.pos, state.pos + length); + return state.pos += length, result; +}, parseAlgorithmOID = (state) => { + expectTag(state, 6, "Expected algorithm OID"); + const oidLen = parseLength(state); + return getSubarray(state, oidLen); +}; +function parseKeyHeader(state, keyFormat) { + if (expectTag(state, 48, `Invalid ${keyFormat === "spki" ? "SPKI" : "PKCS#8"} structure`), parseLength(state), keyFormat === "pkcs8") { + expectTag(state, 2, "Expected version field"); + const length = parseLength(state); + state.pos += length; + } + expectTag(state, 48, "Expected algorithm identifier"), parseLength(state); +} +const parseECAlgorithmIdentifier = (state) => { + const algOid = parseAlgorithmOID(state); + if (bytesEqual(algOid, [43, 101, 110])) + return "X25519"; + if (!bytesEqual(algOid, [42, 134, 72, 206, 61, 2, 1])) + throw new Error("Unsupported key algorithm"); + expectTag(state, 6, "Expected curve OID"); + const curveOidLen = parseLength(state), curveOid = getSubarray(state, curveOidLen); + if (bytesEqual(curveOid, [42, 134, 72, 206, 61, 3, 1, 7])) + return "P-256"; + if (bytesEqual(curveOid, [43, 129, 4, 0, 34])) + return "P-384"; + if (bytesEqual(curveOid, [43, 129, 4, 0, 35])) + return "P-521"; + throw new Error("Unsupported named curve"); +}, genericImport = async (keyFormat, keyData, alg, options) => { + const extractable = validateExtractableOption(options?.extractable), entry = keyAlgorithm(alg, algArgument); + entry.secret && unsupportedAlg(algArgument); + const isPublic = keyFormat === "spki"; + let algorithm; + if (entry.resolve) + try { + const state = createASN1State(keyData); + parseKeyHeader(state, keyFormat), algorithm = entry.resolve({ crv: parseECAlgorithmIdentifier(state) }); + } catch { + throw new JOSENotSupported("Invalid or unsupported key format"); + } + else + algorithm = entry.subtle; + return crypto.subtle.importKey(keyFormat, keyData, algorithm, extractable ?? isPublic, entry.usages[isPublic ? 0 : 1]); +}, processPEMData = (pem, pattern) => decodeBase64(pem.replace(pattern, "")), fromPKCS8 = (pem, alg, options) => { + const keyData = processPEMData(pem, /(?:-----(?:BEGIN|END) PRIVATE KEY-----|\s)/g); + return genericImport("pkcs8", keyData, alg, options); +}, fromSPKI = (pem, alg, options) => { + const keyData = processPEMData(pem, /(?:-----(?:BEGIN|END) PUBLIC KEY-----|\s)/g); + return genericImport("spki", keyData, alg, options); +}; +function spkiFromX509(buf) { + const state = createASN1State(buf); + expectTag(state, 48, "Invalid certificate structure"); + const certificateLength = parseLength(state); + if (certificateLength < 0 || state.pos + certificateLength > state.data.length) + throw new Error("Unexpected end of ASN.1 input"); + expectTag(state, 48, "Invalid tbsCertificate structure"), parseLength(state), buf[state.pos] === 160 ? skipElement(state, 6) : skipElement(state, 5); + const spkiStart = state.pos; + expectTag(state, 48, "Invalid SPKI structure"); + const spkiContentLen = parseLength(state); + return buf.subarray(spkiStart, spkiStart + spkiContentLen + (state.pos - spkiStart)); +} +const fromX509 = (pem, alg, options) => { + let spki; + try { + const certificate = processPEMData(pem, /(?:-----(?:BEGIN|END) CERTIFICATE-----|\s)/g); + spki = spkiFromX509(certificate); + } catch (cause) { + throw new TypeError("Failed to parse the X.509 certificate", { cause }); + } + return genericImport("spki", spki, alg, options); +}; +export { + fromPKCS8, + fromSPKI, + fromX509, + toPKCS8, + toSPKI +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/buffer_utils.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/buffer_utils.js new file mode 100644 index 000000000..b3cad85a9 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/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/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/content_encryption.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/content_encryption.js new file mode 100644 index 000000000..1ea355e91 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/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/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/deflate.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/deflate.js new file mode 100644 index 000000000..27ffba278 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/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/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/jwe_algorithms.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/jwe_algorithms.js new file mode 100644 index 000000000..a87af4eb5 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/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/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/jwe_decrypt.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/jwe_decrypt.js new file mode 100644 index 000000000..e5453b9ba --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/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/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/jwe_encrypt.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/jwe_encrypt.js new file mode 100644 index 000000000..5ab33766b --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/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/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/jws_algorithms.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/jws_algorithms.js new file mode 100644 index 000000000..08e2e22ac --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/jws_algorithms.js @@ -0,0 +1,74 @@ +import { JOSENotSupported } from "../util/errors.js"; +import { table } from "./key_descriptor.js"; +const sig = [["verify"], ["sign"]]; +function hmac(bits) { + const subtle = { name: "HMAC", hash: `SHA-${bits}` }; + return { kty: ["oct"], secret: !0, subtle, signing: subtle, usages: sig }; +} +function rsa(bits, saltLength) { + const subtle = { name: saltLength ? "RSA-PSS" : "RSASSA-PKCS1-v1_5", hash: `SHA-${bits}` }; + return { + kty: ["RSA"], + subtle, + signing: saltLength ? { ...subtle, saltLength } : subtle, + usages: sig, + minRsaBits: 2048 + }; +} +function ecdsa(crv, bits) { + return { + kty: ["EC"], + crv, + subtle: { name: "ECDSA", namedCurve: crv }, + signing: { name: "ECDSA", hash: `SHA-${bits}` }, + usages: sig + }; +} +function eddsa() { + const subtle = { name: "Ed25519" }; + return { + kty: ["OKP"], + crv: "Ed25519", + subtle, + signing: subtle, + usages: sig + }; +} +function mldsa(bits) { + const subtle = { name: `ML-DSA-${bits}` }; + return { + kty: ["AKP"], + subtle, + signing: subtle, + usages: sig + }; +} +const JWS = table({ + HS256: hmac(256), + HS384: hmac(384), + HS512: hmac(512), + RS256: rsa(256), + RS384: rsa(384), + RS512: rsa(512), + PS256: rsa(256, 32), + PS384: rsa(384, 48), + PS512: rsa(512, 64), + ES256: ecdsa("P-256", 256), + ES384: ecdsa("P-384", 384), + ES512: ecdsa("P-521", 512), + EdDSA: eddsa(), + Ed25519: eddsa(), + "ML-DSA-44": mldsa(44), + "ML-DSA-65": mldsa(65), + "ML-DSA-87": mldsa(87) +}); +function jwsAlgorithm(alg) { + const entry = typeof alg == "string" ? JWS[alg] : void 0; + if (!entry) + throw new JOSENotSupported(`alg ${alg} is not supported either by JOSE or your javascript runtime`); + return entry; +} +export { + JWS, + jwsAlgorithm +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/jws_sign.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/jws_sign.js new file mode 100644 index 000000000..76e6e02dd --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/jws_sign.js @@ -0,0 +1,46 @@ +import { encode as b64u } from "../util/base64url.js"; +import { jwsAlgorithm } from "./jws_algorithms.js"; +import { isDisjoint, serializeJoseHeader, validateB64, validateCrit, validateCritDuplicates, JWS_RECOGNIZED } from "./validate.js"; +import { JWSInvalid } from "../util/errors.js"; +import { concat, encode, encoder } from "./buffer_utils.js"; +import { prepareKey, rawKey, checkModulusLength } from "./key.js"; +async function createSignature(input, key, rejectUnencoded) { + let [payload, protectedHeader, unprotectedHeader, crit] = input, protectedHeaderString = ""; + if (protectedHeader !== void 0) { + const normalized = serializeJoseHeader(JWSInvalid, protectedHeader); + protectedHeader = normalized[0], protectedHeaderString = b64u(normalized[1]); + } + if (unprotectedHeader !== void 0 && (unprotectedHeader = serializeJoseHeader(JWSInvalid, unprotectedHeader)[0]), !protectedHeader && !unprotectedHeader) + throw new JWSInvalid("either setProtectedHeader or setUnprotectedHeader must be called before #sign()"); + if (!isDisjoint(protectedHeader, unprotectedHeader)) + throw new JWSInvalid("JWS Protected and JWS Unprotected Header Parameter names must be disjoint"); + const joseHeader = { ...protectedHeader, ...unprotectedHeader }; + validateCritDuplicates(JWSInvalid, protectedHeader); + const b64 = validateB64(protectedHeader, validateCrit(JWSInvalid, JWS_RECOGNIZED, crit, protectedHeader, joseHeader)); + b64 || rejectUnencoded?.(); + const { alg } = joseHeader; + if (typeof alg != "string" || !alg) + throw new JWSInvalid('JWS "alg" (Algorithm) Header Parameter missing or invalid'); + const entry = jwsAlgorithm(alg); + let payloadS = "", payloadB = payload, data; + if (b64) { + const encoded = input[4]; + encoded ? (payloadS = encoded[0] ??= b64u(payload), payloadB = encoded[1] ??= encode(payloadS)) : (payloadS = b64u(payload), data = encoder.encode(`${protectedHeaderString}.${payloadS}`)); + } + data ??= concat(encode(protectedHeaderString), encode("."), payloadB); + const k = await rawKey(await prepareKey(entry, key, "sign"), entry.subtle, "sign"); + entry.minRsaBits && checkModulusLength(entry.alg, k); + const jws = { + signature: b64u(new Uint8Array(await crypto.subtle.sign(entry.signing, k, data))), + payload: payloadS + }; + return protectedHeader && (jws.protected = protectedHeaderString), unprotectedHeader && (jws.header = unprotectedHeader), [jws, b64]; +} +async function createCompactSignature(payload, protectedHeader, crit, key, rejectUnencoded) { + const [jws] = await createSignature([payload, protectedHeader, void 0, crit], key, rejectUnencoded); + return `${jws.protected}.${jws.payload}.${jws.signature}`; +} +export { + createCompactSignature, + createSignature +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/jws_verify.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/jws_verify.js new file mode 100644 index 000000000..7337b9c8e --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/jws_verify.js @@ -0,0 +1,86 @@ +import { jwsAlgorithm } from "./jws_algorithms.js"; +import { JOSEAlgNotAllowed, JWSInvalid, JWSSignatureVerificationFailed } from "../util/errors.js"; +import { concat, decoder, encoder, encode } from "./buffer_utils.js"; +import { decodeBase64url, encodeBase64url, parseJoseHeader, isDisjoint, isObject, validateB64, validateCrit, validateAlgorithms, JWS_RECOGNIZED } from "./validate.js"; +import { prepareKey, rawKey, checkModulusLength } from "./key.js"; +function snapshotJws(jws, sharedPayload) { + const encodedProtected = jws.protected, inputHeader = jws.header, header = isObject(inputHeader) ? { ...inputHeader } : inputHeader; + let payload = sharedPayload ? sharedPayload[0] : jws.payload; + !sharedPayload && payload instanceof Uint8Array && (payload = new Uint8Array(payload)); + const signature = jws.signature, snapshot = { payload, signature }; + if (encodedProtected !== void 0 && (snapshot.protected = encodedProtected), inputHeader !== void 0 && (snapshot.header = header), encodedProtected === void 0 && header === void 0) + throw new JWSInvalid('Flattened JWS must have either of the "protected" or "header" members'); + if (encodedProtected !== void 0 && typeof encodedProtected != "string") + throw new JWSInvalid("JWS Protected Header incorrect type"); + if (payload === void 0) + throw new JWSInvalid("JWS Payload missing"); + if (typeof signature != "string") + throw new JWSInvalid("JWS Signature missing or incorrect type"); + if (header !== void 0 && !isObject(header)) + throw new JWSInvalid("JWS Unprotected Header incorrect type"); + return snapshot; +} +function prepareVerify(options) { + return [options && validateAlgorithms("algorithms", options.algorithms), options?.crit]; +} +function parseProtectedHeader(encodedProtected) { + return encodedProtected === void 0 ? {} : parseJoseHeader(encodedProtected, JWSInvalid, "JWS Protected Header is invalid"); +} +function encodeJsonUnencodedPayload(payload) { + const invalid = /[\p{Cs}\p{Cn}]/u.exec(payload)?.[0]; + if (invalid !== void 0) + throw new JWSInvalid(/\p{Cs}/u.test(invalid) ? "JWS Payload must be a well-formed Unicode string" : "JWS Payload must not contain unassigned Unicode code points"); + return encoder.encode(payload); +} +function encodeCompactUnencodedPayload(payload) { + try { + return encode(payload); + } catch { + throw new JWSInvalid("JWS Compact Serialization payload must use only ASCII characters"); + } +} +async function verifySignature(jws, shared, key, encodeUnencodedPayload, parsedProtected) { + const { protected: encodedProtected, header, payload: inputPayload } = jws, parsedProt = parsedProtected ?? parseProtectedHeader(encodedProtected); + if (!isDisjoint(parsedProt, header)) + throw new JWSInvalid("JWS Protected and JWS Unprotected Header Parameter names must be disjoint"); + const joseHeader = { ...parsedProt, ...header }, b64 = validateB64(parsedProt, validateCrit(JWSInvalid, JWS_RECOGNIZED, shared[1], parsedProt, joseHeader)), { alg } = joseHeader; + if (typeof alg != "string" || !alg) + throw new JWSInvalid('JWS "alg" (Algorithm) Header Parameter missing or invalid'); + if (shared[0] && !shared[0].has(alg)) + throw new JOSEAlgNotAllowed('"alg" (Algorithm) Header Parameter value not allowed'); + if (b64) { + if (typeof inputPayload != "string") + throw new JWSInvalid("JWS Payload must be a string"); + } else if (typeof inputPayload != "string" && !(inputPayload instanceof Uint8Array)) + throw new JWSInvalid("JWS Payload must be a string or an Uint8Array instance"); + const signingPayload = b64 || typeof inputPayload != "string" ? inputPayload : encodeUnencodedPayload(inputPayload); + let resolvedKey = !1; + typeof key == "function" && (key = await key(parsedProt, jws), resolvedKey = !0); + const entry = jwsAlgorithm(alg), data = concat(encodedProtected !== void 0 ? encode(encodedProtected) : new Uint8Array(), encode("."), typeof signingPayload == "string" ? shared[2] ??= encodeBase64url(signingPayload, "payload", JWSInvalid) : signingPayload), signature = decodeBase64url(jws.signature, "signature", JWSInvalid), k = await prepareKey(entry, key, "verify"), cryptoKey = await rawKey(k, entry.subtle, "verify"); + entry.minRsaBits && checkModulusLength(entry.alg, cryptoKey); + let verified = !1; + try { + verified = await crypto.subtle.verify(entry.signing, cryptoKey, signature, data); + } catch { + } + if (!verified) + throw new JWSSignatureVerificationFailed(); + const result = { payload: typeof signingPayload == "string" ? decodeBase64url(signingPayload, "payload", JWSInvalid) : signingPayload }; + return encodedProtected !== void 0 && (result.protectedHeader = parsedProt), header !== void 0 && (result.unprotectedHeader = header), resolvedKey ? [{ ...result, key: k }, b64] : [result, b64]; +} +async function verifyCompact(jws, shared, key) { + if (jws instanceof Uint8Array && (jws = decoder.decode(jws)), typeof jws != "string") + throw new JWSInvalid("Compact JWS must be a string or Uint8Array"); + const { 0: protectedHeader, 1: payload, 2: signature, length } = jws.split("."); + if (length !== 3) + throw new JWSInvalid("Invalid Compact JWS"); + return verifySignature({ payload, protected: protectedHeader, signature }, shared, key, encodeCompactUnencodedPayload); +} +export { + encodeJsonUnencodedPayload, + parseProtectedHeader, + prepareVerify, + snapshotJws, + verifyCompact, + verifySignature +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/jwt_claims_set.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/jwt_claims_set.js new file mode 100644 index 000000000..b99943f9c --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/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/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/key.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/key.js new file mode 100644 index 000000000..7dc5602f1 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/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/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/key_algorithm.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/key_algorithm.js new file mode 100644 index 000000000..8a4d3fc93 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/key_algorithm.js @@ -0,0 +1,15 @@ +import { JOSENotSupported } from "../util/errors.js"; +import { JWS } from "./jws_algorithms.js"; +import { JWE } from "./jwe_algorithms.js"; +const algArgument = '"alg" (Algorithm)'; +function unsupportedAlg(source = 'JWK "alg" (Algorithm) Parameter') { + throw new JOSENotSupported(`Invalid or unsupported ${source} value`); +} +function keyAlgorithm(alg, source) { + return (typeof alg == "string" ? JWS[alg] ?? JWE[alg] : void 0) ?? unsupportedAlg(source); +} +export { + algArgument, + keyAlgorithm, + unsupportedAlg +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/key_descriptor.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/key_descriptor.js new file mode 100644 index 000000000..cf0afbd90 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/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/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/key_management.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/key_management.js new file mode 100644 index 000000000..7b7533b70 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/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/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/validate.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/validate.js new file mode 100644 index 000000000..51b7dfe89 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/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/6331-jose-root-entry/node_modules/jose/dist/webapi/util/base64url.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/util/base64url.js new file mode 100644 index 000000000..4447baa6d --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/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/6331-jose-root-entry/node_modules/jose/dist/webapi/util/decode_jwt.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/util/decode_jwt.js new file mode 100644 index 000000000..a4b277f18 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/util/decode_jwt.js @@ -0,0 +1,33 @@ +import { decode as b64u } from "./base64url.js"; +import { strictDecoder } from "../lib/buffer_utils.js"; +import { isObject } from "../lib/validate.js"; +import { JWTInvalid } from "./errors.js"; +function decodeJwt(jwt) { + if (typeof jwt != "string") + throw new JWTInvalid("JWTs must use Compact JWS serialization, JWT must be a string"); + const { 1: payload, length } = jwt.split("."); + if (length === 5) + throw new JWTInvalid("Only JWTs using Compact JWS serialization can be decoded"); + if (length !== 3) + throw new JWTInvalid("Invalid JWT"); + if (!payload) + throw new JWTInvalid("JWTs must contain a payload"); + let decoded; + try { + decoded = b64u(payload); + } catch { + throw new JWTInvalid("Failed to base64url decode the payload"); + } + let result; + try { + result = JSON.parse(strictDecoder.decode(decoded)); + } catch { + throw new JWTInvalid("Failed to parse the decoded payload as JSON"); + } + if (!isObject(result)) + throw new JWTInvalid("Invalid JWT Claims Set"); + return result; +} +export { + decodeJwt +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/util/decode_protected_header.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/util/decode_protected_header.js new file mode 100644 index 000000000..274e03617 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/util/decode_protected_header.js @@ -0,0 +1,19 @@ +import { parseJoseHeader } from "../lib/validate.js"; +function decodeProtectedHeader(token) { + let protectedB64u; + if (typeof token == "string") { + const parts = token.split("."); + (parts.length === 3 || parts.length === 5) && ([protectedB64u] = parts); + } else if (typeof token == "object" && token) + if ("protected" in token) + protectedB64u = token.protected; + else + throw new TypeError("Token does not contain a Protected Header"); + const invalid = "Invalid Token or Protected Header formatting"; + if (typeof protectedB64u != "string" || !protectedB64u) + throw new TypeError(invalid); + return parseJoseHeader(protectedB64u, TypeError, invalid); +} +export { + decodeProtectedHeader +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/util/errors.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/util/errors.js new file mode 100644 index 000000000..b7d579f51 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/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/6331-jose-root-entry/node_modules/jose/package.json b/tests/corpus/6331-jose-root-entry/node_modules/jose/package.json new file mode 100644 index 000000000..2d7d92f72 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/package.json @@ -0,0 +1,134 @@ +{ + "name": "jose", + "version": "6.2.12", + "description": "JWA, JWS, JWE, JWT, JWK, JWKS for Node.js, Browser, Cloudflare Workers, Deno, Bun, and other Web-interoperable runtimes", + "keywords": [ + "akp", + "browser", + "bun", + "cloudflare", + "compact", + "decode", + "decrypt", + "deno", + "detached", + "ec", + "ecdsa", + "ed25519", + "eddsa", + "edge", + "electron", + "embedded", + "encrypt", + "flattened", + "general", + "jose", + "json web token", + "jsonwebtoken", + "jwa", + "jwe", + "jwk", + "jwks", + "jws", + "jwt-decode", + "jwt", + "ml-dsa", + "netlify", + "next", + "nextjs", + "oct", + "okp", + "payload", + "pem", + "pkcs8", + "rsa", + "sign", + "signature", + "spki", + "validate", + "vercel", + "verify", + "webcrypto", + "workerd", + "workers", + "x509" + ], + "homepage": "https://github.com/panva/jose", + "repository": "panva/jose", + "funding": { + "url": "https://github.com/sponsors/panva" + }, + "license": "MIT", + "author": "Filip Skokan ", + "sideEffects": false, + "type": "module", + "exports": { + ".": { + "types": "./dist/types/index.d.ts", + "default": "./dist/webapi/index.js" + }, + "./jwk/*": { + "types": "./dist/types/jwk/*.d.ts", + "default": "./dist/webapi/jwk/*.js" + }, + "./key/import": { + "types": "./dist/types/key/import.d.ts", + "default": "./dist/webapi/key/import.js" + }, + "./key/export": { + "types": "./dist/types/key/export.d.ts", + "default": "./dist/webapi/key/export.js" + }, + "./key/generate/keypair": { + "types": "./dist/types/key/generate_key_pair.d.ts", + "default": "./dist/webapi/key/generate_key_pair.js" + }, + "./key/generate/secret": { + "types": "./dist/types/key/generate_secret.d.ts", + "default": "./dist/webapi/key/generate_secret.js" + }, + "./jwks/*": { + "types": "./dist/types/jwks/*.d.ts", + "default": "./dist/webapi/jwks/*.js" + }, + "./jwt/decode": { + "types": "./dist/types/util/decode_jwt.d.ts", + "default": "./dist/webapi/util/decode_jwt.js" + }, + "./jwt/*": { + "types": "./dist/types/jwt/*.d.ts", + "default": "./dist/webapi/jwt/*.js" + }, + "./decode/protected_header": { + "types": "./dist/types/util/decode_protected_header.d.ts", + "default": "./dist/webapi/util/decode_protected_header.js" + }, + "./jws/*": { + "types": "./dist/types/jws/*.d.ts", + "default": "./dist/webapi/jws/*.js" + }, + "./jwe/*": { + "types": "./dist/types/jwe/*.d.ts", + "default": "./dist/webapi/jwe/*.js" + }, + "./errors": { + "types": "./dist/types/util/errors.d.ts", + "default": "./dist/webapi/util/errors.js" + }, + "./base64url": { + "types": "./dist/types/util/base64url.d.ts", + "default": "./dist/webapi/util/base64url.js" + }, + "./package.json": "./package.json" + }, + "main": "./dist/webapi/index.js", + "types": "./dist/types/index.d.ts", + "files": [ + "dist/webapi/**/*.js", + "dist/types/**/*.d.ts", + "!dist/**/*.bundle.js", + "!dist/**/*.umd.js", + "!dist/**/*.min.js", + "!dist/types/lib/**" + ] +} diff --git a/tests/corpus/6332-fluent-this-declaration/main.ts b/tests/corpus/6332-fluent-this-declaration/main.ts new file mode 100644 index 000000000..10804083f --- /dev/null +++ b/tests/corpus/6332-fluent-this-declaration/main.ts @@ -0,0 +1,17 @@ +// A declaration file types a JavaScript class's inherited setters as +// returning `this` (jose's SignJWT over JWTClaimsBuilder): chained calls +// on the subclass keep the subclass, so its own methods stay callable. +import { Counter, Signer } from "fluent"; + +console.log(new Signer({ role: "admin" }).setHeader("HS256").setSubject("alice").setIssuer("me").sign()); +console.log(new Signer().setSubject("bob").setHeader("HS384").sign()); +const signer = new Signer({ n: 1 }).setIssuer("you").setSubject("carol"); +console.log(signer.setHeader("HS512").sign()); +const counter = new Counter({ a: 1 }).setSubject("dave").setIssuer("eve"); +console.log(counter.count(), counter.setHeader("x").sign(), counter instanceof Counter); +function described(s: Signer): string { + return s.setSubject("frank").sign(); +} +console.log(described(new Counter().setHeader("y"))); +const reset = new Counter({ b: 2 }).reset(); +console.log(reset.count(), reset.setSubject("gina").count()); diff --git a/tests/corpus/6332-fluent-this-declaration/node_modules/fluent/index.d.ts b/tests/corpus/6332-fluent-this-declaration/node_modules/fluent/index.d.ts new file mode 100644 index 000000000..486f72652 --- /dev/null +++ b/tests/corpus/6332-fluent-this-declaration/node_modules/fluent/index.d.ts @@ -0,0 +1,14 @@ +export interface Claims { + setSubject(subject: string): this; + setIssuer(issuer: string): this; + reset(): this; +} +declare const Signer_base: new (payload?: Record) => Claims; +export declare class Signer extends Signer_base { + #private; + setHeader(header: string): this; + sign(): string; +} +export declare class Counter extends Signer { + count(): number; +} diff --git a/tests/corpus/6332-fluent-this-declaration/node_modules/fluent/index.js b/tests/corpus/6332-fluent-this-declaration/node_modules/fluent/index.js new file mode 100644 index 000000000..9334722a9 --- /dev/null +++ b/tests/corpus/6332-fluent-this-declaration/node_modules/fluent/index.js @@ -0,0 +1,31 @@ +const claims = new WeakMap(); +class ClaimsBuilder { + constructor(payload = {}) { + claims.set(this, structuredClone(payload)); + } + setSubject(subject) { + return claims.get(this).sub = subject, this; + } + setIssuer(issuer) { + return claims.get(this).iss = issuer, this; + } + reset() { + return claims.set(this, {}), this; + } +} +const Signer_base = ClaimsBuilder; +class Signer extends Signer_base { + #header = "none"; + setHeader(header) { + return this.#header = header, this; + } + sign() { + return `${this.#header}.${JSON.stringify(claims.get(this))}`; + } +} +class Counter extends Signer { + count() { + return Object.keys(claims.get(this)).length; + } +} +export { Signer, Counter }; diff --git a/tests/corpus/6332-fluent-this-declaration/node_modules/fluent/package.json b/tests/corpus/6332-fluent-this-declaration/node_modules/fluent/package.json new file mode 100644 index 000000000..428e91d97 --- /dev/null +++ b/tests/corpus/6332-fluent-this-declaration/node_modules/fluent/package.json @@ -0,0 +1,11 @@ +{ + "name": "fluent", + "version": "1.0.0", + "type": "module", + "exports": { + ".": { + "types": "./index.d.ts", + "default": "./index.js" + } + } +} diff --git a/tests/corpus/6333-fluent-this-declaration-js/main.mjs b/tests/corpus/6333-fluent-this-declaration-js/main.mjs new file mode 100644 index 000000000..44ac698cc --- /dev/null +++ b/tests/corpus/6333-fluent-this-declaration-js/main.mjs @@ -0,0 +1,17 @@ +// A declaration file types a JavaScript class's inherited setters as +// returning `this` (jose's SignJWT over JWTClaimsBuilder): chained calls +// on the subclass keep the subclass, so its own methods stay callable. +import { Counter, Signer } from "fluent"; + +console.log(new Signer({ role: "admin" }).setHeader("HS256").setSubject("alice").setIssuer("me").sign()); +console.log(new Signer().setSubject("bob").setHeader("HS384").sign()); +const signer = new Signer({ n: 1 }).setIssuer("you").setSubject("carol"); +console.log(signer.setHeader("HS512").sign()); +const counter = new Counter({ a: 1 }).setSubject("dave").setIssuer("eve"); +console.log(counter.count(), counter.setHeader("x").sign(), counter instanceof Counter); +function described(s) { + return s.setSubject("frank").sign(); +} +console.log(described(new Counter().setHeader("y"))); +const reset = new Counter({ b: 2 }).reset(); +console.log(reset.count(), reset.setSubject("gina").count()); diff --git a/tests/corpus/6333-fluent-this-declaration-js/node_modules/fluent/index.d.ts b/tests/corpus/6333-fluent-this-declaration-js/node_modules/fluent/index.d.ts new file mode 100644 index 000000000..486f72652 --- /dev/null +++ b/tests/corpus/6333-fluent-this-declaration-js/node_modules/fluent/index.d.ts @@ -0,0 +1,14 @@ +export interface Claims { + setSubject(subject: string): this; + setIssuer(issuer: string): this; + reset(): this; +} +declare const Signer_base: new (payload?: Record) => Claims; +export declare class Signer extends Signer_base { + #private; + setHeader(header: string): this; + sign(): string; +} +export declare class Counter extends Signer { + count(): number; +} diff --git a/tests/corpus/6333-fluent-this-declaration-js/node_modules/fluent/index.js b/tests/corpus/6333-fluent-this-declaration-js/node_modules/fluent/index.js new file mode 100644 index 000000000..9334722a9 --- /dev/null +++ b/tests/corpus/6333-fluent-this-declaration-js/node_modules/fluent/index.js @@ -0,0 +1,31 @@ +const claims = new WeakMap(); +class ClaimsBuilder { + constructor(payload = {}) { + claims.set(this, structuredClone(payload)); + } + setSubject(subject) { + return claims.get(this).sub = subject, this; + } + setIssuer(issuer) { + return claims.get(this).iss = issuer, this; + } + reset() { + return claims.set(this, {}), this; + } +} +const Signer_base = ClaimsBuilder; +class Signer extends Signer_base { + #header = "none"; + setHeader(header) { + return this.#header = header, this; + } + sign() { + return `${this.#header}.${JSON.stringify(claims.get(this))}`; + } +} +class Counter extends Signer { + count() { + return Object.keys(claims.get(this)).length; + } +} +export { Signer, Counter }; diff --git a/tests/corpus/6333-fluent-this-declaration-js/node_modules/fluent/package.json b/tests/corpus/6333-fluent-this-declaration-js/node_modules/fluent/package.json new file mode 100644 index 000000000..428e91d97 --- /dev/null +++ b/tests/corpus/6333-fluent-this-declaration-js/node_modules/fluent/package.json @@ -0,0 +1,11 @@ +{ + "name": "fluent", + "version": "1.0.0", + "type": "module", + "exports": { + ".": { + "types": "./index.d.ts", + "default": "./index.js" + } + } +} diff --git a/tests/corpus/6334-discarded-logical-statements.mjs b/tests/corpus/6334-discarded-logical-statements.mjs new file mode 100644 index 000000000..2e1df91a8 --- /dev/null +++ b/tests/corpus/6334-discarded-logical-statements.mjs @@ -0,0 +1,42 @@ +// Minified `a && b;` / `a || b;` statements whose operands share no value +// representation lower as the control flow they are (jose's secs()), and +// element reads on a RegExpExecArray | null the code ruled non-null +// without narrowing answer undefined for groups that did not participate. +const multipliers = { s: 1, m: 60, h: 3600, d: 86400 }; +const REGEX = /^(\+|\-)? ?(\d+|\d+\.\d+) ?(seconds?|s|minutes?|m|hours?|h|days?|d)(?: (ago|from now))?$/i; +function invalid() { + throw new TypeError("Invalid time period format"); +} +function secs(str) { + typeof str != "string" && invalid(); + const matched = REGEX.exec(str); + (!matched || matched[4] && matched[1]) && invalid(); + const value = parseFloat(matched[2]); + const n = Math.round(value * multipliers[matched[3][0].toLowerCase()]); + return Number.isFinite(n) || invalid(), matched[1] === "-" || matched[4] === "ago" ? -n : n; +} +for (const input of ["10 minutes", "2h", "-1d", "+3 s", "1.5 hours ago", "4 days from now", "-2h ago", "bogus", "", 7]) { + try { + console.log(JSON.stringify(input), secs(input)); + } catch (e) { + console.log(JSON.stringify(input), e.name, e.message); + } +} + +const log = []; +function note(v) { + log.push(v); + return v; +} +const values = [0, 1, "", "x", undefined, null, true, false]; +for (const v of values) { + (note(v) || typeof v === "number") && note("and"); + (v && typeof v === "string") || note("or"); + !v || (v === true && note("bool")) || note("rest"); +} +console.log(JSON.stringify(log)); + +const m = /(a)?(b)(c)?/.exec("b"); +console.log(m[1], m[2], m[3], m[7], m[1] === undefined, m.length); +const none = /(z)/.exec("b"); +console.log(none?.[1], none === null); diff --git a/tests/corpus/6335-discarded-logical-statements.ts b/tests/corpus/6335-discarded-logical-statements.ts new file mode 100644 index 000000000..84b89f7b1 --- /dev/null +++ b/tests/corpus/6335-discarded-logical-statements.ts @@ -0,0 +1,27 @@ +// `a && b;` / `a || b;` statements whose operands share no value +// representation lower as control flow: only the deciding test runs. +function invalid(): never { + throw new TypeError("invalid"); +} +function check(input: string): string { + const matched = /^(\+|-)?(\d+)(s|m)( ago)?$/.exec(input); + (!matched || (matched[4] && matched[1])) && invalid(); + return matched ? `${matched[2]}${matched[3]}` : "none"; +} +for (const input of ["5s", "-5m", "5m ago", "-5m ago", "x"]) { + try { + console.log(input, check(input)); + } catch (e) { + console.log(input, (e as Error).message); + } +} +const seen: string[] = []; +function flag(s: string, v: boolean | string | undefined): boolean | string | undefined { + seen.push(s); + return v; +} +for (const v of [true, false, "", "y", undefined]) { + (flag("a", v) || flag("b", v === undefined ? "u" : false)) && seen.push("then"); + (flag("c", v) && flag("d", typeof v === "string")) || seen.push("else"); +} +console.log(seen.join(",")); diff --git a/tests/corpus/6340-prototype-reflection-optional-view-param.ts b/tests/corpus/6340-prototype-reflection-optional-view-param.ts new file mode 100644 index 000000000..58e21164d --- /dev/null +++ b/tests/corpus/6340-prototype-reflection-optional-view-param.ts @@ -0,0 +1,42 @@ +// A generic method whose optional parameter is an interface view (or an +// erased type parameter) reflects through the prototype of an instance +// that crossed into checked-dynamic code, and calls directly with the +// argument omitted. +interface Handle { + close(): string; +} +class Plain implements Handle { + close(): string { + return "closed"; + } +} +class Registry { + private current: Handle | null = null; + use(handle: Handle | null): void { + this.current = handle; + } + wrap(fn: F, handle?: Handle | null): F { + if (handle) this.current = handle; + return fn; + } + tag(value: T, label?: T): string { + return `${String(value)}:${label === undefined ? "none" : String(label)}`; + } + size(): number { + return this.current === null ? 0 : 1; + } +} + +const registry = new Registry(); +const inc = registry.wrap((x: number) => x + 1); +console.log(inc(1), registry.size()); +const dec = registry.wrap((x: number) => x - 1, new Plain()); +console.log(dec(1), registry.size()); +console.log(registry.tag(1), registry.tag("a", "b")); + +const boxed: unknown = registry; +const proto = Object.getPrototypeOf(boxed); +console.log(Object.getOwnPropertyNames(proto).join(",")); +console.log(typeof proto.wrap, typeof proto.tag, proto.size.call(registry)); +const loose: any = registry; +console.log(loose.size(), proto.tag.call(registry, 2), proto.tag.call(registry, 2, 3)); diff --git a/tests/corpus/6350-globalthis-assertion-state.ts b/tests/corpus/6350-globalthis-assertion-state.ts new file mode 100644 index 000000000..4a7f5a990 --- /dev/null +++ b/tests/corpus/6350-globalthis-assertion-state.ts @@ -0,0 +1,50 @@ +// Process-global state on the global object through a type assertion: +// every assertion of globalThis denotes the global object itself, so a +// write through one view is visible through every other view and through +// a view bound before the write. +const KEY = Symbol.for("example.state"); +interface State { + n: number; + log: string[]; +} +type Holder = { [KEY]?: State; label?: string }; + +const early = globalThis as Holder; + +function get(): State | undefined { + return early[KEY]; +} + +early[KEY] = { n: 1, log: [] }; +console.log(get()?.n, (globalThis as Holder)[KEY]?.n); + +function touch(entry: string): void { + const view = globalThis as Holder; + const state = view[KEY] ?? (view[KEY] = { n: 0, log: [] }); + state.n++; + state.log.push(entry); +} +touch("a"); +touch("b"); +console.log(early[KEY]?.n, early[KEY]?.log.join(","), get() === early[KEY]); + +// string-keyed members of the same object +(globalThis as Holder).label = "ready"; +console.log(early.label, (globalThis as unknown as { label?: string }).label); + +// ??= initializes once; delete removes the member for every view +const COUNTER = Symbol.for("example.counter"); +type Counters = { [COUNTER]?: { hits: number } }; +(globalThis as Counters)[COUNTER] ??= { hits: 0 }; +(globalThis as Counters)[COUNTER] ??= { hits: 100 }; +const counters = globalThis as Counters; +counters[COUNTER]!.hits += 2; +console.log((globalThis as Counters)[COUNTER]?.hits, COUNTER in globalThis); +delete counters[COUNTER]; +console.log((globalThis as Counters)[COUNTER], COUNTER in globalThis); + +// an index-signature view +const dict = globalThis as unknown as Record; +dict.exampleTotal = 1; +dict["exampleTotal"] = (dict["exampleTotal"] ?? 0) + 1; +console.log((globalThis as unknown as { exampleTotal?: number }).exampleTotal); diff --git a/tests/corpus/6352-globalthis-state-modules/boot.ts b/tests/corpus/6352-globalthis-state-modules/boot.ts new file mode 100644 index 000000000..856ff1baa --- /dev/null +++ b/tests/corpus/6352-globalthis-state-modules/boot.ts @@ -0,0 +1,14 @@ +// The boot module writes the state through its own assertion of globalThis. +const STATE = Symbol.for("example.boot-state"); +const PENDING = Symbol.for("example.boot-pending"); + +interface Early { + startedAt: number; + steps: string[]; +} + +export function boot(): void { + const view = globalThis as { [STATE]?: Early; [PENDING]?: Promise }; + view[STATE] = { startedAt: 42, steps: ["boot"] }; + view[PENDING] = Promise.resolve("container-1"); +} diff --git a/tests/corpus/6352-globalthis-state-modules/main.ts b/tests/corpus/6352-globalthis-state-modules/main.ts new file mode 100644 index 000000000..5b5aee7eb --- /dev/null +++ b/tests/corpus/6352-globalthis-state-modules/main.ts @@ -0,0 +1,18 @@ +// Boot writes the state; main reads it through a binding exported by +// another module, then takes the pending value out of the global object. +import { boot } from "./boot.ts"; +import { PENDING_KEY, STATE_KEY, globalState, readState } from "./state.ts"; + +console.log(readState() === undefined); +boot(); +const state = readState(); +console.log(state?.startedAt, state?.steps.join(",")); +state?.steps.push("main"); +console.log(globalState[STATE_KEY]?.steps.join(",")); + +async function main(): Promise { + const pending = globalState[PENDING_KEY]; + globalState[PENDING_KEY] = undefined; + console.log(await pending, globalState[PENDING_KEY]); +} +void main(); diff --git a/tests/corpus/6352-globalthis-state-modules/state.ts b/tests/corpus/6352-globalthis-state-modules/state.ts new file mode 100644 index 000000000..cef92c25f --- /dev/null +++ b/tests/corpus/6352-globalthis-state-modules/state.ts @@ -0,0 +1,18 @@ +// Shared by the boot and main modules: the state lives on the global +// object, keyed by registered symbols. +export const STATE_KEY = Symbol.for("example.boot-state"); +export const PENDING_KEY = Symbol.for("example.boot-pending"); + +export interface BootState { + startedAt: number; + steps: string[]; +} + +export const globalState = globalThis as { + [STATE_KEY]?: BootState; + [PENDING_KEY]?: Promise; +}; + +export function readState(): BootState | undefined { + return globalState[STATE_KEY]; +} diff --git a/tests/corpus/bound-constructor-inspect-names.cjs b/tests/corpus/bound-constructor-inspect-names.cjs new file mode 100644 index 000000000..52faa689c --- /dev/null +++ b/tests/corpus/bound-constructor-inspect-names.cjs @@ -0,0 +1,36 @@ +'use strict'; +// util.inspect names an object after the function in the first own +// `constructor` property on its prototype chain. Here that function is a +// bound function, whose name ("bound " and the target's name) is built at +// bind time, and the property is replaced after construction, which +// releases the first bound function. The instance keeps the name. The +// targets are anonymous, so the bound names are "bound " and "bound bound ". +const util = require('util'); + +// Array elements give the functions no name. +const [Point, Pair] = [ + function (x) { + this.x = x; + }, + function (a, b) { + this.a = a; + this.b = b; + }, +]; +Point.prototype.constructor = Point.bind(null); +const p = new Point(1); +console.log(util.inspect(p)); + +Point.prototype.constructor = Point.bind(null); +const filler = []; +for (let i = 0; i < 200; i++) filler.push('bound '.repeat(i % 5) + i); +console.log(util.inspect(p)); +console.log(p); +console.log(filler.length); + +// A bound function of a bound function, replaced the same way. +Pair.prototype.constructor = Pair.bind(null).bind(null); +const q = new Pair(1, 2); +Pair.prototype.constructor = Pair.bind(null).bind(null); +console.log(util.inspect(q)); +console.log(q); diff --git a/tests/corpus/builtin-function-record-values/main.ts b/tests/corpus/builtin-function-record-values/main.ts new file mode 100644 index 000000000..c742acc16 --- /dev/null +++ b/tests/corpus/builtin-function-record-values/main.ts @@ -0,0 +1,10 @@ +// A builtin function stored in a JavaScript record is a real function: +// TypeScript consumers call it through the record. +import native from "./native.js"; +const shape = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/; +const id = native.randomUUID(); +console.log(typeof id, id.length, shape.test(id)); +const ids = new Set(); +for (let i = 0; i < 50; i++) ids.add(native.randomUUID()); +console.log(ids.size); +console.log(shape.test(native.randomUUID({ disableEntropyCache: true }))); diff --git a/tests/corpus/builtin-function-record-values/native.js b/tests/corpus/builtin-function-record-values/native.js new file mode 100644 index 000000000..7009a2cd9 --- /dev/null +++ b/tests/corpus/builtin-function-record-values/native.js @@ -0,0 +1,3 @@ +// uuid's native.js layout: a record exporting a builtin function value. +import { randomUUID } from "crypto"; +export default { randomUUID }; diff --git a/tests/corpus/builtin-function-values.mjs b/tests/corpus/builtin-function-values.mjs new file mode 100644 index 000000000..ad0331291 --- /dev/null +++ b/tests/corpus/builtin-function-values.mjs @@ -0,0 +1,17 @@ +// Builtin functions used as JavaScript values are functions: typeof, +// identity, their name, and calls through the value. +import { randomUUID, scryptSync } from "crypto"; +const table = { randomUUID, scryptSync }; +console.log(typeof table.randomUUID, table.randomUUID === randomUUID); +console.log(/^[0-9a-f-]{36}$/.test(table.randomUUID())); +console.log(/^[0-9a-f-]{36}$/.test(table.randomUUID(undefined))); +try { + table.randomUUID(5); +} catch (e) { + console.log(e.code, e.name); +} +console.log(table.randomUUID); +console.log(typeof table.scryptSync, table.scryptSync === scryptSync); +console.log(new Set([scryptSync]).has(table.scryptSync), table.scryptSync.name); +const pick = (useNative) => (useNative && table.randomUUID ? table.randomUUID() : "fallback"); +console.log(pick(true).length, pick(false)); diff --git a/tests/corpus/constructor-returns-chained-this.js b/tests/corpus/constructor-returns-chained-this.js new file mode 100644 index 000000000..c3a2a0469 --- /dev/null +++ b/tests/corpus/constructor-returns-chained-this.js @@ -0,0 +1,40 @@ +// The JavaScript shape three.js ships: the constructor returns a chainable +// setter's result and other classes extend it. +class Color { + constructor(r, g, b) { + this.isColor = true; + this.r = 1; + this.g = 1; + this.b = 1; + return this.set(r, g, b); + } + set(r, g, b) { + if (g === undefined && b === undefined) { + if (r !== undefined) this.setScalar(r); + } else { + this.r = r; + this.g = g; + this.b = b; + } + return this; + } + setScalar(s) { + this.r = this.g = this.b = s; + return this; + } +} +class Tinted extends Color { + constructor(r, g, b, tint) { + super(r, g, b); + this.tint = tint; + } + set(r, g, b, a = 1) { + this.a = a; + return super.set(r, g, b); + } +} +const c = new Color(0.5); +console.log(c.r, c.g, c.b, c instanceof Color); +const t = new Tinted(0.1, 0.2, 0.3, 'warm'); +console.log(t.r, t.g, t.b, t.a, t.tint, t instanceof Tinted, t instanceof Color); +console.log(new Color().r); diff --git a/tests/corpus/constructor-returns-chained-this.ts b/tests/corpus/constructor-returns-chained-this.ts new file mode 100644 index 000000000..d2a9f3955 --- /dev/null +++ b/tests/corpus/constructor-returns-chained-this.ts @@ -0,0 +1,38 @@ +// A constructor that ends with `return this.set(...)`, where every +// reachable `set` returns `this`, answers the constructed instance, so +// subclasses still construct through super() (three.js's Color). +class Color { + r = 0; + g = 0; + b = 0; + constructor(r?: number, g?: number, b?: number) { + return this.set(r ?? 0, g ?? 0, b ?? 0); + } + set(r: number, g: number, b: number): this { + if (r < 0) return this.clear(); + this.r = r; + this.g = g; + this.b = b; + return this; + } + clear(): this { + this.r = this.g = this.b = 0; + return this; + } +} +class NamedColor extends Color { + name: string; + constructor(name: string, r: number, g: number, b: number) { + super(r, g, b); + this.name = name; + } + override set(r: number, g: number, b: number): this { + super.set(r * 2, g * 2, b * 2); + return this; + } +} +const plain = new Color(1, 2, 3); +console.log(plain instanceof Color, plain.r, plain.g, plain.b); +console.log(new Color(-1, 5, 5).r, new Color().b); +const named = new NamedColor("teal", 0, 1, 2); +console.log(named instanceof NamedColor, named instanceof Color, named.name, named.r, named.g, named.b); diff --git a/tests/corpus/constructor-returns-instance.js b/tests/corpus/constructor-returns-instance.js new file mode 100644 index 000000000..ab7af8b2c --- /dev/null +++ b/tests/corpus/constructor-returns-instance.js @@ -0,0 +1,45 @@ +// JavaScript constructors whose returned value is typed only at run time: +// an instance of the class replaces `this`, a primitive keeps it. +class Connection { + constructor(url) { + if (Connection.open && Connection.open.url === url) return Connection.open; + this.url = url; + this.uses = 0; + Connection.open = this; + } + use() { + this.uses++; + return this; + } +} +const c1 = new Connection("db://a").use(); +const c2 = new Connection("db://a").use(); +const c3 = new Connection("db://b").use(); +console.log(c1 === c2, c2.uses, c3 === c1, c3.uses, c3.url); + +class Counter { + constructor(start, answer) { + this.count = start; + return answer; + } +} +console.log(new Counter(1, 5).count, new Counter(2, "x").count, new Counter(3, null).count); +console.log(new Counter(4, undefined).count, new Counter(5, true).count); +const kept = new Counter(6, 0); +console.log(new Counter(7, kept) === kept, kept.count); + +const memoCache = new Map(); +let memoCreated = 0; +class Memo { + constructor(key) { + const hit = memoCache.get(key); + if (hit !== undefined) return hit; + this.key = key; + this.created = ++memoCreated; + memoCache.set(key, this); + } +} +const m1 = new Memo("k"); +const m2 = new Memo("k"); +const m3 = new Memo("j"); +console.log(m1 === m2, m3 === m1, m2.created, m3.created, memoCreated); diff --git a/tests/corpus/constructor-returns-instance.ts b/tests/corpus/constructor-returns-instance.ts new file mode 100644 index 000000000..81e25a12d --- /dev/null +++ b/tests/corpus/constructor-returns-instance.ts @@ -0,0 +1,112 @@ +// A constructor's returned object replaces the constructed instance when it +// is an object; primitive, bare and `this` returns answer the instance. +class Registry { + static current: Registry | undefined = undefined; + readonly id: number = 0; + entries: string[] = []; + constructor(id: number) { + if (Registry.current) return Registry.current; + this.id = id; + Registry.current = this; + } + add(entry: string): this { + this.entries.push(entry); + return this; + } +} +const first = new Registry(1).add("a"); +const second = new Registry(2).add("b"); +console.log(first === second, second.id, second.entries.join(",")); + +// An instance returned only on some paths. +class Slot { + static previous: Slot | undefined = undefined; + label: string; + constructor(label: string, reuse: boolean) { + this.label = label; + const previous = Slot.previous; + Slot.previous = this; + if (reuse && previous) return previous; + } +} +const s1 = new Slot("one", true); +const s2 = new Slot("two", true); +const s3 = new Slot("three", false); +console.log(s1.label, s2.label, s3.label, s2 === s1, s3 === s2); + +// Constructing the class again inside its own constructor. +class Node2 { + static made = 0; + value: number; + child: Node2 | null = null; + constructor(depth: number, share: Node2 | null) { + Node2.made++; + this.value = depth; + if (share !== null) return share; + if (depth > 0) this.child = new Node2(depth - 1, null); + } +} +const root = new Node2(2, null); +const alias = new Node2(9, root); +console.log(alias === root, alias.value, root.child?.value, root.child?.child?.value, Node2.made); + +// A constructor that throws leaves later constructions unaffected. +class Guarded { + static kept: Guarded | undefined = undefined; + n: number; + constructor(n: number) { + if (n < 0) throw new RangeError(`negative: ${n}`); + this.n = n; + if (n === 0 && Guarded.kept) return Guarded.kept; + Guarded.kept = this; + } +} +try { + new Guarded(-1); +} catch (e) { + console.log((e as Error).message); +} +const g1 = new Guarded(5); +const g2 = new Guarded(0); +const g3 = new Guarded(7); +console.log(g1.n, g2.n, g3.n, g1 === g2, g3 === g1); + +// Primitive, bare and `this` returns keep the constructed instance. +class Plain { + v = 3; + constructor(mode: number) { + if (mode === 1) return; + if (mode === 2) return this; + } +} +const plain = new Plain(2); +console.log(new Plain(0).v, new Plain(1).v, plain.v, plain instanceof Plain); + +// The derived class's own constructor may return an instance of itself. +class Animal { + sound = "..."; +} +class Dog extends Animal { + static pack: Dog[] = []; + name: string; + constructor(name: string) { + super(); + this.name = name; + this.sound = "woof"; + const known = Dog.pack.find((d) => d.name === name); + if (known) return known; + Dog.pack.push(this); + } +} +const rex = new Dog("rex"); +const again = new Dog("rex"); +const fido = new Dog("fido"); +console.log(rex === again, fido === rex, Dog.pack.length, again.sound, again instanceof Animal); + +// Constructions through class values take the returned instance too. +const Ctor: typeof Registry = Registry; +console.log(new Ctor(3) === first); +function build(make: new (id: number) => T, id: number): T { + return new make(id); +} +console.log(build(Registry, 4) === first, build(Registry, 5).entries.length); diff --git a/tests/corpus/constructor-returns-this-super.js b/tests/corpus/constructor-returns-this-super.js new file mode 100644 index 000000000..3901c5553 --- /dev/null +++ b/tests/corpus/constructor-returns-this-super.js @@ -0,0 +1,33 @@ +// A JavaScript base constructor that returns `this` through a method, as +// three.js's Color does: `super()` from a subclass keeps the subclass's own +// instance. +class Color { + constructor(r, g, b) { + this.r = 1; + this.g = 1; + this.b = 1; + return this.set(r, g, b); + } + set(r, g, b) { + if (g === undefined) { + this.r = this.g = this.b = r; + } else { + this.r = r; + this.g = g; + this.b = b; + } + return this; + } +} +class Color4 extends Color { + constructor(r, g, b, a = 1) { + super(r, g, b); + this.a = a; + } +} +const c = new Color(0.5); +const c4 = new Color4(0.1, 0.2, 0.3); +const c4b = new Color4(1, 0, 0, 0.5); +console.log(c.r, c.g, c.b, c instanceof Color); +console.log(c4.r, c4.g, c4.b, c4.a, c4 instanceof Color4, c4 instanceof Color); +console.log(c4b.a, c4b !== c4, Object.keys(c4b).join(",")); diff --git a/tests/corpus/es5-constructor-inspect-names.cjs b/tests/corpus/es5-constructor-inspect-names.cjs new file mode 100644 index 000000000..b7cc84349 --- /dev/null +++ b/tests/corpus/es5-constructor-inspect-names.cjs @@ -0,0 +1,41 @@ +// util.inspect names objects built by plain constructor functions after +// the constructor their prototype chain records, as Node does. +'use strict'; +const util = require('util'); +function Point(x, y) { + this.x = x; + this.y = y; +} +Point.prototype.norm = function () { + return Math.hypot(this.x, this.y); +}; +const p = new Point(3, 4); +console.log(p, p.norm()); +p.z = 0; +console.log(p); +function Empty() {} +console.log(new Empty(), [new Empty()], { inner: new Empty() }); +console.log(util.inspect(new Point(1, 2))); +console.log(`${util.inspect({ list: [new Point(0, 1)] })}`); +// A subclass whose prototype was replaced without a constructor reports +// the nearest constructor on the chain. +function Base(n) { + this.n = n; +} +function Derived(n) { + Base.call(this, n); + this.extra = true; +} +Derived.prototype = Object.create(Base.prototype); +console.log(new Derived(1)); +Derived.prototype.constructor = Derived; +console.log(new Derived(2)); +// A replaced prototype object without a constructor prints plainly. +function Plain() { + this.v = 1; +} +Plain.prototype = { get() { return this.v; } }; +console.log(new Plain()); +const made = Object.create(Point.prototype); +made.x = 9; +console.log(made); diff --git a/tests/corpus/exponent-unit-base.ts b/tests/corpus/exponent-unit-base.ts new file mode 100644 index 000000000..d8d4cd1ac --- /dev/null +++ b/tests/corpus/exponent-unit-base.ts @@ -0,0 +1,16 @@ +// `**` answers NaN for a base of ±1 with a NaN or infinite exponent, where +// libm's pow (and LLVM's folding of pow(1, x)) answers 1. +const values = [Infinity, -Infinity, NaN, 0, -0, 2, 0.5]; +const read = (i: number): number => values[i % values.length]!; +for (let i = 0; i < values.length; i++) { + const x = read(i); + console.log(x, 1 ** x, (-1) ** x, x ** 0, x ** 1, 2 ** x, x ** -1); +} +let base = 1; +let exponent = Infinity; +console.log(base ** exponent, (-base) ** exponent, base ** NaN); +exponent = 3; +console.log(base ** exponent, (-1) ** exponent); +let acc = 1; +acc **= Infinity; +console.log(acc, Math.pow(1, Infinity), NaN ** 0, (-0) ** -1, 0 ** -1); diff --git a/tests/corpus/key-order-late-optional.ts b/tests/corpus/key-order-late-optional.ts new file mode 100644 index 000000000..f4742acb0 --- /dev/null +++ b/tests/corpus/key-order-late-optional.ts @@ -0,0 +1,84 @@ +// An optional key that no literal creates is appended when it is first +// assigned, after every key the value was built with, and straight-line +// assignments of several such keys keep their program order, including +// assignments behind conditions. Assigning a key that already exists never +// moves it. +interface Item { + label?: string; + id: number; + done: boolean; +} +function makeItem(id: number): Item { + return { id, done: false }; +} +const item = makeItem(1); +item.label = "first"; +console.log(Object.keys(item).join(","), JSON.stringify(item)); +console.log(item); +const direct: Item = { id: 2, done: true }; +direct.label = "second"; +console.log(Object.keys(direct).join(","), JSON.stringify(direct)); +const untouched = makeItem(3); +console.log(JSON.stringify(untouched), Object.keys(untouched).join(",")); + +// Two late keys assigned in reverse declaration order. +interface Pos { + id: number; + x?: number; + y?: number; +} +function makePos(): Pos { + return { id: 0 }; +} +const pos = makePos(); +pos.y = 1; +pos.x = 2; +console.log(Object.keys(pos).join(","), JSON.stringify(pos)); +console.log(pos); +pos.id = 3; +console.log(JSON.stringify(pos)); + +// An options object filled key by key, some keys conditionally. +interface Opts { + verbose?: boolean; + depth?: number; + name?: string; +} +function build(deep: boolean, loud: boolean): Opts { + const o: Opts = {}; + o.name = "n"; + if (deep) o.depth = 2; + if (loud) o.verbose = true; + return o; +} +for (const deep of [true, false]) + for (const loud of [true, false]) console.log(JSON.stringify(build(deep, loud))); +console.log(build(true, true)); +const forIn: string[] = []; +for (const k in build(true, true)) forIn.push(k); +console.log(forIn.join(",")); + +// Late keys on records stored in arrays and Maps. +interface Task { + owner?: string; + title: string; + priority: number; +} +const tasks: Task[] = []; +for (let i = 0; i < 3; i++) tasks.push({ title: "t" + i, priority: i }); +for (const t of tasks) if (t.priority > 0) t.owner = "o" + t.priority; +console.log(JSON.stringify(tasks)); +const byTitle = new Map(); +for (const t of tasks) byTitle.set(t.title, t); +console.log(byTitle); + +// An explicit undefined is a present key at its literal position. +interface Maybe { + a?: number; + b: number; +} +function makeMaybe(): Maybe { + return { b: 1, a: undefined }; +} +console.log(Object.keys(makeMaybe()).join(","), JSON.stringify(makeMaybe())); +console.log(makeMaybe()); diff --git a/tests/corpus/key-order-literal-flows.ts b/tests/corpus/key-order-literal-flows.ts new file mode 100644 index 000000000..350e78ffa --- /dev/null +++ b/tests/corpus/key-order-literal-flows.ts @@ -0,0 +1,150 @@ +// Records enumerate their keys in the order their object literals create +// them, even when every literal writes the keys in another order than the +// type declares them and the value is observed far from the literal: +// factory returns, arrays, Maps, nested records, generic factories, union +// arms, records widened to unknown/any, structuredClone, and every +// order-observing surface (keys, values, entries, for...in, JSON.stringify +// with replacers and indentation, inspect, console.log, util.format). +import { format, inspect } from "node:util"; + +interface Point { + x: number; + y: number; + label: string; +} +function makePoint(i: number): Point { + return { label: "p" + i, y: i * 2, x: i }; +} + +const p = makePoint(1); +console.log(Object.keys(p).join(",")); +console.log(Object.values(p).join(",")); +console.log(JSON.stringify(Object.entries(p))); +const forIn: string[] = []; +for (const k in p) forIn.push(k); +console.log(forIn.join(",")); +console.log(JSON.stringify(p)); +console.log(JSON.stringify(p, null, 2)); +console.log(JSON.stringify(p, ["y", "x"])); +const visited: string[] = []; +JSON.stringify(p, (key: string, value: unknown) => { + visited.push(key); + return value; +}); +console.log(visited.join(",")); +console.log(p); +console.log(inspect(p)); +console.log(format("%o|%j|%O", p, p, p)); + +// Arrays of records: literals pushed in a loop and produced by map. +interface Row { + id: number; + name: string; + score: number; +} +const rows: Row[] = []; +for (let i = 0; i < 3; i++) rows.push({ name: "n" + i, score: i * 10, id: i }); +for (const r of rows) console.log(Object.values(r).join("|")); +console.log(JSON.stringify(rows)); +const mapped: Row[] = [4, 5].map((i) => ({ name: "m" + i, score: i, id: i * 100 })); +console.log(JSON.stringify(mapped)); +console.log(mapped); + +// Records stored in a Map, read back, serialized and inspected. +const byName = new Map(); +for (const r of rows) byName.set(r.name, r); +byName.set("extra", { name: "extra", score: 9, id: 99 }); +console.log(byName); +console.log(JSON.stringify([...byName.values()])); +console.log(Object.keys(byName.get("extra")!).join(",")); + +// Nested records: every level keeps its own literal's order. +interface Inner { + b: number; + a: number; +} +interface Outer { + inner: Inner; + id: number; + tags: string[]; +} +function makeOuter(id: number): Outer { + return { id, tags: ["t" + id], inner: { a: id, b: id + 1 } }; +} +console.log(JSON.stringify(makeOuter(1))); +console.log(makeOuter(2)); +console.log(JSON.stringify([makeOuter(3), makeOuter(4)], null, 1)); + +// Generic factories instantiate per type argument. +function boxed(value: T): { value: T; tag: string; count: number } { + return { tag: "box", count: 1, value }; +} +console.log(JSON.stringify(boxed(7))); +console.log(JSON.stringify(boxed("s"))); +console.log(Object.keys(boxed(true)).join(",")); + +// Union arms keep their own literal orders. +type Shape = { kind: "circle"; r: number } | { kind: "rect"; w: number; h: number }; +function shapeAt(i: number): Shape { + return i % 2 === 0 ? { r: i, kind: "circle" } : { h: i, kind: "rect", w: i + 1 }; +} +const shapes: Shape[] = [shapeAt(0), shapeAt(1), shapeAt(2)]; +for (const s of shapes) console.log(JSON.stringify(s)); +console.log(shapes); + +// Widened to unknown and any, and cloned. +const u: unknown = makePoint(3); +console.log(JSON.stringify(u)); +const loose: any = makePoint(4); +console.log(Object.keys(loose).join(",")); +const cloned = structuredClone(makePoint(5)); +console.log(JSON.stringify(cloned), Object.keys(cloned).join(",")); + +// Integer-like keys still come first, ascending, ahead of the literal order. +interface Indexed { + name: string; + "10": number; + "2": number; + "01": number; +} +function makeIndexed(): Indexed { + return { "01": 1, name: "n", "10": 10, "2": 2 }; +} +console.log(JSON.stringify(makeIndexed())); +console.log(Object.keys(makeIndexed()).join(",")); + +// A literal whose fields are flipped by later assignments: assigning an +// existing key never moves it (the pattern of scriptc's own +// RuntimeFeatures scan). +interface Features { + regex: boolean; + eventTarget: boolean; + warningReplace: boolean; + stdioExtended: boolean; + processEvents: boolean; + emitter: boolean; +} +function scanFeatures(classes: string[], workers: boolean): Features { + const features: Features = { + regex: false, + eventTarget: classes.some((c) => c === "EventTarget"), + processEvents: false, + warningReplace: false, + stdioExtended: false, + emitter: workers, + }; + if (classes.length > 1) features.regex = true; + features.stdioExtended = workers; + return features; +} +console.log(JSON.stringify(scanFeatures(["EventTarget", "X"], true))); +console.log(JSON.stringify(scanFeatures([], false))); + +// Literals written in declaration order keep it. +interface Ordered { + first: string; + second: number; + third: boolean; +} +const ordered: Ordered = { first: "f", second: 2, third: true }; +console.log(JSON.stringify(ordered), Object.keys(ordered).join(",")); diff --git a/tests/corpus/key-order-spread-copies.ts b/tests/corpus/key-order-spread-copies.ts new file mode 100644 index 000000000..b677eb0a4 --- /dev/null +++ b/tests/corpus/key-order-spread-copies.ts @@ -0,0 +1,107 @@ +// Copies keep the key order of the values they copy: a spread places the +// source's keys at the spread's position in the source's own order, an +// override keeps the position the spread gave it, and new keys follow. +// Rest bindings, width copies, Object.assign and structuredClone keep the +// source order too, including when the source literal was written in +// another order than its type declares. +interface Source { + a: number; + b: number; + c: number; +} +function makeSource(i: number): Source { + return { c: i, b: i + 1, a: i + 2 }; +} + +// Spread followed by a new key, and by an override. +interface Extended { + a: number; + b: number; + c: number; + d: number; +} +const extended: Extended = { ...makeSource(1), d: 4 }; +console.log(JSON.stringify(extended), Object.keys(extended).join(",")); +const overridden: Source = { ...makeSource(2), b: 0 }; +console.log(JSON.stringify(overridden)); +interface Led { + a: number; + b: number; + c: number; + lead: number; +} +const leading: Led = { lead: 0, ...makeSource(3) }; +console.log(JSON.stringify(leading)); + +// A spread whose overrides change field types builds a new shape through +// the field-by-field desugar (scriptc's own frontend-cache writer). +interface Entry { + ok: true; + llvm: string; + ir: string[] | null; + sidecarJson: string | null; + features: number; + sources: [string, string][]; + warnings?: string[]; +} +function makeEntry(): Entry { + return { ok: true, llvm: "L", ir: ["x"], sidecarJson: null, features: 1, sources: [] }; +} +const entry = makeEntry(); +console.log(JSON.stringify({ schema: "v3", input: { ...entry, llvm: "", ir: null } })); + +// Same-shape updates keep every key in place. +interface State { + name: string; + count: number; + last: string | undefined; +} +let state: State = { count: 0, last: undefined, name: "s" }; +for (let i = 0; i < 3; i++) state = { ...state, count: state.count + 1, last: "i" + i }; +console.log(JSON.stringify(state), Object.keys(state).join(",")); +console.log({ ...state, name: "shown" }); + +// Rest bindings copy the remaining keys in the source's order. +const { b, ...rest } = makeSource(5); +console.log(b, JSON.stringify(rest), Object.keys(rest).join(",")); +let picked: number; +let restAgain: { a: number; c: number }; +({ b: picked, ...restAgain } = makeSource(6)); +console.log(picked, JSON.stringify(restAgain), Object.keys(restAgain).join(",")); + +// Width copies into an optional-widened type and assignment into a fresh +// target. +interface Wider { + a: number; + b: number; + c: number; + note?: string; +} +function widen(s: Source): Wider { + return s; +} +console.log(JSON.stringify(widen(makeSource(7)))); +const assigned = Object.assign({ z: 0 }, makeSource(8)); +console.log(JSON.stringify(assigned)); +console.log(JSON.stringify(structuredClone(makeSource(9)))); + +// Nested spreads of nested records. +interface Holder { + inner: Source; + id: number; +} +const holder: Holder = { id: 1, inner: makeSource(10) }; +const copied: Holder = { ...holder, id: 2 }; +console.log(JSON.stringify(copied)); +console.log(copied); + +// Conditional spreads place their key at the spread's position. +interface Options { + mode: string; + depth?: number; + verbose: boolean; +} +function options(deep: boolean): Options { + return { mode: "m", ...(deep ? { depth: 3 } : {}), verbose: deep }; +} +console.log(JSON.stringify(options(true)), JSON.stringify(options(false))); diff --git a/tests/corpus/let-uninitialized-function-binding.ts b/tests/corpus/let-uninitialized-function-binding.ts new file mode 100644 index 000000000..ade6748d3 --- /dev/null +++ b/tests/corpus/let-uninitialized-function-binding.ts @@ -0,0 +1,46 @@ +// A module-scope `let` declared without an initializer is initialized (to +// undefined) where the declaration runs, so a later assignment of a closure +// that calls the binding itself is an ordinary write. +type Step = (n: number) => number; + +// A write that runs before the declaration is still in the dead zone. +function early(): void { + late = () => 1; +} +try { + early(); +} catch (e) { + console.log((e as Error).name, (e as Error).message); +} +let late: () => number; +late = () => 2; +early(); +console.log(late()); + +let countdown: Step; +countdown = (n) => (n <= 0 ? 0 : 1 + countdown(n - 1)); +console.log(countdown(5)); + +let log: (s: string) => void; +log = (s) => { + if (s.length < 3) log(s + "!"); + else console.log(s); +}; +log("a"); + +// Reassignment and assignment from a function body after the declaration. +let pick: (xs: number[]) => number; +function install(largest: boolean): void { + pick = largest ? (xs) => Math.max(...xs) : (xs) => Math.min(...xs); +} +install(true); +console.log(pick!([3, 9, 4])); +install(false); +console.log(pick!([3, 9, 4])); + +// Mutual recursion through two such bindings. +let isEven: (n: number) => boolean; +let isOdd: (n: number) => boolean; +isEven = (n) => (n === 0 ? true : isOdd(n - 1)); +isOdd = (n) => (n === 0 ? false : isEven(n - 1)); +console.log(isEven(10), isOdd(7), isEven(3)); diff --git a/tests/corpus/logical-and-truthy-index.ts b/tests/corpus/logical-and-truthy-index.ts new file mode 100644 index 000000000..b89e4d78d --- /dev/null +++ b/tests/corpus/logical-and-truthy-index.ts @@ -0,0 +1,44 @@ +// `&&` whose left operand is an array, object or function value: every +// object is truthy, so the result is the right operand, here an element +// read that may be undefined at run time. +const list = [3, 1, 2]; +const head = list && list[0]; +console.log(head); +const words: string[] = ["x"]; +console.log(words && words[0], words && words[5]); +const empty: number[] = []; +console.log(empty && empty[0]); +function first(xs: (string | number)[]): string | number { + return xs && xs[0]; +} +console.log(first([1]), first(["s"])); +type Item = { n: number } | string; +const items: Item[] = [{ n: 1 }, "z"]; +console.log(items && items[0], items && items[1]); +const nested: number[][] = [[1]]; +console.log(nested && nested[0]); +const flags: boolean[] = [false]; +console.log(flags && flags[0]); +const maybe: (number | null)[] = [null]; +console.log(maybe && maybe[0]); +const holes: (number | undefined)[] = [undefined, 4]; +console.log(holes && holes[0], holes && holes[1]); +const box = { k: [1, 2] }; +console.log(box.k && box.k[1]); +// The left operand still evaluates exactly once. +let reads = 0; +const source = (): number[] => { + reads++; + return [7, 8]; +}; +console.log(source() && source()[1], reads); +const table: Record = { a: [5] }; +const pick = (key: string): number | undefined => table[key] && table[key][0]; +console.log(pick("a"), reads); +class Holder { + xs = [1, 2]; + second(): number | undefined { + return this.xs && this.xs[1]; + } +} +console.log(new Holder().second()); diff --git a/tests/coverage-fixtures/divergences.ts b/tests/coverage-fixtures/divergences.ts index e13701a93..10b0a12e7 100644 --- a/tests/coverage-fixtures/divergences.ts +++ b/tests/coverage-fixtures/divergences.ts @@ -20,14 +20,14 @@ console.log(tagged.label); const view: Labeled = tagged; console.log(view === tagged); -// Key order: the literal's order differs from the declaration and the -// program observes it. +// Key order: values of one type are built in two orders and the program +// observes them, so the type keeps its declaration order. interface Point { x: number; y: number; } const point: Point = { y: 1, x: 2 }; -console.log(JSON.stringify(point)); +console.log(JSON.stringify(point), JSON.stringify({ x: 3, y: 4 })); // Text outside the built-in collation compares by code point natively. console.log(["б", "а"].sort((p, q) => p.localeCompare(q)).join(","), "яблоко".localeCompare("груша")); diff --git a/tests/coverage-fixtures/dynamic-mix.ts b/tests/coverage-fixtures/dynamic-mix.ts index 0c9779a64..d70141b91 100644 --- a/tests/coverage-fixtures/dynamic-mix.ts +++ b/tests/coverage-fixtures/dynamic-mix.ts @@ -1,9 +1,9 @@ // Native checked values and loose equality share this program with -// island-backed APIs (SC2012) with native compilation refusals. +// native compilation refusals (a default-locale toLocaleString, SC2020). const v: any = 21; const doubled = v * 2; const root = Math.cbrt(81); -const up = (19.99).toPrecision(3); +const up = (19.99).toLocaleString(); const parsed = Number.parseFloat("1.5"); // a string argument compiles statically, like the global const unknownScore: unknown = 81; const flags = unknownScore == 81; diff --git a/tests/coverage-fixtures/mixed.ts b/tests/coverage-fixtures/mixed.ts index 73a36ce47..1ae9db26d 100644 --- a/tests/coverage-fixtures/mixed.ts +++ b/tests/coverage-fixtures/mixed.ts @@ -10,7 +10,7 @@ const handler = (x: number) => x * 2; const anyone: unknown = score(1); const flags = anyone == score(2); // unknown/object coercion keeps the fence; static primitive pairs lower -const { length: firstItem } = items; // object patterns over arrays keep the fence (patterns and defaults compile) +const { length: firstItem } = score(3) > 99 ? items : "abc"; // a union source keeps the object-pattern fence (narrow first) let label = "score: "; label += score(10); console.log(label); diff --git a/tests/coverage-fixtures/no-divergences.ts b/tests/coverage-fixtures/no-divergences.ts index 9571add6e..3739336e3 100644 --- a/tests/coverage-fixtures/no-divergences.ts +++ b/tests/coverage-fixtures/no-divergences.ts @@ -1,6 +1,6 @@ // Ordinary code: identity-preserving calls, declaration-ordered literals, -// Latin-script comparisons, related-class casts and ISO dates report no -// divergence. +// Latin-script comparisons, related-class casts, ISO dates and views of the +// global object report no divergence. interface Item { name: string; qty: number; @@ -32,3 +32,9 @@ function wingsOf(a: Animal): number { console.log(wingsOf(new Bird()), wingsOf(new Animal())); console.log(["b", "A", "a"].sort((p, q) => p.localeCompare(q)).join(",")); console.log(Date.parse("2024-01-02"), new Date("2024-01-02T03:04:05+01:00").getTime()); + +// A type assertion of the global object denotes the global object itself. +const COUNT = Symbol.for("no-divergences.count"); +const globalState = globalThis as { [COUNT]?: { n: number } }; +globalState[COUNT] = { n: 1 }; +console.log(globalState[COUNT]?.n); diff --git a/tests/coverage-fixtures/tier-demotions.ts b/tests/coverage-fixtures/tier-demotions.ts index 52f2058f1..3dbd0c3fe 100644 --- a/tests/coverage-fixtures/tier-demotions.ts +++ b/tests/coverage-fixtures/tier-demotions.ts @@ -17,9 +17,9 @@ function readName(text: string): string { return String(data.name); } -// An operation without a static lowering, on a parameter. +// An operation without a static lowering (the host's default locale), on a parameter. function precise(n: number): string { - return n.toPrecision(3); + return n.toLocaleString(); } console.log(readName('{"name":"a"}'), precise(1.5), new Point(1, 2).x); diff --git a/tests/diagnostics/constructor-returns-instance-super.ts b/tests/diagnostics/constructor-returns-instance-super.ts new file mode 100644 index 000000000..c5e34c7a1 --- /dev/null +++ b/tests/diagnostics/constructor-returns-instance-super.ts @@ -0,0 +1,15 @@ +// `super()` into a constructor that can return an existing instance would +// rebind the subclass's `this` to that instance. +class Shared { + static one: Shared | undefined = undefined; + constructor() { + if (Shared.one) return Shared.one; + Shared.one = this; + } +} +class Derived extends Shared { + constructor() { + super(); + } +} +console.log(new Derived() instanceof Derived); diff --git a/tests/diagnostics/constructor-returns-object.ts b/tests/diagnostics/constructor-returns-object.ts new file mode 100644 index 000000000..63abf4c4d --- /dev/null +++ b/tests/diagnostics/constructor-returns-object.ts @@ -0,0 +1,10 @@ +// Node's `new Point()` answers the object a constructor returns. A plain +// object has no representation as a Point instance, so the return refuses +// instead of answering the constructed instance. +class Point { + x = 1; + constructor() { + return { x: 2 }; + } +} +console.log(new Point().x); diff --git a/tests/diagnostics/deferred-class-receiver/shelf.ts b/tests/diagnostics/deferred-class-receiver/shelf.ts index 1308ce5dc..d0fba73b3 100644 --- a/tests/diagnostics/deferred-class-receiver/shelf.ts +++ b/tests/diagnostics/deferred-class-receiver/shelf.ts @@ -1,6 +1,6 @@ export class Shelf { readonly name: string; - private readonly notes = new WeakMap(); + private readonly cursor = new Map().keys(); constructor(name: string) { this.name = name; } diff --git a/tests/diagnostics/destructuring-fences.ts b/tests/diagnostics/destructuring-fences.ts index b79836920..e42f8a07a 100644 --- a/tests/diagnostics/destructuring-fences.ts +++ b/tests/diagnostics/destructuring-fences.ts @@ -48,8 +48,8 @@ function fromUnion(u: E1 | E2): string { console.log(fromUnion({ kind: "one", b: "s", count: 1 })); const xs = [1, 2, 3]; -const { length: len } = xs; // object patterns over arrays keep the fence -console.log(len); +const { push: append } = xs; // object patterns over arrays read positions and length only +console.log(typeof append); const indexed: Record = { a: 1 }; const { ...allOfIt } = indexed; // index-signature rest needs overflow packing diff --git a/tests/diagnostics/dynamic-surface.ts b/tests/diagnostics/dynamic-surface.ts index 537a992eb..3af6671f4 100644 --- a/tests/diagnostics/dynamic-surface.ts +++ b/tests/diagnostics/dynamic-surface.ts @@ -4,11 +4,12 @@ // flag — never an ICE, never a link error. Static Math methods include // the scalar functions and variadic Math.hypot. The .split(string), // trim/pad variants, parseInt, isNaN, the global parseFloat/isFinite and -// Number.parseFloat/Number.parseInt over exactly-typed arguments, and -// string-pattern replacement compile statically now and no longer appear -// here.) +// Number.parseFloat/Number.parseInt over exactly-typed arguments, +// string-pattern replacement and toPrecision compile statically now and no +// longer appear here; toLocaleString without a locale still has no +// lowering.) const up = Math.cbrt(2); const tau = Math.PI * 2; -const price = (19.99).toPrecision(4); +const price = (19.99).toLocaleString(); const n = Number.parseFloat("3.14"); void n; diff --git a/tests/diagnostics/webcrypto-refusals.ts b/tests/diagnostics/webcrypto-refusals.ts new file mode 100644 index 000000000..d162872fb --- /dev/null +++ b/tests/diagnostics/webcrypto-refusals.ts @@ -0,0 +1,18 @@ +// The native WebCrypto surface is HMAC importKey('raw'), sign and verify, +// and the SHA-1/SHA-2 digests, called directly on crypto.subtle. The 'jwk' +// key format and 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 fromJwk = await crypto.subtle.importKey( + "jwk", + { kty: "oct", k: "c2VjcmV0" }, + { name: "HMAC", hash: "SHA-256" }, + false, + ["sign"], +); +console.log(fromJwk.type); + +const subtle = crypto.subtle; +console.log(typeof subtle); + +console.log(typeof crypto.subtle.sign); diff --git a/tests/fixtures/node-types/fenced.ts b/tests/fixtures/node-types/fenced.ts index b884de3ce..78fbf2af7 100644 --- a/tests/fixtures/node-types/fenced.ts +++ b/tests/fixtures/node-types/fenced.ts @@ -97,4 +97,15 @@ void callbackified; * getAuthTag(), and setAuthTag(); the stream.Transform members are fenced. */ const cipherStream = createCipheriv("aes-128-cbc", Buffer.alloc(16), Buffer.alloc(16)); cipherStream.on("data", () => {}); +/* WebCrypto beyond the HMAC slice: @types/node declares the whole + * SubtleCrypto surface, and the methods outside importKey, sign, verify + * and digest fence by name. */ +async function encryptWithWebCrypto(key: import("node:crypto").webcrypto.CryptoKey): Promise { + await crypto.subtle.encrypt({ name: "AES-GCM", iv: new Uint8Array(12) }, key, new Uint8Array(1)); +} +void encryptWithWebCrypto; +/* Node's navigator global: typeof navigator and navigator.userAgent + * compile; the other members fence by name. */ +const cores = navigator.hardwareConcurrency; +void cores; // End of the declared-but-not-lowered surface. diff --git a/tests/fixtures/node-types/navigator.ts b/tests/fixtures/node-types/navigator.ts new file mode 100644 index 000000000..2f1ee2d9f --- /dev/null +++ b/tests/fixtures/node-types/navigator.ts @@ -0,0 +1,6 @@ +/* Node 24's navigator global under @types/node: the runtime probe + * libraries use (jose's user-agent check) folds to Node's values. */ +const browser = !navigator.userAgent.startsWith("Mozilla/5.0 "); +console.log(typeof navigator, navigator.userAgent, browser); +console.log(typeof navigator !== "undefined" && navigator.userAgent === "Cloudflare-Workers"); +console.log(navigator.userAgent.split("/")[0], navigator.userAgent.length); diff --git a/tests/fixtures/node-types/webcrypto-hmac.ts b/tests/fixtures/node-types/webcrypto-hmac.ts new file mode 100644 index 000000000..4b37d7de4 --- /dev/null +++ b/tests/fixtures/node-types/webcrypto-hmac.ts @@ -0,0 +1,25 @@ +/* WebCrypto's HMAC slice under @types/node: the crypto global's subtle + * methods and CryptoKey members lower to the same native values as under + * the bundled declarations. @types/node names the dictionary and key + * types in node:crypto's webcrypto namespace. */ +import type { webcrypto } from "node:crypto"; + +async function main(): Promise { + const encoder = new TextEncoder(); + const params: webcrypto.HmacImportParams = { name: "HMAC", hash: "SHA-512" }; + const key: webcrypto.CryptoKey = await crypto.subtle.importKey("raw", encoder.encode("node-types"), params, true, [ + "verify", + "sign", + ]); + const algorithm = key.algorithm as webcrypto.HmacKeyAlgorithm; + console.log(key.type, key.extractable, key.usages.join(","), algorithm.name, algorithm.length, algorithm.hash.name); + const signature = await crypto.subtle.sign("HMAC", key, encoder.encode("message")); + console.log(signature.byteLength, Buffer.from(signature).toString("base64")); + console.log(await crypto.subtle.verify("HMAC", key, signature, encoder.encode("message"))); + console.log(await crypto.subtle.verify("HMAC", key, signature, encoder.encode("massage"))); + const digest = await crypto.subtle.digest("SHA-384", encoder.encode("message")); + console.log(Buffer.from(digest).toString("hex")); + console.log(key instanceof CryptoKey); +} + +void main(); diff --git a/tests/fixtures/npm/cases/chalk5/main.ts b/tests/fixtures/npm/cases/chalk5/main.ts index 6a33a1466..0bfa806c1 100644 --- a/tests/fixtures/npm/cases/chalk5/main.ts +++ b/tests/fixtures/npm/cases/chalk5/main.ts @@ -15,6 +15,13 @@ console.log(modifierNames.join(",")); console.log(foregroundColorNames.length + " " + backgroundColorNames.length + " " + colorNames.length); show("red", chalk.red("red")); +// Root members named like String.prototype's HTML methods, called from +// TypeScript (pi's `chalk.bold(APP_NAME)`): the checker types chalk's +// JavaScript default export as a plain function, so the member is `any`. +show("bold", chalk.bold("bold")); +show("bold template", `${chalk.bold("Usage:")} pi`); +const alias = chalk; +show("bold alias", alias.bold("alias") + alias.italic("i")); show("red.bold", chalk.red.bold("bold red")); show("nested", chalk.blue("a " + chalk.red("b") + " c")); show("multi-arg", chalk.green("one", "two", 3)); @@ -36,6 +43,7 @@ show("gray-grey", chalk.gray("x") + chalk.grey("y")); for (const level of [0, 1, 2, 3] as const) { const c = new Chalk({ level }); show(`L${level} red.bold`, c.red.bold("x")); + show(`L${level} bold`, c.bold("x")); show(`L${level} hex`, c.hex("#123456")("x")); show(`L${level} rgb`, c.rgb(255, 255, 255).bgRgb(0, 0, 0)("x")); show(`L${level} ansi256`, c.ansi256(100).bgAnsi256(200)("x")); diff --git a/tests/fixtures/npm/cases/undici5/main.ts b/tests/fixtures/npm/cases/undici5/main.ts new file mode 100644 index 000000000..a65601a09 --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/main.ts @@ -0,0 +1,298 @@ +// undici 5.x through scriptc's built-in implementation: the native build +// compiles the package's shim in place of the vendored undici the Node side +// loads. Covers request(), Agent/Pool/Client, a transport-style Agent +// subclass (per-origin pool factory, wrapped connector, client +// interceptors, pool stats), dispatch overrides, keep-alive reuse, error +// classes and codes, and the diagnostics_channel request lifecycle. +import { channel } from "node:diagnostics_channel"; +import * as http from "node:http"; +import type { Socket } from "node:net"; +import { + Agent, + Client, + Pool, + buildConnector, + errors, + getGlobalDispatcher, + request, + setGlobalDispatcher, +} from "undici"; +import type { Dispatcher } from "undici"; + +const server = http.createServer((req, res) => { + const url = req.url ?? "/"; + if (url === "/json") { + res.setHeader("content-type", "application/json"); + res.end(JSON.stringify({ ok: true, n: 3 })); + } else if (url === "/cookies") { + res.setHeader("set-cookie", ["a=1", "b=2"]); + res.end("cookies"); + } else if (url === "/slow") { + setTimeout(() => res.end("late"), 400); + } else if (url === "/missing") { + res.statusCode = 404; + res.setHeader("content-type", "application/json"); + res.end(JSON.stringify({ error: "not found" })); + } else if (url === "/chunked") { + res.write("part1,"); + setTimeout(() => res.end("part2"), 10); + } else if (url === "/echo") { + let body = ""; + req.on("data", (chunk: Buffer) => { + body += chunk.toString(); + }); + req.on("end", () => { + const test = req.headers["x-test"]; + const length = req.headers["content-length"]; + const shown = (v: string | string[] | undefined): string => (typeof v === "string" ? v : "-"); + res.end(`${req.method} ${shown(test)} ${shown(length)} ${body}`); + }); + } else { + res.end(`hello ${req.method} ${url}`); + } +}); + +const turn = (): Promise => new Promise((resolve) => setImmediate(resolve)); + +interface Telemetry { + label: string; + dispatched: number; +} + +let current: Telemetry | null = null; +const byRequest = new WeakMap(); + +channel("undici:request:create").subscribe((message) => { + const { request } = message as { request: object }; + if (current !== null) byRequest.set(request, current); +}); +channel("undici:request:headers").subscribe((message) => { + const { request, response } = message as { + request: object; + response: { statusCode: number }; + }; + const t = byRequest.get(request); + if (t !== undefined) console.log(` [diag] ${t.label} headers ${response.statusCode}`); +}); +channel("undici:request:trailers").subscribe((message) => { + const { request } = message as { request: object }; + const t = byRequest.get(request); + if (t !== undefined) console.log(` [diag] ${t.label} done after ${t.dispatched} dispatch`); +}); + +/** A transport-style Agent: one Pool per origin built by the factory, a + * wrapped connector, and a Client interceptor that marks dispatches. */ +class TracingAgent extends Agent { + readonly origins: Map; + connections = 0; + + constructor(maxConnections: number) { + const origins = new Map(); + const connect = buildConnector({}) as ( + options: buildConnector.Options, + callback: buildConnector.Callback, + ) => Socket; + let opened = 0; + super({ + factory(origin) { + const pool = new Pool(origin, { + connections: maxConnections, + keepAliveTimeout: 10_000, + pipelining: 1, + connect(options, callback) { + opened++; + return connect(options, callback); + }, + interceptors: { + Client: [ + (dispatch) => (params, handler) => { + const t = (params as Dispatcher.RequestOptions).opaque; + if (t !== null && typeof t === "object") { + const tel = t as Telemetry; + tel.dispatched++; + const previous = current; + current = tel; + try { + return dispatch(params, handler); + } finally { + current = previous; + } + } + return dispatch(params, handler); + }, + ], + }, + }); + origins.set(String(origin), pool); + return pool; + }, + }); + this.origins = origins; + this.on("connect", () => { + this.connections = opened; + }); + } + + statsFor(origin: string): string { + const pool = this.origins.get(origin); + if (pool === undefined) return "no pool"; + const s = pool.stats; + return `connected=${s.connected} free=${s.free} pending=${s.pending} queued=${s.queued} running=${s.running} size=${s.size}`; + } +} + +/** A subclass that overrides dispatch and forwards to the base Agent. */ +class CountingAgent extends Agent { + count = 0; + paths: string[] = []; + dispatch(options: Dispatcher.DispatchOptions, handler: Dispatcher.DispatchHandlers): boolean { + this.count++; + this.paths.push(`${options.method} ${options.path}`); + return super.dispatch(options, handler); + } +} + +function describe(err: unknown): string { + if (err instanceof errors.UndiciError) return `${err.name} ${err.code} ${err.message}`; + if (err instanceof DOMException) return `${err.name} ${err.message}`; + if (err instanceof Error) { + const code = (err as NodeJS.ErrnoException).code ?? ""; + return `${err.name} ${code} ${err.message}`; + } + return String(err); +} + +server.listen(0, "127.0.0.1", async () => { + const address = server.address(); + const port = address !== null && typeof address === "object" ? address.port : 0; + const origin = `http://127.0.0.1:${port}`; + + console.log("== request() through a TracingAgent"); + const agent = new TracingAgent(2); + const tel: Telemetry = { label: "hello", dispatched: 0 }; + const res = await request(`${origin}/hello`, { dispatcher: agent, opaque: tel }); + console.log(res.statusCode, res.headers["content-length"], await res.body.text()); + console.log("opaque passthrough:", res.opaque === tel); + const json = await request(`${origin}/json`, { dispatcher: agent }); + const data = (await json.body.json()) as { ok: boolean; n: number }; + console.log("json:", data.ok, data.n, json.headers["content-type"]); + const cookies = await request(`${origin}/cookies`, { dispatcher: agent }); + const setCookie = cookies.headers["set-cookie"]; + console.log("set-cookie:", Array.isArray(setCookie) ? setCookie.join("|") : setCookie); + await cookies.body.text(); + const chunked = await request(`${origin}/chunked`, { dispatcher: agent }); + console.log("chunked:", chunked.headers["transfer-encoding"], await chunked.body.text()); + const echo = await request(`${origin}/echo`, { + dispatcher: agent, + body: "payload", + headers: { "x-test": "yes" }, + }); + console.log("default method with a body:", await echo.body.text()); + const ab = await request(`${origin}/hello`, { dispatcher: agent, method: "POST" }); + console.log("arrayBuffer bytes:", (await ab.body.arrayBuffer()).byteLength); + + console.log("== keep-alive reuse"); + for (let i = 0; i < 4; i++) { + const r = await request(`${origin}/r${i}`, { dispatcher: agent }); + await r.body.text(); + await turn(); + } + console.log("connections opened:", agent.connections); + console.log("stats:", agent.statsFor(origin)); + + console.log("== Pool with a connection limit"); + const pool = new Pool(origin, { connections: 2 }); + const inflight: Promise[] = []; + for (let i = 0; i < 4; i++) { + inflight.push( + pool.request({ path: `/p${i}`, method: "GET" }).then((r) => r.body.text()), + ); + } + console.log("queued while busy:", pool.stats.queued, "size:", pool.stats.size); + console.log((await Promise.all(inflight)).join(", ")); + await pool.close(); + console.log("pool closed:", pool.closed, pool.destroyed); + + console.log("== Client"); + const client = new Client(origin); + const cr = await client.request({ path: "/client", method: "GET" }); + console.log(cr.statusCode, await cr.body.text()); + await client.close(); + + console.log("== connect.lookup"); + const looked: string[] = []; + const pinned = new Agent({ + connect: { + lookup(hostname, _options, callback) { + looked.push(hostname); + callback(null, [{ address: "127.0.0.1", family: 4 }]); + }, + }, + }); + const viaLookup = await request(`http://pinned.invalid:${port}/lookup`, { dispatcher: pinned }); + console.log(viaLookup.statusCode, await viaLookup.body.text(), looked.join(",")); + await pinned.close(); + + console.log("== dispatch override"); + const counting = new CountingAgent(); + for (const path of ["/a", "/b"]) { + const r = await request(`${origin}${path}`, { dispatcher: counting }); + await r.body.text(); + } + console.log("dispatched:", counting.count, counting.paths.join(", ")); + await counting.close(); + + console.log("== diagnostics channel lifecycle"); + const traced: Telemetry = { label: "traced", dispatched: 0 }; + const tr = await request(`${origin}/traced`, { dispatcher: agent, opaque: traced }); + await tr.body.text(); + + console.log("== errors"); + try { + await request("http://127.0.0.1:1/refused", {}); + } catch (err) { + console.log("refused:", describe(err)); + } + try { + await request(`${origin}/slow`, { dispatcher: agent, headersTimeout: 100 }); + } catch (err) { + console.log("headers timeout:", describe(err)); + } + const controller = new AbortController(); + const pending = request(`${origin}/slow`, { dispatcher: agent, signal: controller.signal }); + setTimeout(() => controller.abort(), 50); + try { + await pending; + } catch (err) { + console.log("abort:", describe(err)); + } + try { + await request(`${origin}/missing`, { dispatcher: agent, throwOnError: true }); + } catch (err) { + if (err instanceof errors.ResponseStatusCodeError) { + console.log("status:", err.message, err.statusCode, JSON.stringify(err.body)); + } else { + console.log("status:", describe(err)); + } + } + const notFound = await request(`${origin}/missing`, { dispatcher: agent }); + console.log("404 without throwOnError:", notFound.statusCode, await notFound.body.text()); + try { + await request("ftp://127.0.0.1/x", {}); + } catch (err) { + console.log("protocol:", describe(err)); + } + const early = await request(`${origin}/hello`, { dispatcher: agent }); + early.body.on("error", (err: Error) => console.log("destroyed body:", describe(err))); + early.body.destroy(); + await turn(); + console.log("instanceof:", new errors.HeadersTimeoutError() instanceof errors.UndiciError); + + console.log("== global dispatcher"); + setGlobalDispatcher(counting); + console.log("global is counting:", getGlobalDispatcher() === counting); + + await agent.close(); + server.close(); + console.log("done"); +}); diff --git a/tests/fixtures/npm/cases/undici5/node_modules/busboy/LICENSE b/tests/fixtures/npm/cases/undici5/node_modules/busboy/LICENSE new file mode 100644 index 000000000..290762e94 --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/busboy/LICENSE @@ -0,0 +1,19 @@ +Copyright Brian White. All rights reserved. + +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. \ No newline at end of file diff --git a/tests/fixtures/npm/cases/undici5/node_modules/busboy/lib/index.js b/tests/fixtures/npm/cases/undici5/node_modules/busboy/lib/index.js new file mode 100644 index 000000000..873272d93 --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/busboy/lib/index.js @@ -0,0 +1,57 @@ +'use strict'; + +const { parseContentType } = require('./utils.js'); + +function getInstance(cfg) { + const headers = cfg.headers; + const conType = parseContentType(headers['content-type']); + if (!conType) + throw new Error('Malformed content type'); + + for (const type of TYPES) { + const matched = type.detect(conType); + if (!matched) + continue; + + const instanceCfg = { + limits: cfg.limits, + headers, + conType, + highWaterMark: undefined, + fileHwm: undefined, + defCharset: undefined, + defParamCharset: undefined, + preservePath: false, + }; + if (cfg.highWaterMark) + instanceCfg.highWaterMark = cfg.highWaterMark; + if (cfg.fileHwm) + instanceCfg.fileHwm = cfg.fileHwm; + instanceCfg.defCharset = cfg.defCharset; + instanceCfg.defParamCharset = cfg.defParamCharset; + instanceCfg.preservePath = cfg.preservePath; + return new type(instanceCfg); + } + + throw new Error(`Unsupported content type: ${headers['content-type']}`); +} + +// Note: types are explicitly listed here for easier bundling +// See: https://github.com/mscdex/busboy/issues/121 +const TYPES = [ + require('./types/multipart'), + require('./types/urlencoded'), +].filter(function(typemod) { return typeof typemod.detect === 'function'; }); + +module.exports = (cfg) => { + if (typeof cfg !== 'object' || cfg === null) + cfg = {}; + + if (typeof cfg.headers !== 'object' + || cfg.headers === null + || typeof cfg.headers['content-type'] !== 'string') { + throw new Error('Missing Content-Type'); + } + + return getInstance(cfg); +}; diff --git a/tests/fixtures/npm/cases/undici5/node_modules/busboy/lib/types/multipart.js b/tests/fixtures/npm/cases/undici5/node_modules/busboy/lib/types/multipart.js new file mode 100644 index 000000000..cc0d7bb66 --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/busboy/lib/types/multipart.js @@ -0,0 +1,653 @@ +'use strict'; + +const { Readable, Writable } = require('stream'); + +const StreamSearch = require('streamsearch'); + +const { + basename, + convertToUTF8, + getDecoder, + parseContentType, + parseDisposition, +} = require('../utils.js'); + +const BUF_CRLF = Buffer.from('\r\n'); +const BUF_CR = Buffer.from('\r'); +const BUF_DASH = Buffer.from('-'); + +function noop() {} + +const MAX_HEADER_PAIRS = 2000; // From node +const MAX_HEADER_SIZE = 16 * 1024; // From node (its default value) + +const HPARSER_NAME = 0; +const HPARSER_PRE_OWS = 1; +const HPARSER_VALUE = 2; +class HeaderParser { + constructor(cb) { + this.header = Object.create(null); + this.pairCount = 0; + this.byteCount = 0; + this.state = HPARSER_NAME; + this.name = ''; + this.value = ''; + this.crlf = 0; + this.cb = cb; + } + + reset() { + this.header = Object.create(null); + this.pairCount = 0; + this.byteCount = 0; + this.state = HPARSER_NAME; + this.name = ''; + this.value = ''; + this.crlf = 0; + } + + push(chunk, pos, end) { + let start = pos; + while (pos < end) { + switch (this.state) { + case HPARSER_NAME: { + let done = false; + for (; pos < end; ++pos) { + if (this.byteCount === MAX_HEADER_SIZE) + return -1; + ++this.byteCount; + const code = chunk[pos]; + if (TOKEN[code] !== 1) { + if (code !== 58/* ':' */) + return -1; + this.name += chunk.latin1Slice(start, pos); + if (this.name.length === 0) + return -1; + ++pos; + done = true; + this.state = HPARSER_PRE_OWS; + break; + } + } + if (!done) { + this.name += chunk.latin1Slice(start, pos); + break; + } + // FALLTHROUGH + } + case HPARSER_PRE_OWS: { + // Skip optional whitespace + let done = false; + for (; pos < end; ++pos) { + if (this.byteCount === MAX_HEADER_SIZE) + return -1; + ++this.byteCount; + const code = chunk[pos]; + if (code !== 32/* ' ' */ && code !== 9/* '\t' */) { + start = pos; + done = true; + this.state = HPARSER_VALUE; + break; + } + } + if (!done) + break; + // FALLTHROUGH + } + case HPARSER_VALUE: + switch (this.crlf) { + case 0: // Nothing yet + for (; pos < end; ++pos) { + if (this.byteCount === MAX_HEADER_SIZE) + return -1; + ++this.byteCount; + const code = chunk[pos]; + if (FIELD_VCHAR[code] !== 1) { + if (code !== 13/* '\r' */) + return -1; + ++this.crlf; + break; + } + } + this.value += chunk.latin1Slice(start, pos++); + break; + case 1: // Received CR + if (this.byteCount === MAX_HEADER_SIZE) + return -1; + ++this.byteCount; + if (chunk[pos++] !== 10/* '\n' */) + return -1; + ++this.crlf; + break; + case 2: { // Received CR LF + if (this.byteCount === MAX_HEADER_SIZE) + return -1; + ++this.byteCount; + const code = chunk[pos]; + if (code === 32/* ' ' */ || code === 9/* '\t' */) { + // Folded value + start = pos; + this.crlf = 0; + } else { + if (++this.pairCount < MAX_HEADER_PAIRS) { + this.name = this.name.toLowerCase(); + if (this.header[this.name] === undefined) + this.header[this.name] = [this.value]; + else + this.header[this.name].push(this.value); + } + if (code === 13/* '\r' */) { + ++this.crlf; + ++pos; + } else { + // Assume start of next header field name + start = pos; + this.crlf = 0; + this.state = HPARSER_NAME; + this.name = ''; + this.value = ''; + } + } + break; + } + case 3: { // Received CR LF CR + if (this.byteCount === MAX_HEADER_SIZE) + return -1; + ++this.byteCount; + if (chunk[pos++] !== 10/* '\n' */) + return -1; + // End of header + const header = this.header; + this.reset(); + this.cb(header); + return pos; + } + } + break; + } + } + + return pos; + } +} + +class FileStream extends Readable { + constructor(opts, owner) { + super(opts); + this.truncated = false; + this._readcb = null; + this.once('end', () => { + // We need to make sure that we call any outstanding _writecb() that is + // associated with this file so that processing of the rest of the form + // can continue. This may not happen if the file stream ends right after + // backpressure kicks in, so we force it here. + this._read(); + if (--owner._fileEndsLeft === 0 && owner._finalcb) { + const cb = owner._finalcb; + owner._finalcb = null; + // Make sure other 'end' event handlers get a chance to be executed + // before busboy's 'finish' event is emitted + process.nextTick(cb); + } + }); + } + _read(n) { + const cb = this._readcb; + if (cb) { + this._readcb = null; + cb(); + } + } +} + +const ignoreData = { + push: (chunk, pos) => {}, + destroy: () => {}, +}; + +function callAndUnsetCb(self, err) { + const cb = self._writecb; + self._writecb = null; + if (err) + self.destroy(err); + else if (cb) + cb(); +} + +function nullDecoder(val, hint) { + return val; +} + +class Multipart extends Writable { + constructor(cfg) { + const streamOpts = { + autoDestroy: true, + emitClose: true, + highWaterMark: (typeof cfg.highWaterMark === 'number' + ? cfg.highWaterMark + : undefined), + }; + super(streamOpts); + + if (!cfg.conType.params || typeof cfg.conType.params.boundary !== 'string') + throw new Error('Multipart: Boundary not found'); + + const boundary = cfg.conType.params.boundary; + const paramDecoder = (typeof cfg.defParamCharset === 'string' + && cfg.defParamCharset + ? getDecoder(cfg.defParamCharset) + : nullDecoder); + const defCharset = (cfg.defCharset || 'utf8'); + const preservePath = cfg.preservePath; + const fileOpts = { + autoDestroy: true, + emitClose: true, + highWaterMark: (typeof cfg.fileHwm === 'number' + ? cfg.fileHwm + : undefined), + }; + + const limits = cfg.limits; + const fieldSizeLimit = (limits && typeof limits.fieldSize === 'number' + ? limits.fieldSize + : 1 * 1024 * 1024); + const fileSizeLimit = (limits && typeof limits.fileSize === 'number' + ? limits.fileSize + : Infinity); + const filesLimit = (limits && typeof limits.files === 'number' + ? limits.files + : Infinity); + const fieldsLimit = (limits && typeof limits.fields === 'number' + ? limits.fields + : Infinity); + const partsLimit = (limits && typeof limits.parts === 'number' + ? limits.parts + : Infinity); + + let parts = -1; // Account for initial boundary + let fields = 0; + let files = 0; + let skipPart = false; + + this._fileEndsLeft = 0; + this._fileStream = undefined; + this._complete = false; + let fileSize = 0; + + let field; + let fieldSize = 0; + let partCharset; + let partEncoding; + let partType; + let partName; + let partTruncated = false; + + let hitFilesLimit = false; + let hitFieldsLimit = false; + + this._hparser = null; + const hparser = new HeaderParser((header) => { + this._hparser = null; + skipPart = false; + + partType = 'text/plain'; + partCharset = defCharset; + partEncoding = '7bit'; + partName = undefined; + partTruncated = false; + + let filename; + if (!header['content-disposition']) { + skipPart = true; + return; + } + + const disp = parseDisposition(header['content-disposition'][0], + paramDecoder); + if (!disp || disp.type !== 'form-data') { + skipPart = true; + return; + } + + if (disp.params) { + if (disp.params.name) + partName = disp.params.name; + + if (disp.params['filename*']) + filename = disp.params['filename*']; + else if (disp.params.filename) + filename = disp.params.filename; + + if (filename !== undefined && !preservePath) + filename = basename(filename); + } + + if (header['content-type']) { + const conType = parseContentType(header['content-type'][0]); + if (conType) { + partType = `${conType.type}/${conType.subtype}`; + if (conType.params && typeof conType.params.charset === 'string') + partCharset = conType.params.charset.toLowerCase(); + } + } + + if (header['content-transfer-encoding']) + partEncoding = header['content-transfer-encoding'][0].toLowerCase(); + + if (partType === 'application/octet-stream' || filename !== undefined) { + // File + + if (files === filesLimit) { + if (!hitFilesLimit) { + hitFilesLimit = true; + this.emit('filesLimit'); + } + skipPart = true; + return; + } + ++files; + + if (this.listenerCount('file') === 0) { + skipPart = true; + return; + } + + fileSize = 0; + this._fileStream = new FileStream(fileOpts, this); + ++this._fileEndsLeft; + this.emit( + 'file', + partName, + this._fileStream, + { filename, + encoding: partEncoding, + mimeType: partType } + ); + } else { + // Non-file + + if (fields === fieldsLimit) { + if (!hitFieldsLimit) { + hitFieldsLimit = true; + this.emit('fieldsLimit'); + } + skipPart = true; + return; + } + ++fields; + + if (this.listenerCount('field') === 0) { + skipPart = true; + return; + } + + field = []; + fieldSize = 0; + } + }); + + let matchPostBoundary = 0; + const ssCb = (isMatch, data, start, end, isDataSafe) => { +retrydata: + while (data) { + if (this._hparser !== null) { + const ret = this._hparser.push(data, start, end); + if (ret === -1) { + this._hparser = null; + hparser.reset(); + this.emit('error', new Error('Malformed part header')); + break; + } + start = ret; + } + + if (start === end) + break; + + if (matchPostBoundary !== 0) { + if (matchPostBoundary === 1) { + switch (data[start]) { + case 45: // '-' + // Try matching '--' after boundary + matchPostBoundary = 2; + ++start; + break; + case 13: // '\r' + // Try matching CR LF before header + matchPostBoundary = 3; + ++start; + break; + default: + matchPostBoundary = 0; + } + if (start === end) + return; + } + + if (matchPostBoundary === 2) { + matchPostBoundary = 0; + if (data[start] === 45/* '-' */) { + // End of multipart data + this._complete = true; + this._bparser = ignoreData; + return; + } + // We saw something other than '-', so put the dash we consumed + // "back" + const writecb = this._writecb; + this._writecb = noop; + ssCb(false, BUF_DASH, 0, 1, false); + this._writecb = writecb; + } else if (matchPostBoundary === 3) { + matchPostBoundary = 0; + if (data[start] === 10/* '\n' */) { + ++start; + if (parts >= partsLimit) + break; + // Prepare the header parser + this._hparser = hparser; + if (start === end) + break; + // Process the remaining data as a header + continue retrydata; + } else { + // We saw something other than LF, so put the CR we consumed + // "back" + const writecb = this._writecb; + this._writecb = noop; + ssCb(false, BUF_CR, 0, 1, false); + this._writecb = writecb; + } + } + } + + if (!skipPart) { + if (this._fileStream) { + let chunk; + const actualLen = Math.min(end - start, fileSizeLimit - fileSize); + if (!isDataSafe) { + chunk = Buffer.allocUnsafe(actualLen); + data.copy(chunk, 0, start, start + actualLen); + } else { + chunk = data.slice(start, start + actualLen); + } + + fileSize += chunk.length; + if (fileSize === fileSizeLimit) { + if (chunk.length > 0) + this._fileStream.push(chunk); + this._fileStream.emit('limit'); + this._fileStream.truncated = true; + skipPart = true; + } else if (!this._fileStream.push(chunk)) { + if (this._writecb) + this._fileStream._readcb = this._writecb; + this._writecb = null; + } + } else if (field !== undefined) { + let chunk; + const actualLen = Math.min( + end - start, + fieldSizeLimit - fieldSize + ); + if (!isDataSafe) { + chunk = Buffer.allocUnsafe(actualLen); + data.copy(chunk, 0, start, start + actualLen); + } else { + chunk = data.slice(start, start + actualLen); + } + + fieldSize += actualLen; + field.push(chunk); + if (fieldSize === fieldSizeLimit) { + skipPart = true; + partTruncated = true; + } + } + } + + break; + } + + if (isMatch) { + matchPostBoundary = 1; + + if (this._fileStream) { + // End the active file stream if the previous part was a file + this._fileStream.push(null); + this._fileStream = null; + } else if (field !== undefined) { + let data; + switch (field.length) { + case 0: + data = ''; + break; + case 1: + data = convertToUTF8(field[0], partCharset, 0); + break; + default: + data = convertToUTF8( + Buffer.concat(field, fieldSize), + partCharset, + 0 + ); + } + field = undefined; + fieldSize = 0; + this.emit( + 'field', + partName, + data, + { nameTruncated: false, + valueTruncated: partTruncated, + encoding: partEncoding, + mimeType: partType } + ); + } + + if (++parts === partsLimit) + this.emit('partsLimit'); + } + }; + this._bparser = new StreamSearch(`\r\n--${boundary}`, ssCb); + + this._writecb = null; + this._finalcb = null; + + // Just in case there is no preamble + this.write(BUF_CRLF); + } + + static detect(conType) { + return (conType.type === 'multipart' && conType.subtype === 'form-data'); + } + + _write(chunk, enc, cb) { + this._writecb = cb; + this._bparser.push(chunk, 0); + if (this._writecb) + callAndUnsetCb(this); + } + + _destroy(err, cb) { + this._hparser = null; + this._bparser = ignoreData; + if (!err) + err = checkEndState(this); + const fileStream = this._fileStream; + if (fileStream) { + this._fileStream = null; + fileStream.destroy(err); + } + cb(err); + } + + _final(cb) { + this._bparser.destroy(); + if (!this._complete) + return cb(new Error('Unexpected end of form')); + if (this._fileEndsLeft) + this._finalcb = finalcb.bind(null, this, cb); + else + finalcb(this, cb); + } +} + +function finalcb(self, cb, err) { + if (err) + return cb(err); + err = checkEndState(self); + cb(err); +} + +function checkEndState(self) { + if (self._hparser) + return new Error('Malformed part header'); + const fileStream = self._fileStream; + if (fileStream) { + self._fileStream = null; + fileStream.destroy(new Error('Unexpected end of file')); + } + if (!self._complete) + return new Error('Unexpected end of form'); +} + +const TOKEN = [ + 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, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, + 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 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, +]; + +const FIELD_VCHAR = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, +]; + +module.exports = Multipart; diff --git a/tests/fixtures/npm/cases/undici5/node_modules/busboy/lib/types/urlencoded.js b/tests/fixtures/npm/cases/undici5/node_modules/busboy/lib/types/urlencoded.js new file mode 100644 index 000000000..5c463a258 --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/busboy/lib/types/urlencoded.js @@ -0,0 +1,350 @@ +'use strict'; + +const { Writable } = require('stream'); + +const { getDecoder } = require('../utils.js'); + +class URLEncoded extends Writable { + constructor(cfg) { + const streamOpts = { + autoDestroy: true, + emitClose: true, + highWaterMark: (typeof cfg.highWaterMark === 'number' + ? cfg.highWaterMark + : undefined), + }; + super(streamOpts); + + let charset = (cfg.defCharset || 'utf8'); + if (cfg.conType.params && typeof cfg.conType.params.charset === 'string') + charset = cfg.conType.params.charset; + + this.charset = charset; + + const limits = cfg.limits; + this.fieldSizeLimit = (limits && typeof limits.fieldSize === 'number' + ? limits.fieldSize + : 1 * 1024 * 1024); + this.fieldsLimit = (limits && typeof limits.fields === 'number' + ? limits.fields + : Infinity); + this.fieldNameSizeLimit = ( + limits && typeof limits.fieldNameSize === 'number' + ? limits.fieldNameSize + : 100 + ); + + this._inKey = true; + this._keyTrunc = false; + this._valTrunc = false; + this._bytesKey = 0; + this._bytesVal = 0; + this._fields = 0; + this._key = ''; + this._val = ''; + this._byte = -2; + this._lastPos = 0; + this._encode = 0; + this._decoder = getDecoder(charset); + } + + static detect(conType) { + return (conType.type === 'application' + && conType.subtype === 'x-www-form-urlencoded'); + } + + _write(chunk, enc, cb) { + if (this._fields >= this.fieldsLimit) + return cb(); + + let i = 0; + const len = chunk.length; + this._lastPos = 0; + + // Check if we last ended mid-percent-encoded byte + if (this._byte !== -2) { + i = readPctEnc(this, chunk, i, len); + if (i === -1) + return cb(new Error('Malformed urlencoded form')); + if (i >= len) + return cb(); + if (this._inKey) + ++this._bytesKey; + else + ++this._bytesVal; + } + +main: + while (i < len) { + if (this._inKey) { + // Parsing key + + i = skipKeyBytes(this, chunk, i, len); + + while (i < len) { + switch (chunk[i]) { + case 61: // '=' + if (this._lastPos < i) + this._key += chunk.latin1Slice(this._lastPos, i); + this._lastPos = ++i; + this._key = this._decoder(this._key, this._encode); + this._encode = 0; + this._inKey = false; + continue main; + case 38: // '&' + if (this._lastPos < i) + this._key += chunk.latin1Slice(this._lastPos, i); + this._lastPos = ++i; + this._key = this._decoder(this._key, this._encode); + this._encode = 0; + if (this._bytesKey > 0) { + this.emit( + 'field', + this._key, + '', + { nameTruncated: this._keyTrunc, + valueTruncated: false, + encoding: this.charset, + mimeType: 'text/plain' } + ); + } + this._key = ''; + this._val = ''; + this._keyTrunc = false; + this._valTrunc = false; + this._bytesKey = 0; + this._bytesVal = 0; + if (++this._fields >= this.fieldsLimit) { + this.emit('fieldsLimit'); + return cb(); + } + continue; + case 43: // '+' + if (this._lastPos < i) + this._key += chunk.latin1Slice(this._lastPos, i); + this._key += ' '; + this._lastPos = i + 1; + break; + case 37: // '%' + if (this._encode === 0) + this._encode = 1; + if (this._lastPos < i) + this._key += chunk.latin1Slice(this._lastPos, i); + this._lastPos = i + 1; + this._byte = -1; + i = readPctEnc(this, chunk, i + 1, len); + if (i === -1) + return cb(new Error('Malformed urlencoded form')); + if (i >= len) + return cb(); + ++this._bytesKey; + i = skipKeyBytes(this, chunk, i, len); + continue; + } + ++i; + ++this._bytesKey; + i = skipKeyBytes(this, chunk, i, len); + } + if (this._lastPos < i) + this._key += chunk.latin1Slice(this._lastPos, i); + } else { + // Parsing value + + i = skipValBytes(this, chunk, i, len); + + while (i < len) { + switch (chunk[i]) { + case 38: // '&' + if (this._lastPos < i) + this._val += chunk.latin1Slice(this._lastPos, i); + this._lastPos = ++i; + this._inKey = true; + this._val = this._decoder(this._val, this._encode); + this._encode = 0; + if (this._bytesKey > 0 || this._bytesVal > 0) { + this.emit( + 'field', + this._key, + this._val, + { nameTruncated: this._keyTrunc, + valueTruncated: this._valTrunc, + encoding: this.charset, + mimeType: 'text/plain' } + ); + } + this._key = ''; + this._val = ''; + this._keyTrunc = false; + this._valTrunc = false; + this._bytesKey = 0; + this._bytesVal = 0; + if (++this._fields >= this.fieldsLimit) { + this.emit('fieldsLimit'); + return cb(); + } + continue main; + case 43: // '+' + if (this._lastPos < i) + this._val += chunk.latin1Slice(this._lastPos, i); + this._val += ' '; + this._lastPos = i + 1; + break; + case 37: // '%' + if (this._encode === 0) + this._encode = 1; + if (this._lastPos < i) + this._val += chunk.latin1Slice(this._lastPos, i); + this._lastPos = i + 1; + this._byte = -1; + i = readPctEnc(this, chunk, i + 1, len); + if (i === -1) + return cb(new Error('Malformed urlencoded form')); + if (i >= len) + return cb(); + ++this._bytesVal; + i = skipValBytes(this, chunk, i, len); + continue; + } + ++i; + ++this._bytesVal; + i = skipValBytes(this, chunk, i, len); + } + if (this._lastPos < i) + this._val += chunk.latin1Slice(this._lastPos, i); + } + } + + cb(); + } + + _final(cb) { + if (this._byte !== -2) + return cb(new Error('Malformed urlencoded form')); + if (!this._inKey || this._bytesKey > 0 || this._bytesVal > 0) { + if (this._inKey) + this._key = this._decoder(this._key, this._encode); + else + this._val = this._decoder(this._val, this._encode); + this.emit( + 'field', + this._key, + this._val, + { nameTruncated: this._keyTrunc, + valueTruncated: this._valTrunc, + encoding: this.charset, + mimeType: 'text/plain' } + ); + } + cb(); + } +} + +function readPctEnc(self, chunk, pos, len) { + if (pos >= len) + return len; + + if (self._byte === -1) { + // We saw a '%' but no hex characters yet + const hexUpper = HEX_VALUES[chunk[pos++]]; + if (hexUpper === -1) + return -1; + + if (hexUpper >= 8) + self._encode = 2; // Indicate high bits detected + + if (pos < len) { + // Both hex characters are in this chunk + const hexLower = HEX_VALUES[chunk[pos++]]; + if (hexLower === -1) + return -1; + + if (self._inKey) + self._key += String.fromCharCode((hexUpper << 4) + hexLower); + else + self._val += String.fromCharCode((hexUpper << 4) + hexLower); + + self._byte = -2; + self._lastPos = pos; + } else { + // Only one hex character was available in this chunk + self._byte = hexUpper; + } + } else { + // We saw only one hex character so far + const hexLower = HEX_VALUES[chunk[pos++]]; + if (hexLower === -1) + return -1; + + if (self._inKey) + self._key += String.fromCharCode((self._byte << 4) + hexLower); + else + self._val += String.fromCharCode((self._byte << 4) + hexLower); + + self._byte = -2; + self._lastPos = pos; + } + + return pos; +} + +function skipKeyBytes(self, chunk, pos, len) { + // Skip bytes if we've truncated + if (self._bytesKey > self.fieldNameSizeLimit) { + if (!self._keyTrunc) { + if (self._lastPos < pos) + self._key += chunk.latin1Slice(self._lastPos, pos - 1); + } + self._keyTrunc = true; + for (; pos < len; ++pos) { + const code = chunk[pos]; + if (code === 61/* '=' */ || code === 38/* '&' */) + break; + ++self._bytesKey; + } + self._lastPos = pos; + } + + return pos; +} + +function skipValBytes(self, chunk, pos, len) { + // Skip bytes if we've truncated + if (self._bytesVal > self.fieldSizeLimit) { + if (!self._valTrunc) { + if (self._lastPos < pos) + self._val += chunk.latin1Slice(self._lastPos, pos - 1); + } + self._valTrunc = true; + for (; pos < len; ++pos) { + if (chunk[pos] === 38/* '&' */) + break; + ++self._bytesVal; + } + self._lastPos = pos; + } + + return pos; +} + +/* eslint-disable no-multi-spaces */ +const HEX_VALUES = [ + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, -1, -1, -1, -1, -1, -1, + -1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, +]; +/* eslint-enable no-multi-spaces */ + +module.exports = URLEncoded; diff --git a/tests/fixtures/npm/cases/undici5/node_modules/busboy/lib/utils.js b/tests/fixtures/npm/cases/undici5/node_modules/busboy/lib/utils.js new file mode 100644 index 000000000..8274f6c3a --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/busboy/lib/utils.js @@ -0,0 +1,596 @@ +'use strict'; + +function parseContentType(str) { + if (str.length === 0) + return; + + const params = Object.create(null); + let i = 0; + + // Parse type + for (; i < str.length; ++i) { + const code = str.charCodeAt(i); + if (TOKEN[code] !== 1) { + if (code !== 47/* '/' */ || i === 0) + return; + break; + } + } + // Check for type without subtype + if (i === str.length) + return; + + const type = str.slice(0, i).toLowerCase(); + + // Parse subtype + const subtypeStart = ++i; + for (; i < str.length; ++i) { + const code = str.charCodeAt(i); + if (TOKEN[code] !== 1) { + // Make sure we have a subtype + if (i === subtypeStart) + return; + + if (parseContentTypeParams(str, i, params) === undefined) + return; + break; + } + } + // Make sure we have a subtype + if (i === subtypeStart) + return; + + const subtype = str.slice(subtypeStart, i).toLowerCase(); + + return { type, subtype, params }; +} + +function parseContentTypeParams(str, i, params) { + while (i < str.length) { + // Consume whitespace + for (; i < str.length; ++i) { + const code = str.charCodeAt(i); + if (code !== 32/* ' ' */ && code !== 9/* '\t' */) + break; + } + + // Ended on whitespace + if (i === str.length) + break; + + // Check for malformed parameter + if (str.charCodeAt(i++) !== 59/* ';' */) + return; + + // Consume whitespace + for (; i < str.length; ++i) { + const code = str.charCodeAt(i); + if (code !== 32/* ' ' */ && code !== 9/* '\t' */) + break; + } + + // Ended on whitespace (malformed) + if (i === str.length) + return; + + let name; + const nameStart = i; + // Parse parameter name + for (; i < str.length; ++i) { + const code = str.charCodeAt(i); + if (TOKEN[code] !== 1) { + if (code !== 61/* '=' */) + return; + break; + } + } + + // No value (malformed) + if (i === str.length) + return; + + name = str.slice(nameStart, i); + ++i; // Skip over '=' + + // No value (malformed) + if (i === str.length) + return; + + let value = ''; + let valueStart; + if (str.charCodeAt(i) === 34/* '"' */) { + valueStart = ++i; + let escaping = false; + // Parse quoted value + for (; i < str.length; ++i) { + const code = str.charCodeAt(i); + if (code === 92/* '\\' */) { + if (escaping) { + valueStart = i; + escaping = false; + } else { + value += str.slice(valueStart, i); + escaping = true; + } + continue; + } + if (code === 34/* '"' */) { + if (escaping) { + valueStart = i; + escaping = false; + continue; + } + value += str.slice(valueStart, i); + break; + } + if (escaping) { + valueStart = i - 1; + escaping = false; + } + // Invalid unescaped quoted character (malformed) + if (QDTEXT[code] !== 1) + return; + } + + // No end quote (malformed) + if (i === str.length) + return; + + ++i; // Skip over double quote + } else { + valueStart = i; + // Parse unquoted value + for (; i < str.length; ++i) { + const code = str.charCodeAt(i); + if (TOKEN[code] !== 1) { + // No value (malformed) + if (i === valueStart) + return; + break; + } + } + value = str.slice(valueStart, i); + } + + name = name.toLowerCase(); + if (params[name] === undefined) + params[name] = value; + } + + return params; +} + +function parseDisposition(str, defDecoder) { + if (str.length === 0) + return; + + const params = Object.create(null); + let i = 0; + + for (; i < str.length; ++i) { + const code = str.charCodeAt(i); + if (TOKEN[code] !== 1) { + if (parseDispositionParams(str, i, params, defDecoder) === undefined) + return; + break; + } + } + + const type = str.slice(0, i).toLowerCase(); + + return { type, params }; +} + +function parseDispositionParams(str, i, params, defDecoder) { + while (i < str.length) { + // Consume whitespace + for (; i < str.length; ++i) { + const code = str.charCodeAt(i); + if (code !== 32/* ' ' */ && code !== 9/* '\t' */) + break; + } + + // Ended on whitespace + if (i === str.length) + break; + + // Check for malformed parameter + if (str.charCodeAt(i++) !== 59/* ';' */) + return; + + // Consume whitespace + for (; i < str.length; ++i) { + const code = str.charCodeAt(i); + if (code !== 32/* ' ' */ && code !== 9/* '\t' */) + break; + } + + // Ended on whitespace (malformed) + if (i === str.length) + return; + + let name; + const nameStart = i; + // Parse parameter name + for (; i < str.length; ++i) { + const code = str.charCodeAt(i); + if (TOKEN[code] !== 1) { + if (code === 61/* '=' */) + break; + return; + } + } + + // No value (malformed) + if (i === str.length) + return; + + let value = ''; + let valueStart; + let charset; + //~ let lang; + name = str.slice(nameStart, i); + if (name.charCodeAt(name.length - 1) === 42/* '*' */) { + // Extended value + + const charsetStart = ++i; + // Parse charset name + for (; i < str.length; ++i) { + const code = str.charCodeAt(i); + if (CHARSET[code] !== 1) { + if (code !== 39/* '\'' */) + return; + break; + } + } + + // Incomplete charset (malformed) + if (i === str.length) + return; + + charset = str.slice(charsetStart, i); + ++i; // Skip over the '\'' + + //~ const langStart = ++i; + // Parse language name + for (; i < str.length; ++i) { + const code = str.charCodeAt(i); + if (code === 39/* '\'' */) + break; + } + + // Incomplete language (malformed) + if (i === str.length) + return; + + //~ lang = str.slice(langStart, i); + ++i; // Skip over the '\'' + + // No value (malformed) + if (i === str.length) + return; + + valueStart = i; + + let encode = 0; + // Parse value + for (; i < str.length; ++i) { + const code = str.charCodeAt(i); + if (EXTENDED_VALUE[code] !== 1) { + if (code === 37/* '%' */) { + let hexUpper; + let hexLower; + if (i + 2 < str.length + && (hexUpper = HEX_VALUES[str.charCodeAt(i + 1)]) !== -1 + && (hexLower = HEX_VALUES[str.charCodeAt(i + 2)]) !== -1) { + const byteVal = (hexUpper << 4) + hexLower; + value += str.slice(valueStart, i); + value += String.fromCharCode(byteVal); + i += 2; + valueStart = i + 1; + if (byteVal >= 128) + encode = 2; + else if (encode === 0) + encode = 1; + continue; + } + // '%' disallowed in non-percent encoded contexts (malformed) + return; + } + break; + } + } + + value += str.slice(valueStart, i); + value = convertToUTF8(value, charset, encode); + if (value === undefined) + return; + } else { + // Non-extended value + + ++i; // Skip over '=' + + // No value (malformed) + if (i === str.length) + return; + + if (str.charCodeAt(i) === 34/* '"' */) { + valueStart = ++i; + let escaping = false; + // Parse quoted value + for (; i < str.length; ++i) { + const code = str.charCodeAt(i); + if (code === 92/* '\\' */) { + if (escaping) { + valueStart = i; + escaping = false; + } else { + value += str.slice(valueStart, i); + escaping = true; + } + continue; + } + if (code === 34/* '"' */) { + if (escaping) { + valueStart = i; + escaping = false; + continue; + } + value += str.slice(valueStart, i); + break; + } + if (escaping) { + valueStart = i - 1; + escaping = false; + } + // Invalid unescaped quoted character (malformed) + if (QDTEXT[code] !== 1) + return; + } + + // No end quote (malformed) + if (i === str.length) + return; + + ++i; // Skip over double quote + } else { + valueStart = i; + // Parse unquoted value + for (; i < str.length; ++i) { + const code = str.charCodeAt(i); + if (TOKEN[code] !== 1) { + // No value (malformed) + if (i === valueStart) + return; + break; + } + } + value = str.slice(valueStart, i); + } + + value = defDecoder(value, 2); + if (value === undefined) + return; + } + + name = name.toLowerCase(); + if (params[name] === undefined) + params[name] = value; + } + + return params; +} + +function getDecoder(charset) { + let lc; + while (true) { + switch (charset) { + case 'utf-8': + case 'utf8': + return decoders.utf8; + case 'latin1': + case 'ascii': // TODO: Make these a separate, strict decoder? + case 'us-ascii': + case 'iso-8859-1': + case 'iso8859-1': + case 'iso88591': + case 'iso_8859-1': + case 'windows-1252': + case 'iso_8859-1:1987': + case 'cp1252': + case 'x-cp1252': + return decoders.latin1; + case 'utf16le': + case 'utf-16le': + case 'ucs2': + case 'ucs-2': + return decoders.utf16le; + case 'base64': + return decoders.base64; + default: + if (lc === undefined) { + lc = true; + charset = charset.toLowerCase(); + continue; + } + return decoders.other.bind(charset); + } + } +} + +const decoders = { + utf8: (data, hint) => { + if (data.length === 0) + return ''; + if (typeof data === 'string') { + // If `data` never had any percent-encoded bytes or never had any that + // were outside of the ASCII range, then we can safely just return the + // input since UTF-8 is ASCII compatible + if (hint < 2) + return data; + + data = Buffer.from(data, 'latin1'); + } + return data.utf8Slice(0, data.length); + }, + + latin1: (data, hint) => { + if (data.length === 0) + return ''; + if (typeof data === 'string') + return data; + return data.latin1Slice(0, data.length); + }, + + utf16le: (data, hint) => { + if (data.length === 0) + return ''; + if (typeof data === 'string') + data = Buffer.from(data, 'latin1'); + return data.ucs2Slice(0, data.length); + }, + + base64: (data, hint) => { + if (data.length === 0) + return ''; + if (typeof data === 'string') + data = Buffer.from(data, 'latin1'); + return data.base64Slice(0, data.length); + }, + + other: (data, hint) => { + if (data.length === 0) + return ''; + if (typeof data === 'string') + data = Buffer.from(data, 'latin1'); + try { + const decoder = new TextDecoder(this); + return decoder.decode(data); + } catch {} + }, +}; + +function convertToUTF8(data, charset, hint) { + const decode = getDecoder(charset); + if (decode) + return decode(data, hint); +} + +function basename(path) { + if (typeof path !== 'string') + return ''; + for (let i = path.length - 1; i >= 0; --i) { + switch (path.charCodeAt(i)) { + case 0x2F: // '/' + case 0x5C: // '\' + path = path.slice(i + 1); + return (path === '..' || path === '.' ? '' : path); + } + } + return (path === '..' || path === '.' ? '' : path); +} + +const TOKEN = [ + 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, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, + 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 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, +]; + +const QDTEXT = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, +]; + +const CHARSET = [ + 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, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, + 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 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, +]; + +const EXTENDED_VALUE = [ + 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, 0, 1, 1, 0, 1, 0, 0, 0, 0, 1, 0, 1, 1, 0, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, + 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 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, +]; + +/* eslint-disable no-multi-spaces */ +const HEX_VALUES = [ + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, -1, -1, -1, -1, -1, -1, + -1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, +]; +/* eslint-enable no-multi-spaces */ + +module.exports = { + basename, + convertToUTF8, + getDecoder, + parseContentType, + parseDisposition, +}; diff --git a/tests/fixtures/npm/cases/undici5/node_modules/busboy/package.json b/tests/fixtures/npm/cases/undici5/node_modules/busboy/package.json new file mode 100644 index 000000000..ac2577fe2 --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/busboy/package.json @@ -0,0 +1,22 @@ +{ "name": "busboy", + "version": "1.6.0", + "author": "Brian White ", + "description": "A streaming parser for HTML form data for node.js", + "main": "./lib/index.js", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "devDependencies": { + "@mscdex/eslint-config": "^1.1.0", + "eslint": "^7.32.0" + }, + "scripts": { + "test": "node test/test.js", + "lint": "eslint --cache --report-unused-disable-directives --ext=.js .eslintrc.js lib test bench", + "lint:fix": "npm run lint -- --fix" + }, + "engines": { "node": ">=10.16.0" }, + "keywords": [ "uploads", "forms", "multipart", "form-data" ], + "licenses": [ { "type": "MIT", "url": "http://github.com/mscdex/busboy/raw/master/LICENSE" } ], + "repository": { "type": "git", "url": "http://github.com/mscdex/busboy.git" } +} diff --git a/tests/fixtures/npm/cases/undici5/node_modules/streamsearch/LICENSE b/tests/fixtures/npm/cases/undici5/node_modules/streamsearch/LICENSE new file mode 100644 index 000000000..9ea90e039 --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/streamsearch/LICENSE @@ -0,0 +1,19 @@ +Copyright Brian White. All rights reserved. + +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. \ No newline at end of file diff --git a/tests/fixtures/npm/cases/undici5/node_modules/streamsearch/lib/sbmh.js b/tests/fixtures/npm/cases/undici5/node_modules/streamsearch/lib/sbmh.js new file mode 100644 index 000000000..510cae26e --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/streamsearch/lib/sbmh.js @@ -0,0 +1,267 @@ +'use strict'; +/* + Based heavily on the Streaming Boyer-Moore-Horspool C++ implementation + by Hongli Lai at: https://github.com/FooBarWidget/boyer-moore-horspool +*/ +function memcmp(buf1, pos1, buf2, pos2, num) { + for (let i = 0; i < num; ++i) { + if (buf1[pos1 + i] !== buf2[pos2 + i]) + return false; + } + return true; +} + +class SBMH { + constructor(needle, cb) { + if (typeof cb !== 'function') + throw new Error('Missing match callback'); + + if (typeof needle === 'string') + needle = Buffer.from(needle); + else if (!Buffer.isBuffer(needle)) + throw new Error(`Expected Buffer for needle, got ${typeof needle}`); + + const needleLen = needle.length; + + this.maxMatches = Infinity; + this.matches = 0; + + this._cb = cb; + this._lookbehindSize = 0; + this._needle = needle; + this._bufPos = 0; + + this._lookbehind = Buffer.allocUnsafe(needleLen); + + // Initialize occurrence table. + this._occ = [ + needleLen, needleLen, needleLen, needleLen, needleLen, needleLen, + needleLen, needleLen, needleLen, needleLen, needleLen, needleLen, + needleLen, needleLen, needleLen, needleLen, needleLen, needleLen, + needleLen, needleLen, needleLen, needleLen, needleLen, needleLen, + needleLen, needleLen, needleLen, needleLen, needleLen, needleLen, + needleLen, needleLen, needleLen, needleLen, needleLen, needleLen, + needleLen, needleLen, needleLen, needleLen, needleLen, needleLen, + needleLen, needleLen, needleLen, needleLen, needleLen, needleLen, + needleLen, needleLen, needleLen, needleLen, needleLen, needleLen, + needleLen, needleLen, needleLen, needleLen, needleLen, needleLen, + needleLen, needleLen, needleLen, needleLen, needleLen, needleLen, + needleLen, needleLen, needleLen, needleLen, needleLen, needleLen, + needleLen, needleLen, needleLen, needleLen, needleLen, needleLen, + needleLen, needleLen, needleLen, needleLen, needleLen, needleLen, + needleLen, needleLen, needleLen, needleLen, needleLen, needleLen, + needleLen, needleLen, needleLen, needleLen, needleLen, needleLen, + needleLen, needleLen, needleLen, needleLen, needleLen, needleLen, + needleLen, needleLen, needleLen, needleLen, needleLen, needleLen, + needleLen, needleLen, needleLen, needleLen, needleLen, needleLen, + needleLen, needleLen, needleLen, needleLen, needleLen, needleLen, + needleLen, needleLen, needleLen, needleLen, needleLen, needleLen, + needleLen, needleLen, needleLen, needleLen, needleLen, needleLen, + needleLen, needleLen, needleLen, needleLen, needleLen, needleLen, + needleLen, needleLen, needleLen, needleLen, needleLen, needleLen, + needleLen, needleLen, needleLen, needleLen, needleLen, needleLen, + needleLen, needleLen, needleLen, needleLen, needleLen, needleLen, + needleLen, needleLen, needleLen, needleLen, needleLen, needleLen, + needleLen, needleLen, needleLen, needleLen, needleLen, needleLen, + needleLen, needleLen, needleLen, needleLen, needleLen, needleLen, + needleLen, needleLen, needleLen, needleLen, needleLen, needleLen, + needleLen, needleLen, needleLen, needleLen, needleLen, needleLen, + needleLen, needleLen, needleLen, needleLen, needleLen, needleLen, + needleLen, needleLen, needleLen, needleLen, needleLen, needleLen, + needleLen, needleLen, needleLen, needleLen, needleLen, needleLen, + needleLen, needleLen, needleLen, needleLen, needleLen, needleLen, + needleLen, needleLen, needleLen, needleLen, needleLen, needleLen, + needleLen, needleLen, needleLen, needleLen, needleLen, needleLen, + needleLen, needleLen, needleLen, needleLen, needleLen, needleLen, + needleLen, needleLen, needleLen, needleLen, needleLen, needleLen, + needleLen, needleLen, needleLen, needleLen, needleLen, needleLen, + needleLen, needleLen, needleLen, needleLen, needleLen, needleLen, + needleLen, needleLen, needleLen, needleLen, needleLen, needleLen, + needleLen, needleLen, needleLen, needleLen + ]; + + // Populate occurrence table with analysis of the needle, ignoring the last + // letter. + if (needleLen > 1) { + for (let i = 0; i < needleLen - 1; ++i) + this._occ[needle[i]] = needleLen - 1 - i; + } + } + + reset() { + this.matches = 0; + this._lookbehindSize = 0; + this._bufPos = 0; + } + + push(chunk, pos) { + let result; + if (!Buffer.isBuffer(chunk)) + chunk = Buffer.from(chunk, 'latin1'); + const chunkLen = chunk.length; + this._bufPos = pos || 0; + while (result !== chunkLen && this.matches < this.maxMatches) + result = feed(this, chunk); + return result; + } + + destroy() { + const lbSize = this._lookbehindSize; + if (lbSize) + this._cb(false, this._lookbehind, 0, lbSize, false); + this.reset(); + } +} + +function feed(self, data) { + const len = data.length; + const needle = self._needle; + const needleLen = needle.length; + + // Positive: points to a position in `data` + // pos == 3 points to data[3] + // Negative: points to a position in the lookbehind buffer + // pos == -2 points to lookbehind[lookbehindSize - 2] + let pos = -self._lookbehindSize; + const lastNeedleCharPos = needleLen - 1; + const lastNeedleChar = needle[lastNeedleCharPos]; + const end = len - needleLen; + const occ = self._occ; + const lookbehind = self._lookbehind; + + if (pos < 0) { + // Lookbehind buffer is not empty. Perform Boyer-Moore-Horspool + // search with character lookup code that considers both the + // lookbehind buffer and the current round's haystack data. + // + // Loop until + // there is a match. + // or until + // we've moved past the position that requires the + // lookbehind buffer. In this case we switch to the + // optimized loop. + // or until + // the character to look at lies outside the haystack. + while (pos < 0 && pos <= end) { + const nextPos = pos + lastNeedleCharPos; + const ch = (nextPos < 0 + ? lookbehind[self._lookbehindSize + nextPos] + : data[nextPos]); + + if (ch === lastNeedleChar + && matchNeedle(self, data, pos, lastNeedleCharPos)) { + self._lookbehindSize = 0; + ++self.matches; + if (pos > -self._lookbehindSize) + self._cb(true, lookbehind, 0, self._lookbehindSize + pos, false); + else + self._cb(true, undefined, 0, 0, true); + + return (self._bufPos = pos + needleLen); + } + + pos += occ[ch]; + } + + // No match. + + // There's too few data for Boyer-Moore-Horspool to run, + // so let's use a different algorithm to skip as much as + // we can. + // Forward pos until + // the trailing part of lookbehind + data + // looks like the beginning of the needle + // or until + // pos == 0 + while (pos < 0 && !matchNeedle(self, data, pos, len - pos)) + ++pos; + + if (pos < 0) { + // Cut off part of the lookbehind buffer that has + // been processed and append the entire haystack + // into it. + const bytesToCutOff = self._lookbehindSize + pos; + + if (bytesToCutOff > 0) { + // The cut off data is guaranteed not to contain the needle. + self._cb(false, lookbehind, 0, bytesToCutOff, false); + } + + self._lookbehindSize -= bytesToCutOff; + lookbehind.copy(lookbehind, 0, bytesToCutOff, self._lookbehindSize); + lookbehind.set(data, self._lookbehindSize); + self._lookbehindSize += len; + + self._bufPos = len; + return len; + } + + // Discard lookbehind buffer. + self._cb(false, lookbehind, 0, self._lookbehindSize, false); + self._lookbehindSize = 0; + } + + pos += self._bufPos; + + const firstNeedleChar = needle[0]; + + // Lookbehind buffer is now empty. Perform Boyer-Moore-Horspool + // search with optimized character lookup code that only considers + // the current round's haystack data. + while (pos <= end) { + const ch = data[pos + lastNeedleCharPos]; + + if (ch === lastNeedleChar + && data[pos] === firstNeedleChar + && memcmp(needle, 0, data, pos, lastNeedleCharPos)) { + ++self.matches; + if (pos > 0) + self._cb(true, data, self._bufPos, pos, true); + else + self._cb(true, undefined, 0, 0, true); + + return (self._bufPos = pos + needleLen); + } + + pos += occ[ch]; + } + + // There was no match. If there's trailing haystack data that we cannot + // match yet using the Boyer-Moore-Horspool algorithm (because the trailing + // data is less than the needle size) then match using a modified + // algorithm that starts matching from the beginning instead of the end. + // Whatever trailing data is left after running this algorithm is added to + // the lookbehind buffer. + while (pos < len) { + if (data[pos] !== firstNeedleChar + || !memcmp(data, pos, needle, 0, len - pos)) { + ++pos; + continue; + } + data.copy(lookbehind, 0, pos, len); + self._lookbehindSize = len - pos; + break; + } + + // Everything until `pos` is guaranteed not to contain needle data. + if (pos > 0) + self._cb(false, data, self._bufPos, pos < len ? pos : len, true); + + self._bufPos = len; + return len; +} + +function matchNeedle(self, data, pos, len) { + const lb = self._lookbehind; + const lbSize = self._lookbehindSize; + const needle = self._needle; + + for (let i = 0; i < len; ++i, ++pos) { + const ch = (pos < 0 ? lb[lbSize + pos] : data[pos]); + if (ch !== needle[i]) + return false; + } + return true; +} + +module.exports = SBMH; diff --git a/tests/fixtures/npm/cases/undici5/node_modules/streamsearch/package.json b/tests/fixtures/npm/cases/undici5/node_modules/streamsearch/package.json new file mode 100644 index 000000000..51df8f970 --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/streamsearch/package.json @@ -0,0 +1,34 @@ +{ + "name": "streamsearch", + "version": "1.1.0", + "author": "Brian White ", + "description": "Streaming Boyer-Moore-Horspool searching for node.js", + "main": "./lib/sbmh.js", + "engines": { + "node": ">=10.0.0" + }, + "devDependencies": { + "@mscdex/eslint-config": "^1.1.0", + "eslint": "^7.32.0" + }, + "scripts": { + "test": "node test/test.js", + "lint": "eslint --cache --report-unused-disable-directives --ext=.js .eslintrc.js lib test", + "lint:fix": "npm run lint -- --fix" + }, + "keywords": [ + "stream", + "horspool", + "boyer-moore-horspool", + "boyer-moore", + "search" + ], + "licenses": [{ + "type": "MIT", + "url": "http://github.com/mscdex/streamsearch/raw/master/LICENSE" + }], + "repository": { + "type": "git", + "url": "http://github.com/mscdex/streamsearch.git" + } +} diff --git a/tests/fixtures/npm/cases/undici5/node_modules/undici/LICENSE b/tests/fixtures/npm/cases/undici5/node_modules/undici/LICENSE new file mode 100644 index 000000000..e7323bb52 --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/undici/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) Matteo Collina and Undici contributors + +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/fixtures/npm/cases/undici5/node_modules/undici/index-fetch.js b/tests/fixtures/npm/cases/undici5/node_modules/undici/index-fetch.js new file mode 100644 index 000000000..0d59d254f --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/undici/index-fetch.js @@ -0,0 +1,16 @@ +'use strict' + +const fetchImpl = require('./lib/fetch').fetch + +module.exports.fetch = async function fetch (resource) { + try { + return await fetchImpl(...arguments) + } catch (err) { + Error.captureStackTrace(err, this) + throw err + } +} +module.exports.FormData = require('./lib/fetch/formdata').FormData +module.exports.Headers = require('./lib/fetch/headers').Headers +module.exports.Response = require('./lib/fetch/response').Response +module.exports.Request = require('./lib/fetch/request').Request diff --git a/tests/fixtures/npm/cases/undici5/node_modules/undici/index.d.ts b/tests/fixtures/npm/cases/undici5/node_modules/undici/index.d.ts new file mode 100644 index 000000000..1aaf14d38 --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/undici/index.d.ts @@ -0,0 +1,52 @@ +import Dispatcher = require('./types/dispatcher') +import { setGlobalDispatcher, getGlobalDispatcher } from './types/global-dispatcher' +import { setGlobalOrigin, getGlobalOrigin } from './types/global-origin' +import Pool = require('./types/pool') +import { RedirectHandler, DecoratorHandler } from './types/handlers' + +import BalancedPool = require('./types/balanced-pool') +import Client = require('./types/client') +import buildConnector = require('./types/connector') +import errors = require('./types/errors') +import Agent = require('./types/agent') +import MockClient = require('./types/mock-client') +import MockPool = require('./types/mock-pool') +import MockAgent = require('./types/mock-agent') +import mockErrors = require('./types/mock-errors') +import ProxyAgent = require('./types/proxy-agent') +import { request, pipeline, stream, connect, upgrade } from './types/api' + +export * from './types/fetch' +export * from './types/file' +export * from './types/filereader' +export * from './types/formdata' +export * from './types/diagnostics-channel' +export { Interceptable } from './types/mock-interceptor' + +export { Dispatcher, BalancedPool, Pool, Client, buildConnector, errors, Agent, request, stream, pipeline, connect, upgrade, setGlobalDispatcher, getGlobalDispatcher, setGlobalOrigin, getGlobalOrigin, MockClient, MockPool, MockAgent, mockErrors, ProxyAgent, RedirectHandler, DecoratorHandler } +export default Undici + +declare namespace Undici { + var Dispatcher: typeof import('./types/dispatcher') + var Pool: typeof import('./types/pool'); + var RedirectHandler: typeof import ('./types/handlers').RedirectHandler + var DecoratorHandler: typeof import ('./types/handlers').DecoratorHandler + var createRedirectInterceptor: typeof import ('./types/interceptors').createRedirectInterceptor + var BalancedPool: typeof import('./types/balanced-pool'); + var Client: typeof import('./types/client'); + var buildConnector: typeof import('./types/connector'); + var errors: typeof import('./types/errors'); + var Agent: typeof import('./types/agent'); + var setGlobalDispatcher: typeof import('./types/global-dispatcher').setGlobalDispatcher; + var getGlobalDispatcher: typeof import('./types/global-dispatcher').getGlobalDispatcher; + var request: typeof import('./types/api').request; + var stream: typeof import('./types/api').stream; + var pipeline: typeof import('./types/api').pipeline; + var connect: typeof import('./types/api').connect; + var upgrade: typeof import('./types/api').upgrade; + var MockClient: typeof import('./types/mock-client'); + var MockPool: typeof import('./types/mock-pool'); + var MockAgent: typeof import('./types/mock-agent'); + var mockErrors: typeof import('./types/mock-errors'); + var fetch: typeof import('./types/fetch').fetch; +} diff --git a/tests/fixtures/npm/cases/undici5/node_modules/undici/index.js b/tests/fixtures/npm/cases/undici5/node_modules/undici/index.js new file mode 100644 index 000000000..ad01a4692 --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/undici/index.js @@ -0,0 +1,131 @@ +'use strict' + +const Client = require('./lib/client') +const Dispatcher = require('./lib/dispatcher') +const errors = require('./lib/core/errors') +const Pool = require('./lib/pool') +const BalancedPool = require('./lib/balanced-pool') +const Agent = require('./lib/agent') +const util = require('./lib/core/util') +const { InvalidArgumentError } = errors +const api = require('./lib/api') +const buildConnector = require('./lib/core/connect') +const MockClient = require('./lib/mock/mock-client') +const MockAgent = require('./lib/mock/mock-agent') +const MockPool = require('./lib/mock/mock-pool') +const mockErrors = require('./lib/mock/mock-errors') +const ProxyAgent = require('./lib/proxy-agent') +const { getGlobalDispatcher, setGlobalDispatcher } = require('./lib/global') +const DecoratorHandler = require('./lib/handler/DecoratorHandler') +const RedirectHandler = require('./lib/handler/RedirectHandler') +const createRedirectInterceptor = require('./lib/interceptor/redirectInterceptor') + +const nodeVersion = process.versions.node.split('.') +const nodeMajor = Number(nodeVersion[0]) +const nodeMinor = Number(nodeVersion[1]) + +Object.assign(Dispatcher.prototype, api) + +module.exports.Dispatcher = Dispatcher +module.exports.Client = Client +module.exports.Pool = Pool +module.exports.BalancedPool = BalancedPool +module.exports.Agent = Agent +module.exports.ProxyAgent = ProxyAgent + +module.exports.DecoratorHandler = DecoratorHandler +module.exports.RedirectHandler = RedirectHandler +module.exports.createRedirectInterceptor = createRedirectInterceptor + +module.exports.buildConnector = buildConnector +module.exports.errors = errors + +function makeDispatcher (fn) { + return (url, opts, handler) => { + if (typeof opts === 'function') { + handler = opts + opts = null + } + + if (!url || (typeof url !== 'string' && typeof url !== 'object' && !(url instanceof URL))) { + throw new InvalidArgumentError('invalid url') + } + + if (opts != null && typeof opts !== 'object') { + throw new InvalidArgumentError('invalid opts') + } + + if (opts && opts.path != null) { + if (typeof opts.path !== 'string') { + throw new InvalidArgumentError('invalid opts.path') + } + + let path = opts.path + if (!opts.path.startsWith('/')) { + path = `/${path}` + } + + url = new URL(util.parseOrigin(url).origin + path) + } else { + if (!opts) { + opts = typeof url === 'object' ? url : {} + } + + url = util.parseURL(url) + } + + const { agent, dispatcher = getGlobalDispatcher() } = opts + + if (agent) { + throw new InvalidArgumentError('unsupported opts.agent. Did you mean opts.client?') + } + + return fn.call(dispatcher, { + ...opts, + origin: url.origin, + path: url.search ? `${url.pathname}${url.search}` : url.pathname, + method: opts.method || (opts.body ? 'PUT' : 'GET') + }, handler) + } +} + +module.exports.setGlobalDispatcher = setGlobalDispatcher +module.exports.getGlobalDispatcher = getGlobalDispatcher + +if (nodeMajor > 16 || (nodeMajor === 16 && nodeMinor >= 8)) { + let fetchImpl = null + module.exports.fetch = async function fetch (resource) { + if (!fetchImpl) { + fetchImpl = require('./lib/fetch').fetch + } + + try { + return await fetchImpl(...arguments) + } catch (err) { + Error.captureStackTrace(err, this) + throw err + } + } + module.exports.Headers = require('./lib/fetch/headers').Headers + module.exports.Response = require('./lib/fetch/response').Response + module.exports.Request = require('./lib/fetch/request').Request + module.exports.FormData = require('./lib/fetch/formdata').FormData + module.exports.File = require('./lib/fetch/file').File + module.exports.FileReader = require('./lib/fileapi/filereader').FileReader + + const { setGlobalOrigin, getGlobalOrigin } = require('./lib/fetch/global') + + module.exports.setGlobalOrigin = setGlobalOrigin + module.exports.getGlobalOrigin = getGlobalOrigin +} + +module.exports.request = makeDispatcher(api.request) +module.exports.stream = makeDispatcher(api.stream) +module.exports.pipeline = makeDispatcher(api.pipeline) +module.exports.connect = makeDispatcher(api.connect) +module.exports.upgrade = makeDispatcher(api.upgrade) + +module.exports.MockClient = MockClient +module.exports.MockPool = MockPool +module.exports.MockAgent = MockAgent +module.exports.mockErrors = mockErrors diff --git a/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/agent.js b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/agent.js new file mode 100644 index 000000000..0b18f2a91 --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/agent.js @@ -0,0 +1,148 @@ +'use strict' + +const { InvalidArgumentError } = require('./core/errors') +const { kClients, kRunning, kClose, kDestroy, kDispatch, kInterceptors } = require('./core/symbols') +const DispatcherBase = require('./dispatcher-base') +const Pool = require('./pool') +const Client = require('./client') +const util = require('./core/util') +const createRedirectInterceptor = require('./interceptor/redirectInterceptor') +const { WeakRef, FinalizationRegistry } = require('./compat/dispatcher-weakref')() + +const kOnConnect = Symbol('onConnect') +const kOnDisconnect = Symbol('onDisconnect') +const kOnConnectionError = Symbol('onConnectionError') +const kMaxRedirections = Symbol('maxRedirections') +const kOnDrain = Symbol('onDrain') +const kFactory = Symbol('factory') +const kFinalizer = Symbol('finalizer') +const kOptions = Symbol('options') + +function defaultFactory (origin, opts) { + return opts && opts.connections === 1 + ? new Client(origin, opts) + : new Pool(origin, opts) +} + +class Agent extends DispatcherBase { + constructor ({ factory = defaultFactory, maxRedirections = 0, connect, ...options } = {}) { + super() + + if (typeof factory !== 'function') { + throw new InvalidArgumentError('factory must be a function.') + } + + if (connect != null && typeof connect !== 'function' && typeof connect !== 'object') { + throw new InvalidArgumentError('connect must be a function or an object') + } + + if (!Number.isInteger(maxRedirections) || maxRedirections < 0) { + throw new InvalidArgumentError('maxRedirections must be a positive number') + } + + if (connect && typeof connect !== 'function') { + connect = { ...connect } + } + + this[kInterceptors] = options.interceptors && options.interceptors.Agent && Array.isArray(options.interceptors.Agent) + ? options.interceptors.Agent + : [createRedirectInterceptor({ maxRedirections })] + + this[kOptions] = { ...util.deepClone(options), connect } + this[kOptions].interceptors = options.interceptors + ? { ...options.interceptors } + : undefined + this[kMaxRedirections] = maxRedirections + this[kFactory] = factory + this[kClients] = new Map() + this[kFinalizer] = new FinalizationRegistry(/* istanbul ignore next: gc is undeterministic */ key => { + const ref = this[kClients].get(key) + if (ref !== undefined && ref.deref() === undefined) { + this[kClients].delete(key) + } + }) + + const agent = this + + this[kOnDrain] = (origin, targets) => { + agent.emit('drain', origin, [agent, ...targets]) + } + + this[kOnConnect] = (origin, targets) => { + agent.emit('connect', origin, [agent, ...targets]) + } + + this[kOnDisconnect] = (origin, targets, err) => { + agent.emit('disconnect', origin, [agent, ...targets], err) + } + + this[kOnConnectionError] = (origin, targets, err) => { + agent.emit('connectionError', origin, [agent, ...targets], err) + } + } + + get [kRunning] () { + let ret = 0 + for (const ref of this[kClients].values()) { + const client = ref.deref() + /* istanbul ignore next: gc is undeterministic */ + if (client) { + ret += client[kRunning] + } + } + return ret + } + + [kDispatch] (opts, handler) { + let key + if (opts.origin && (typeof opts.origin === 'string' || opts.origin instanceof URL)) { + key = String(opts.origin) + } else { + throw new InvalidArgumentError('opts.origin must be a non-empty string or URL.') + } + + const ref = this[kClients].get(key) + + let dispatcher = ref ? ref.deref() : null + if (!dispatcher) { + dispatcher = this[kFactory](opts.origin, this[kOptions]) + .on('drain', this[kOnDrain]) + .on('connect', this[kOnConnect]) + .on('disconnect', this[kOnDisconnect]) + .on('connectionError', this[kOnConnectionError]) + + this[kClients].set(key, new WeakRef(dispatcher)) + this[kFinalizer].register(dispatcher, key) + } + + return dispatcher.dispatch(opts, handler) + } + + async [kClose] () { + const closePromises = [] + for (const ref of this[kClients].values()) { + const client = ref.deref() + /* istanbul ignore else: gc is undeterministic */ + if (client) { + closePromises.push(client.close()) + } + } + + await Promise.all(closePromises) + } + + async [kDestroy] (err) { + const destroyPromises = [] + for (const ref of this[kClients].values()) { + const client = ref.deref() + /* istanbul ignore else: gc is undeterministic */ + if (client) { + destroyPromises.push(client.destroy(err)) + } + } + + await Promise.all(destroyPromises) + } +} + +module.exports = Agent diff --git a/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/api/abort-signal.js b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/api/abort-signal.js new file mode 100644 index 000000000..895629aa4 --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/api/abort-signal.js @@ -0,0 +1,57 @@ +const { RequestAbortedError } = require('../core/errors') + +const kListener = Symbol('kListener') +const kSignal = Symbol('kSignal') + +function abort (self) { + if (self.abort) { + self.abort() + } else { + self.onError(new RequestAbortedError()) + } +} + +function addSignal (self, signal) { + self[kSignal] = null + self[kListener] = null + + if (!signal) { + return + } + + if (signal.aborted) { + abort(self) + return + } + + self[kSignal] = signal + self[kListener] = () => { + abort(self) + } + + if ('addEventListener' in self[kSignal]) { + self[kSignal].addEventListener('abort', self[kListener]) + } else { + self[kSignal].addListener('abort', self[kListener]) + } +} + +function removeSignal (self) { + if (!self[kSignal]) { + return + } + + if ('removeEventListener' in self[kSignal]) { + self[kSignal].removeEventListener('abort', self[kListener]) + } else { + self[kSignal].removeListener('abort', self[kListener]) + } + + self[kSignal] = null + self[kListener] = null +} + +module.exports = { + addSignal, + removeSignal +} diff --git a/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/api/api-connect.js b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/api/api-connect.js new file mode 100644 index 000000000..0503b1a2f --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/api/api-connect.js @@ -0,0 +1,98 @@ +'use strict' + +const { InvalidArgumentError, RequestAbortedError, SocketError } = require('../core/errors') +const { AsyncResource } = require('async_hooks') +const util = require('../core/util') +const { addSignal, removeSignal } = require('./abort-signal') + +class ConnectHandler extends AsyncResource { + constructor (opts, callback) { + if (!opts || typeof opts !== 'object') { + throw new InvalidArgumentError('invalid opts') + } + + if (typeof callback !== 'function') { + throw new InvalidArgumentError('invalid callback') + } + + const { signal, opaque, responseHeaders } = opts + + if (signal && typeof signal.on !== 'function' && typeof signal.addEventListener !== 'function') { + throw new InvalidArgumentError('signal must be an EventEmitter or EventTarget') + } + + super('UNDICI_CONNECT') + + this.opaque = opaque || null + this.responseHeaders = responseHeaders || null + this.callback = callback + this.abort = null + + addSignal(this, signal) + } + + onConnect (abort, context) { + if (!this.callback) { + throw new RequestAbortedError() + } + + this.abort = abort + this.context = context + } + + onHeaders () { + throw new SocketError('bad connect', null) + } + + onUpgrade (statusCode, rawHeaders, socket) { + const { callback, opaque, context } = this + + removeSignal(this) + + this.callback = null + const headers = this.responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders) + this.runInAsyncScope(callback, null, null, { + statusCode, + headers, + socket, + opaque, + context + }) + } + + onError (err) { + const { callback, opaque } = this + + removeSignal(this) + + if (callback) { + this.callback = null + queueMicrotask(() => { + this.runInAsyncScope(callback, null, err, { opaque }) + }) + } + } +} + +function connect (opts, callback) { + if (callback === undefined) { + return new Promise((resolve, reject) => { + connect.call(this, opts, (err, data) => { + return err ? reject(err) : resolve(data) + }) + }) + } + + try { + const connectHandler = new ConnectHandler(opts, callback) + this.dispatch({ ...opts, method: 'CONNECT' }, connectHandler) + } catch (err) { + if (typeof callback !== 'function') { + throw err + } + const opaque = opts && opts.opaque + queueMicrotask(() => callback(err, { opaque })) + } +} + +module.exports = connect diff --git a/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/api/api-pipeline.js b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/api/api-pipeline.js new file mode 100644 index 000000000..af4a1803b --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/api/api-pipeline.js @@ -0,0 +1,249 @@ +'use strict' + +const { + Readable, + Duplex, + PassThrough +} = require('stream') +const { + InvalidArgumentError, + InvalidReturnValueError, + RequestAbortedError +} = require('../core/errors') +const util = require('../core/util') +const { AsyncResource } = require('async_hooks') +const { addSignal, removeSignal } = require('./abort-signal') +const assert = require('assert') + +const kResume = Symbol('resume') + +class PipelineRequest extends Readable { + constructor () { + super({ autoDestroy: true }) + + this[kResume] = null + } + + _read () { + const { [kResume]: resume } = this + + if (resume) { + this[kResume] = null + resume() + } + } + + _destroy (err, callback) { + this._read() + + callback(err) + } +} + +class PipelineResponse extends Readable { + constructor (resume) { + super({ autoDestroy: true }) + this[kResume] = resume + } + + _read () { + this[kResume]() + } + + _destroy (err, callback) { + if (!err && !this._readableState.endEmitted) { + err = new RequestAbortedError() + } + + callback(err) + } +} + +class PipelineHandler extends AsyncResource { + constructor (opts, handler) { + if (!opts || typeof opts !== 'object') { + throw new InvalidArgumentError('invalid opts') + } + + if (typeof handler !== 'function') { + throw new InvalidArgumentError('invalid handler') + } + + const { signal, method, opaque, onInfo, responseHeaders } = opts + + if (signal && typeof signal.on !== 'function' && typeof signal.addEventListener !== 'function') { + throw new InvalidArgumentError('signal must be an EventEmitter or EventTarget') + } + + if (method === 'CONNECT') { + throw new InvalidArgumentError('invalid method') + } + + if (onInfo && typeof onInfo !== 'function') { + throw new InvalidArgumentError('invalid onInfo callback') + } + + super('UNDICI_PIPELINE') + + this.opaque = opaque || null + this.responseHeaders = responseHeaders || null + this.handler = handler + this.abort = null + this.context = null + this.onInfo = onInfo || null + + this.req = new PipelineRequest().on('error', util.nop) + + this.ret = new Duplex({ + readableObjectMode: opts.objectMode, + autoDestroy: true, + read: () => { + const { body } = this + + if (body && body.resume) { + body.resume() + } + }, + write: (chunk, encoding, callback) => { + const { req } = this + + if (req.push(chunk, encoding) || req._readableState.destroyed) { + callback() + } else { + req[kResume] = callback + } + }, + destroy: (err, callback) => { + const { body, req, res, ret, abort } = this + + if (!err && !ret._readableState.endEmitted) { + err = new RequestAbortedError() + } + + if (abort && err) { + abort() + } + + util.destroy(body, err) + util.destroy(req, err) + util.destroy(res, err) + + removeSignal(this) + + callback(err) + } + }).on('prefinish', () => { + const { req } = this + + // Node < 15 does not call _final in same tick. + req.push(null) + }) + + this.res = null + + addSignal(this, signal) + } + + onConnect (abort, context) { + const { ret, res } = this + + assert(!res, 'pipeline cannot be retried') + + if (ret.destroyed) { + throw new RequestAbortedError() + } + + this.abort = abort + this.context = context + } + + onHeaders (statusCode, rawHeaders, resume) { + const { opaque, handler, context } = this + + if (statusCode < 200) { + if (this.onInfo) { + const headers = this.responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders) + this.onInfo({ statusCode, headers }) + } + return + } + + this.res = new PipelineResponse(resume) + + let body + try { + this.handler = null + const headers = this.responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders) + body = this.runInAsyncScope(handler, null, { + statusCode, + headers, + opaque, + body: this.res, + context + }) + } catch (err) { + this.res.on('error', util.nop) + throw err + } + + if (!body || typeof body.on !== 'function') { + throw new InvalidReturnValueError('expected Readable') + } + + body + .on('data', (chunk) => { + const { ret, body } = this + + if (!ret.push(chunk) && body.pause) { + body.pause() + } + }) + .on('error', (err) => { + const { ret } = this + + util.destroy(ret, err) + }) + .on('end', () => { + const { ret } = this + + ret.push(null) + }) + .on('close', () => { + const { ret } = this + + if (!ret._readableState.ended) { + util.destroy(ret, new RequestAbortedError()) + } + }) + + this.body = body + } + + onData (chunk) { + const { res } = this + return res.push(chunk) + } + + onComplete (trailers) { + const { res } = this + res.push(null) + } + + onError (err) { + const { ret } = this + this.handler = null + util.destroy(ret, err) + } +} + +function pipeline (opts, handler) { + try { + const pipelineHandler = new PipelineHandler(opts, handler) + this.dispatch({ ...opts, body: pipelineHandler.req }, pipelineHandler) + return pipelineHandler.ret + } catch (err) { + return new PassThrough().destroy(err) + } +} + +module.exports = pipeline diff --git a/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/api/api-request.js b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/api/api-request.js new file mode 100644 index 000000000..b4674878d --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/api/api-request.js @@ -0,0 +1,203 @@ +'use strict' + +const Readable = require('./readable') +const { + InvalidArgumentError, + RequestAbortedError, + ResponseStatusCodeError +} = require('../core/errors') +const util = require('../core/util') +const { AsyncResource } = require('async_hooks') +const { addSignal, removeSignal } = require('./abort-signal') + +class RequestHandler extends AsyncResource { + constructor (opts, callback) { + if (!opts || typeof opts !== 'object') { + throw new InvalidArgumentError('invalid opts') + } + + const { signal, method, opaque, body, onInfo, responseHeaders, throwOnError } = opts + + try { + if (typeof callback !== 'function') { + throw new InvalidArgumentError('invalid callback') + } + + if (signal && typeof signal.on !== 'function' && typeof signal.addEventListener !== 'function') { + throw new InvalidArgumentError('signal must be an EventEmitter or EventTarget') + } + + if (method === 'CONNECT') { + throw new InvalidArgumentError('invalid method') + } + + if (onInfo && typeof onInfo !== 'function') { + throw new InvalidArgumentError('invalid onInfo callback') + } + + super('UNDICI_REQUEST') + } catch (err) { + if (util.isStream(body)) { + util.destroy(body.on('error', util.nop), err) + } + throw err + } + + this.responseHeaders = responseHeaders || null + this.opaque = opaque || null + this.callback = callback + this.res = null + this.abort = null + this.body = body + this.trailers = {} + this.context = null + this.onInfo = onInfo || null + this.throwOnError = throwOnError + + if (util.isStream(body)) { + body.on('error', (err) => { + this.onError(err) + }) + } + + addSignal(this, signal) + } + + onConnect (abort, context) { + if (!this.callback) { + throw new RequestAbortedError() + } + + this.abort = abort + this.context = context + } + + onHeaders (statusCode, rawHeaders, resume, statusMessage) { + const { callback, opaque, abort, context } = this + + if (statusCode < 200) { + if (this.onInfo) { + const headers = this.responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders) + this.onInfo({ statusCode, headers }) + } + return + } + + const parsedHeaders = util.parseHeaders(rawHeaders) + const contentType = parsedHeaders['content-type'] + const body = new Readable(resume, abort, contentType) + + this.callback = null + this.res = body + const headers = this.responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders) + + if (callback !== null) { + if (this.throwOnError && statusCode >= 400) { + this.runInAsyncScope(getResolveErrorBodyCallback, null, + { callback, body, contentType, statusCode, statusMessage, headers } + ) + return + } + + this.runInAsyncScope(callback, null, null, { + statusCode, + headers, + trailers: this.trailers, + opaque, + body, + context + }) + } + } + + onData (chunk) { + const { res } = this + return res.push(chunk) + } + + onComplete (trailers) { + const { res } = this + + removeSignal(this) + + util.parseHeaders(trailers, this.trailers) + + res.push(null) + } + + onError (err) { + const { res, callback, body, opaque } = this + + removeSignal(this) + + if (callback) { + // TODO: Does this need queueMicrotask? + this.callback = null + queueMicrotask(() => { + this.runInAsyncScope(callback, null, err, { opaque }) + }) + } + + if (res) { + this.res = null + // Ensure all queued handlers are invoked before destroying res. + queueMicrotask(() => { + util.destroy(res, err) + }) + } + + if (body) { + this.body = null + util.destroy(body, err) + } + } +} + +async function getResolveErrorBodyCallback ({ callback, body, contentType, statusCode, statusMessage, headers }) { + if (statusCode === 204 || !contentType) { + body.dump() + process.nextTick(callback, new ResponseStatusCodeError(`Response status code ${statusCode}${statusMessage ? `: ${statusMessage}` : ''}`, statusCode, headers)) + return + } + + try { + if (contentType.startsWith('application/json')) { + const payload = await body.json() + process.nextTick(callback, new ResponseStatusCodeError(`Response status code ${statusCode}${statusMessage ? `: ${statusMessage}` : ''}`, statusCode, headers, payload)) + return + } + + if (contentType.startsWith('text/')) { + const payload = await body.text() + process.nextTick(callback, new ResponseStatusCodeError(`Response status code ${statusCode}${statusMessage ? `: ${statusMessage}` : ''}`, statusCode, headers, payload)) + return + } + } catch (err) { + // Process in a fallback if error + } + + body.dump() + process.nextTick(callback, new ResponseStatusCodeError(`Response status code ${statusCode}${statusMessage ? `: ${statusMessage}` : ''}`, statusCode, headers)) +} + +function request (opts, callback) { + if (callback === undefined) { + return new Promise((resolve, reject) => { + request.call(this, opts, (err, data) => { + return err ? reject(err) : resolve(data) + }) + }) + } + + try { + this.dispatch(opts, new RequestHandler(opts, callback)) + } catch (err) { + if (typeof callback !== 'function') { + throw err + } + const opaque = opts && opts.opaque + queueMicrotask(() => callback(err, { opaque })) + } +} + +module.exports = request diff --git a/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/api/api-stream.js b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/api/api-stream.js new file mode 100644 index 000000000..f33f459f9 --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/api/api-stream.js @@ -0,0 +1,195 @@ +'use strict' + +const { finished } = require('stream') +const { + InvalidArgumentError, + InvalidReturnValueError, + RequestAbortedError +} = require('../core/errors') +const util = require('../core/util') +const { AsyncResource } = require('async_hooks') +const { addSignal, removeSignal } = require('./abort-signal') + +class StreamHandler extends AsyncResource { + constructor (opts, factory, callback) { + if (!opts || typeof opts !== 'object') { + throw new InvalidArgumentError('invalid opts') + } + + const { signal, method, opaque, body, onInfo, responseHeaders } = opts + + try { + if (typeof callback !== 'function') { + throw new InvalidArgumentError('invalid callback') + } + + if (typeof factory !== 'function') { + throw new InvalidArgumentError('invalid factory') + } + + if (signal && typeof signal.on !== 'function' && typeof signal.addEventListener !== 'function') { + throw new InvalidArgumentError('signal must be an EventEmitter or EventTarget') + } + + if (method === 'CONNECT') { + throw new InvalidArgumentError('invalid method') + } + + if (onInfo && typeof onInfo !== 'function') { + throw new InvalidArgumentError('invalid onInfo callback') + } + + super('UNDICI_STREAM') + } catch (err) { + if (util.isStream(body)) { + util.destroy(body.on('error', util.nop), err) + } + throw err + } + + this.responseHeaders = responseHeaders || null + this.opaque = opaque || null + this.factory = factory + this.callback = callback + this.res = null + this.abort = null + this.context = null + this.trailers = null + this.body = body + this.onInfo = onInfo || null + + if (util.isStream(body)) { + body.on('error', (err) => { + this.onError(err) + }) + } + + addSignal(this, signal) + } + + onConnect (abort, context) { + if (!this.callback) { + throw new RequestAbortedError() + } + + this.abort = abort + this.context = context + } + + onHeaders (statusCode, rawHeaders, resume) { + const { factory, opaque, context } = this + + if (statusCode < 200) { + if (this.onInfo) { + const headers = this.responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders) + this.onInfo({ statusCode, headers }) + } + return + } + + this.factory = null + const headers = this.responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders) + const res = this.runInAsyncScope(factory, null, { + statusCode, + headers, + opaque, + context + }) + + if ( + !res || + typeof res.write !== 'function' || + typeof res.end !== 'function' || + typeof res.on !== 'function' + ) { + throw new InvalidReturnValueError('expected Writable') + } + + res.on('drain', resume) + // TODO: Avoid finished. It registers an unnecessary amount of listeners. + finished(res, { readable: false }, (err) => { + const { callback, res, opaque, trailers, abort } = this + + this.res = null + if (err || !res.readable) { + util.destroy(res, err) + } + + this.callback = null + this.runInAsyncScope(callback, null, err || null, { opaque, trailers }) + + if (err) { + abort() + } + }) + + this.res = res + + const needDrain = res.writableNeedDrain !== undefined + ? res.writableNeedDrain + : res._writableState && res._writableState.needDrain + + return needDrain !== true + } + + onData (chunk) { + const { res } = this + + return res.write(chunk) + } + + onComplete (trailers) { + const { res } = this + + removeSignal(this) + + this.trailers = util.parseHeaders(trailers) + + res.end() + } + + onError (err) { + const { res, callback, opaque, body } = this + + removeSignal(this) + + this.factory = null + + if (res) { + this.res = null + util.destroy(res, err) + } else if (callback) { + this.callback = null + queueMicrotask(() => { + this.runInAsyncScope(callback, null, err, { opaque }) + }) + } + + if (body) { + this.body = null + util.destroy(body, err) + } + } +} + +function stream (opts, factory, callback) { + if (callback === undefined) { + return new Promise((resolve, reject) => { + stream.call(this, opts, factory, (err, data) => { + return err ? reject(err) : resolve(data) + }) + }) + } + + try { + this.dispatch(opts, new StreamHandler(opts, factory, callback)) + } catch (err) { + if (typeof callback !== 'function') { + throw err + } + const opaque = opts && opts.opaque + queueMicrotask(() => callback(err, { opaque })) + } +} + +module.exports = stream diff --git a/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/api/api-upgrade.js b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/api/api-upgrade.js new file mode 100644 index 000000000..ef783e829 --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/api/api-upgrade.js @@ -0,0 +1,105 @@ +'use strict' + +const { InvalidArgumentError, RequestAbortedError, SocketError } = require('../core/errors') +const { AsyncResource } = require('async_hooks') +const util = require('../core/util') +const { addSignal, removeSignal } = require('./abort-signal') +const assert = require('assert') + +class UpgradeHandler extends AsyncResource { + constructor (opts, callback) { + if (!opts || typeof opts !== 'object') { + throw new InvalidArgumentError('invalid opts') + } + + if (typeof callback !== 'function') { + throw new InvalidArgumentError('invalid callback') + } + + const { signal, opaque, responseHeaders } = opts + + if (signal && typeof signal.on !== 'function' && typeof signal.addEventListener !== 'function') { + throw new InvalidArgumentError('signal must be an EventEmitter or EventTarget') + } + + super('UNDICI_UPGRADE') + + this.responseHeaders = responseHeaders || null + this.opaque = opaque || null + this.callback = callback + this.abort = null + this.context = null + + addSignal(this, signal) + } + + onConnect (abort, context) { + if (!this.callback) { + throw new RequestAbortedError() + } + + this.abort = abort + this.context = null + } + + onHeaders () { + throw new SocketError('bad upgrade', null) + } + + onUpgrade (statusCode, rawHeaders, socket) { + const { callback, opaque, context } = this + + assert.strictEqual(statusCode, 101) + + removeSignal(this) + + this.callback = null + const headers = this.responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders) + this.runInAsyncScope(callback, null, null, { + headers, + socket, + opaque, + context + }) + } + + onError (err) { + const { callback, opaque } = this + + removeSignal(this) + + if (callback) { + this.callback = null + queueMicrotask(() => { + this.runInAsyncScope(callback, null, err, { opaque }) + }) + } + } +} + +function upgrade (opts, callback) { + if (callback === undefined) { + return new Promise((resolve, reject) => { + upgrade.call(this, opts, (err, data) => { + return err ? reject(err) : resolve(data) + }) + }) + } + + try { + const upgradeHandler = new UpgradeHandler(opts, callback) + this.dispatch({ + ...opts, + method: opts.method || 'GET', + upgrade: opts.protocol || 'Websocket' + }, upgradeHandler) + } catch (err) { + if (typeof callback !== 'function') { + throw err + } + const opaque = opts && opts.opaque + queueMicrotask(() => callback(err, { opaque })) + } +} + +module.exports = upgrade diff --git a/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/api/index.js b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/api/index.js new file mode 100644 index 000000000..8983a5e74 --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/api/index.js @@ -0,0 +1,7 @@ +'use strict' + +module.exports.request = require('./api-request') +module.exports.stream = require('./api-stream') +module.exports.pipeline = require('./api-pipeline') +module.exports.upgrade = require('./api-upgrade') +module.exports.connect = require('./api-connect') diff --git a/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/api/readable.js b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/api/readable.js new file mode 100644 index 000000000..9c184d14e --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/api/readable.js @@ -0,0 +1,283 @@ +// Ported from https://github.com/nodejs/undici/pull/907 + +'use strict' + +const assert = require('assert') +const { Readable } = require('stream') +const { RequestAbortedError, NotSupportedError } = require('../core/errors') +const util = require('../core/util') +const { ReadableStreamFrom, toUSVString } = require('../core/util') + +let Blob + +const kConsume = Symbol('kConsume') +const kReading = Symbol('kReading') +const kBody = Symbol('kBody') +const kAbort = Symbol('abort') +const kContentType = Symbol('kContentType') + +module.exports = class BodyReadable extends Readable { + constructor (resume, abort, contentType = '') { + super({ + autoDestroy: true, + read: resume, + highWaterMark: 64 * 1024 // Same as nodejs fs streams. + }) + + this._readableState.dataEmitted = false + + this[kAbort] = abort + this[kConsume] = null + this[kBody] = null + this[kContentType] = contentType + + // Is stream being consumed through Readable API? + // This is an optimization so that we avoid checking + // for 'data' and 'readable' listeners in the hot path + // inside push(). + this[kReading] = false + } + + destroy (err) { + if (this.destroyed) { + // Node < 16 + return this + } + + if (!err && !this._readableState.endEmitted) { + err = new RequestAbortedError() + } + + if (err) { + this[kAbort]() + } + + return super.destroy(err) + } + + emit (ev, ...args) { + if (ev === 'data') { + // Node < 16.7 + this._readableState.dataEmitted = true + } else if (ev === 'error') { + // Node < 16 + this._readableState.errorEmitted = true + } + return super.emit(ev, ...args) + } + + on (ev, ...args) { + if (ev === 'data' || ev === 'readable') { + this[kReading] = true + } + return super.on(ev, ...args) + } + + addListener (ev, ...args) { + return this.on(ev, ...args) + } + + off (ev, ...args) { + const ret = super.off(ev, ...args) + if (ev === 'data' || ev === 'readable') { + this[kReading] = ( + this.listenerCount('data') > 0 || + this.listenerCount('readable') > 0 + ) + } + return ret + } + + removeListener (ev, ...args) { + return this.off(ev, ...args) + } + + push (chunk) { + if (this[kConsume] && chunk !== null && this.readableLength === 0) { + consumePush(this[kConsume], chunk) + return this[kReading] ? super.push(chunk) : true + } + return super.push(chunk) + } + + // https://fetch.spec.whatwg.org/#dom-body-text + async text () { + return consume(this, 'text') + } + + // https://fetch.spec.whatwg.org/#dom-body-json + async json () { + return consume(this, 'json') + } + + // https://fetch.spec.whatwg.org/#dom-body-blob + async blob () { + return consume(this, 'blob') + } + + // https://fetch.spec.whatwg.org/#dom-body-arraybuffer + async arrayBuffer () { + return consume(this, 'arrayBuffer') + } + + // https://fetch.spec.whatwg.org/#dom-body-formdata + async formData () { + // TODO: Implement. + throw new NotSupportedError() + } + + // https://fetch.spec.whatwg.org/#dom-body-bodyused + get bodyUsed () { + return util.isDisturbed(this) + } + + // https://fetch.spec.whatwg.org/#dom-body-body + get body () { + if (!this[kBody]) { + this[kBody] = ReadableStreamFrom(this) + if (this[kConsume]) { + // TODO: Is this the best way to force a lock? + this[kBody].getReader() // Ensure stream is locked. + assert(this[kBody].locked) + } + } + return this[kBody] + } + + async dump (opts) { + let limit = opts && Number.isFinite(opts.limit) ? opts.limit : 262144 + try { + for await (const chunk of this) { + limit -= Buffer.byteLength(chunk) + if (limit < 0) { + return + } + } + } catch { + // Do nothing... + } + } +} + +// https://streams.spec.whatwg.org/#readablestream-locked +function isLocked (self) { + // Consume is an implicit lock. + return (self[kBody] && self[kBody].locked === true) || self[kConsume] +} + +// https://fetch.spec.whatwg.org/#body-unusable +function isUnusable (self) { + return util.isDisturbed(self) || isLocked(self) +} + +async function consume (stream, type) { + if (isUnusable(stream)) { + throw new TypeError('unusable') + } + + assert(!stream[kConsume]) + + return new Promise((resolve, reject) => { + stream[kConsume] = { + type, + stream, + resolve, + reject, + length: 0, + body: [] + } + + stream + .on('error', function (err) { + consumeFinish(this[kConsume], err) + }) + .on('close', function () { + if (this[kConsume].body !== null) { + consumeFinish(this[kConsume], new RequestAbortedError()) + } + }) + + process.nextTick(consumeStart, stream[kConsume]) + }) +} + +function consumeStart (consume) { + if (consume.body === null) { + return + } + + const { _readableState: state } = consume.stream + + for (const chunk of state.buffer) { + consumePush(consume, chunk) + } + + if (state.endEmitted) { + consumeEnd(this[kConsume]) + } else { + consume.stream.on('end', function () { + consumeEnd(this[kConsume]) + }) + } + + consume.stream.resume() + + while (consume.stream.read() != null) { + // Loop + } +} + +function consumeEnd (consume) { + const { type, body, resolve, stream, length } = consume + + try { + if (type === 'text') { + resolve(toUSVString(Buffer.concat(body))) + } else if (type === 'json') { + resolve(JSON.parse(Buffer.concat(body))) + } else if (type === 'arrayBuffer') { + const dst = new Uint8Array(length) + + let pos = 0 + for (const buf of body) { + dst.set(buf, pos) + pos += buf.byteLength + } + + resolve(dst) + } else if (type === 'blob') { + if (!Blob) { + Blob = require('buffer').Blob + } + resolve(new Blob(body, { type: stream[kContentType] })) + } + + consumeFinish(consume) + } catch (err) { + stream.destroy(err) + } +} + +function consumePush (consume, chunk) { + consume.length += chunk.length + consume.body.push(chunk) +} + +function consumeFinish (consume, err) { + if (consume.body === null) { + return + } + + if (err) { + consume.reject(err) + } else { + consume.resolve() + } + + consume.type = null + consume.stream = null + consume.resolve = null + consume.reject = null + consume.length = 0 + consume.body = null +} diff --git a/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/balanced-pool.js b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/balanced-pool.js new file mode 100644 index 000000000..10bc6a47b --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/balanced-pool.js @@ -0,0 +1,190 @@ +'use strict' + +const { + BalancedPoolMissingUpstreamError, + InvalidArgumentError +} = require('./core/errors') +const { + PoolBase, + kClients, + kNeedDrain, + kAddClient, + kRemoveClient, + kGetDispatcher +} = require('./pool-base') +const Pool = require('./pool') +const { kUrl, kInterceptors } = require('./core/symbols') +const { parseOrigin } = require('./core/util') +const kFactory = Symbol('factory') + +const kOptions = Symbol('options') +const kGreatestCommonDivisor = Symbol('kGreatestCommonDivisor') +const kCurrentWeight = Symbol('kCurrentWeight') +const kIndex = Symbol('kIndex') +const kWeight = Symbol('kWeight') +const kMaxWeightPerServer = Symbol('kMaxWeightPerServer') +const kErrorPenalty = Symbol('kErrorPenalty') + +function getGreatestCommonDivisor (a, b) { + if (b === 0) return a + return getGreatestCommonDivisor(b, a % b) +} + +function defaultFactory (origin, opts) { + return new Pool(origin, opts) +} + +class BalancedPool extends PoolBase { + constructor (upstreams = [], { factory = defaultFactory, ...opts } = {}) { + super() + + this[kOptions] = opts + this[kIndex] = -1 + this[kCurrentWeight] = 0 + + this[kMaxWeightPerServer] = this[kOptions].maxWeightPerServer || 100 + this[kErrorPenalty] = this[kOptions].errorPenalty || 15 + + if (!Array.isArray(upstreams)) { + upstreams = [upstreams] + } + + if (typeof factory !== 'function') { + throw new InvalidArgumentError('factory must be a function.') + } + + this[kInterceptors] = opts.interceptors && opts.interceptors.BalancedPool && Array.isArray(opts.interceptors.BalancedPool) + ? opts.interceptors.BalancedPool + : [] + this[kFactory] = factory + + for (const upstream of upstreams) { + this.addUpstream(upstream) + } + this._updateBalancedPoolStats() + } + + addUpstream (upstream) { + const upstreamOrigin = parseOrigin(upstream).origin + + if (this[kClients].find((pool) => ( + pool[kUrl].origin === upstreamOrigin && + pool.closed !== true && + pool.destroyed !== true + ))) { + return this + } + const pool = this[kFactory](upstreamOrigin, Object.assign({}, this[kOptions])) + + this[kAddClient](pool) + pool.on('connect', () => { + pool[kWeight] = Math.min(this[kMaxWeightPerServer], pool[kWeight] + this[kErrorPenalty]) + }) + + pool.on('connectionError', () => { + pool[kWeight] = Math.max(1, pool[kWeight] - this[kErrorPenalty]) + this._updateBalancedPoolStats() + }) + + pool.on('disconnect', (...args) => { + const err = args[2] + if (err && err.code === 'UND_ERR_SOCKET') { + // decrease the weight of the pool. + pool[kWeight] = Math.max(1, pool[kWeight] - this[kErrorPenalty]) + this._updateBalancedPoolStats() + } + }) + + for (const client of this[kClients]) { + client[kWeight] = this[kMaxWeightPerServer] + } + + this._updateBalancedPoolStats() + + return this + } + + _updateBalancedPoolStats () { + this[kGreatestCommonDivisor] = this[kClients].map(p => p[kWeight]).reduce(getGreatestCommonDivisor, 0) + } + + removeUpstream (upstream) { + const upstreamOrigin = parseOrigin(upstream).origin + + const pool = this[kClients].find((pool) => ( + pool[kUrl].origin === upstreamOrigin && + pool.closed !== true && + pool.destroyed !== true + )) + + if (pool) { + this[kRemoveClient](pool) + } + + return this + } + + get upstreams () { + return this[kClients] + .filter(dispatcher => dispatcher.closed !== true && dispatcher.destroyed !== true) + .map((p) => p[kUrl].origin) + } + + [kGetDispatcher] () { + // We validate that pools is greater than 0, + // otherwise we would have to wait until an upstream + // is added, which might never happen. + if (this[kClients].length === 0) { + throw new BalancedPoolMissingUpstreamError() + } + + const dispatcher = this[kClients].find(dispatcher => ( + !dispatcher[kNeedDrain] && + dispatcher.closed !== true && + dispatcher.destroyed !== true + )) + + if (!dispatcher) { + return + } + + const allClientsBusy = this[kClients].map(pool => pool[kNeedDrain]).reduce((a, b) => a && b, true) + + if (allClientsBusy) { + return + } + + let counter = 0 + + let maxWeightIndex = this[kClients].findIndex(pool => !pool[kNeedDrain]) + + while (counter++ < this[kClients].length) { + this[kIndex] = (this[kIndex] + 1) % this[kClients].length + const pool = this[kClients][this[kIndex]] + + // find pool index with the largest weight + if (pool[kWeight] > this[kClients][maxWeightIndex][kWeight] && !pool[kNeedDrain]) { + maxWeightIndex = this[kIndex] + } + + // decrease the current weight every `this[kClients].length`. + if (this[kIndex] === 0) { + // Set the current weight to the next lower weight. + this[kCurrentWeight] = this[kCurrentWeight] - this[kGreatestCommonDivisor] + + if (this[kCurrentWeight] <= 0) { + this[kCurrentWeight] = this[kMaxWeightPerServer] + } + } + if (pool[kWeight] >= this[kCurrentWeight] && (!pool[kNeedDrain])) { + return pool + } + } + + this[kCurrentWeight] = this[kClients][maxWeightIndex][kWeight] + this[kIndex] = maxWeightIndex + return this[kClients][maxWeightIndex] + } +} + +module.exports = BalancedPool diff --git a/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/client.js b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/client.js new file mode 100644 index 000000000..dd9531f93 --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/client.js @@ -0,0 +1,1746 @@ +'use strict' + +/* global WebAssembly */ + +const assert = require('assert') +const net = require('net') +const util = require('./core/util') +const Request = require('./core/request') +const DispatcherBase = require('./dispatcher-base') +const { + RequestContentLengthMismatchError, + ResponseContentLengthMismatchError, + InvalidArgumentError, + RequestAbortedError, + HeadersTimeoutError, + HeadersOverflowError, + SocketError, + InformationalError, + BodyTimeoutError, + HTTPParserError, + ResponseExceededMaxSizeError +} = require('./core/errors') +const buildConnector = require('./core/connect') +const { + kUrl, + kReset, + kServerName, + kClient, + kBusy, + kParser, + kConnect, + kBlocking, + kResuming, + kRunning, + kPending, + kSize, + kWriting, + kQueue, + kConnected, + kConnecting, + kNeedDrain, + kNoRef, + kKeepAliveDefaultTimeout, + kHostHeader, + kPendingIdx, + kRunningIdx, + kError, + kPipelining, + kSocket, + kKeepAliveTimeoutValue, + kMaxHeadersSize, + kKeepAliveMaxTimeout, + kKeepAliveTimeoutThreshold, + kHeadersTimeout, + kBodyTimeout, + kStrictContentLength, + kConnector, + kMaxRedirections, + kMaxRequests, + kCounter, + kClose, + kDestroy, + kDispatch, + kInterceptors, + kLocalAddress, + kMaxResponseSize +} = require('./core/symbols') + +const kClosedResolve = Symbol('kClosedResolve') + +const channels = {} + +try { + const diagnosticsChannel = require('diagnostics_channel') + channels.sendHeaders = diagnosticsChannel.channel('undici:client:sendHeaders') + channels.beforeConnect = diagnosticsChannel.channel('undici:client:beforeConnect') + channels.connectError = diagnosticsChannel.channel('undici:client:connectError') + channels.connected = diagnosticsChannel.channel('undici:client:connected') +} catch { + channels.sendHeaders = { hasSubscribers: false } + channels.beforeConnect = { hasSubscribers: false } + channels.connectError = { hasSubscribers: false } + channels.connected = { hasSubscribers: false } +} + +class Client extends DispatcherBase { + constructor (url, { + interceptors, + maxHeaderSize, + headersTimeout, + socketTimeout, + requestTimeout, + connectTimeout, + bodyTimeout, + idleTimeout, + keepAlive, + keepAliveTimeout, + maxKeepAliveTimeout, + keepAliveMaxTimeout, + keepAliveTimeoutThreshold, + socketPath, + pipelining, + tls, + strictContentLength, + maxCachedSessions, + maxRedirections, + connect, + maxRequestsPerClient, + localAddress, + maxResponseSize + } = {}) { + super() + + if (keepAlive !== undefined) { + throw new InvalidArgumentError('unsupported keepAlive, use pipelining=0 instead') + } + + if (socketTimeout !== undefined) { + throw new InvalidArgumentError('unsupported socketTimeout, use headersTimeout & bodyTimeout instead') + } + + if (requestTimeout !== undefined) { + throw new InvalidArgumentError('unsupported requestTimeout, use headersTimeout & bodyTimeout instead') + } + + if (idleTimeout !== undefined) { + throw new InvalidArgumentError('unsupported idleTimeout, use keepAliveTimeout instead') + } + + if (maxKeepAliveTimeout !== undefined) { + throw new InvalidArgumentError('unsupported maxKeepAliveTimeout, use keepAliveMaxTimeout instead') + } + + if (maxHeaderSize != null && !Number.isFinite(maxHeaderSize)) { + throw new InvalidArgumentError('invalid maxHeaderSize') + } + + if (socketPath != null && typeof socketPath !== 'string') { + throw new InvalidArgumentError('invalid socketPath') + } + + if (connectTimeout != null && (!Number.isFinite(connectTimeout) || connectTimeout < 0)) { + throw new InvalidArgumentError('invalid connectTimeout') + } + + if (keepAliveTimeout != null && (!Number.isFinite(keepAliveTimeout) || keepAliveTimeout <= 0)) { + throw new InvalidArgumentError('invalid keepAliveTimeout') + } + + if (keepAliveMaxTimeout != null && (!Number.isFinite(keepAliveMaxTimeout) || keepAliveMaxTimeout <= 0)) { + throw new InvalidArgumentError('invalid keepAliveMaxTimeout') + } + + if (keepAliveTimeoutThreshold != null && !Number.isFinite(keepAliveTimeoutThreshold)) { + throw new InvalidArgumentError('invalid keepAliveTimeoutThreshold') + } + + if (headersTimeout != null && (!Number.isInteger(headersTimeout) || headersTimeout < 0)) { + throw new InvalidArgumentError('headersTimeout must be a positive integer or zero') + } + + if (bodyTimeout != null && (!Number.isInteger(bodyTimeout) || bodyTimeout < 0)) { + throw new InvalidArgumentError('bodyTimeout must be a positive integer or zero') + } + + if (connect != null && typeof connect !== 'function' && typeof connect !== 'object') { + throw new InvalidArgumentError('connect must be a function or an object') + } + + if (maxRedirections != null && (!Number.isInteger(maxRedirections) || maxRedirections < 0)) { + throw new InvalidArgumentError('maxRedirections must be a positive number') + } + + if (maxRequestsPerClient != null && (!Number.isInteger(maxRequestsPerClient) || maxRequestsPerClient < 0)) { + throw new InvalidArgumentError('maxRequestsPerClient must be a positive number') + } + + if (localAddress != null && (typeof localAddress !== 'string' || net.isIP(localAddress) === 0)) { + throw new InvalidArgumentError('localAddress must be valid string IP address') + } + + if (maxResponseSize != null && (!Number.isInteger(maxResponseSize) || maxResponseSize < -1)) { + throw new InvalidArgumentError('maxResponseSize must be a positive number') + } + + if (typeof connect !== 'function') { + connect = buildConnector({ + ...tls, + maxCachedSessions, + socketPath, + timeout: connectTimeout, + ...connect + }) + } + + this[kInterceptors] = interceptors && interceptors.Client && Array.isArray(interceptors.Client) + ? interceptors.Client + : [createRedirectInterceptor({ maxRedirections })] + this[kUrl] = util.parseOrigin(url) + this[kConnector] = connect + this[kSocket] = null + this[kPipelining] = pipelining != null ? pipelining : 1 + this[kMaxHeadersSize] = maxHeaderSize || 16384 + this[kKeepAliveDefaultTimeout] = keepAliveTimeout == null ? 4e3 : keepAliveTimeout + this[kKeepAliveMaxTimeout] = keepAliveMaxTimeout == null ? 600e3 : keepAliveMaxTimeout + this[kKeepAliveTimeoutThreshold] = keepAliveTimeoutThreshold == null ? 1e3 : keepAliveTimeoutThreshold + this[kKeepAliveTimeoutValue] = this[kKeepAliveDefaultTimeout] + this[kServerName] = null + this[kLocalAddress] = localAddress != null ? localAddress : null + this[kResuming] = 0 // 0, idle, 1, scheduled, 2 resuming + this[kNeedDrain] = 0 // 0, idle, 1, scheduled, 2 resuming + this[kHostHeader] = `host: ${this[kUrl].hostname}${this[kUrl].port ? `:${this[kUrl].port}` : ''}\r\n` + this[kBodyTimeout] = bodyTimeout != null ? bodyTimeout : 30e3 + this[kHeadersTimeout] = headersTimeout != null ? headersTimeout : 30e3 + this[kStrictContentLength] = strictContentLength == null ? true : strictContentLength + this[kMaxRedirections] = maxRedirections + this[kMaxRequests] = maxRequestsPerClient + this[kClosedResolve] = null + this[kMaxResponseSize] = maxResponseSize > -1 ? maxResponseSize : -1 + + // kQueue is built up of 3 sections separated by + // the kRunningIdx and kPendingIdx indices. + // | complete | running | pending | + // ^ kRunningIdx ^ kPendingIdx ^ kQueue.length + // kRunningIdx points to the first running element. + // kPendingIdx points to the first pending element. + // This implements a fast queue with an amortized + // time of O(1). + + this[kQueue] = [] + this[kRunningIdx] = 0 + this[kPendingIdx] = 0 + } + + get pipelining () { + return this[kPipelining] + } + + set pipelining (value) { + this[kPipelining] = value + resume(this, true) + } + + get [kPending] () { + return this[kQueue].length - this[kPendingIdx] + } + + get [kRunning] () { + return this[kPendingIdx] - this[kRunningIdx] + } + + get [kSize] () { + return this[kQueue].length - this[kRunningIdx] + } + + get [kConnected] () { + return !!this[kSocket] && !this[kConnecting] && !this[kSocket].destroyed + } + + get [kBusy] () { + const socket = this[kSocket] + return ( + (socket && (socket[kReset] || socket[kWriting] || socket[kBlocking])) || + (this[kSize] >= (this[kPipelining] || 1)) || + this[kPending] > 0 + ) + } + + /* istanbul ignore: only used for test */ + [kConnect] (cb) { + connect(this) + this.once('connect', cb) + } + + [kDispatch] (opts, handler) { + const origin = opts.origin || this[kUrl].origin + + const request = new Request(origin, opts, handler) + + this[kQueue].push(request) + if (this[kResuming]) { + // Do nothing. + } else if (util.bodyLength(request.body) == null && util.isIterable(request.body)) { + // Wait a tick in case stream/iterator is ended in the same tick. + this[kResuming] = 1 + process.nextTick(resume, this) + } else { + resume(this, true) + } + + if (this[kResuming] && this[kNeedDrain] !== 2 && this[kBusy]) { + this[kNeedDrain] = 2 + } + + return this[kNeedDrain] < 2 + } + + async [kClose] () { + return new Promise((resolve) => { + if (!this[kSize]) { + this.destroy(resolve) + } else { + this[kClosedResolve] = resolve + } + }) + } + + async [kDestroy] (err) { + return new Promise((resolve) => { + const requests = this[kQueue].splice(this[kPendingIdx]) + for (let i = 0; i < requests.length; i++) { + const request = requests[i] + errorRequest(this, request, err) + } + + const callback = () => { + if (this[kClosedResolve]) { + this[kClosedResolve]() + this[kClosedResolve] = null + } + resolve() + } + + if (!this[kSocket]) { + queueMicrotask(callback) + } else { + util.destroy(this[kSocket].on('close', callback), err) + } + + resume(this) + }) + } +} + +const constants = require('./llhttp/constants') +const createRedirectInterceptor = require('./interceptor/redirectInterceptor') +const EMPTY_BUF = Buffer.alloc(0) + +async function lazyllhttp () { + const llhttpWasmData = process.env.JEST_WORKER_ID ? require('./llhttp/llhttp.wasm.js') : undefined + + let mod + try { + mod = await WebAssembly.compile(Buffer.from(require('./llhttp/llhttp_simd.wasm.js'), 'base64')) + } catch (e) { + /* istanbul ignore next */ + + // We could check if the error was caused by the simd option not + // being enabled, but the occurring of this other error + // * https://github.com/emscripten-core/emscripten/issues/11495 + // got me to remove that check to avoid breaking Node 12. + mod = await WebAssembly.compile(Buffer.from(llhttpWasmData || require('./llhttp/llhttp.wasm.js'), 'base64')) + } + + return await WebAssembly.instantiate(mod, { + env: { + /* eslint-disable camelcase */ + + wasm_on_url: (p, at, len) => { + /* istanbul ignore next */ + return 0 + }, + wasm_on_status: (p, at, len) => { + assert.strictEqual(currentParser.ptr, p) + const start = at - currentBufferPtr + const end = start + len + return currentParser.onStatus(currentBufferRef.slice(start, end)) || 0 + }, + wasm_on_message_begin: (p) => { + assert.strictEqual(currentParser.ptr, p) + return currentParser.onMessageBegin() || 0 + }, + wasm_on_header_field: (p, at, len) => { + assert.strictEqual(currentParser.ptr, p) + const start = at - currentBufferPtr + const end = start + len + return currentParser.onHeaderField(currentBufferRef.slice(start, end)) || 0 + }, + wasm_on_header_value: (p, at, len) => { + assert.strictEqual(currentParser.ptr, p) + const start = at - currentBufferPtr + const end = start + len + return currentParser.onHeaderValue(currentBufferRef.slice(start, end)) || 0 + }, + wasm_on_headers_complete: (p, statusCode, upgrade, shouldKeepAlive) => { + assert.strictEqual(currentParser.ptr, p) + return currentParser.onHeadersComplete(statusCode, Boolean(upgrade), Boolean(shouldKeepAlive)) || 0 + }, + wasm_on_body: (p, at, len) => { + assert.strictEqual(currentParser.ptr, p) + const start = at - currentBufferPtr + const end = start + len + return currentParser.onBody(currentBufferRef.slice(start, end)) || 0 + }, + wasm_on_message_complete: (p) => { + assert.strictEqual(currentParser.ptr, p) + return currentParser.onMessageComplete() || 0 + } + + /* eslint-enable camelcase */ + } + }) +} + +let llhttpInstance = null +let llhttpPromise = lazyllhttp() + .catch(() => { + }) + +let currentParser = null +let currentBufferRef = null +let currentBufferSize = 0 +let currentBufferPtr = null + +const TIMEOUT_HEADERS = 1 +const TIMEOUT_BODY = 2 +const TIMEOUT_IDLE = 3 + +class Parser { + constructor (client, socket, { exports }) { + assert(Number.isFinite(client[kMaxHeadersSize]) && client[kMaxHeadersSize] > 0) + + this.llhttp = exports + this.ptr = this.llhttp.llhttp_alloc(constants.TYPE.RESPONSE) + this.client = client + this.socket = socket + this.timeout = null + this.timeoutValue = null + this.timeoutType = null + this.statusCode = null + this.statusText = '' + this.upgrade = false + this.headers = [] + this.headersSize = 0 + this.headersMaxSize = client[kMaxHeadersSize] + this.shouldKeepAlive = false + this.paused = false + this.resume = this.resume.bind(this) + + this.bytesRead = 0 + + this.keepAlive = '' + this.contentLength = '' + this.maxResponseSize = client[kMaxResponseSize] + } + + setTimeout (value, type) { + this.timeoutType = type + if (value !== this.timeoutValue) { + clearTimeout(this.timeout) + if (value) { + this.timeout = setTimeout(onParserTimeout, value, this) + // istanbul ignore else: only for jest + if (this.timeout.unref) { + this.timeout.unref() + } + } else { + this.timeout = null + } + this.timeoutValue = value + } else if (this.timeout) { + // istanbul ignore else: only for jest + if (this.timeout.refresh) { + this.timeout.refresh() + } + } + } + + resume () { + if (this.socket.destroyed || !this.paused) { + return + } + + assert(this.ptr != null) + assert(currentParser == null) + + this.llhttp.llhttp_resume(this.ptr) + + assert(this.timeoutType === TIMEOUT_BODY) + if (this.timeout) { + // istanbul ignore else: only for jest + if (this.timeout.refresh) { + this.timeout.refresh() + } + } + + this.paused = false + this.execute(this.socket.read() || EMPTY_BUF) // Flush parser. + this.readMore() + } + + readMore () { + while (!this.paused && this.ptr) { + const chunk = this.socket.read() + if (chunk === null) { + break + } + this.execute(chunk) + } + } + + execute (data) { + assert(this.ptr != null) + assert(currentParser == null) + assert(!this.paused) + + const { socket, llhttp } = this + + if (data.length > currentBufferSize) { + if (currentBufferPtr) { + llhttp.free(currentBufferPtr) + } + currentBufferSize = Math.ceil(data.length / 4096) * 4096 + currentBufferPtr = llhttp.malloc(currentBufferSize) + } + + new Uint8Array(llhttp.memory.buffer, currentBufferPtr, currentBufferSize).set(data) + + // Call `execute` on the wasm parser. + // We pass the `llhttp_parser` pointer address, the pointer address of buffer view data, + // and finally the length of bytes to parse. + // The return value is an error code or `constants.ERROR.OK`. + try { + let ret + + try { + currentBufferRef = data + currentParser = this + ret = llhttp.llhttp_execute(this.ptr, currentBufferPtr, data.length) + /* eslint-disable-next-line no-useless-catch */ + } catch (err) { + /* istanbul ignore next: difficult to make a test case for */ + throw err + } finally { + currentParser = null + currentBufferRef = null + } + + const offset = llhttp.llhttp_get_error_pos(this.ptr) - currentBufferPtr + + if (ret === constants.ERROR.PAUSED_UPGRADE) { + this.onUpgrade(data.slice(offset)) + } else if (ret === constants.ERROR.PAUSED) { + this.paused = true + socket.unshift(data.slice(offset)) + } else if (ret !== constants.ERROR.OK) { + const ptr = llhttp.llhttp_get_error_reason(this.ptr) + let message = '' + /* istanbul ignore else: difficult to make a test case for */ + if (ptr) { + const len = new Uint8Array(llhttp.memory.buffer, ptr).indexOf(0) + message = Buffer.from(llhttp.memory.buffer, ptr, len).toString() + } + throw new HTTPParserError(message, constants.ERROR[ret], data.slice(offset)) + } + } catch (err) { + util.destroy(socket, err) + } + } + + destroy () { + assert(this.ptr != null) + assert(currentParser == null) + + this.llhttp.llhttp_free(this.ptr) + this.ptr = null + + clearTimeout(this.timeout) + this.timeout = null + this.timeoutValue = null + this.timeoutType = null + + this.paused = false + } + + onStatus (buf) { + this.statusText = buf.toString() + } + + onMessageBegin () { + const { socket, client } = this + + /* istanbul ignore next: difficult to make a test case for */ + if (socket.destroyed) { + return -1 + } + + const request = client[kQueue][client[kRunningIdx]] + if (!request) { + return -1 + } + } + + onHeaderField (buf) { + const len = this.headers.length + + if ((len & 1) === 0) { + this.headers.push(buf) + } else { + this.headers[len - 1] = Buffer.concat([this.headers[len - 1], buf]) + } + + this.trackHeader(buf.length) + } + + onHeaderValue (buf) { + let len = this.headers.length + + if ((len & 1) === 1) { + this.headers.push(buf) + len += 1 + } else { + this.headers[len - 1] = Buffer.concat([this.headers[len - 1], buf]) + } + + const key = this.headers[len - 2] + if (key.length === 10 && key.toString().toLowerCase() === 'keep-alive') { + this.keepAlive += buf.toString() + } else if (key.length === 14 && key.toString().toLowerCase() === 'content-length') { + this.contentLength += buf.toString() + } + + this.trackHeader(buf.length) + } + + trackHeader (len) { + this.headersSize += len + if (this.headersSize >= this.headersMaxSize) { + util.destroy(this.socket, new HeadersOverflowError()) + } + } + + onUpgrade (head) { + const { upgrade, client, socket, headers, statusCode } = this + + assert(upgrade) + + const request = client[kQueue][client[kRunningIdx]] + assert(request) + + assert(!socket.destroyed) + assert(socket === client[kSocket]) + assert(!this.paused) + assert(request.upgrade || request.method === 'CONNECT') + + this.statusCode = null + this.statusText = '' + this.shouldKeepAlive = null + + assert(this.headers.length % 2 === 0) + this.headers = [] + this.headersSize = 0 + + socket.unshift(head) + + socket[kParser].destroy() + socket[kParser] = null + + socket[kClient] = null + socket[kError] = null + socket + .removeListener('error', onSocketError) + .removeListener('readable', onSocketReadable) + .removeListener('end', onSocketEnd) + .removeListener('close', onSocketClose) + + client[kSocket] = null + client[kQueue][client[kRunningIdx]++] = null + client.emit('disconnect', client[kUrl], [client], new InformationalError('upgrade')) + + try { + request.onUpgrade(statusCode, headers, socket) + } catch (err) { + util.destroy(socket, err) + } + + resume(client) + } + + onHeadersComplete (statusCode, upgrade, shouldKeepAlive) { + const { client, socket, headers, statusText } = this + + /* istanbul ignore next: difficult to make a test case for */ + if (socket.destroyed) { + return -1 + } + + const request = client[kQueue][client[kRunningIdx]] + + /* istanbul ignore next: difficult to make a test case for */ + if (!request) { + return -1 + } + + assert(!this.upgrade) + assert(this.statusCode < 200) + + if (statusCode === 100) { + util.destroy(socket, new SocketError('bad response', util.getSocketInfo(socket))) + return -1 + } + + /* this can only happen if server is misbehaving */ + if (upgrade && !request.upgrade) { + util.destroy(socket, new SocketError('bad upgrade', util.getSocketInfo(socket))) + return -1 + } + + assert.strictEqual(this.timeoutType, TIMEOUT_HEADERS) + + this.statusCode = statusCode + this.shouldKeepAlive = shouldKeepAlive + + if (this.statusCode >= 200) { + const bodyTimeout = request.bodyTimeout != null + ? request.bodyTimeout + : client[kBodyTimeout] + this.setTimeout(bodyTimeout, TIMEOUT_BODY) + } else if (this.timeout) { + // istanbul ignore else: only for jest + if (this.timeout.refresh) { + this.timeout.refresh() + } + } + + if (request.method === 'CONNECT') { + assert(client[kRunning] === 1) + this.upgrade = true + return 2 + } + + if (upgrade) { + assert(client[kRunning] === 1) + this.upgrade = true + return 2 + } + + assert(this.headers.length % 2 === 0) + this.headers = [] + this.headersSize = 0 + + if (shouldKeepAlive && client[kPipelining]) { + const keepAliveTimeout = this.keepAlive ? util.parseKeepAliveTimeout(this.keepAlive) : null + + if (keepAliveTimeout != null) { + const timeout = Math.min( + keepAliveTimeout - client[kKeepAliveTimeoutThreshold], + client[kKeepAliveMaxTimeout] + ) + if (timeout <= 0) { + socket[kReset] = true + } else { + client[kKeepAliveTimeoutValue] = timeout + } + } else { + client[kKeepAliveTimeoutValue] = client[kKeepAliveDefaultTimeout] + } + } else { + // Stop more requests from being dispatched. + socket[kReset] = true + } + + let pause + try { + pause = request.onHeaders(statusCode, headers, this.resume, statusText) === false + } catch (err) { + util.destroy(socket, err) + return -1 + } + + if (request.method === 'HEAD') { + assert(socket[kReset]) + return 1 + } + + if (statusCode < 200) { + return 1 + } + + if (socket[kBlocking]) { + socket[kBlocking] = false + resume(client) + } + + return pause ? constants.ERROR.PAUSED : 0 + } + + onBody (buf) { + const { client, socket, statusCode, maxResponseSize } = this + + if (socket.destroyed) { + return -1 + } + + const request = client[kQueue][client[kRunningIdx]] + assert(request) + + assert.strictEqual(this.timeoutType, TIMEOUT_BODY) + if (this.timeout) { + // istanbul ignore else: only for jest + if (this.timeout.refresh) { + this.timeout.refresh() + } + } + + assert(statusCode >= 200) + + if (maxResponseSize > -1 && this.bytesRead + buf.length > maxResponseSize) { + util.destroy(socket, new ResponseExceededMaxSizeError()) + return -1 + } + + this.bytesRead += buf.length + + try { + if (request.onData(buf) === false) { + return constants.ERROR.PAUSED + } + } catch (err) { + util.destroy(socket, err) + return -1 + } + } + + onMessageComplete () { + const { client, socket, statusCode, upgrade, headers, contentLength, bytesRead, shouldKeepAlive } = this + + if (socket.destroyed && (!statusCode || shouldKeepAlive)) { + return -1 + } + + if (upgrade) { + return + } + + const request = client[kQueue][client[kRunningIdx]] + assert(request) + + assert(statusCode >= 100) + + this.statusCode = null + this.statusText = '' + this.bytesRead = 0 + this.contentLength = '' + this.keepAlive = '' + + assert(this.headers.length % 2 === 0) + this.headers = [] + this.headersSize = 0 + + if (statusCode < 200) { + return + } + + /* istanbul ignore next: should be handled by llhttp? */ + if (request.method !== 'HEAD' && contentLength && bytesRead !== parseInt(contentLength, 10)) { + util.destroy(socket, new ResponseContentLengthMismatchError()) + return -1 + } + + try { + request.onComplete(headers) + } catch (err) { + errorRequest(client, request, err) + } + + client[kQueue][client[kRunningIdx]++] = null + + if (socket[kWriting]) { + assert.strictEqual(client[kRunning], 0) + // Response completed before request. + util.destroy(socket, new InformationalError('reset')) + return constants.ERROR.PAUSED + } else if (!shouldKeepAlive) { + util.destroy(socket, new InformationalError('reset')) + return constants.ERROR.PAUSED + } else if (socket[kReset] && client[kRunning] === 0) { + // Destroy socket once all requests have completed. + // The request at the tail of the pipeline is the one + // that requested reset and no further requests should + // have been queued since then. + util.destroy(socket, new InformationalError('reset')) + return constants.ERROR.PAUSED + } else if (client[kPipelining] === 1) { + // We must wait a full event loop cycle to reuse this socket to make sure + // that non-spec compliant servers are not closing the connection even if they + // said they won't. + setImmediate(resume, client) + } else { + resume(client) + } + } +} + +function onParserTimeout (parser) { + const { socket, timeoutType, client } = parser + + /* istanbul ignore else */ + if (timeoutType === TIMEOUT_HEADERS) { + if (!socket[kWriting] || socket.writableNeedDrain || client[kRunning] > 1) { + assert(!parser.paused, 'cannot be paused while waiting for headers') + util.destroy(socket, new HeadersTimeoutError()) + } + } else if (timeoutType === TIMEOUT_BODY) { + if (!parser.paused) { + util.destroy(socket, new BodyTimeoutError()) + } + } else if (timeoutType === TIMEOUT_IDLE) { + assert(client[kRunning] === 0 && client[kKeepAliveTimeoutValue]) + util.destroy(socket, new InformationalError('socket idle timeout')) + } +} + +function onSocketReadable () { + const { [kParser]: parser } = this + parser.readMore() +} + +function onSocketError (err) { + const { [kParser]: parser } = this + + assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID') + + // On Mac OS, we get an ECONNRESET even if there is a full body to be forwarded + // to the user. + if (err.code === 'ECONNRESET' && parser.statusCode && !parser.shouldKeepAlive) { + // We treat all incoming data so for as a valid response. + parser.onMessageComplete() + return + } + + this[kError] = err + + onError(this[kClient], err) +} + +function onError (client, err) { + if ( + client[kRunning] === 0 && + err.code !== 'UND_ERR_INFO' && + err.code !== 'UND_ERR_SOCKET' + ) { + // Error is not caused by running request and not a recoverable + // socket error. + + assert(client[kPendingIdx] === client[kRunningIdx]) + + const requests = client[kQueue].splice(client[kRunningIdx]) + for (let i = 0; i < requests.length; i++) { + const request = requests[i] + errorRequest(client, request, err) + } + assert(client[kSize] === 0) + } +} + +function onSocketEnd () { + const { [kParser]: parser } = this + + if (parser.statusCode && !parser.shouldKeepAlive) { + // We treat all incoming data so far as a valid response. + parser.onMessageComplete() + return + } + + util.destroy(this, new SocketError('other side closed', util.getSocketInfo(this))) +} + +function onSocketClose () { + const { [kClient]: client } = this + + if (!this[kError] && this[kParser].statusCode && !this[kParser].shouldKeepAlive) { + // We treat all incoming data so far as a valid response. + this[kParser].onMessageComplete() + } + + this[kParser].destroy() + this[kParser] = null + + const err = this[kError] || new SocketError('closed', util.getSocketInfo(this)) + + client[kSocket] = null + + if (client.destroyed) { + assert(client[kPending] === 0) + + // Fail entire queue. + const requests = client[kQueue].splice(client[kRunningIdx]) + for (let i = 0; i < requests.length; i++) { + const request = requests[i] + errorRequest(client, request, err) + } + } else if (client[kRunning] > 0 && err.code !== 'UND_ERR_INFO') { + // Fail head of pipeline. + const request = client[kQueue][client[kRunningIdx]] + client[kQueue][client[kRunningIdx]++] = null + + errorRequest(client, request, err) + } + + client[kPendingIdx] = client[kRunningIdx] + + assert(client[kRunning] === 0) + + client.emit('disconnect', client[kUrl], [client], err) + + resume(client) +} + +async function connect (client) { + assert(!client[kConnecting]) + assert(!client[kSocket]) + + let { host, hostname, protocol, port } = client[kUrl] + + // Resolve ipv6 + if (hostname[0] === '[') { + const idx = hostname.indexOf(']') + + assert(idx !== -1) + const ip = hostname.substr(1, idx - 1) + + assert(net.isIP(ip)) + hostname = ip + } + + client[kConnecting] = true + + if (channels.beforeConnect.hasSubscribers) { + channels.beforeConnect.publish({ + connectParams: { + host, + hostname, + protocol, + port, + servername: client[kServerName], + localAddress: client[kLocalAddress] + }, + connector: client[kConnector] + }) + } + + try { + const socket = await new Promise((resolve, reject) => { + client[kConnector]({ + host, + hostname, + protocol, + port, + servername: client[kServerName], + localAddress: client[kLocalAddress] + }, (err, socket) => { + if (err) { + reject(err) + } else { + resolve(socket) + } + }) + }) + + if (!llhttpInstance) { + llhttpInstance = await llhttpPromise + llhttpPromise = null + } + + client[kConnecting] = false + + assert(socket) + + client[kSocket] = socket + + socket[kNoRef] = false + socket[kWriting] = false + socket[kReset] = false + socket[kBlocking] = false + socket[kError] = null + socket[kParser] = new Parser(client, socket, llhttpInstance) + socket[kClient] = client + socket[kCounter] = 0 + socket[kMaxRequests] = client[kMaxRequests] + socket + .on('error', onSocketError) + .on('readable', onSocketReadable) + .on('end', onSocketEnd) + .on('close', onSocketClose) + + if (channels.connected.hasSubscribers) { + channels.connected.publish({ + connectParams: { + host, + hostname, + protocol, + port, + servername: client[kServerName], + localAddress: client[kLocalAddress] + }, + connector: client[kConnector], + socket + }) + } + client.emit('connect', client[kUrl], [client]) + } catch (err) { + client[kConnecting] = false + + if (channels.connectError.hasSubscribers) { + channels.connectError.publish({ + connectParams: { + host, + hostname, + protocol, + port, + servername: client[kServerName], + localAddress: client[kLocalAddress] + }, + connector: client[kConnector], + error: err + }) + } + + if (err.code === 'ERR_TLS_CERT_ALTNAME_INVALID') { + assert(client[kRunning] === 0) + while (client[kPending] > 0 && client[kQueue][client[kPendingIdx]].servername === client[kServerName]) { + const request = client[kQueue][client[kPendingIdx]++] + errorRequest(client, request, err) + } + } else { + onError(client, err) + } + + client.emit('connectionError', client[kUrl], [client], err) + } + + resume(client) +} + +function emitDrain (client) { + client[kNeedDrain] = 0 + client.emit('drain', client[kUrl], [client]) +} + +function resume (client, sync) { + if (client[kResuming] === 2) { + return + } + + client[kResuming] = 2 + + _resume(client, sync) + client[kResuming] = 0 + + if (client[kRunningIdx] > 256) { + client[kQueue].splice(0, client[kRunningIdx]) + client[kPendingIdx] -= client[kRunningIdx] + client[kRunningIdx] = 0 + } +} + +function _resume (client, sync) { + while (true) { + if (client.destroyed) { + assert(client[kPending] === 0) + return + } + + if (client.closed && !client[kSize]) { + client.destroy() + return + } + + const socket = client[kSocket] + + if (socket) { + if (client[kSize] === 0) { + if (!socket[kNoRef] && socket.unref) { + socket.unref() + socket[kNoRef] = true + } + } else if (socket[kNoRef] && socket.ref) { + socket.ref() + socket[kNoRef] = false + } + + if (client[kSize] === 0) { + if (socket[kParser].timeoutType !== TIMEOUT_IDLE) { + socket[kParser].setTimeout(client[kKeepAliveTimeoutValue], TIMEOUT_IDLE) + } + } else if (client[kRunning] > 0 && socket[kParser].statusCode < 200) { + if (socket[kParser].timeoutType !== TIMEOUT_HEADERS) { + const request = client[kQueue][client[kRunningIdx]] + const headersTimeout = request.headersTimeout != null + ? request.headersTimeout + : client[kHeadersTimeout] + socket[kParser].setTimeout(headersTimeout, TIMEOUT_HEADERS) + } + } + } + + if (client[kBusy]) { + client[kNeedDrain] = 2 + } else if (client[kNeedDrain] === 2) { + if (sync) { + client[kNeedDrain] = 1 + process.nextTick(emitDrain, client) + } else { + emitDrain(client) + } + continue + } + + if (client[kPending] === 0) { + return + } + + if (client[kRunning] >= (client[kPipelining] || 1)) { + return + } + + const request = client[kQueue][client[kPendingIdx]] + + if (client[kUrl].protocol === 'https:' && client[kServerName] !== request.servername) { + if (client[kRunning] > 0) { + return + } + + client[kServerName] = request.servername + + if (socket && socket.servername !== request.servername) { + util.destroy(socket, new InformationalError('servername changed')) + return + } + } + + if (client[kConnecting]) { + return + } + + if (!socket) { + connect(client) + continue + } + + if (socket.destroyed || socket[kWriting] || socket[kReset] || socket[kBlocking]) { + return + } + + if (client[kRunning] > 0 && !request.idempotent) { + // Non-idempotent request cannot be retried. + // Ensure that no other requests are inflight and + // could cause failure. + return + } + + if (client[kRunning] > 0 && (request.upgrade || request.method === 'CONNECT')) { + // Don't dispatch an upgrade until all preceding requests have completed. + // A misbehaving server might upgrade the connection before all pipelined + // request has completed. + return + } + + if (util.isStream(request.body) && util.bodyLength(request.body) === 0) { + request.body + .on('data', /* istanbul ignore next */ function () { + /* istanbul ignore next */ + assert(false) + }) + .on('error', function (err) { + errorRequest(client, request, err) + }) + .on('end', function () { + util.destroy(this) + }) + + request.body = null + } + + if (client[kRunning] > 0 && + (util.isStream(request.body) || util.isAsyncIterable(request.body))) { + // Request with stream or iterator body can error while other requests + // are inflight and indirectly error those as well. + // Ensure this doesn't happen by waiting for inflight + // to complete before dispatching. + + // Request with stream or iterator body cannot be retried. + // Ensure that no other requests are inflight and + // could cause failure. + return + } + + if (!request.aborted && write(client, request)) { + client[kPendingIdx]++ + } else { + client[kQueue].splice(client[kPendingIdx], 1) + } + } +} + +function write (client, request) { + const { body, method, path, host, upgrade, headers, blocking } = request + + // https://tools.ietf.org/html/rfc7231#section-4.3.1 + // https://tools.ietf.org/html/rfc7231#section-4.3.2 + // https://tools.ietf.org/html/rfc7231#section-4.3.5 + + // Sending a payload body on a request that does not + // expect it can cause undefined behavior on some + // servers and corrupt connection state. Do not + // re-use the connection for further requests. + + const expectsPayload = ( + method === 'PUT' || + method === 'POST' || + method === 'PATCH' + ) + + if (body && typeof body.read === 'function') { + // Try to read EOF in order to get length. + body.read(0) + } + + let contentLength = util.bodyLength(body) + + if (contentLength === null) { + contentLength = request.contentLength + } + + if (contentLength === 0 && !expectsPayload) { + // https://tools.ietf.org/html/rfc7230#section-3.3.2 + // A user agent SHOULD NOT send a Content-Length header field when + // the request message does not contain a payload body and the method + // semantics do not anticipate such a body. + + contentLength = null + } + + if (request.contentLength !== null && request.contentLength !== contentLength) { + if (client[kStrictContentLength]) { + errorRequest(client, request, new RequestContentLengthMismatchError()) + return false + } + + process.emitWarning(new RequestContentLengthMismatchError()) + } + + const socket = client[kSocket] + + try { + request.onConnect((err) => { + if (request.aborted || request.completed) { + return + } + + errorRequest(client, request, err || new RequestAbortedError()) + + util.destroy(socket, new InformationalError('aborted')) + }) + } catch (err) { + errorRequest(client, request, err) + } + + if (request.aborted) { + return false + } + + if (method === 'HEAD') { + // https://github.com/mcollina/undici/issues/258 + + // Close after a HEAD request to interop with misbehaving servers + // that may send a body in the response. + + socket[kReset] = true + } + + if (upgrade || method === 'CONNECT') { + // On CONNECT or upgrade, block pipeline from dispatching further + // requests on this connection. + + socket[kReset] = true + } + + if (client[kMaxRequests] && socket[kCounter]++ >= client[kMaxRequests]) { + socket[kReset] = true + } + + if (blocking) { + socket[kBlocking] = true + } + + let header = `${method} ${path} HTTP/1.1\r\n` + + if (typeof host === 'string') { + header += `host: ${host}\r\n` + } else { + header += client[kHostHeader] + } + + if (upgrade) { + header += `connection: upgrade\r\nupgrade: ${upgrade}\r\n` + } else if (client[kPipelining]) { + header += 'connection: keep-alive\r\n' + } else { + header += 'connection: close\r\n' + } + + if (headers) { + header += headers + } + + if (channels.sendHeaders.hasSubscribers) { + channels.sendHeaders.publish({ request, headers: header, socket }) + } + + /* istanbul ignore else: assertion */ + if (!body) { + if (contentLength === 0) { + socket.write(`${header}content-length: 0\r\n\r\n`, 'ascii') + } else { + assert(contentLength === null, 'no body must not have content length') + socket.write(`${header}\r\n`, 'ascii') + } + request.onRequestSent() + } else if (util.isBuffer(body)) { + assert(contentLength === body.byteLength, 'buffer body must have content length') + + socket.cork() + socket.write(`${header}content-length: ${contentLength}\r\n\r\n`, 'ascii') + socket.write(body) + socket.uncork() + request.onBodySent(body) + request.onRequestSent() + if (!expectsPayload) { + socket[kReset] = true + } + } else if (util.isBlobLike(body)) { + if (typeof body.stream === 'function') { + writeIterable({ body: body.stream(), client, request, socket, contentLength, header, expectsPayload }) + } else { + writeBlob({ body, client, request, socket, contentLength, header, expectsPayload }) + } + } else if (util.isStream(body)) { + writeStream({ body, client, request, socket, contentLength, header, expectsPayload }) + } else if (util.isIterable(body)) { + writeIterable({ body, client, request, socket, contentLength, header, expectsPayload }) + } else { + assert(false) + } + + return true +} + +function writeStream ({ body, client, request, socket, contentLength, header, expectsPayload }) { + assert(contentLength !== 0 || client[kRunning] === 0, 'stream body cannot be pipelined') + + let finished = false + + const writer = new AsyncWriter({ socket, request, contentLength, client, expectsPayload, header }) + + const onData = function (chunk) { + try { + assert(!finished) + + if (!writer.write(chunk) && this.pause) { + this.pause() + } + } catch (err) { + util.destroy(this, err) + } + } + const onDrain = function () { + assert(!finished) + + if (body.resume) { + body.resume() + } + } + const onAbort = function () { + onFinished(new RequestAbortedError()) + } + const onFinished = function (err) { + if (finished) { + return + } + + finished = true + + assert(socket.destroyed || (socket[kWriting] && client[kRunning] <= 1)) + + socket + .off('drain', onDrain) + .off('error', onFinished) + + body + .removeListener('data', onData) + .removeListener('end', onFinished) + .removeListener('error', onFinished) + .removeListener('close', onAbort) + + if (!err) { + try { + writer.end() + } catch (er) { + err = er + } + } + + writer.destroy(err) + + if (err && (err.code !== 'UND_ERR_INFO' || err.message !== 'reset')) { + util.destroy(body, err) + } else { + util.destroy(body) + } + } + + body + .on('data', onData) + .on('end', onFinished) + .on('error', onFinished) + .on('close', onAbort) + + if (body.resume) { + body.resume() + } + + socket + .on('drain', onDrain) + .on('error', onFinished) +} + +async function writeBlob ({ body, client, request, socket, contentLength, header, expectsPayload }) { + assert(contentLength === body.size, 'blob body must have content length') + + try { + if (contentLength != null && contentLength !== body.size) { + throw new RequestContentLengthMismatchError() + } + + const buffer = Buffer.from(await body.arrayBuffer()) + + socket.cork() + socket.write(`${header}content-length: ${contentLength}\r\n\r\n`, 'ascii') + socket.write(buffer) + socket.uncork() + + request.onBodySent(buffer) + request.onRequestSent() + + if (!expectsPayload) { + socket[kReset] = true + } + + resume(client) + } catch (err) { + util.destroy(socket, err) + } +} + +async function writeIterable ({ body, client, request, socket, contentLength, header, expectsPayload }) { + assert(contentLength !== 0 || client[kRunning] === 0, 'iterator body cannot be pipelined') + + let callback = null + function onDrain () { + if (callback) { + const cb = callback + callback = null + cb() + } + } + + const waitForDrain = () => new Promise((resolve, reject) => { + assert(callback === null) + + if (socket[kError]) { + reject(socket[kError]) + } else { + callback = resolve + } + }) + + socket + .on('close', onDrain) + .on('drain', onDrain) + + const writer = new AsyncWriter({ socket, request, contentLength, client, expectsPayload, header }) + try { + // It's up to the user to somehow abort the async iterable. + for await (const chunk of body) { + if (socket[kError]) { + throw socket[kError] + } + + if (!writer.write(chunk)) { + await waitForDrain() + } + } + + writer.end() + } catch (err) { + writer.destroy(err) + } finally { + socket + .off('close', onDrain) + .off('drain', onDrain) + } +} + +class AsyncWriter { + constructor ({ socket, request, contentLength, client, expectsPayload, header }) { + this.socket = socket + this.request = request + this.contentLength = contentLength + this.client = client + this.bytesWritten = 0 + this.expectsPayload = expectsPayload + this.header = header + + socket[kWriting] = true + } + + write (chunk) { + const { socket, request, contentLength, client, bytesWritten, expectsPayload, header } = this + + if (socket[kError]) { + throw socket[kError] + } + + if (socket.destroyed) { + return false + } + + const len = Buffer.byteLength(chunk) + if (!len) { + return true + } + + // We should defer writing chunks. + if (contentLength !== null && bytesWritten + len > contentLength) { + if (client[kStrictContentLength]) { + throw new RequestContentLengthMismatchError() + } + + process.emitWarning(new RequestContentLengthMismatchError()) + } + + if (bytesWritten === 0) { + if (!expectsPayload) { + socket[kReset] = true + } + + if (contentLength === null) { + socket.write(`${header}transfer-encoding: chunked\r\n`, 'ascii') + } else { + socket.write(`${header}content-length: ${contentLength}\r\n\r\n`, 'ascii') + } + } + + if (contentLength === null) { + socket.write(`\r\n${len.toString(16)}\r\n`, 'ascii') + } + + this.bytesWritten += len + + const ret = socket.write(chunk) + + request.onBodySent(chunk) + + if (!ret) { + if (socket[kParser].timeout && socket[kParser].timeoutType === TIMEOUT_HEADERS) { + // istanbul ignore else: only for jest + if (socket[kParser].timeout.refresh) { + socket[kParser].timeout.refresh() + } + } + } + + return ret + } + + end () { + const { socket, contentLength, client, bytesWritten, expectsPayload, header, request } = this + request.onRequestSent() + + socket[kWriting] = false + + if (socket[kError]) { + throw socket[kError] + } + + if (socket.destroyed) { + return + } + + if (bytesWritten === 0) { + if (expectsPayload) { + // https://tools.ietf.org/html/rfc7230#section-3.3.2 + // A user agent SHOULD send a Content-Length in a request message when + // no Transfer-Encoding is sent and the request method defines a meaning + // for an enclosed payload body. + + socket.write(`${header}content-length: 0\r\n\r\n`, 'ascii') + } else { + socket.write(`${header}\r\n`, 'ascii') + } + } else if (contentLength === null) { + socket.write('\r\n0\r\n\r\n', 'ascii') + } + + if (contentLength !== null && bytesWritten !== contentLength) { + if (client[kStrictContentLength]) { + throw new RequestContentLengthMismatchError() + } else { + process.emitWarning(new RequestContentLengthMismatchError()) + } + } + + if (socket[kParser].timeout && socket[kParser].timeoutType === TIMEOUT_HEADERS) { + // istanbul ignore else: only for jest + if (socket[kParser].timeout.refresh) { + socket[kParser].timeout.refresh() + } + } + + resume(client) + } + + destroy (err) { + const { socket, client } = this + + socket[kWriting] = false + + if (err) { + assert(client[kRunning] <= 1, 'pipeline should only contain this request') + util.destroy(socket, err) + } + } +} + +function errorRequest (client, request, err) { + try { + request.onError(err) + assert(request.aborted) + } catch (err) { + client.emit('error', err) + } +} + +module.exports = Client diff --git a/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/compat/dispatcher-weakref.js b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/compat/dispatcher-weakref.js new file mode 100644 index 000000000..dbca85804 --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/compat/dispatcher-weakref.js @@ -0,0 +1,38 @@ +'use strict' + +/* istanbul ignore file: only for Node 12 */ + +const { kConnected, kSize } = require('../core/symbols') + +class CompatWeakRef { + constructor (value) { + this.value = value + } + + deref () { + return this.value[kConnected] === 0 && this.value[kSize] === 0 + ? undefined + : this.value + } +} + +class CompatFinalizer { + constructor (finalizer) { + this.finalizer = finalizer + } + + register (dispatcher, key) { + dispatcher.on('disconnect', () => { + if (dispatcher[kConnected] === 0 && dispatcher[kSize] === 0) { + this.finalizer(key) + } + }) + } +} + +module.exports = function () { + return { + WeakRef: global.WeakRef || CompatWeakRef, + FinalizationRegistry: global.FinalizationRegistry || CompatFinalizer + } +} diff --git a/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/core/connect.js b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/core/connect.js new file mode 100644 index 000000000..a73f3c32b --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/core/connect.js @@ -0,0 +1,136 @@ +'use strict' + +const net = require('net') +const assert = require('assert') +const util = require('./util') +const { InvalidArgumentError, ConnectTimeoutError } = require('./errors') +let tls // include tls conditionally since it is not always available + +// TODO: session re-use does not wait for the first +// connection to resolve the session and might therefore +// resolve the same servername multiple times even when +// re-use is enabled. + +function buildConnector ({ maxCachedSessions, socketPath, timeout, ...opts }) { + if (maxCachedSessions != null && (!Number.isInteger(maxCachedSessions) || maxCachedSessions < 0)) { + throw new InvalidArgumentError('maxCachedSessions must be a positive integer or zero') + } + + const options = { path: socketPath, ...opts } + const sessionCache = new Map() + timeout = timeout == null ? 10e3 : timeout + maxCachedSessions = maxCachedSessions == null ? 100 : maxCachedSessions + + return function connect ({ hostname, host, protocol, port, servername, localAddress, httpSocket }, callback) { + let socket + if (protocol === 'https:') { + if (!tls) { + tls = require('tls') + } + servername = servername || options.servername || util.getServerName(host) || null + + const sessionKey = servername || hostname + const session = sessionCache.get(sessionKey) || null + + assert(sessionKey) + + socket = tls.connect({ + highWaterMark: 16384, // TLS in node can't have bigger HWM anyway... + ...options, + servername, + session, + localAddress, + socket: httpSocket, // upgrade socket connection + port: port || 443, + host: hostname + }) + + socket + .on('session', function (session) { + // cache is disabled + if (maxCachedSessions === 0) { + return + } + + if (sessionCache.size >= maxCachedSessions) { + // remove the oldest session + const { value: oldestKey } = sessionCache.keys().next() + sessionCache.delete(oldestKey) + } + + sessionCache.set(sessionKey, session) + }) + .on('error', function (err) { + if (sessionKey && err.code !== 'UND_ERR_INFO') { + // TODO (fix): Only delete for session related errors. + sessionCache.delete(sessionKey) + } + }) + } else { + assert(!httpSocket, 'httpSocket can only be sent on TLS update') + socket = net.connect({ + highWaterMark: 64 * 1024, // Same as nodejs fs streams. + ...options, + localAddress, + port: port || 80, + host: hostname + }) + } + + const cancelTimeout = setupTimeout(() => onConnectTimeout(socket), timeout) + + socket + .setNoDelay(true) + .once(protocol === 'https:' ? 'secureConnect' : 'connect', function () { + cancelTimeout() + + if (callback) { + const cb = callback + callback = null + cb(null, this) + } + }) + .on('error', function (err) { + cancelTimeout() + + if (callback) { + const cb = callback + callback = null + cb(err) + } + }) + + return socket + } +} + +function setupTimeout (onConnectTimeout, timeout) { + if (!timeout) { + return () => {} + } + + let s1 = null + let s2 = null + const timeoutId = setTimeout(() => { + // setImmediate is added to make sure that we priotorise socket error events over timeouts + s1 = setImmediate(() => { + if (process.platform === 'win32') { + // Windows needs an extra setImmediate probably due to implementation differences in the socket logic + s2 = setImmediate(() => onConnectTimeout()) + } else { + onConnectTimeout() + } + }) + }, timeout) + return () => { + clearTimeout(timeoutId) + clearImmediate(s1) + clearImmediate(s2) + } +} + +function onConnectTimeout (socket) { + util.destroy(socket, new ConnectTimeoutError()) +} + +module.exports = buildConnector diff --git a/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/core/errors.js b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/core/errors.js new file mode 100644 index 000000000..653782d9b --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/core/errors.js @@ -0,0 +1,216 @@ +'use strict' + +class UndiciError extends Error { + constructor (message) { + super(message) + this.name = 'UndiciError' + this.code = 'UND_ERR' + } +} + +class ConnectTimeoutError extends UndiciError { + constructor (message) { + super(message) + Error.captureStackTrace(this, ConnectTimeoutError) + this.name = 'ConnectTimeoutError' + this.message = message || 'Connect Timeout Error' + this.code = 'UND_ERR_CONNECT_TIMEOUT' + } +} + +class HeadersTimeoutError extends UndiciError { + constructor (message) { + super(message) + Error.captureStackTrace(this, HeadersTimeoutError) + this.name = 'HeadersTimeoutError' + this.message = message || 'Headers Timeout Error' + this.code = 'UND_ERR_HEADERS_TIMEOUT' + } +} + +class HeadersOverflowError extends UndiciError { + constructor (message) { + super(message) + Error.captureStackTrace(this, HeadersOverflowError) + this.name = 'HeadersOverflowError' + this.message = message || 'Headers Overflow Error' + this.code = 'UND_ERR_HEADERS_OVERFLOW' + } +} + +class BodyTimeoutError extends UndiciError { + constructor (message) { + super(message) + Error.captureStackTrace(this, BodyTimeoutError) + this.name = 'BodyTimeoutError' + this.message = message || 'Body Timeout Error' + this.code = 'UND_ERR_BODY_TIMEOUT' + } +} + +class ResponseStatusCodeError extends UndiciError { + constructor (message, statusCode, headers, body) { + super(message) + Error.captureStackTrace(this, ResponseStatusCodeError) + this.name = 'ResponseStatusCodeError' + this.message = message || 'Response Status Code Error' + this.code = 'UND_ERR_RESPONSE_STATUS_CODE' + this.body = body + this.status = statusCode + this.statusCode = statusCode + this.headers = headers + } +} + +class InvalidArgumentError extends UndiciError { + constructor (message) { + super(message) + Error.captureStackTrace(this, InvalidArgumentError) + this.name = 'InvalidArgumentError' + this.message = message || 'Invalid Argument Error' + this.code = 'UND_ERR_INVALID_ARG' + } +} + +class InvalidReturnValueError extends UndiciError { + constructor (message) { + super(message) + Error.captureStackTrace(this, InvalidReturnValueError) + this.name = 'InvalidReturnValueError' + this.message = message || 'Invalid Return Value Error' + this.code = 'UND_ERR_INVALID_RETURN_VALUE' + } +} + +class RequestAbortedError extends UndiciError { + constructor (message) { + super(message) + Error.captureStackTrace(this, RequestAbortedError) + this.name = 'AbortError' + this.message = message || 'Request aborted' + this.code = 'UND_ERR_ABORTED' + } +} + +class InformationalError extends UndiciError { + constructor (message) { + super(message) + Error.captureStackTrace(this, InformationalError) + this.name = 'InformationalError' + this.message = message || 'Request information' + this.code = 'UND_ERR_INFO' + } +} + +class RequestContentLengthMismatchError extends UndiciError { + constructor (message) { + super(message) + Error.captureStackTrace(this, RequestContentLengthMismatchError) + this.name = 'RequestContentLengthMismatchError' + this.message = message || 'Request body length does not match content-length header' + this.code = 'UND_ERR_REQ_CONTENT_LENGTH_MISMATCH' + } +} + +class ResponseContentLengthMismatchError extends UndiciError { + constructor (message) { + super(message) + Error.captureStackTrace(this, ResponseContentLengthMismatchError) + this.name = 'ResponseContentLengthMismatchError' + this.message = message || 'Response body length does not match content-length header' + this.code = 'UND_ERR_RES_CONTENT_LENGTH_MISMATCH' + } +} + +class ClientDestroyedError extends UndiciError { + constructor (message) { + super(message) + Error.captureStackTrace(this, ClientDestroyedError) + this.name = 'ClientDestroyedError' + this.message = message || 'The client is destroyed' + this.code = 'UND_ERR_DESTROYED' + } +} + +class ClientClosedError extends UndiciError { + constructor (message) { + super(message) + Error.captureStackTrace(this, ClientClosedError) + this.name = 'ClientClosedError' + this.message = message || 'The client is closed' + this.code = 'UND_ERR_CLOSED' + } +} + +class SocketError extends UndiciError { + constructor (message, socket) { + super(message) + Error.captureStackTrace(this, SocketError) + this.name = 'SocketError' + this.message = message || 'Socket error' + this.code = 'UND_ERR_SOCKET' + this.socket = socket + } +} + +class NotSupportedError extends UndiciError { + constructor (message) { + super(message) + Error.captureStackTrace(this, NotSupportedError) + this.name = 'NotSupportedError' + this.message = message || 'Not supported error' + this.code = 'UND_ERR_NOT_SUPPORTED' + } +} + +class BalancedPoolMissingUpstreamError extends UndiciError { + constructor (message) { + super(message) + Error.captureStackTrace(this, NotSupportedError) + this.name = 'MissingUpstreamError' + this.message = message || 'No upstream has been added to the BalancedPool' + this.code = 'UND_ERR_BPL_MISSING_UPSTREAM' + } +} + +class HTTPParserError extends Error { + constructor (message, code, data) { + super(message) + Error.captureStackTrace(this, HTTPParserError) + this.name = 'HTTPParserError' + this.code = code ? `HPE_${code}` : undefined + this.data = data ? data.toString() : undefined + } +} + +class ResponseExceededMaxSizeError extends UndiciError { + constructor (message) { + super(message) + Error.captureStackTrace(this, ResponseExceededMaxSizeError) + this.name = 'ResponseExceededMaxSizeError' + this.message = message || 'Response content exceeded max size' + this.code = 'UND_ERR_RES_EXCEEDED_MAX_SIZE' + } +} + +module.exports = { + HTTPParserError, + UndiciError, + HeadersTimeoutError, + HeadersOverflowError, + BodyTimeoutError, + RequestContentLengthMismatchError, + ConnectTimeoutError, + ResponseStatusCodeError, + InvalidArgumentError, + InvalidReturnValueError, + RequestAbortedError, + ClientDestroyedError, + ClientClosedError, + InformationalError, + SocketError, + NotSupportedError, + ResponseContentLengthMismatchError, + BalancedPoolMissingUpstreamError, + ResponseExceededMaxSizeError +} diff --git a/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/core/request.js b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/core/request.js new file mode 100644 index 000000000..b05a16d7a --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/core/request.js @@ -0,0 +1,339 @@ +'use strict' + +const { + InvalidArgumentError, + NotSupportedError +} = require('./errors') +const assert = require('assert') +const util = require('./util') + +// tokenRegExp and headerCharRegex have been lifted from +// https://github.com/nodejs/node/blob/main/lib/_http_common.js + +/** + * Verifies that the given val is a valid HTTP token + * per the rules defined in RFC 7230 + * See https://tools.ietf.org/html/rfc7230#section-3.2.6 + */ +const tokenRegExp = /^[\^_`a-zA-Z\-0-9!#$%&'*+.|~]+$/ + +/** + * Matches if val contains an invalid field-vchar + * field-value = *( field-content / obs-fold ) + * field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ] + * field-vchar = VCHAR / obs-text + */ +const headerCharRegex = /[^\t\x20-\x7e\x80-\xff]/ + +// Verifies that a given path is valid does not contain control chars \x00 to \x20 +const invalidPathRegex = /[^\u0021-\u00ff]/ + +const kHandler = Symbol('handler') + +const channels = {} + +let extractBody + +const nodeVersion = process.versions.node.split('.') +const nodeMajor = Number(nodeVersion[0]) +const nodeMinor = Number(nodeVersion[1]) + +try { + const diagnosticsChannel = require('diagnostics_channel') + channels.create = diagnosticsChannel.channel('undici:request:create') + channels.bodySent = diagnosticsChannel.channel('undici:request:bodySent') + channels.headers = diagnosticsChannel.channel('undici:request:headers') + channels.trailers = diagnosticsChannel.channel('undici:request:trailers') + channels.error = diagnosticsChannel.channel('undici:request:error') +} catch { + channels.create = { hasSubscribers: false } + channels.bodySent = { hasSubscribers: false } + channels.headers = { hasSubscribers: false } + channels.trailers = { hasSubscribers: false } + channels.error = { hasSubscribers: false } +} + +class Request { + constructor (origin, { + path, + method, + body, + headers, + query, + idempotent, + blocking, + upgrade, + headersTimeout, + bodyTimeout, + throwOnError + }, handler) { + if (typeof path !== 'string') { + throw new InvalidArgumentError('path must be a string') + } else if ( + path[0] !== '/' && + !(path.startsWith('http://') || path.startsWith('https://')) && + method !== 'CONNECT' + ) { + throw new InvalidArgumentError('path must be an absolute URL or start with a slash') + } else if (invalidPathRegex.exec(path) !== null) { + throw new InvalidArgumentError('invalid request path') + } + + if (typeof method !== 'string') { + throw new InvalidArgumentError('method must be a string') + } else if (tokenRegExp.exec(method) === null) { + throw new InvalidArgumentError('invalid request method') + } + + if (upgrade && typeof upgrade !== 'string') { + throw new InvalidArgumentError('upgrade must be a string') + } + + if (headersTimeout != null && (!Number.isFinite(headersTimeout) || headersTimeout < 0)) { + throw new InvalidArgumentError('invalid headersTimeout') + } + + if (bodyTimeout != null && (!Number.isFinite(bodyTimeout) || bodyTimeout < 0)) { + throw new InvalidArgumentError('invalid bodyTimeout') + } + + this.headersTimeout = headersTimeout + + this.bodyTimeout = bodyTimeout + + this.throwOnError = throwOnError === true + + this.method = method + + if (body == null) { + this.body = null + } else if (util.isStream(body)) { + this.body = body + } else if (util.isBuffer(body)) { + this.body = body.byteLength ? body : null + } else if (ArrayBuffer.isView(body)) { + this.body = body.buffer.byteLength ? Buffer.from(body.buffer, body.byteOffset, body.byteLength) : null + } else if (body instanceof ArrayBuffer) { + this.body = body.byteLength ? Buffer.from(body) : null + } else if (typeof body === 'string') { + this.body = body.length ? Buffer.from(body) : null + } else if (util.isFormDataLike(body) || util.isIterable(body) || util.isBlobLike(body)) { + this.body = body + } else { + throw new InvalidArgumentError('body must be a string, a Buffer, a Readable stream, an iterable, or an async iterable') + } + + this.completed = false + + this.aborted = false + + this.upgrade = upgrade || null + + this.path = query ? util.buildURL(path, query) : path + + this.origin = origin + + this.idempotent = idempotent == null + ? method === 'HEAD' || method === 'GET' + : idempotent + + this.blocking = blocking == null ? false : blocking + + this.host = null + + this.contentLength = null + + this.contentType = null + + this.headers = '' + + if (Array.isArray(headers)) { + if (headers.length % 2 !== 0) { + throw new InvalidArgumentError('headers array must be even') + } + for (let i = 0; i < headers.length; i += 2) { + processHeader(this, headers[i], headers[i + 1]) + } + } else if (headers && typeof headers === 'object') { + const keys = Object.keys(headers) + for (let i = 0; i < keys.length; i++) { + const key = keys[i] + processHeader(this, key, headers[key]) + } + } else if (headers != null) { + throw new InvalidArgumentError('headers must be an object or an array') + } + + if (util.isFormDataLike(this.body)) { + if (nodeMajor < 16 || (nodeMajor === 16 && nodeMinor < 8)) { + throw new InvalidArgumentError('Form-Data bodies are only supported in node v16.8 and newer.') + } + + if (!extractBody) { + extractBody = require('../fetch/body.js').extractBody + } + + const [bodyStream, contentType] = extractBody(body) + if (this.contentType == null) { + this.contentType = contentType + this.headers += `content-type: ${contentType}\r\n` + } + this.body = bodyStream.stream + } else if (util.isBlobLike(body) && this.contentType == null && body.type) { + this.contentType = body.type + this.headers += `content-type: ${body.type}\r\n` + } + + util.validateHandler(handler, method, upgrade) + + this.servername = util.getServerName(this.host) + + this[kHandler] = handler + + if (channels.create.hasSubscribers) { + channels.create.publish({ request: this }) + } + } + + onBodySent (chunk) { + if (this[kHandler].onBodySent) { + try { + this[kHandler].onBodySent(chunk) + } catch (err) { + this.onError(err) + } + } + } + + onRequestSent () { + if (channels.bodySent.hasSubscribers) { + channels.bodySent.publish({ request: this }) + } + } + + onConnect (abort) { + assert(!this.aborted) + assert(!this.completed) + + return this[kHandler].onConnect(abort) + } + + onHeaders (statusCode, headers, resume, statusText) { + assert(!this.aborted) + assert(!this.completed) + + if (channels.headers.hasSubscribers) { + channels.headers.publish({ request: this, response: { statusCode, headers, statusText } }) + } + + return this[kHandler].onHeaders(statusCode, headers, resume, statusText) + } + + onData (chunk) { + assert(!this.aborted) + assert(!this.completed) + + return this[kHandler].onData(chunk) + } + + onUpgrade (statusCode, headers, socket) { + assert(!this.aborted) + assert(!this.completed) + + return this[kHandler].onUpgrade(statusCode, headers, socket) + } + + onComplete (trailers) { + assert(!this.aborted) + + this.completed = true + if (channels.trailers.hasSubscribers) { + channels.trailers.publish({ request: this, trailers }) + } + return this[kHandler].onComplete(trailers) + } + + onError (error) { + if (channels.error.hasSubscribers) { + channels.error.publish({ request: this, error }) + } + + if (this.aborted) { + return + } + this.aborted = true + return this[kHandler].onError(error) + } + + addHeader (key, value) { + processHeader(this, key, value) + return this + } +} + +function processHeader (request, key, val) { + if (val && typeof val === 'object') { + throw new InvalidArgumentError(`invalid ${key} header`) + } else if (val === undefined) { + return + } + + if ( + request.host === null && + key.length === 4 && + key.toLowerCase() === 'host' + ) { + // Consumed by Client + request.host = val + } else if ( + request.contentLength === null && + key.length === 14 && + key.toLowerCase() === 'content-length' + ) { + request.contentLength = parseInt(val, 10) + if (!Number.isFinite(request.contentLength)) { + throw new InvalidArgumentError('invalid content-length header') + } + } else if ( + request.contentType === null && + key.length === 12 && + key.toLowerCase() === 'content-type' && + headerCharRegex.exec(val) === null + ) { + request.contentType = val + request.headers += `${key}: ${val}\r\n` + } else if ( + key.length === 17 && + key.toLowerCase() === 'transfer-encoding' + ) { + throw new InvalidArgumentError('invalid transfer-encoding header') + } else if ( + key.length === 10 && + key.toLowerCase() === 'connection' + ) { + throw new InvalidArgumentError('invalid connection header') + } else if ( + key.length === 10 && + key.toLowerCase() === 'keep-alive' + ) { + throw new InvalidArgumentError('invalid keep-alive header') + } else if ( + key.length === 7 && + key.toLowerCase() === 'upgrade' + ) { + throw new InvalidArgumentError('invalid upgrade header') + } else if ( + key.length === 6 && + key.toLowerCase() === 'expect' + ) { + throw new NotSupportedError('expect header not supported') + } else if (tokenRegExp.exec(key) === null) { + throw new InvalidArgumentError('invalid header key') + } else if (headerCharRegex.exec(val) !== null) { + throw new InvalidArgumentError(`invalid ${key} header`) + } else { + request.headers += `${key}: ${val}\r\n` + } +} + +module.exports = Request diff --git a/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/core/symbols.js b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/core/symbols.js new file mode 100644 index 000000000..ff36337a5 --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/core/symbols.js @@ -0,0 +1,55 @@ +module.exports = { + kClose: Symbol('close'), + kDestroy: Symbol('destroy'), + kDispatch: Symbol('dispatch'), + kUrl: Symbol('url'), + kWriting: Symbol('writing'), + kResuming: Symbol('resuming'), + kQueue: Symbol('queue'), + kConnect: Symbol('connect'), + kConnecting: Symbol('connecting'), + kHeadersList: Symbol('headers list'), + kKeepAliveDefaultTimeout: Symbol('default keep alive timeout'), + kKeepAliveMaxTimeout: Symbol('max keep alive timeout'), + kKeepAliveTimeoutThreshold: Symbol('keep alive timeout threshold'), + kKeepAliveTimeoutValue: Symbol('keep alive timeout'), + kKeepAlive: Symbol('keep alive'), + kHeadersTimeout: Symbol('headers timeout'), + kBodyTimeout: Symbol('body timeout'), + kServerName: Symbol('server name'), + kLocalAddress: Symbol('local address'), + kHost: Symbol('host'), + kNoRef: Symbol('no ref'), + kBodyUsed: Symbol('used'), + kRunning: Symbol('running'), + kBlocking: Symbol('blocking'), + kPending: Symbol('pending'), + kSize: Symbol('size'), + kBusy: Symbol('busy'), + kQueued: Symbol('queued'), + kFree: Symbol('free'), + kConnected: Symbol('connected'), + kClosed: Symbol('closed'), + kNeedDrain: Symbol('need drain'), + kReset: Symbol('reset'), + kDestroyed: Symbol('destroyed'), + kMaxHeadersSize: Symbol('max headers size'), + kRunningIdx: Symbol('running index'), + kPendingIdx: Symbol('pending index'), + kError: Symbol('error'), + kClients: Symbol('clients'), + kClient: Symbol('client'), + kParser: Symbol('parser'), + kOnDestroyed: Symbol('destroy callbacks'), + kPipelining: Symbol('pipelinig'), + kSocket: Symbol('socket'), + kHostHeader: Symbol('host header'), + kConnector: Symbol('connector'), + kStrictContentLength: Symbol('strict content length'), + kMaxRedirections: Symbol('maxRedirections'), + kMaxRequests: Symbol('maxRequestsPerClient'), + kProxy: Symbol('proxy agent options'), + kCounter: Symbol('socket request counter'), + kInterceptors: Symbol('dispatch interceptors'), + kMaxResponseSize: Symbol('max response size') +} diff --git a/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/core/util.js b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/core/util.js new file mode 100644 index 000000000..c2dcf79fb --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/core/util.js @@ -0,0 +1,392 @@ +'use strict' + +const assert = require('assert') +const { kDestroyed, kBodyUsed } = require('./symbols') +const { IncomingMessage } = require('http') +const stream = require('stream') +const net = require('net') +const { InvalidArgumentError } = require('./errors') +const { Blob } = require('buffer') +const nodeUtil = require('util') +const { stringify } = require('querystring') + +function nop () {} + +function isStream (obj) { + return obj && typeof obj.pipe === 'function' +} + +// based on https://github.com/node-fetch/fetch-blob/blob/8ab587d34080de94140b54f07168451e7d0b655e/index.js#L229-L241 (MIT License) +function isBlobLike (object) { + return (Blob && object instanceof Blob) || ( + object && + typeof object === 'object' && + (typeof object.stream === 'function' || + typeof object.arrayBuffer === 'function') && + /^(Blob|File)$/.test(object[Symbol.toStringTag]) + ) +} + +function buildURL (url, queryParams) { + if (url.includes('?') || url.includes('#')) { + throw new Error('Query params cannot be passed when url already contains "?" or "#".') + } + + const stringified = stringify(queryParams) + + if (stringified) { + url += '?' + stringified + } + + return url +} + +function parseURL (url) { + if (typeof url === 'string') { + url = new URL(url) + } + + if (!url || typeof url !== 'object') { + throw new InvalidArgumentError('invalid url') + } + + if (url.port != null && url.port !== '' && !Number.isFinite(parseInt(url.port))) { + throw new InvalidArgumentError('invalid port') + } + + if (url.path != null && typeof url.path !== 'string') { + throw new InvalidArgumentError('invalid path') + } + + if (url.pathname != null && typeof url.pathname !== 'string') { + throw new InvalidArgumentError('invalid pathname') + } + + if (url.hostname != null && typeof url.hostname !== 'string') { + throw new InvalidArgumentError('invalid hostname') + } + + if (url.origin != null && typeof url.origin !== 'string') { + throw new InvalidArgumentError('invalid origin') + } + + if (!/^https?:/.test(url.origin || url.protocol)) { + throw new InvalidArgumentError('invalid protocol') + } + + if (!(url instanceof URL)) { + const port = url.port != null + ? url.port + : (url.protocol === 'https:' ? 443 : 80) + let origin = url.origin != null + ? url.origin + : `${url.protocol}//${url.hostname}:${port}` + let path = url.path != null + ? url.path + : `${url.pathname || ''}${url.search || ''}` + + if (origin.endsWith('/')) { + origin = origin.substring(0, origin.length - 1) + } + + if (path && !path.startsWith('/')) { + path = `/${path}` + } + // new URL(path, origin) is unsafe when `path` contains an absolute URL + // From https://developer.mozilla.org/en-US/docs/Web/API/URL/URL: + // If first parameter is a relative URL, second param is required, and will be used as the base URL. + // If first parameter is an absolute URL, a given second param will be ignored. + url = new URL(origin + path) + } + + return url +} + +function parseOrigin (url) { + url = parseURL(url) + + if (url.pathname !== '/' || url.search || url.hash) { + throw new InvalidArgumentError('invalid url') + } + + return url +} + +function getHostname (host) { + if (host[0] === '[') { + const idx = host.indexOf(']') + + assert(idx !== -1) + return host.substr(1, idx - 1) + } + + const idx = host.indexOf(':') + if (idx === -1) return host + + return host.substr(0, idx) +} + +// IP addresses are not valid server names per RFC6066 +// > Currently, the only server names supported are DNS hostnames +function getServerName (host) { + if (!host) { + return null + } + + assert.strictEqual(typeof host, 'string') + + const servername = getHostname(host) + if (net.isIP(servername)) { + return '' + } + + return servername +} + +function deepClone (obj) { + return JSON.parse(JSON.stringify(obj)) +} + +function isAsyncIterable (obj) { + return !!(obj != null && typeof obj[Symbol.asyncIterator] === 'function') +} + +function isIterable (obj) { + return !!(obj != null && (typeof obj[Symbol.iterator] === 'function' || typeof obj[Symbol.asyncIterator] === 'function')) +} + +function bodyLength (body) { + if (body == null) { + return 0 + } else if (isStream(body)) { + const state = body._readableState + return state && state.ended === true && Number.isFinite(state.length) + ? state.length + : null + } else if (isBlobLike(body)) { + return body.size != null ? body.size : null + } else if (isBuffer(body)) { + return body.byteLength + } + + return null +} + +function isDestroyed (stream) { + return !stream || !!(stream.destroyed || stream[kDestroyed]) +} + +function isReadableAborted (stream) { + const state = stream && stream._readableState + return isDestroyed(stream) && state && !state.endEmitted +} + +function destroy (stream, err) { + if (!isStream(stream) || isDestroyed(stream)) { + return + } + + if (typeof stream.destroy === 'function') { + if (Object.getPrototypeOf(stream).constructor === IncomingMessage) { + // See: https://github.com/nodejs/node/pull/38505/files + stream.socket = null + } + stream.destroy(err) + } else if (err) { + process.nextTick((stream, err) => { + stream.emit('error', err) + }, stream, err) + } + + if (stream.destroyed !== true) { + stream[kDestroyed] = true + } +} + +const KEEPALIVE_TIMEOUT_EXPR = /timeout=(\d+)/ +function parseKeepAliveTimeout (val) { + const m = val.toString().match(KEEPALIVE_TIMEOUT_EXPR) + return m ? parseInt(m[1], 10) * 1000 : null +} + +function parseHeaders (headers, obj = {}) { + for (let i = 0; i < headers.length; i += 2) { + const key = headers[i].toString().toLowerCase() + let val = obj[key] + if (!val) { + if (Array.isArray(headers[i + 1])) { + obj[key] = headers[i + 1] + } else { + obj[key] = headers[i + 1].toString() + } + } else { + if (!Array.isArray(val)) { + val = [val] + obj[key] = val + } + val.push(headers[i + 1].toString()) + } + } + return obj +} + +function parseRawHeaders (headers) { + return headers.map(header => header.toString()) +} + +function isBuffer (buffer) { + // See, https://github.com/mcollina/undici/pull/319 + return buffer instanceof Uint8Array || Buffer.isBuffer(buffer) +} + +function validateHandler (handler, method, upgrade) { + if (!handler || typeof handler !== 'object') { + throw new InvalidArgumentError('handler must be an object') + } + + if (typeof handler.onConnect !== 'function') { + throw new InvalidArgumentError('invalid onConnect method') + } + + if (typeof handler.onError !== 'function') { + throw new InvalidArgumentError('invalid onError method') + } + + if (typeof handler.onBodySent !== 'function' && handler.onBodySent !== undefined) { + throw new InvalidArgumentError('invalid onBodySent method') + } + + if (upgrade || method === 'CONNECT') { + if (typeof handler.onUpgrade !== 'function') { + throw new InvalidArgumentError('invalid onUpgrade method') + } + } else { + if (typeof handler.onHeaders !== 'function') { + throw new InvalidArgumentError('invalid onHeaders method') + } + + if (typeof handler.onData !== 'function') { + throw new InvalidArgumentError('invalid onData method') + } + + if (typeof handler.onComplete !== 'function') { + throw new InvalidArgumentError('invalid onComplete method') + } + } +} + +// A body is disturbed if it has been read from and it cannot +// be re-used without losing state or data. +function isDisturbed (body) { + return !!(body && ( + stream.isDisturbed + ? stream.isDisturbed(body) || body[kBodyUsed] // TODO (fix): Why is body[kBodyUsed] needed? + : body[kBodyUsed] || + body.readableDidRead || + (body._readableState && body._readableState.dataEmitted) || + isReadableAborted(body) + )) +} + +function isErrored (body) { + return !!(body && ( + stream.isErrored + ? stream.isErrored(body) + : /state: 'errored'/.test(nodeUtil.inspect(body) + ))) +} + +function isReadable (body) { + return !!(body && ( + stream.isReadable + ? stream.isReadable(body) + : /state: 'readable'/.test(nodeUtil.inspect(body) + ))) +} + +function getSocketInfo (socket) { + return { + localAddress: socket.localAddress, + localPort: socket.localPort, + remoteAddress: socket.remoteAddress, + remotePort: socket.remotePort, + remoteFamily: socket.remoteFamily, + timeout: socket.timeout, + bytesWritten: socket.bytesWritten, + bytesRead: socket.bytesRead + } +} + +let ReadableStream +function ReadableStreamFrom (iterable) { + if (!ReadableStream) { + ReadableStream = require('stream/web').ReadableStream + } + + if (ReadableStream.from) { + // https://github.com/whatwg/streams/pull/1083 + return ReadableStream.from(iterable) + } + + let iterator + return new ReadableStream( + { + async start () { + iterator = iterable[Symbol.asyncIterator]() + }, + async pull (controller) { + const { done, value } = await iterator.next() + if (done) { + queueMicrotask(() => { + controller.close() + }) + } else { + const buf = Buffer.isBuffer(value) ? value : Buffer.from(value) + controller.enqueue(new Uint8Array(buf)) + } + return controller.desiredSize > 0 + }, + async cancel (reason) { + await iterator.return() + } + }, + 0 + ) +} + +function isFormDataLike (chunk) { + return chunk && chunk.constructor && chunk.constructor.name === 'FormData' +} + +const kEnumerableProperty = Object.create(null) +kEnumerableProperty.enumerable = true + +module.exports = { + kEnumerableProperty, + nop, + isDisturbed, + isErrored, + isReadable, + toUSVString: nodeUtil.toUSVString || ((val) => `${val}`), + isReadableAborted, + isBlobLike, + parseOrigin, + parseURL, + getServerName, + isStream, + isIterable, + isAsyncIterable, + isDestroyed, + parseRawHeaders, + parseHeaders, + parseKeepAliveTimeout, + destroy, + bodyLength, + deepClone, + ReadableStreamFrom, + isBuffer, + validateHandler, + getSocketInfo, + isFormDataLike, + buildURL +} diff --git a/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/dispatcher-base.js b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/dispatcher-base.js new file mode 100644 index 000000000..14a5c0acd --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/dispatcher-base.js @@ -0,0 +1,191 @@ +'use strict' + +const Dispatcher = require('./dispatcher') +const { + ClientDestroyedError, + ClientClosedError, + InvalidArgumentError +} = require('./core/errors') +const { kDestroy, kClose, kDispatch, kInterceptors } = require('./core/symbols') + +const kDestroyed = Symbol('destroyed') +const kClosed = Symbol('closed') +const kOnDestroyed = Symbol('onDestroyed') +const kOnClosed = Symbol('onClosed') +const kInterceptedDispatch = Symbol('Intercepted Dispatch') + +class DispatcherBase extends Dispatcher { + constructor () { + super() + + this[kDestroyed] = false + this[kOnDestroyed] = [] + this[kClosed] = false + this[kOnClosed] = [] + } + + get destroyed () { + return this[kDestroyed] + } + + get closed () { + return this[kClosed] + } + + get interceptors () { + return this[kInterceptors] + } + + set interceptors (newInterceptors) { + if (newInterceptors) { + for (let i = newInterceptors.length - 1; i >= 0; i--) { + const interceptor = this[kInterceptors][i] + if (typeof interceptor !== 'function') { + throw new InvalidArgumentError('interceptor must be an function') + } + } + } + + this[kInterceptors] = newInterceptors + } + + close (callback) { + if (callback === undefined) { + return new Promise((resolve, reject) => { + this.close((err, data) => { + return err ? reject(err) : resolve(data) + }) + }) + } + + if (typeof callback !== 'function') { + throw new InvalidArgumentError('invalid callback') + } + + if (this[kDestroyed]) { + queueMicrotask(() => callback(new ClientDestroyedError(), null)) + return + } + + if (this[kClosed]) { + if (this[kOnClosed]) { + this[kOnClosed].push(callback) + } else { + queueMicrotask(() => callback(null, null)) + } + return + } + + this[kClosed] = true + this[kOnClosed].push(callback) + + const onClosed = () => { + const callbacks = this[kOnClosed] + this[kOnClosed] = null + for (let i = 0; i < callbacks.length; i++) { + callbacks[i](null, null) + } + } + + // Should not error. + this[kClose]() + .then(() => this.destroy()) + .then(() => { + queueMicrotask(onClosed) + }) + } + + destroy (err, callback) { + if (typeof err === 'function') { + callback = err + err = null + } + + if (callback === undefined) { + return new Promise((resolve, reject) => { + this.destroy(err, (err, data) => { + return err ? /* istanbul ignore next: should never error */ reject(err) : resolve(data) + }) + }) + } + + if (typeof callback !== 'function') { + throw new InvalidArgumentError('invalid callback') + } + + if (this[kDestroyed]) { + if (this[kOnDestroyed]) { + this[kOnDestroyed].push(callback) + } else { + queueMicrotask(() => callback(null, null)) + } + return + } + + if (!err) { + err = new ClientDestroyedError() + } + + this[kDestroyed] = true + this[kOnDestroyed].push(callback) + + const onDestroyed = () => { + const callbacks = this[kOnDestroyed] + this[kOnDestroyed] = null + for (let i = 0; i < callbacks.length; i++) { + callbacks[i](null, null) + } + } + + // Should not error. + this[kDestroy](err).then(() => { + queueMicrotask(onDestroyed) + }) + } + + [kInterceptedDispatch] (opts, handler) { + if (!this[kInterceptors] || this[kInterceptors].length === 0) { + this[kInterceptedDispatch] = this[kDispatch] + return this[kDispatch](opts, handler) + } + + let dispatch = this[kDispatch].bind(this) + for (let i = this[kInterceptors].length - 1; i >= 0; i--) { + dispatch = this[kInterceptors][i](dispatch) + } + this[kInterceptedDispatch] = dispatch + return dispatch(opts, handler) + } + + dispatch (opts, handler) { + if (!handler || typeof handler !== 'object') { + throw new InvalidArgumentError('handler must be an object') + } + + try { + if (!opts || typeof opts !== 'object') { + throw new InvalidArgumentError('opts must be an object.') + } + + if (this[kDestroyed]) { + throw new ClientDestroyedError() + } + + if (this[kClosed]) { + throw new ClientClosedError() + } + + return this[kInterceptedDispatch](opts, handler) + } catch (err) { + if (typeof handler.onError !== 'function') { + throw new InvalidArgumentError('invalid onError method') + } + + handler.onError(err) + + return false + } + } +} + +module.exports = DispatcherBase diff --git a/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/dispatcher.js b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/dispatcher.js new file mode 100644 index 000000000..9b809d892 --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/dispatcher.js @@ -0,0 +1,19 @@ +'use strict' + +const EventEmitter = require('events') + +class Dispatcher extends EventEmitter { + dispatch () { + throw new Error('not implemented') + } + + close () { + throw new Error('not implemented') + } + + destroy () { + throw new Error('not implemented') + } +} + +module.exports = Dispatcher diff --git a/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/fetch/LICENSE b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/fetch/LICENSE new file mode 100644 index 000000000..294350045 --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/fetch/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Ethan Arrowood + +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/fixtures/npm/cases/undici5/node_modules/undici/lib/fetch/body.js b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/fetch/body.js new file mode 100644 index 000000000..7de30264d --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/fetch/body.js @@ -0,0 +1,628 @@ +'use strict' + +const Busboy = require('busboy') +const util = require('../core/util') +const { ReadableStreamFrom, toUSVString, isBlobLike, isReadableStreamLike, readableStreamClose } = require('./util') +const { FormData } = require('./formdata') +const { kState } = require('./symbols') +const { webidl } = require('./webidl') +const { DOMException, structuredClone } = require('./constants') +const { Blob } = require('buffer') +const { kBodyUsed } = require('../core/symbols') +const assert = require('assert') +const { isErrored } = require('../core/util') +const { isUint8Array, isArrayBuffer } = require('util/types') +const { File } = require('./file') +const { StringDecoder } = require('string_decoder') +const { parseMIMEType, serializeAMimeType } = require('./dataURL') + +/** @type {globalThis['ReadableStream']} */ +let ReadableStream + +// https://fetch.spec.whatwg.org/#concept-bodyinit-extract +function extractBody (object, keepalive = false) { + if (!ReadableStream) { + ReadableStream = require('stream/web').ReadableStream + } + + // 1. Let stream be null. + let stream = null + + // 2. If object is a ReadableStream object, then set stream to object. + if (object instanceof ReadableStream) { + stream = object + } else if (isBlobLike(object)) { + // 3. Otherwise, if object is a Blob object, set stream to the + // result of running object’s get stream. + stream = object.stream() + } else { + // 4. Otherwise, set stream to a new ReadableStream object, and set + // up stream. + stream = new ReadableStream({ + async pull (controller) { + controller.enqueue( + typeof source === 'string' ? new TextEncoder().encode(source) : source + ) + queueMicrotask(() => readableStreamClose(controller)) + }, + start () {}, + type: undefined + }) + } + + // 5. Assert: stream is a ReadableStream object. + assert(isReadableStreamLike(stream)) + + // 6. Let action be null. + let action = null + + // 7. Let source be null. + let source = null + + // 8. Let length be null. + let length = null + + // 9. Let type be null. + let type = null + + // 10. Switch on object: + if (object == null) { + // Note: The IDL processor cannot handle this situation. See + // https://crbug.com/335871. + } else if (object instanceof URLSearchParams) { + // URLSearchParams + + // spec says to run application/x-www-form-urlencoded on body.list + // this is implemented in Node.js as apart of an URLSearchParams instance toString method + // See: https://github.com/nodejs/node/blob/e46c680bf2b211bbd52cf959ca17ee98c7f657f5/lib/internal/url.js#L490 + // and https://github.com/nodejs/node/blob/e46c680bf2b211bbd52cf959ca17ee98c7f657f5/lib/internal/url.js#L1100 + + // Set source to the result of running the application/x-www-form-urlencoded serializer with object’s list. + source = object.toString() + + // Set type to `application/x-www-form-urlencoded;charset=UTF-8`. + type = 'application/x-www-form-urlencoded;charset=UTF-8' + } else if (isArrayBuffer(object)) { + // BufferSource/ArrayBuffer + + // Set source to a copy of the bytes held by object. + source = new Uint8Array(object.slice()) + } else if (ArrayBuffer.isView(object)) { + // BufferSource/ArrayBufferView + + // Set source to a copy of the bytes held by object. + source = new Uint8Array(object.buffer.slice(object.byteOffset, object.byteOffset + object.byteLength)) + } else if (util.isFormDataLike(object)) { + const boundary = '----formdata-undici-' + Math.random() + const prefix = `--${boundary}\r\nContent-Disposition: form-data` + + /*! formdata-polyfill. MIT License. Jimmy Wärting */ + const escape = (str) => + str.replace(/\n/g, '%0A').replace(/\r/g, '%0D').replace(/"/g, '%22') + const normalizeLinefeeds = (value) => value.replace(/\r?\n|\r/g, '\r\n') + + // Set action to this step: run the multipart/form-data + // encoding algorithm, with object’s entry list and UTF-8. + action = async function * (object) { + const enc = new TextEncoder() + + for (const [name, value] of object) { + if (typeof value === 'string') { + yield enc.encode( + prefix + + `; name="${escape(normalizeLinefeeds(name))}"` + + `\r\n\r\n${normalizeLinefeeds(value)}\r\n` + ) + } else { + yield enc.encode( + prefix + + `; name="${escape(normalizeLinefeeds(name))}"` + + (value.name ? `; filename="${escape(value.name)}"` : '') + + '\r\n' + + `Content-Type: ${ + value.type || 'application/octet-stream' + }\r\n\r\n` + ) + + yield * value.stream() + + yield enc.encode('\r\n') + } + } + + yield enc.encode(`--${boundary}--`) + } + + // Set source to object. + source = object + + // Set length to unclear, see html/6424 for improving this. + // TODO + + // Set type to `multipart/form-data; boundary=`, + // followed by the multipart/form-data boundary string generated + // by the multipart/form-data encoding algorithm. + type = 'multipart/form-data; boundary=' + boundary + } else if (isBlobLike(object)) { + // Blob + + // Set source to object. + source = object + + // Set length to object’s size. + length = object.size + + // If object’s type attribute is not the empty byte sequence, set + // type to its value. + if (object.type) { + type = object.type + } + } else if (typeof object[Symbol.asyncIterator] === 'function') { + // If keepalive is true, then throw a TypeError. + if (keepalive) { + throw new TypeError('keepalive') + } + + // If object is disturbed or locked, then throw a TypeError. + if (util.isDisturbed(object) || object.locked) { + throw new TypeError( + 'Response body object should not be disturbed or locked' + ) + } + + stream = + object instanceof ReadableStream ? object : ReadableStreamFrom(object) + } else { + // TODO: byte sequence? + // TODO: scalar value string? + // TODO: else? + source = toUSVString(object) + type = 'text/plain;charset=UTF-8' + } + + // 11. If source is a byte sequence, then set action to a + // step that returns source and length to source’s length. + // TODO: What is a "byte sequence?" + if (typeof source === 'string' || util.isBuffer(source)) { + length = Buffer.byteLength(source) + } + + // 12. If action is non-null, then run these steps in in parallel: + if (action != null) { + // Run action. + let iterator + stream = new ReadableStream({ + async start () { + iterator = action(object)[Symbol.asyncIterator]() + }, + async pull (controller) { + const { value, done } = await iterator.next() + if (done) { + // When running action is done, close stream. + queueMicrotask(() => { + controller.close() + }) + } else { + // Whenever one or more bytes are available and stream is not errored, + // enqueue a Uint8Array wrapping an ArrayBuffer containing the available + // bytes into stream. + if (!isErrored(stream)) { + controller.enqueue(new Uint8Array(value)) + } + } + return controller.desiredSize > 0 + }, + async cancel (reason) { + await iterator.return() + }, + type: undefined + }) + } + + // 13. Let body be a body whose stream is stream, source is source, + // and length is length. + const body = { stream, source, length } + + // 14. Return (body, type). + return [body, type] +} + +// https://fetch.spec.whatwg.org/#bodyinit-safely-extract +function safelyExtractBody (object, keepalive = false) { + if (!ReadableStream) { + // istanbul ignore next + ReadableStream = require('stream/web').ReadableStream + } + + // To safely extract a body and a `Content-Type` value from + // a byte sequence or BodyInit object object, run these steps: + + // 1. If object is a ReadableStream object, then: + if (object instanceof ReadableStream) { + // Assert: object is neither disturbed nor locked. + // istanbul ignore next + assert(!util.isDisturbed(object), 'The body has already been consumed.') + // istanbul ignore next + assert(!object.locked, 'The stream is locked.') + } + + // 2. Return the results of extracting object. + return extractBody(object, keepalive) +} + +function cloneBody (body) { + // To clone a body body, run these steps: + + // https://fetch.spec.whatwg.org/#concept-body-clone + + // 1. Let « out1, out2 » be the result of teeing body’s stream. + const [out1, out2] = body.stream.tee() + const out2Clone = structuredClone(out2, { transfer: [out2] }) + // This, for whatever reasons, unrefs out2Clone which allows + // the process to exit by itself. + const [, finalClone] = out2Clone.tee() + + // 2. Set body’s stream to out1. + body.stream = out1 + + // 3. Return a body whose stream is out2 and other members are copied from body. + return { + stream: finalClone, + length: body.length, + source: body.source + } +} + +async function * consumeBody (body) { + if (body) { + if (isUint8Array(body)) { + yield body + } else { + const stream = body.stream + + if (util.isDisturbed(stream)) { + throw new TypeError('The body has already been consumed.') + } + + if (stream.locked) { + throw new TypeError('The stream is locked.') + } + + // Compat. + stream[kBodyUsed] = true + + yield * stream + } + } +} + +function throwIfAborted (state) { + if (state.aborted) { + throw new DOMException('The operation was aborted.', 'AbortError') + } +} + +function bodyMixinMethods (instance) { + const methods = { + blob () { + // The blob() method steps are to return the result of + // running consume body with this and Blob. + return specConsumeBody(this, 'Blob', instance) + }, + + arrayBuffer () { + // The arrayBuffer() method steps are to return the + // result of running consume body with this and ArrayBuffer. + return specConsumeBody(this, 'ArrayBuffer', instance) + }, + + text () { + // The text() method steps are to return the result of + // running consume body with this and text. + return specConsumeBody(this, 'text', instance) + }, + + json () { + // The json() method steps are to return the result of + // running consume body with this and JSON. + return specConsumeBody(this, 'JSON', instance) + }, + + async formData () { + if (!(this instanceof instance)) { + throw new TypeError('Illegal invocation') + } + + throwIfAborted(this[kState]) + + const contentType = this.headers.get('Content-Type') + + // If mimeType’s essence is "multipart/form-data", then: + if (/multipart\/form-data/.test(contentType)) { + const headers = {} + for (const [key, value] of this.headers) headers[key.toLowerCase()] = value + + const responseFormData = new FormData() + + let busboy + + try { + busboy = Busboy({ headers }) + } catch (err) { + // Error due to headers: + throw Object.assign(new TypeError(), { cause: err }) + } + + busboy.on('field', (name, value) => { + responseFormData.append(name, value) + }) + busboy.on('file', (name, value, info) => { + const { filename, encoding, mimeType } = info + const chunks = [] + + if (encoding.toLowerCase() === 'base64') { + let base64chunk = '' + + value.on('data', (chunk) => { + base64chunk += chunk.toString().replace(/[\r\n]/gm, '') + + const end = base64chunk.length - base64chunk.length % 4 + chunks.push(Buffer.from(base64chunk.slice(0, end), 'base64')) + + base64chunk = base64chunk.slice(end) + }) + value.on('end', () => { + chunks.push(Buffer.from(base64chunk, 'base64')) + responseFormData.append(name, new File(chunks, filename, { type: mimeType })) + }) + } else { + value.on('data', (chunk) => { + chunks.push(chunk) + }) + value.on('end', () => { + responseFormData.append(name, new File(chunks, filename, { type: mimeType })) + }) + } + }) + + const busboyResolve = new Promise((resolve, reject) => { + busboy.on('finish', resolve) + busboy.on('error', (err) => reject(new TypeError(err))) + }) + + if (this.body !== null) for await (const chunk of consumeBody(this[kState].body)) busboy.write(chunk) + busboy.end() + await busboyResolve + + return responseFormData + } else if (/application\/x-www-form-urlencoded/.test(contentType)) { + // Otherwise, if mimeType’s essence is "application/x-www-form-urlencoded", then: + + // 1. Let entries be the result of parsing bytes. + let entries + try { + let text = '' + // application/x-www-form-urlencoded parser will keep the BOM. + // https://url.spec.whatwg.org/#concept-urlencoded-parser + const textDecoder = new TextDecoder('utf-8', { ignoreBOM: true }) + for await (const chunk of consumeBody(this[kState].body)) { + if (!isUint8Array(chunk)) { + throw new TypeError('Expected Uint8Array chunk') + } + text += textDecoder.decode(chunk, { stream: true }) + } + text += textDecoder.decode() + entries = new URLSearchParams(text) + } catch (err) { + // istanbul ignore next: Unclear when new URLSearchParams can fail on a string. + // 2. If entries is failure, then throw a TypeError. + throw Object.assign(new TypeError(), { cause: err }) + } + + // 3. Return a new FormData object whose entries are entries. + const formData = new FormData() + for (const [name, value] of entries) { + formData.append(name, value) + } + return formData + } else { + // Wait a tick before checking if the request has been aborted. + // Otherwise, a TypeError can be thrown when an AbortError should. + await Promise.resolve() + + throwIfAborted(this[kState]) + + // Otherwise, throw a TypeError. + webidl.errors.exception({ + header: `${instance.name}.formData`, + message: 'Could not parse content as FormData.' + }) + } + } + } + + return methods +} + +function mixinBody (prototype) { + Object.assign(prototype.prototype, bodyMixinMethods(prototype)) +} + +// https://fetch.spec.whatwg.org/#concept-body-consume-body +async function specConsumeBody (object, type, instance) { + if (!(object instanceof instance)) { + throw new TypeError('Illegal invocation') + } + + // TODO: why is this needed? + throwIfAborted(object[kState]) + + // 1. If object is unusable, then return a promise rejected + // with a TypeError. + if (bodyUnusable(object[kState].body)) { + throw new TypeError('Body is unusable') + } + + // 2. Let promise be a promise resolved with an empty byte + // sequence. + let promise + + // 3. If object’s body is non-null, then set promise to the + // result of fully reading body as promise given object’s + // body. + if (object[kState].body != null) { + promise = await fullyReadBodyAsPromise(object[kState].body) + } else { + // step #2 + promise = { size: 0, bytes: [new Uint8Array()] } + } + + // 4. Let steps be to return the result of package data with + // the first argument given, type, and object’s MIME type. + const mimeType = type === 'Blob' || type === 'FormData' + ? bodyMimeType(object) + : undefined + + // 5. Return the result of upon fulfillment of promise given + // steps. + return packageData(promise, type, mimeType) +} + +/** + * @see https://fetch.spec.whatwg.org/#concept-body-package-data + * @param {{ size: number, bytes: Uint8Array[] }} bytes + * @param {string} type + * @param {ReturnType|undefined} mimeType + */ +function packageData ({ bytes, size }, type, mimeType) { + switch (type) { + case 'ArrayBuffer': { + // Return a new ArrayBuffer whose contents are bytes. + const uint8 = new Uint8Array(size) + let offset = 0 + + for (const chunk of bytes) { + uint8.set(chunk, offset) + offset += chunk.byteLength + } + + return uint8.buffer + } + case 'Blob': { + if (mimeType === 'failure') { + mimeType = '' + } else if (mimeType) { + mimeType = serializeAMimeType(mimeType) + } + + // Return a Blob whose contents are bytes and type attribute + // is mimeType. + return new Blob(bytes, { type: mimeType }) + } + case 'JSON': { + // Return the result of running parse JSON from bytes on bytes. + return JSON.parse(utf8DecodeBytes(bytes)) + } + case 'text': { + // 1. Return the result of running UTF-8 decode on bytes. + return utf8DecodeBytes(bytes) + } + } +} + +// https://fetch.spec.whatwg.org/#body-unusable +function bodyUnusable (body) { + // An object including the Body interface mixin is + // said to be unusable if its body is non-null and + // its body’s stream is disturbed or locked. + return body != null && (body.stream.locked || util.isDisturbed(body.stream)) +} + +// https://fetch.spec.whatwg.org/#fully-reading-body-as-promise +async function fullyReadBodyAsPromise (body) { + // 1. Let reader be the result of getting a reader for body’s + // stream. If that threw an exception, then return a promise + // rejected with that exception. + const reader = body.stream.getReader() + + // 2. Return the result of reading all bytes from reader. + /** @type {Uint8Array[]} */ + const bytes = [] + let size = 0 + + while (true) { + const { done, value } = await reader.read() + + if (done) { + break + } + + // https://streams.spec.whatwg.org/#read-loop + // If chunk is not a Uint8Array object, reject promise with + // a TypeError and abort these steps. + if (!isUint8Array(value)) { + throw new TypeError('Value is not a Uint8Array.') + } + + bytes.push(value) + size += value.byteLength + } + + return { size, bytes } +} + +/** + * @see https://encoding.spec.whatwg.org/#utf-8-decode + * @param {Uint8Array[]} ioQueue + */ +function utf8DecodeBytes (ioQueue) { + if (ioQueue.length === 0) { + return '' + } + + // 1. Let buffer be the result of peeking three bytes + // from ioQueue, converted to a byte sequence. + const buffer = ioQueue[0] + + // 2. If buffer is 0xEF 0xBB 0xBF, then read three + // bytes from ioQueue. (Do nothing with those bytes.) + if (buffer[0] === 0xEF && buffer[1] === 0xBB && buffer[2] === 0xBF) { + ioQueue[0] = ioQueue[0].subarray(3) + } + + // 3. Process a queue with an instance of UTF-8’s + // decoder, ioQueue, output, and "replacement". + const decoder = new StringDecoder('utf-8') + let output = '' + + for (const chunk of ioQueue) { + output += decoder.write(chunk) + } + + output += decoder.end() + + // 4. Return output. + return output +} + +/** + * @see https://fetch.spec.whatwg.org/#concept-body-mime-type + * @param {import('./response').Response|import('./request').Request} object + */ +function bodyMimeType (object) { + const { headersList } = object[kState] + const contentType = headersList.get('content-type') + + if (contentType === null) { + return 'failure' + } + + return parseMIMEType(contentType) +} + +module.exports = { + extractBody, + safelyExtractBody, + cloneBody, + mixinBody +} diff --git a/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/fetch/constants.js b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/fetch/constants.js new file mode 100644 index 000000000..fe5028304 --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/fetch/constants.js @@ -0,0 +1,112 @@ +'use strict' + +const { MessageChannel, receiveMessageOnPort } = require('worker_threads') + +const corsSafeListedMethods = ['GET', 'HEAD', 'POST'] + +const nullBodyStatus = [101, 204, 205, 304] + +const redirectStatus = [301, 302, 303, 307, 308] + +const referrerPolicy = [ + '', + 'no-referrer', + 'no-referrer-when-downgrade', + 'same-origin', + 'origin', + 'strict-origin', + 'origin-when-cross-origin', + 'strict-origin-when-cross-origin', + 'unsafe-url' +] + +const requestRedirect = ['follow', 'manual', 'error'] + +const safeMethods = ['GET', 'HEAD', 'OPTIONS', 'TRACE'] + +const requestMode = ['navigate', 'same-origin', 'no-cors', 'cors'] + +const requestCredentials = ['omit', 'same-origin', 'include'] + +const requestCache = [ + 'default', + 'no-store', + 'reload', + 'no-cache', + 'force-cache', + 'only-if-cached' +] + +const requestBodyHeader = [ + 'content-encoding', + 'content-language', + 'content-location', + 'content-type' +] + +// http://fetch.spec.whatwg.org/#forbidden-method +const forbiddenMethods = ['CONNECT', 'TRACE', 'TRACK'] + +const subresource = [ + 'audio', + 'audioworklet', + 'font', + 'image', + 'manifest', + 'paintworklet', + 'script', + 'style', + 'track', + 'video', + 'xslt', + '' +] + +/** @type {globalThis['DOMException']} */ +const DOMException = globalThis.DOMException ?? (() => { + // DOMException was only made a global in Node v17.0.0, + // but fetch supports >= v16.8. + try { + atob('~') + } catch (err) { + return Object.getPrototypeOf(err).constructor + } +})() + +let channel + +/** @type {globalThis['structuredClone']} */ +const structuredClone = + globalThis.structuredClone ?? + // https://github.com/nodejs/node/blob/b27ae24dcc4251bad726d9d84baf678d1f707fed/lib/internal/structured_clone.js + // structuredClone was added in v17.0.0, but fetch supports v16.8 + function structuredClone (value, options = undefined) { + if (arguments.length === 0) { + throw new TypeError('missing argument') + } + + if (!channel) { + channel = new MessageChannel() + } + channel.port1.unref() + channel.port2.unref() + channel.port1.postMessage(value, options?.transfer) + return receiveMessageOnPort(channel.port2).message + } + +module.exports = { + DOMException, + structuredClone, + subresource, + forbiddenMethods, + requestBodyHeader, + referrerPolicy, + requestRedirect, + requestMode, + requestCredentials, + requestCache, + redirectStatus, + corsSafeListedMethods, + nullBodyStatus, + safeMethods +} diff --git a/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/fetch/dataURL.js b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/fetch/dataURL.js new file mode 100644 index 000000000..a4269120e --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/fetch/dataURL.js @@ -0,0 +1,606 @@ +const assert = require('assert') +const { atob } = require('buffer') +const { isValidHTTPToken } = require('./util') + +const encoder = new TextEncoder() + +// https://fetch.spec.whatwg.org/#data-url-processor +/** @param {URL} dataURL */ +function dataURLProcessor (dataURL) { + // 1. Assert: dataURL’s scheme is "data". + assert(dataURL.protocol === 'data:') + + // 2. Let input be the result of running the URL + // serializer on dataURL with exclude fragment + // set to true. + let input = URLSerializer(dataURL, true) + + // 3. Remove the leading "data:" string from input. + input = input.slice(5) + + // 4. Let position point at the start of input. + const position = { position: 0 } + + // 5. Let mimeType be the result of collecting a + // sequence of code points that are not equal + // to U+002C (,), given position. + let mimeType = collectASequenceOfCodePoints( + (char) => char !== ',', + input, + position + ) + + // 6. Strip leading and trailing ASCII whitespace + // from mimeType. + // Note: This will only remove U+0020 SPACE code + // points, if any. + // Undici implementation note: we need to store the + // length because if the mimetype has spaces removed, + // the wrong amount will be sliced from the input in + // step #9 + const mimeTypeLength = mimeType.length + mimeType = mimeType.replace(/^(\u0020)+|(\u0020)+$/g, '') + + // 7. If position is past the end of input, then + // return failure + if (position.position >= input.length) { + return 'failure' + } + + // 8. Advance position by 1. + position.position++ + + // 9. Let encodedBody be the remainder of input. + const encodedBody = input.slice(mimeTypeLength + 1) + + // 10. Let body be the percent-decoding of encodedBody. + /** @type {Uint8Array|string} */ + let body = stringPercentDecode(encodedBody) + + // 11. If mimeType ends with U+003B (;), followed by + // zero or more U+0020 SPACE, followed by an ASCII + // case-insensitive match for "base64", then: + if (/;(\u0020){0,}base64$/i.test(mimeType)) { + // 1. Let stringBody be the isomorphic decode of body. + const stringBody = decodeURIComponent(new TextDecoder('utf-8').decode(body)) + // 2. Set body to the forgiving-base64 decode of + // stringBody. + body = forgivingBase64(stringBody) + + // 3. If body is failure, then return failure. + if (body === 'failure') { + return 'failure' + } + + // 4. Remove the last 6 code points from mimeType. + mimeType = mimeType.slice(0, -6) + + // 5. Remove trailing U+0020 SPACE code points from mimeType, + // if any. + mimeType = mimeType.replace(/(\u0020)+$/, '') + + // 6. Remove the last U+003B (;) code point from mimeType. + mimeType = mimeType.slice(0, -1) + } + + // 12. If mimeType starts with U+003B (;), then prepend + // "text/plain" to mimeType. + if (mimeType.startsWith(';')) { + mimeType = 'text/plain' + mimeType + } + + // 13. Let mimeTypeRecord be the result of parsing + // mimeType. + let mimeTypeRecord = parseMIMEType(mimeType) + + // 14. If mimeTypeRecord is failure, then set + // mimeTypeRecord to text/plain;charset=US-ASCII. + if (mimeTypeRecord === 'failure') { + mimeTypeRecord = parseMIMEType('text/plain;charset=US-ASCII') + } + + // 15. Return a new data: URL struct whose MIME + // type is mimeTypeRecord and body is body. + // https://fetch.spec.whatwg.org/#data-url-struct + return { mimeType: mimeTypeRecord, body } +} + +// https://url.spec.whatwg.org/#concept-url-serializer +/** + * @param {URL} url + * @param {boolean} excludeFragment + */ +function URLSerializer (url, excludeFragment = false) { + // 1. Let output be url’s scheme and U+003A (:) concatenated. + let output = url.protocol + + // 2. If url’s host is non-null: + if (url.host.length > 0) { + // 1. Append "//" to output. + output += '//' + + // 2. If url includes credentials, then: + if (url.username.length > 0 || url.password.length > 0) { + // 1. Append url’s username to output. + output += url.username + + // 2. If url’s password is not the empty string, then append U+003A (:), + // followed by url’s password, to output. + if (url.password.length > 0) { + output += ':' + url.password + } + + // 3. Append U+0040 (@) to output. + output += '@' + } + + // 3. Append url’s host, serialized, to output. + output += decodeURIComponent(url.hostname) + + // 4. If url’s port is non-null, append U+003A (:) followed by url’s port, + // serialized, to output. + if (url.port.length > 0) { + output += ':' + url.port + } + } + + // 3. If url’s host is null, url does not have an opaque path, + // url’s path’s size is greater than 1, and url’s path[0] + // is the empty string, then append U+002F (/) followed by + // U+002E (.) to output. + // Note: This prevents web+demo:/.//not-a-host/ or web+demo:/path/..//not-a-host/, + // when parsed and then serialized, from ending up as web+demo://not-a-host/ + // (they end up as web+demo:/.//not-a-host/). + // Undici implementation note: url's path[0] can never be an + // empty string, so we have to slightly alter what the spec says. + if ( + url.host.length === 0 && + url.pathname.length > 1 && + url.href.slice(url.protocol.length + 1)[0] === '.' + ) { + output += '/.' + } + + // 4. Append the result of URL path serializing url to output. + output += url.pathname + + // 5. If url’s query is non-null, append U+003F (?), + // followed by url’s query, to output. + if (url.search.length > 0) { + output += url.search + } + + // 6. If exclude fragment is false and url’s fragment is non-null, + // then append U+0023 (#), followed by url’s fragment, to output. + if (excludeFragment === false && url.hash.length > 0) { + output += url.hash + } + + // 7. Return output. + return output +} + +// https://infra.spec.whatwg.org/#collect-a-sequence-of-code-points +/** + * @param {(char: string) => boolean} condition + * @param {string} input + * @param {{ position: number }} position + */ +function collectASequenceOfCodePoints (condition, input, position) { + // 1. Let result be the empty string. + let result = '' + + // 2. While position doesn’t point past the end of input and the + // code point at position within input meets the condition condition: + while (position.position < input.length && condition(input[position.position])) { + // 1. Append that code point to the end of result. + result += input[position.position] + + // 2. Advance position by 1. + position.position++ + } + + // 3. Return result. + return result +} + +// https://url.spec.whatwg.org/#string-percent-decode +/** @param {string} input */ +function stringPercentDecode (input) { + // 1. Let bytes be the UTF-8 encoding of input. + const bytes = encoder.encode(input) + + // 2. Return the percent-decoding of bytes. + return percentDecode(bytes) +} + +// https://url.spec.whatwg.org/#percent-decode +/** @param {Uint8Array} input */ +function percentDecode (input) { + // 1. Let output be an empty byte sequence. + /** @type {number[]} */ + const output = [] + + // 2. For each byte byte in input: + for (let i = 0; i < input.length; i++) { + const byte = input[i] + + // 1. If byte is not 0x25 (%), then append byte to output. + if (byte !== 0x25) { + output.push(byte) + + // 2. Otherwise, if byte is 0x25 (%) and the next two bytes + // after byte in input are not in the ranges + // 0x30 (0) to 0x39 (9), 0x41 (A) to 0x46 (F), + // and 0x61 (a) to 0x66 (f), all inclusive, append byte + // to output. + } else if ( + byte === 0x25 && + !/^[0-9A-Fa-f]{2}$/i.test(String.fromCharCode(input[i + 1], input[i + 2])) + ) { + output.push(0x25) + + // 3. Otherwise: + } else { + // 1. Let bytePoint be the two bytes after byte in input, + // decoded, and then interpreted as hexadecimal number. + const nextTwoBytes = String.fromCharCode(input[i + 1], input[i + 2]) + const bytePoint = Number.parseInt(nextTwoBytes, 16) + + // 2. Append a byte whose value is bytePoint to output. + output.push(bytePoint) + + // 3. Skip the next two bytes in input. + i += 2 + } + } + + // 3. Return output. + return Uint8Array.from(output) +} + +// https://mimesniff.spec.whatwg.org/#parse-a-mime-type +/** @param {string} input */ +function parseMIMEType (input) { + // 1. Remove any leading and trailing HTTP whitespace + // from input. + input = input.trim() + + // 2. Let position be a position variable for input, + // initially pointing at the start of input. + const position = { position: 0 } + + // 3. Let type be the result of collecting a sequence + // of code points that are not U+002F (/) from + // input, given position. + const type = collectASequenceOfCodePoints( + (char) => char !== '/', + input, + position + ) + + // 4. If type is the empty string or does not solely + // contain HTTP token code points, then return failure. + // https://mimesniff.spec.whatwg.org/#http-token-code-point + if (type.length === 0 || !/^[!#$%&'*+-.^_|~A-z0-9]+$/.test(type)) { + return 'failure' + } + + // 5. If position is past the end of input, then return + // failure + if (position.position > input.length) { + return 'failure' + } + + // 6. Advance position by 1. (This skips past U+002F (/).) + position.position++ + + // 7. Let subtype be the result of collecting a sequence of + // code points that are not U+003B (;) from input, given + // position. + let subtype = collectASequenceOfCodePoints( + (char) => char !== ';', + input, + position + ) + + // 8. Remove any trailing HTTP whitespace from subtype. + subtype = subtype.trimEnd() + + // 9. If subtype is the empty string or does not solely + // contain HTTP token code points, then return failure. + if (subtype.length === 0 || !/^[!#$%&'*+-.^_|~A-z0-9]+$/.test(subtype)) { + return 'failure' + } + + // 10. Let mimeType be a new MIME type record whose type + // is type, in ASCII lowercase, and subtype is subtype, + // in ASCII lowercase. + // https://mimesniff.spec.whatwg.org/#mime-type + const mimeType = { + type: type.toLowerCase(), + subtype: subtype.toLowerCase(), + /** @type {Map} */ + parameters: new Map(), + // https://mimesniff.spec.whatwg.org/#mime-type-essence + get essence () { + return `${this.type}/${this.subtype}` + } + } + + // 11. While position is not past the end of input: + while (position.position < input.length) { + // 1. Advance position by 1. (This skips past U+003B (;).) + position.position++ + + // 2. Collect a sequence of code points that are HTTP + // whitespace from input given position. + collectASequenceOfCodePoints( + // https://fetch.spec.whatwg.org/#http-whitespace + (char) => /(\u000A|\u000D|\u0009|\u0020)/.test(char), // eslint-disable-line + input, + position + ) + + // 3. Let parameterName be the result of collecting a + // sequence of code points that are not U+003B (;) + // or U+003D (=) from input, given position. + let parameterName = collectASequenceOfCodePoints( + (char) => char !== ';' && char !== '=', + input, + position + ) + + // 4. Set parameterName to parameterName, in ASCII + // lowercase. + parameterName = parameterName.toLowerCase() + + // 5. If position is not past the end of input, then: + if (position.position < input.length) { + // 1. If the code point at position within input is + // U+003B (;), then continue. + if (input[position.position] === ';') { + continue + } + + // 2. Advance position by 1. (This skips past U+003D (=).) + position.position++ + } + + // 6. If position is past the end of input, then break. + if (position.position > input.length) { + break + } + + // 7. Let parameterValue be null. + let parameterValue = null + + // 8. If the code point at position within input is + // U+0022 ("), then: + if (input[position.position] === '"') { + // 1. Set parameterValue to the result of collecting + // an HTTP quoted string from input, given position + // and the extract-value flag. + parameterValue = collectAnHTTPQuotedString(input, position, true) + + // 2. Collect a sequence of code points that are not + // U+003B (;) from input, given position. + collectASequenceOfCodePoints( + (char) => char !== ';', + input, + position + ) + + // 9. Otherwise: + } else { + // 1. Set parameterValue to the result of collecting + // a sequence of code points that are not U+003B (;) + // from input, given position. + parameterValue = collectASequenceOfCodePoints( + (char) => char !== ';', + input, + position + ) + + // 2. Remove any trailing HTTP whitespace from parameterValue. + // Note: it says "trailing" whitespace; leading is fine. + parameterValue = parameterValue.trimEnd() + + // 3. If parameterValue is the empty string, then continue. + if (parameterValue.length === 0) { + continue + } + } + + // 10. If all of the following are true + // - parameterName is not the empty string + // - parameterName solely contains HTTP token code points + // - parameterValue solely contains HTTP quoted-string token code points + // - mimeType’s parameters[parameterName] does not exist + // then set mimeType’s parameters[parameterName] to parameterValue. + if ( + parameterName.length !== 0 && + /^[!#$%&'*+-.^_|~A-z0-9]+$/.test(parameterName) && + // https://mimesniff.spec.whatwg.org/#http-quoted-string-token-code-point + !/^(\u0009|\x{0020}-\x{007E}|\x{0080}-\x{00FF})+$/.test(parameterValue) && // eslint-disable-line + !mimeType.parameters.has(parameterName) + ) { + mimeType.parameters.set(parameterName, parameterValue) + } + } + + // 12. Return mimeType. + return mimeType +} + +// https://infra.spec.whatwg.org/#forgiving-base64-decode +/** @param {string} data */ +function forgivingBase64 (data) { + // 1. Remove all ASCII whitespace from data. + data = data.replace(/[\u0009\u000A\u000C\u000D\u0020]/g, '') // eslint-disable-line + + // 2. If data’s code point length divides by 4 leaving + // no remainder, then: + if (data.length % 4 === 0) { + // 1. If data ends with one or two U+003D (=) code points, + // then remove them from data. + data = data.replace(/=?=$/, '') + } + + // 3. If data’s code point length divides by 4 leaving + // a remainder of 1, then return failure. + if (data.length % 4 === 1) { + return 'failure' + } + + // 4. If data contains a code point that is not one of + // U+002B (+) + // U+002F (/) + // ASCII alphanumeric + // then return failure. + if (/[^+/0-9A-Za-z]/.test(data)) { + return 'failure' + } + + const binary = atob(data) + const bytes = new Uint8Array(binary.length) + + for (let byte = 0; byte < binary.length; byte++) { + bytes[byte] = binary.charCodeAt(byte) + } + + return bytes +} + +// https://fetch.spec.whatwg.org/#collect-an-http-quoted-string +// tests: https://fetch.spec.whatwg.org/#example-http-quoted-string +/** + * @param {string} input + * @param {{ position: number }} position + * @param {boolean?} extractValue + */ +function collectAnHTTPQuotedString (input, position, extractValue) { + // 1. Let positionStart be position. + const positionStart = position.position + + // 2. Let value be the empty string. + let value = '' + + // 3. Assert: the code point at position within input + // is U+0022 ("). + assert(input[position.position] === '"') + + // 4. Advance position by 1. + position.position++ + + // 5. While true: + while (true) { + // 1. Append the result of collecting a sequence of code points + // that are not U+0022 (") or U+005C (\) from input, given + // position, to value. + value += collectASequenceOfCodePoints( + (char) => char !== '"' && char !== '\\', + input, + position + ) + + // 2. If position is past the end of input, then break. + if (position.position >= input.length) { + break + } + + // 3. Let quoteOrBackslash be the code point at position within + // input. + const quoteOrBackslash = input[position.position] + + // 4. Advance position by 1. + position.position++ + + // 5. If quoteOrBackslash is U+005C (\), then: + if (quoteOrBackslash === '\\') { + // 1. If position is past the end of input, then append + // U+005C (\) to value and break. + if (position.position >= input.length) { + value += '\\' + break + } + + // 2. Append the code point at position within input to value. + value += input[position.position] + + // 3. Advance position by 1. + position.position++ + + // 6. Otherwise: + } else { + // 1. Assert: quoteOrBackslash is U+0022 ("). + assert(quoteOrBackslash === '"') + + // 2. Break. + break + } + } + + // 6. If the extract-value flag is set, then return value. + if (extractValue) { + return value + } + + // 7. Return the code points from positionStart to position, + // inclusive, within input. + return input.slice(positionStart, position.position) +} + +/** + * @see https://mimesniff.spec.whatwg.org/#serialize-a-mime-type + */ +function serializeAMimeType (mimeType) { + assert(mimeType !== 'failure') + const { type, subtype, parameters } = mimeType + + // 1. Let serialization be the concatenation of mimeType’s + // type, U+002F (/), and mimeType’s subtype. + let serialization = `${type}/${subtype}` + + // 2. For each name → value of mimeType’s parameters: + for (let [name, value] of parameters.entries()) { + // 1. Append U+003B (;) to serialization. + serialization += ';' + + // 2. Append name to serialization. + serialization += name + + // 3. Append U+003D (=) to serialization. + serialization += '=' + + // 4. If value does not solely contain HTTP token code + // points or value is the empty string, then: + if (!isValidHTTPToken(value)) { + // 1. Precede each occurence of U+0022 (") or + // U+005C (\) in value with U+005C (\). + value = value.replace(/(\\|")/g, '\\$1') + + // 2. Prepend U+0022 (") to value. + value = '"' + value + + // 3. Append U+0022 (") to value. + value += '"' + } + + // 5. Append value to serialization. + serialization += value + } + + // 3. Return serialization. + return serialization +} + +module.exports = { + dataURLProcessor, + URLSerializer, + collectASequenceOfCodePoints, + stringPercentDecode, + parseMIMEType, + collectAnHTTPQuotedString, + serializeAMimeType +} diff --git a/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/fetch/file.js b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/fetch/file.js new file mode 100644 index 000000000..d0d8c68d7 --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/fetch/file.js @@ -0,0 +1,364 @@ +'use strict' + +const { Blob } = require('buffer') +const { types } = require('util') +const { kState } = require('./symbols') +const { isBlobLike } = require('./util') +const { webidl } = require('./webidl') +const { parseMIMEType, serializeAMimeType } = require('./dataURL') +const { kEnumerableProperty } = require('../core/util') + +class File extends Blob { + constructor (fileBits, fileName, options = {}) { + // The File constructor is invoked with two or three parameters, depending + // on whether the optional dictionary parameter is used. When the File() + // constructor is invoked, user agents must run the following steps: + if (arguments.length < 2) { + throw new TypeError('2 arguments required') + } + + fileBits = webidl.converters['sequence'](fileBits) + fileName = webidl.converters.USVString(fileName) + options = webidl.converters.FilePropertyBag(options) + + // 1. Let bytes be the result of processing blob parts given fileBits and + // options. + // Note: Blob handles this for us + + // 2. Let n be the fileName argument to the constructor. + const n = fileName + + // 3. Process FilePropertyBag dictionary argument by running the following + // substeps: + + // 1. If the type member is provided and is not the empty string, let t + // be set to the type dictionary member. If t contains any characters + // outside the range U+0020 to U+007E, then set t to the empty string + // and return from these substeps. + // 2. Convert every character in t to ASCII lowercase. + let t = options.type + let d + + // eslint-disable-next-line no-labels + substep: { + if (t) { + t = parseMIMEType(t) + + if (t === 'failure') { + t = '' + // eslint-disable-next-line no-labels + break substep + } + + t = serializeAMimeType(t).toLowerCase() + } + + // 3. If the lastModified member is provided, let d be set to the + // lastModified dictionary member. If it is not provided, set d to the + // current date and time represented as the number of milliseconds since + // the Unix Epoch (which is the equivalent of Date.now() [ECMA-262]). + d = options.lastModified + } + + // 4. Return a new File object F such that: + // F refers to the bytes byte sequence. + // F.size is set to the number of total bytes in bytes. + // F.name is set to n. + // F.type is set to t. + // F.lastModified is set to d. + + super(processBlobParts(fileBits, options), { type: t }) + this[kState] = { + name: n, + lastModified: d, + type: t + } + } + + get name () { + if (!(this instanceof File)) { + throw new TypeError('Illegal invocation') + } + + return this[kState].name + } + + get lastModified () { + if (!(this instanceof File)) { + throw new TypeError('Illegal invocation') + } + + return this[kState].lastModified + } + + get type () { + if (!(this instanceof File)) { + throw new TypeError('Illegal invocation') + } + + return this[kState].type + } + + get [Symbol.toStringTag] () { + return this.constructor.name + } +} + +class FileLike { + constructor (blobLike, fileName, options = {}) { + // TODO: argument idl type check + + // The File constructor is invoked with two or three parameters, depending + // on whether the optional dictionary parameter is used. When the File() + // constructor is invoked, user agents must run the following steps: + + // 1. Let bytes be the result of processing blob parts given fileBits and + // options. + + // 2. Let n be the fileName argument to the constructor. + const n = fileName + + // 3. Process FilePropertyBag dictionary argument by running the following + // substeps: + + // 1. If the type member is provided and is not the empty string, let t + // be set to the type dictionary member. If t contains any characters + // outside the range U+0020 to U+007E, then set t to the empty string + // and return from these substeps. + // TODO + const t = options.type + + // 2. Convert every character in t to ASCII lowercase. + // TODO + + // 3. If the lastModified member is provided, let d be set to the + // lastModified dictionary member. If it is not provided, set d to the + // current date and time represented as the number of milliseconds since + // the Unix Epoch (which is the equivalent of Date.now() [ECMA-262]). + const d = options.lastModified ?? Date.now() + + // 4. Return a new File object F such that: + // F refers to the bytes byte sequence. + // F.size is set to the number of total bytes in bytes. + // F.name is set to n. + // F.type is set to t. + // F.lastModified is set to d. + + this[kState] = { + blobLike, + name: n, + type: t, + lastModified: d + } + } + + stream (...args) { + if (!(this instanceof FileLike)) { + throw new TypeError('Illegal invocation') + } + + return this[kState].blobLike.stream(...args) + } + + arrayBuffer (...args) { + if (!(this instanceof FileLike)) { + throw new TypeError('Illegal invocation') + } + + return this[kState].blobLike.arrayBuffer(...args) + } + + slice (...args) { + if (!(this instanceof FileLike)) { + throw new TypeError('Illegal invocation') + } + + return this[kState].blobLike.slice(...args) + } + + text (...args) { + if (!(this instanceof FileLike)) { + throw new TypeError('Illegal invocation') + } + + return this[kState].blobLike.text(...args) + } + + get size () { + if (!(this instanceof FileLike)) { + throw new TypeError('Illegal invocation') + } + + return this[kState].blobLike.size + } + + get type () { + if (!(this instanceof FileLike)) { + throw new TypeError('Illegal invocation') + } + + return this[kState].blobLike.type + } + + get name () { + if (!(this instanceof FileLike)) { + throw new TypeError('Illegal invocation') + } + + return this[kState].name + } + + get lastModified () { + if (!(this instanceof FileLike)) { + throw new TypeError('Illegal invocation') + } + + return this[kState].lastModified + } + + get [Symbol.toStringTag] () { + return 'File' + } +} + +Object.defineProperties(File.prototype, { + name: kEnumerableProperty, + lastModified: kEnumerableProperty +}) + +webidl.converters.Blob = webidl.interfaceConverter(Blob) + +webidl.converters.BlobPart = function (V, opts) { + if (webidl.util.Type(V) === 'Object') { + if (isBlobLike(V)) { + return webidl.converters.Blob(V, { strict: false }) + } + + if ( + ArrayBuffer.isView(V) || + types.isAnyArrayBuffer(V) + ) { + return webidl.converters.BufferSource(V, opts) + } + } + + return webidl.converters.USVString(V, opts) +} + +webidl.converters['sequence'] = webidl.sequenceConverter( + webidl.converters.BlobPart +) + +// https://www.w3.org/TR/FileAPI/#dfn-FilePropertyBag +webidl.converters.FilePropertyBag = webidl.dictionaryConverter([ + { + key: 'lastModified', + converter: webidl.converters['long long'], + get defaultValue () { + return Date.now() + } + }, + { + key: 'type', + converter: webidl.converters.DOMString, + defaultValue: '' + }, + { + key: 'endings', + converter: (value) => { + value = webidl.converters.DOMString(value) + value = value.toLowerCase() + + if (value !== 'native') { + value = 'transparent' + } + + return value + }, + defaultValue: 'transparent' + } +]) + +/** + * @see https://www.w3.org/TR/FileAPI/#process-blob-parts + * @param {(NodeJS.TypedArray|Blob|string)[]} parts + * @param {{ type: string, endings: string }} options + */ +function processBlobParts (parts, options) { + // 1. Let bytes be an empty sequence of bytes. + /** @type {NodeJS.TypedArray[]} */ + const bytes = [] + + // 2. For each element in parts: + for (const element of parts) { + // 1. If element is a USVString, run the following substeps: + if (typeof element === 'string') { + // 1. Let s be element. + let s = element + + // 2. If the endings member of options is "native", set s + // to the result of converting line endings to native + // of element. + if (options.endings === 'native') { + s = convertLineEndingsNative(s) + } + + // 3. Append the result of UTF-8 encoding s to bytes. + bytes.push(new TextEncoder().encode(s)) + } else if ( + types.isAnyArrayBuffer(element) || + types.isTypedArray(element) + ) { + // 2. If element is a BufferSource, get a copy of the + // bytes held by the buffer source, and append those + // bytes to bytes. + if (!element.buffer) { // ArrayBuffer + bytes.push(new Uint8Array(element)) + } else { + bytes.push( + new Uint8Array(element.buffer, element.byteOffset, element.byteLength) + ) + } + } else if (isBlobLike(element)) { + // 3. If element is a Blob, append the bytes it represents + // to bytes. + bytes.push(element) + } + } + + // 3. Return bytes. + return bytes +} + +/** + * @see https://www.w3.org/TR/FileAPI/#convert-line-endings-to-native + * @param {string} s + */ +function convertLineEndingsNative (s) { + // 1. Let native line ending be be the code point U+000A LF. + let nativeLineEnding = '\n' + + // 2. If the underlying platform’s conventions are to + // represent newlines as a carriage return and line feed + // sequence, set native line ending to the code point + // U+000D CR followed by the code point U+000A LF. + if (process.platform === 'win32') { + nativeLineEnding = '\r\n' + } + + return s.replace(/\r?\n/g, nativeLineEnding) +} + +// If this function is moved to ./util.js, some tools (such as +// rollup) will warn about circular dependencies. See: +// https://github.com/nodejs/undici/issues/1629 +function isFileLike (object) { + return object instanceof File || ( + object && + (typeof object.stream === 'function' || + typeof object.arrayBuffer === 'function') && + object[Symbol.toStringTag] === 'File' + ) +} + +module.exports = { File, FileLike, isFileLike } diff --git a/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/fetch/formdata.js b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/fetch/formdata.js new file mode 100644 index 000000000..d5e2ba661 --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/fetch/formdata.js @@ -0,0 +1,316 @@ +'use strict' + +const { isBlobLike, toUSVString, makeIterator } = require('./util') +const { kState } = require('./symbols') +const { File, FileLike, isFileLike } = require('./file') +const { webidl } = require('./webidl') +const { Blob } = require('buffer') + +// https://xhr.spec.whatwg.org/#formdata +class FormData { + static name = 'FormData' + + constructor (form) { + if (form !== undefined) { + webidl.errors.conversionFailed({ + prefix: 'FormData constructor', + argument: 'Argument 1', + types: ['undefined'] + }) + } + + this[kState] = [] + } + + append (name, value, filename = undefined) { + if (!(this instanceof FormData)) { + throw new TypeError('Illegal invocation') + } + + if (arguments.length < 2) { + throw new TypeError( + `Failed to execute 'append' on 'FormData': 2 arguments required, but only ${arguments.length} present.` + ) + } + + if (arguments.length === 3 && !isBlobLike(value)) { + throw new TypeError( + "Failed to execute 'append' on 'FormData': parameter 2 is not of type 'Blob'" + ) + } + + // 1. Let value be value if given; otherwise blobValue. + + name = webidl.converters.USVString(name) + value = isBlobLike(value) + ? webidl.converters.Blob(value, { strict: false }) + : webidl.converters.USVString(value) + filename = arguments.length === 3 + ? webidl.converters.USVString(filename) + : undefined + + // 2. Let entry be the result of creating an entry with + // name, value, and filename if given. + const entry = makeEntry(name, value, filename) + + // 3. Append entry to this’s entry list. + this[kState].push(entry) + } + + delete (name) { + if (!(this instanceof FormData)) { + throw new TypeError('Illegal invocation') + } + + if (arguments.length < 1) { + throw new TypeError( + `Failed to execute 'delete' on 'FormData': 1 arguments required, but only ${arguments.length} present.` + ) + } + + name = webidl.converters.USVString(name) + + // The delete(name) method steps are to remove all entries whose name + // is name from this’s entry list. + const next = [] + for (const entry of this[kState]) { + if (entry.name !== name) { + next.push(entry) + } + } + + this[kState] = next + } + + get (name) { + if (!(this instanceof FormData)) { + throw new TypeError('Illegal invocation') + } + + if (arguments.length < 1) { + throw new TypeError( + `Failed to execute 'get' on 'FormData': 1 arguments required, but only ${arguments.length} present.` + ) + } + + name = webidl.converters.USVString(name) + + // 1. If there is no entry whose name is name in this’s entry list, + // then return null. + const idx = this[kState].findIndex((entry) => entry.name === name) + if (idx === -1) { + return null + } + + // 2. Return the value of the first entry whose name is name from + // this’s entry list. + return this[kState][idx].value + } + + getAll (name) { + if (!(this instanceof FormData)) { + throw new TypeError('Illegal invocation') + } + + if (arguments.length < 1) { + throw new TypeError( + `Failed to execute 'getAll' on 'FormData': 1 arguments required, but only ${arguments.length} present.` + ) + } + + name = webidl.converters.USVString(name) + + // 1. If there is no entry whose name is name in this’s entry list, + // then return the empty list. + // 2. Return the values of all entries whose name is name, in order, + // from this’s entry list. + return this[kState] + .filter((entry) => entry.name === name) + .map((entry) => entry.value) + } + + has (name) { + if (!(this instanceof FormData)) { + throw new TypeError('Illegal invocation') + } + + if (arguments.length < 1) { + throw new TypeError( + `Failed to execute 'has' on 'FormData': 1 arguments required, but only ${arguments.length} present.` + ) + } + + name = webidl.converters.USVString(name) + + // The has(name) method steps are to return true if there is an entry + // whose name is name in this’s entry list; otherwise false. + return this[kState].findIndex((entry) => entry.name === name) !== -1 + } + + set (name, value, filename = undefined) { + if (!(this instanceof FormData)) { + throw new TypeError('Illegal invocation') + } + + if (arguments.length < 2) { + throw new TypeError( + `Failed to execute 'set' on 'FormData': 2 arguments required, but only ${arguments.length} present.` + ) + } + + if (arguments.length === 3 && !isBlobLike(value)) { + throw new TypeError( + "Failed to execute 'set' on 'FormData': parameter 2 is not of type 'Blob'" + ) + } + + // The set(name, value) and set(name, blobValue, filename) method steps + // are: + + // 1. Let value be value if given; otherwise blobValue. + + name = webidl.converters.USVString(name) + value = isBlobLike(value) + ? webidl.converters.Blob(value, { strict: false }) + : webidl.converters.USVString(value) + filename = arguments.length === 3 + ? toUSVString(filename) + : undefined + + // 2. Let entry be the result of creating an entry with name, value, and + // filename if given. + const entry = makeEntry(name, value, filename) + + // 3. If there are entries in this’s entry list whose name is name, then + // replace the first such entry with entry and remove the others. + const idx = this[kState].findIndex((entry) => entry.name === name) + if (idx !== -1) { + this[kState] = [ + ...this[kState].slice(0, idx), + entry, + ...this[kState].slice(idx + 1).filter((entry) => entry.name !== name) + ] + } else { + // 4. Otherwise, append entry to this’s entry list. + this[kState].push(entry) + } + } + + get [Symbol.toStringTag] () { + return this.constructor.name + } + + entries () { + if (!(this instanceof FormData)) { + throw new TypeError('Illegal invocation') + } + + return makeIterator( + () => this[kState].map(pair => [pair.name, pair.value]), + 'FormData', + 'key+value' + ) + } + + keys () { + if (!(this instanceof FormData)) { + throw new TypeError('Illegal invocation') + } + + return makeIterator( + () => this[kState].map(pair => [pair.name, pair.value]), + 'FormData', + 'key' + ) + } + + values () { + if (!(this instanceof FormData)) { + throw new TypeError('Illegal invocation') + } + + return makeIterator( + () => this[kState].map(pair => [pair.name, pair.value]), + 'FormData', + 'value' + ) + } + + /** + * @param {(value: string, key: string, self: FormData) => void} callbackFn + * @param {unknown} thisArg + */ + forEach (callbackFn, thisArg = globalThis) { + if (!(this instanceof FormData)) { + throw new TypeError('Illegal invocation') + } + + if (arguments.length < 1) { + throw new TypeError( + `Failed to execute 'forEach' on 'FormData': 1 argument required, but only ${arguments.length} present.` + ) + } + + if (typeof callbackFn !== 'function') { + throw new TypeError( + "Failed to execute 'forEach' on 'FormData': parameter 1 is not of type 'Function'." + ) + } + + for (const [key, value] of this) { + callbackFn.apply(thisArg, [value, key, this]) + } + } +} + +FormData.prototype[Symbol.iterator] = FormData.prototype.entries + +/** + * @see https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#create-an-entry + * @param {string} name + * @param {string|Blob} value + * @param {?string} filename + * @returns + */ +function makeEntry (name, value, filename) { + // 1. Set name to the result of converting name into a scalar value string. + // "To convert a string into a scalar value string, replace any surrogates + // with U+FFFD." + // see: https://nodejs.org/dist/latest-v18.x/docs/api/buffer.html#buftostringencoding-start-end + name = Buffer.from(name).toString('utf8') + + // 2. If value is a string, then set value to the result of converting + // value into a scalar value string. + if (typeof value === 'string') { + value = Buffer.from(value).toString('utf8') + } else { + // 3. Otherwise: + + // 1. If value is not a File object, then set value to a new File object, + // representing the same bytes, whose name attribute value is "blob" + if (!isFileLike(value)) { + value = value instanceof Blob + ? new File([value], 'blob', { type: value.type }) + : new FileLike(value, 'blob', { type: value.type }) + } + + // 2. If filename is given, then set value to a new File object, + // representing the same bytes, whose name attribute is filename. + if (filename !== undefined) { + /** @type {FilePropertyBag} */ + const options = { + type: value.type, + lastModified: value.lastModified + } + + value = value instanceof File + ? new File([value], filename, options) + : new FileLike(value, filename, options) + } + } + + // 4. Return an entry whose name is name and whose value is value. + return { name, value } +} + +module.exports = { FormData } diff --git a/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/fetch/global.js b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/fetch/global.js new file mode 100644 index 000000000..42282acdf --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/fetch/global.js @@ -0,0 +1,48 @@ +'use strict' + +// In case of breaking changes, increase the version +// number to avoid conflicts. +const globalOrigin = Symbol.for('undici.globalOrigin.1') + +function getGlobalOrigin () { + return globalThis[globalOrigin] +} + +function setGlobalOrigin (newOrigin) { + if ( + newOrigin !== undefined && + typeof newOrigin !== 'string' && + !(newOrigin instanceof URL) + ) { + throw new Error('Invalid base url') + } + + if (newOrigin === undefined) { + Object.defineProperty(globalThis, globalOrigin, { + value: undefined, + writable: true, + enumerable: false, + configurable: false + }) + + return + } + + const parsedURL = new URL(newOrigin) + + if (parsedURL.protocol !== 'http:' && parsedURL.protocol !== 'https:') { + throw new TypeError(`Only http & https urls are allowed, received ${parsedURL.protocol}`) + } + + Object.defineProperty(globalThis, globalOrigin, { + value: parsedURL, + writable: true, + enumerable: false, + configurable: false + }) +} + +module.exports = { + getGlobalOrigin, + setGlobalOrigin +} diff --git a/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/fetch/headers.js b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/fetch/headers.js new file mode 100644 index 000000000..3b0eb833e --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/fetch/headers.js @@ -0,0 +1,507 @@ +// https://github.com/Ethan-Arrowood/undici-fetch + +'use strict' + +const { kHeadersList } = require('../core/symbols') +const { kGuard } = require('./symbols') +const { kEnumerableProperty } = require('../core/util') +const { + makeIterator, + isValidHeaderName, + isValidHeaderValue +} = require('./util') +const { webidl } = require('./webidl') + +const kHeadersMap = Symbol('headers map') +const kHeadersSortedMap = Symbol('headers map sorted') + +/** + * @see https://fetch.spec.whatwg.org/#concept-header-value-normalize + * @param {string} potentialValue + */ +function headerValueNormalize (potentialValue) { + // To normalize a byte sequence potentialValue, remove + // any leading and trailing HTTP whitespace bytes from + // potentialValue. + return potentialValue.replace( + /^[\r\n\t ]+|[\r\n\t ]+$/g, + '' + ) +} + +function fill (headers, object) { + // To fill a Headers object headers with a given object object, run these steps: + + // 1. If object is a sequence, then for each header in object: + // Note: webidl conversion to array has already been done. + if (Array.isArray(object)) { + for (const header of object) { + // 1. If header does not contain exactly two items, then throw a TypeError. + if (header.length !== 2) { + webidl.errors.exception({ + header: 'Headers constructor', + message: `expected name/value pair to be length 2, found ${header.length}.` + }) + } + + // 2. Append (header’s first item, header’s second item) to headers. + headers.append(header[0], header[1]) + } + } else if (typeof object === 'object' && object !== null) { + // Note: null should throw + + // 2. Otherwise, object is a record, then for each key → value in object, + // append (key, value) to headers + for (const [key, value] of Object.entries(object)) { + headers.append(key, value) + } + } else { + webidl.errors.conversionFailed({ + prefix: 'Headers constructor', + argument: 'Argument 1', + types: ['sequence>', 'record'] + }) + } +} + +class HeadersList { + constructor (init) { + if (init instanceof HeadersList) { + this[kHeadersMap] = new Map(init[kHeadersMap]) + this[kHeadersSortedMap] = init[kHeadersSortedMap] + } else { + this[kHeadersMap] = new Map(init) + this[kHeadersSortedMap] = null + } + } + + // https://fetch.spec.whatwg.org/#header-list-contains + contains (name) { + // A header list list contains a header name name if list + // contains a header whose name is a byte-case-insensitive + // match for name. + name = name.toLowerCase() + + return this[kHeadersMap].has(name) + } + + clear () { + this[kHeadersMap].clear() + this[kHeadersSortedMap] = null + } + + // https://fetch.spec.whatwg.org/#concept-header-list-append + append (name, value) { + this[kHeadersSortedMap] = null + + // 1. If list contains name, then set name to the first such + // header’s name. + name = name.toLowerCase() + const exists = this[kHeadersMap].get(name) + + // 2. Append (name, value) to list. + if (exists) { + this[kHeadersMap].set(name, `${exists}, ${value}`) + } else { + this[kHeadersMap].set(name, `${value}`) + } + } + + // https://fetch.spec.whatwg.org/#concept-header-list-set + set (name, value) { + this[kHeadersSortedMap] = null + name = name.toLowerCase() + + // 1. If list contains name, then set the value of + // the first such header to value and remove the + // others. + // 2. Otherwise, append header (name, value) to list. + return this[kHeadersMap].set(name, value) + } + + // https://fetch.spec.whatwg.org/#concept-header-list-delete + delete (name) { + this[kHeadersSortedMap] = null + + name = name.toLowerCase() + return this[kHeadersMap].delete(name) + } + + // https://fetch.spec.whatwg.org/#concept-header-list-get + get (name) { + name = name.toLowerCase() + + // 1. If list does not contain name, then return null. + if (!this.contains(name)) { + return null + } + + // 2. Return the values of all headers in list whose name + // is a byte-case-insensitive match for name, + // separated from each other by 0x2C 0x20, in order. + return this[kHeadersMap].get(name) ?? null + } + + has (name) { + name = name.toLowerCase() + return this[kHeadersMap].has(name) + } + + * [Symbol.iterator] () { + for (const pair of this[kHeadersMap]) { + yield pair + } + } +} + +// https://fetch.spec.whatwg.org/#headers-class +class Headers { + constructor (init = undefined) { + this[kHeadersList] = new HeadersList() + + // The new Headers(init) constructor steps are: + + // 1. Set this’s guard to "none". + this[kGuard] = 'none' + + // 2. If init is given, then fill this with init. + if (init !== undefined) { + init = webidl.converters.HeadersInit(init) + fill(this, init) + } + } + + get [Symbol.toStringTag] () { + return this.constructor.name + } + + // https://fetch.spec.whatwg.org/#dom-headers-append + append (name, value) { + if (!(this instanceof Headers)) { + throw new TypeError('Illegal invocation') + } + + if (arguments.length < 2) { + throw new TypeError( + `Failed to execute 'append' on 'Headers': 2 arguments required, but only ${arguments.length} present.` + ) + } + + name = webidl.converters.ByteString(name) + value = webidl.converters.ByteString(value) + + // 1. Normalize value. + value = headerValueNormalize(value) + + // 2. If name is not a header name or value is not a + // header value, then throw a TypeError. + if (!isValidHeaderName(name)) { + webidl.errors.invalidArgument({ + prefix: 'Headers.append', + value: name, + type: 'header name' + }) + } else if (!isValidHeaderValue(value)) { + webidl.errors.invalidArgument({ + prefix: 'Headers.append', + value, + type: 'header value' + }) + } + + // 3. If headers’s guard is "immutable", then throw a TypeError. + // 4. Otherwise, if headers’s guard is "request" and name is a + // forbidden header name, return. + // Note: undici does not implement forbidden header names + if (this[kGuard] === 'immutable') { + throw new TypeError('immutable') + } else if (this[kGuard] === 'request-no-cors') { + // 5. Otherwise, if headers’s guard is "request-no-cors": + // TODO + } + + // 6. Otherwise, if headers’s guard is "response" and name is a + // forbidden response-header name, return. + + // 7. Append (name, value) to headers’s header list. + // 8. If headers’s guard is "request-no-cors", then remove + // privileged no-CORS request headers from headers + return this[kHeadersList].append(name, value) + } + + // https://fetch.spec.whatwg.org/#dom-headers-delete + delete (name) { + if (!(this instanceof Headers)) { + throw new TypeError('Illegal invocation') + } + + if (arguments.length < 1) { + throw new TypeError( + `Failed to execute 'delete' on 'Headers': 1 argument required, but only ${arguments.length} present.` + ) + } + + name = webidl.converters.ByteString(name) + + // 1. If name is not a header name, then throw a TypeError. + if (!isValidHeaderName(name)) { + webidl.errors.invalidArgument({ + prefix: 'Headers.delete', + value: name, + type: 'header name' + }) + } + + // 2. If this’s guard is "immutable", then throw a TypeError. + // 3. Otherwise, if this’s guard is "request" and name is a + // forbidden header name, return. + // 4. Otherwise, if this’s guard is "request-no-cors", name + // is not a no-CORS-safelisted request-header name, and + // name is not a privileged no-CORS request-header name, + // return. + // 5. Otherwise, if this’s guard is "response" and name is + // a forbidden response-header name, return. + // Note: undici does not implement forbidden header names + if (this[kGuard] === 'immutable') { + throw new TypeError('immutable') + } else if (this[kGuard] === 'request-no-cors') { + // TODO + } + + // 6. If this’s header list does not contain name, then + // return. + if (!this[kHeadersList].contains(name)) { + return + } + + // 7. Delete name from this’s header list. + // 8. If this’s guard is "request-no-cors", then remove + // privileged no-CORS request headers from this. + return this[kHeadersList].delete(name) + } + + // https://fetch.spec.whatwg.org/#dom-headers-get + get (name) { + if (!(this instanceof Headers)) { + throw new TypeError('Illegal invocation') + } + + if (arguments.length < 1) { + throw new TypeError( + `Failed to execute 'get' on 'Headers': 1 argument required, but only ${arguments.length} present.` + ) + } + + name = webidl.converters.ByteString(name) + + // 1. If name is not a header name, then throw a TypeError. + if (!isValidHeaderName(name)) { + webidl.errors.invalidArgument({ + prefix: 'Headers.get', + value: name, + type: 'header name' + }) + } + + // 2. Return the result of getting name from this’s header + // list. + return this[kHeadersList].get(name) + } + + // https://fetch.spec.whatwg.org/#dom-headers-has + has (name) { + if (!(this instanceof Headers)) { + throw new TypeError('Illegal invocation') + } + + if (arguments.length < 1) { + throw new TypeError( + `Failed to execute 'has' on 'Headers': 1 argument required, but only ${arguments.length} present.` + ) + } + + name = webidl.converters.ByteString(name) + + // 1. If name is not a header name, then throw a TypeError. + if (!isValidHeaderName(name)) { + webidl.errors.invalidArgument({ + prefix: 'Headers.has', + value: name, + type: 'header name' + }) + } + + // 2. Return true if this’s header list contains name; + // otherwise false. + return this[kHeadersList].contains(name) + } + + // https://fetch.spec.whatwg.org/#dom-headers-set + set (name, value) { + if (!(this instanceof Headers)) { + throw new TypeError('Illegal invocation') + } + + if (arguments.length < 2) { + throw new TypeError( + `Failed to execute 'set' on 'Headers': 2 arguments required, but only ${arguments.length} present.` + ) + } + + name = webidl.converters.ByteString(name) + value = webidl.converters.ByteString(value) + + // 1. Normalize value. + value = headerValueNormalize(value) + + // 2. If name is not a header name or value is not a + // header value, then throw a TypeError. + if (!isValidHeaderName(name)) { + webidl.errors.invalidArgument({ + prefix: 'Headers.set', + value: name, + type: 'header name' + }) + } else if (!isValidHeaderValue(value)) { + webidl.errors.invalidArgument({ + prefix: 'Headers.set', + value, + type: 'header value' + }) + } + + // 3. If this’s guard is "immutable", then throw a TypeError. + // 4. Otherwise, if this’s guard is "request" and name is a + // forbidden header name, return. + // 5. Otherwise, if this’s guard is "request-no-cors" and + // name/value is not a no-CORS-safelisted request-header, + // return. + // 6. Otherwise, if this’s guard is "response" and name is a + // forbidden response-header name, return. + // Note: undici does not implement forbidden header names + if (this[kGuard] === 'immutable') { + throw new TypeError('immutable') + } else if (this[kGuard] === 'request-no-cors') { + // TODO + } + + // 7. Set (name, value) in this’s header list. + // 8. If this’s guard is "request-no-cors", then remove + // privileged no-CORS request headers from this + return this[kHeadersList].set(name, value) + } + + get [kHeadersSortedMap] () { + if (!this[kHeadersList][kHeadersSortedMap]) { + this[kHeadersList][kHeadersSortedMap] = new Map([...this[kHeadersList]].sort((a, b) => a[0] < b[0] ? -1 : 1)) + } + return this[kHeadersList][kHeadersSortedMap] + } + + keys () { + if (!(this instanceof Headers)) { + throw new TypeError('Illegal invocation') + } + + return makeIterator( + () => [...this[kHeadersSortedMap].entries()], + 'Headers', + 'key' + ) + } + + values () { + if (!(this instanceof Headers)) { + throw new TypeError('Illegal invocation') + } + + return makeIterator( + () => [...this[kHeadersSortedMap].entries()], + 'Headers', + 'value' + ) + } + + entries () { + if (!(this instanceof Headers)) { + throw new TypeError('Illegal invocation') + } + + return makeIterator( + () => [...this[kHeadersSortedMap].entries()], + 'Headers', + 'key+value' + ) + } + + /** + * @param {(value: string, key: string, self: Headers) => void} callbackFn + * @param {unknown} thisArg + */ + forEach (callbackFn, thisArg = globalThis) { + if (!(this instanceof Headers)) { + throw new TypeError('Illegal invocation') + } + + if (arguments.length < 1) { + throw new TypeError( + `Failed to execute 'forEach' on 'Headers': 1 argument required, but only ${arguments.length} present.` + ) + } + + if (typeof callbackFn !== 'function') { + throw new TypeError( + "Failed to execute 'forEach' on 'Headers': parameter 1 is not of type 'Function'." + ) + } + + for (const [key, value] of this) { + callbackFn.apply(thisArg, [value, key, this]) + } + } + + [Symbol.for('nodejs.util.inspect.custom')] () { + if (!(this instanceof Headers)) { + throw new TypeError('Illegal invocation') + } + + return this[kHeadersList] + } +} + +Headers.prototype[Symbol.iterator] = Headers.prototype.entries + +Object.defineProperties(Headers.prototype, { + append: kEnumerableProperty, + delete: kEnumerableProperty, + get: kEnumerableProperty, + has: kEnumerableProperty, + set: kEnumerableProperty, + keys: kEnumerableProperty, + values: kEnumerableProperty, + entries: kEnumerableProperty, + forEach: kEnumerableProperty, + [Symbol.iterator]: { enumerable: false } +}) + +webidl.converters.HeadersInit = function (V) { + if (webidl.util.Type(V) === 'Object') { + if (V[Symbol.iterator]) { + return webidl.converters['sequence>'](V) + } + + return webidl.converters['record'](V) + } + + webidl.errors.conversionFailed({ + prefix: 'Headers constructor', + argument: 'Argument 1', + types: ['sequence>', 'record'] + }) +} + +module.exports = { + fill, + Headers, + HeadersList +} diff --git a/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/fetch/index.js b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/fetch/index.js new file mode 100644 index 000000000..0b99d5463 --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/fetch/index.js @@ -0,0 +1,2070 @@ +// https://github.com/Ethan-Arrowood/undici-fetch + +'use strict' + +const { + Response, + makeNetworkError, + makeAppropriateNetworkError, + filterResponse, + makeResponse +} = require('./response') +const { Headers } = require('./headers') +const { Request, makeRequest } = require('./request') +const zlib = require('zlib') +const { + bytesMatch, + makePolicyContainer, + clonePolicyContainer, + requestBadPort, + TAOCheck, + appendRequestOriginHeader, + responseLocationURL, + requestCurrentURL, + setRequestReferrerPolicyOnRedirect, + tryUpgradeRequestToAPotentiallyTrustworthyURL, + createOpaqueTimingInfo, + appendFetchMetadata, + corsCheck, + crossOriginResourcePolicyCheck, + determineRequestsReferrer, + coarsenedSharedCurrentTime, + createDeferredPromise, + isBlobLike, + sameOrigin, + isCancelled, + isAborted, + isErrorLike, + fullyReadBody, + readableStreamClose +} = require('./util') +const { kState, kHeaders, kGuard, kRealm } = require('./symbols') +const assert = require('assert') +const { safelyExtractBody } = require('./body') +const { + redirectStatus, + nullBodyStatus, + safeMethods, + requestBodyHeader, + subresource, + DOMException +} = require('./constants') +const { kHeadersList } = require('../core/symbols') +const EE = require('events') +const { Readable, pipeline } = require('stream') +const { isErrored, isReadable } = require('../core/util') +const { dataURLProcessor, serializeAMimeType } = require('./dataURL') +const { TransformStream } = require('stream/web') +const { getGlobalDispatcher } = require('../../index') + +/** @type {import('buffer').resolveObjectURL} */ +let resolveObjectURL +/** @type {globalThis['ReadableStream']} */ +let ReadableStream + +const nodeVersion = process.versions.node.split('.') +const nodeMajor = Number(nodeVersion[0]) +const nodeMinor = Number(nodeVersion[1]) + +class Fetch extends EE { + constructor (dispatcher) { + super() + + this.dispatcher = dispatcher + this.connection = null + this.dump = false + this.state = 'ongoing' + // 2 terminated listeners get added per request, + // but only 1 gets removed. If there are 20 redirects, + // 21 listeners will be added. + // See https://github.com/nodejs/undici/issues/1711 + // TODO (fix): Find and fix root cause for leaked listener. + this.setMaxListeners(21) + } + + terminate (reason) { + if (this.state !== 'ongoing') { + return + } + + this.state = 'terminated' + this.connection?.destroy(reason) + this.emit('terminated', reason) + } + + // https://fetch.spec.whatwg.org/#fetch-controller-abort + abort (error) { + if (this.state !== 'ongoing') { + return + } + + // 1. Set controller’s state to "aborted". + this.state = 'aborted' + + // 2. Let fallbackError be an "AbortError" DOMException. + // 3. Set error to fallbackError if it is not given. + if (!error) { + error = new DOMException('The operation was aborted.', 'AbortError') + } + + // 4. Let serializedError be StructuredSerialize(error). + // If that threw an exception, catch it, and let + // serializedError be StructuredSerialize(fallbackError). + + // 5. Set controller’s serialized abort reason to serializedError. + this.serializedAbortReason = error + + this.connection?.destroy(error) + this.emit('terminated', error) + } +} + +// https://fetch.spec.whatwg.org/#fetch-method +async function fetch (input, init = {}) { + if (arguments.length < 1) { + throw new TypeError( + `Failed to execute 'fetch' on 'Window': 1 argument required, but only ${arguments.length} present.` + ) + } + + // 1. Let p be a new promise. + const p = createDeferredPromise() + + // 2. Let requestObject be the result of invoking the initial value of + // Request as constructor with input and init as arguments. If this throws + // an exception, reject p with it and return p. + let requestObject + + try { + requestObject = new Request(input, init) + } catch (e) { + p.reject(e) + return p.promise + } + + // 3. Let request be requestObject’s request. + const request = requestObject[kState] + + // 4. If requestObject’s signal’s aborted flag is set, then: + if (requestObject.signal.aborted) { + // 1. Abort the fetch() call with p, request, null, and + // requestObject’s signal’s abort reason. + abortFetch(p, request, null, requestObject.signal.reason) + + // 2. Return p. + return p.promise + } + + // 5. Let globalObject be request’s client’s global object. + const globalObject = request.client.globalObject + + // 6. If globalObject is a ServiceWorkerGlobalScope object, then set + // request’s service-workers mode to "none". + if (globalObject?.constructor?.name === 'ServiceWorkerGlobalScope') { + request.serviceWorkers = 'none' + } + + // 7. Let responseObject be null. + let responseObject = null + + // 8. Let relevantRealm be this’s relevant Realm. + const relevantRealm = null + + // 9. Let locallyAborted be false. + let locallyAborted = false + + // 10. Let controller be null. + let controller = null + + // 11. Add the following abort steps to requestObject’s signal: + requestObject.signal.addEventListener( + 'abort', + () => { + // 1. Set locallyAborted to true. + locallyAborted = true + + // 2. Abort the fetch() call with p, request, responseObject, + // and requestObject’s signal’s abort reason. + abortFetch(p, request, responseObject, requestObject.signal.reason) + + // 3. If controller is not null, then abort controller. + if (controller != null) { + controller.abort() + } + }, + { once: true } + ) + + // 12. Let handleFetchDone given response response be to finalize and + // report timing with response, globalObject, and "fetch". + const handleFetchDone = (response) => + finalizeAndReportTiming(response, 'fetch') + + // 13. Set controller to the result of calling fetch given request, + // with processResponseEndOfBody set to handleFetchDone, and processResponse + // given response being these substeps: + + const processResponse = (response) => { + // 1. If locallyAborted is true, terminate these substeps. + if (locallyAborted) { + return + } + + // 2. If response’s aborted flag is set, then: + if (response.aborted) { + // 1. Let deserializedError be the result of deserialize a serialized + // abort reason given controller’s serialized abort reason and + // relevantRealm. + + // 2. Abort the fetch() call with p, request, responseObject, and + // deserializedError. + + abortFetch(p, request, responseObject, controller.serializedAbortReason) + return + } + + // 3. If response is a network error, then reject p with a TypeError + // and terminate these substeps. + if (response.type === 'error') { + p.reject( + Object.assign(new TypeError('fetch failed'), { cause: response.error }) + ) + return + } + + // 4. Set responseObject to the result of creating a Response object, + // given response, "immutable", and relevantRealm. + responseObject = new Response() + responseObject[kState] = response + responseObject[kRealm] = relevantRealm + responseObject[kHeaders][kHeadersList] = response.headersList + responseObject[kHeaders][kGuard] = 'immutable' + responseObject[kHeaders][kRealm] = relevantRealm + + // 5. Resolve p with responseObject. + p.resolve(responseObject) + } + + controller = fetching({ + request, + processResponseEndOfBody: handleFetchDone, + processResponse, + dispatcher: init.dispatcher ?? getGlobalDispatcher() // undici + }) + + // 14. Return p. + return p.promise +} + +// https://fetch.spec.whatwg.org/#finalize-and-report-timing +function finalizeAndReportTiming (response, initiatorType = 'other') { + // 1. If response is an aborted network error, then return. + if (response.type === 'error' && response.aborted) { + return + } + + // 2. If response’s URL list is null or empty, then return. + if (!response.urlList?.length) { + return + } + + // 3. Let originalURL be response’s URL list[0]. + const originalURL = response.urlList[0] + + // 4. Let timingInfo be response’s timing info. + let timingInfo = response.timingInfo + + // 5. Let cacheState be response’s cache state. + let cacheState = response.cacheState + + // 6. If originalURL’s scheme is not an HTTP(S) scheme, then return. + if (!/^https?:/.test(originalURL.protocol)) { + return + } + + // 7. If timingInfo is null, then return. + if (timingInfo === null) { + return + } + + // 8. If response’s timing allow passed flag is not set, then: + if (!timingInfo.timingAllowPassed) { + // 1. Set timingInfo to a the result of creating an opaque timing info for timingInfo. + timingInfo = createOpaqueTimingInfo({ + startTime: timingInfo.startTime + }) + + // 2. Set cacheState to the empty string. + cacheState = '' + } + + // 9. Set timingInfo’s end time to the coarsened shared current time + // given global’s relevant settings object’s cross-origin isolated + // capability. + // TODO: given global’s relevant settings object’s cross-origin isolated + // capability? + response.timingInfo.endTime = coarsenedSharedCurrentTime() + + // 10. Set response’s timing info to timingInfo. + response.timingInfo = timingInfo + + // 11. Mark resource timing for timingInfo, originalURL, initiatorType, + // global, and cacheState. + markResourceTiming( + timingInfo, + originalURL, + initiatorType, + globalThis, + cacheState + ) +} + +// https://w3c.github.io/resource-timing/#dfn-mark-resource-timing +function markResourceTiming (timingInfo, originalURL, initiatorType, globalThis, cacheState) { + if (nodeMajor >= 18 && nodeMinor >= 2) { + performance.markResourceTiming(timingInfo, originalURL, initiatorType, globalThis, cacheState) + } +} + +// https://fetch.spec.whatwg.org/#abort-fetch +function abortFetch (p, request, responseObject, error) { + // Note: AbortSignal.reason was added in node v17.2.0 + // which would give us an undefined error to reject with. + // Remove this once node v16 is no longer supported. + if (!error) { + error = new DOMException('The operation was aborted.', 'AbortError') + } + + // 1. Reject promise with error. + p.reject(error) + + // 2. If request’s body is not null and is readable, then cancel request’s + // body with error. + if (request.body != null && isReadable(request.body?.stream)) { + request.body.stream.cancel(error).catch((err) => { + if (err.code === 'ERR_INVALID_STATE') { + // Node bug? + return + } + throw err + }) + } + + // 3. If responseObject is null, then return. + if (responseObject == null) { + return + } + + // 4. Let response be responseObject’s response. + const response = responseObject[kState] + + // 5. If response’s body is not null and is readable, then error response’s + // body with error. + if (response.body != null && isReadable(response.body?.stream)) { + response.body.stream.cancel(error).catch((err) => { + if (err.code === 'ERR_INVALID_STATE') { + // Node bug? + return + } + throw err + }) + } +} + +// https://fetch.spec.whatwg.org/#fetching +function fetching ({ + request, + processRequestBodyChunkLength, + processRequestEndOfBody, + processResponse, + processResponseEndOfBody, + processResponseConsumeBody, + useParallelQueue = false, + dispatcher // undici +}) { + // 1. Let taskDestination be null. + let taskDestination = null + + // 2. Let crossOriginIsolatedCapability be false. + let crossOriginIsolatedCapability = false + + // 3. If request’s client is non-null, then: + if (request.client != null) { + // 1. Set taskDestination to request’s client’s global object. + taskDestination = request.client.globalObject + + // 2. Set crossOriginIsolatedCapability to request’s client’s cross-origin + // isolated capability. + crossOriginIsolatedCapability = + request.client.crossOriginIsolatedCapability + } + + // 4. If useParallelQueue is true, then set taskDestination to the result of + // starting a new parallel queue. + // TODO + + // 5. Let timingInfo be a new fetch timing info whose start time and + // post-redirect start time are the coarsened shared current time given + // crossOriginIsolatedCapability. + const currenTime = coarsenedSharedCurrentTime(crossOriginIsolatedCapability) + const timingInfo = createOpaqueTimingInfo({ + startTime: currenTime + }) + + // 6. Let fetchParams be a new fetch params whose + // request is request, + // timing info is timingInfo, + // process request body chunk length is processRequestBodyChunkLength, + // process request end-of-body is processRequestEndOfBody, + // process response is processResponse, + // process response consume body is processResponseConsumeBody, + // process response end-of-body is processResponseEndOfBody, + // task destination is taskDestination, + // and cross-origin isolated capability is crossOriginIsolatedCapability. + const fetchParams = { + controller: new Fetch(dispatcher), + request, + timingInfo, + processRequestBodyChunkLength, + processRequestEndOfBody, + processResponse, + processResponseConsumeBody, + processResponseEndOfBody, + taskDestination, + crossOriginIsolatedCapability + } + + // 7. If request’s body is a byte sequence, then set request’s body to + // request’s body as a body. + // NOTE: Since fetching is only called from fetch, body should already be + // extracted. + assert(!request.body || request.body.stream) + + // 8. If request’s window is "client", then set request’s window to request’s + // client, if request’s client’s global object is a Window object; otherwise + // "no-window". + if (request.window === 'client') { + // TODO: What if request.client is null? + request.window = + request.client?.globalObject?.constructor?.name === 'Window' + ? request.client + : 'no-window' + } + + // 9. If request’s origin is "client", then set request’s origin to request’s + // client’s origin. + if (request.origin === 'client') { + // TODO: What if request.client is null? + request.origin = request.client?.origin + } + + // 10. If all of the following conditions are true: + // TODO + + // 11. If request’s policy container is "client", then: + if (request.policyContainer === 'client') { + // 1. If request’s client is non-null, then set request’s policy + // container to a clone of request’s client’s policy container. [HTML] + if (request.client != null) { + request.policyContainer = clonePolicyContainer( + request.client.policyContainer + ) + } else { + // 2. Otherwise, set request’s policy container to a new policy + // container. + request.policyContainer = makePolicyContainer() + } + } + + // 12. If request’s header list does not contain `Accept`, then: + if (!request.headersList.has('accept')) { + // 1. Let value be `*/*`. + const value = '*/*' + + // 2. A user agent should set value to the first matching statement, if + // any, switching on request’s destination: + // "document" + // "frame" + // "iframe" + // `text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8` + // "image" + // `image/png,image/svg+xml,image/*;q=0.8,*/*;q=0.5` + // "style" + // `text/css,*/*;q=0.1` + // TODO + + // 3. Append `Accept`/value to request’s header list. + request.headersList.append('accept', value) + } + + // 13. If request’s header list does not contain `Accept-Language`, then + // user agents should append `Accept-Language`/an appropriate value to + // request’s header list. + if (!request.headersList.has('accept-language')) { + request.headersList.append('accept-language', '*') + } + + // 14. If request’s priority is null, then use request’s initiator and + // destination appropriately in setting request’s priority to a + // user-agent-defined object. + if (request.priority === null) { + // TODO + } + + // 15. If request is a subresource request, then: + if (subresource.includes(request.destination)) { + // TODO + } + + // 16. Run main fetch given fetchParams. + mainFetch(fetchParams) + .catch(err => { + fetchParams.controller.terminate(err) + }) + + // 17. Return fetchParam's controller + return fetchParams.controller +} + +// https://fetch.spec.whatwg.org/#concept-main-fetch +async function mainFetch (fetchParams, recursive = false) { + // 1. Let request be fetchParams’s request. + const request = fetchParams.request + + // 2. Let response be null. + let response = null + + // 3. If request’s local-URLs-only flag is set and request’s current URL is + // not local, then set response to a network error. + if ( + request.localURLsOnly && + !/^(about|blob|data):/.test(requestCurrentURL(request).protocol) + ) { + response = makeNetworkError('local URLs only') + } + + // 4. Run report Content Security Policy violations for request. + // TODO + + // 5. Upgrade request to a potentially trustworthy URL, if appropriate. + tryUpgradeRequestToAPotentiallyTrustworthyURL(request) + + // 6. If should request be blocked due to a bad port, should fetching request + // be blocked as mixed content, or should request be blocked by Content + // Security Policy returns blocked, then set response to a network error. + if (requestBadPort(request) === 'blocked') { + response = makeNetworkError('bad port') + } + // TODO: should fetching request be blocked as mixed content? + // TODO: should request be blocked by Content Security Policy? + + // 7. If request’s referrer policy is the empty string, then set request’s + // referrer policy to request’s policy container’s referrer policy. + if (request.referrerPolicy === '') { + request.referrerPolicy = request.policyContainer.referrerPolicy + } + + // 8. If request’s referrer is not "no-referrer", then set request’s + // referrer to the result of invoking determine request’s referrer. + if (request.referrer !== 'no-referrer') { + request.referrer = determineRequestsReferrer(request) + } + + // 9. Set request’s current URL’s scheme to "https" if all of the following + // conditions are true: + // - request’s current URL’s scheme is "http" + // - request’s current URL’s host is a domain + // - Matching request’s current URL’s host per Known HSTS Host Domain Name + // Matching results in either a superdomain match with an asserted + // includeSubDomains directive or a congruent match (with or without an + // asserted includeSubDomains directive). [HSTS] + // TODO + + // 10. If recursive is false, then run the remaining steps in parallel. + // TODO + + // 11. If response is null, then set response to the result of running + // the steps corresponding to the first matching statement: + if (response === null) { + response = await (async () => { + const currentURL = requestCurrentURL(request) + + if ( + // - request’s current URL’s origin is same origin with request’s origin, + // and request’s response tainting is "basic" + (sameOrigin(currentURL, request.url) && request.responseTainting === 'basic') || + // request’s current URL’s scheme is "data" + (currentURL.protocol === 'data:') || + // - request’s mode is "navigate" or "websocket" + (request.mode === 'navigate' || request.mode === 'websocket') + ) { + // 1. Set request’s response tainting to "basic". + request.responseTainting = 'basic' + + // 2. Return the result of running scheme fetch given fetchParams. + return await schemeFetch(fetchParams) + } + + // request’s mode is "same-origin" + if (request.mode === 'same-origin') { + // 1. Return a network error. + return makeNetworkError('request mode cannot be "same-origin"') + } + + // request’s mode is "no-cors" + if (request.mode === 'no-cors') { + // 1. If request’s redirect mode is not "follow", then return a network + // error. + if (request.redirect !== 'follow') { + return makeNetworkError( + 'redirect mode cannot be "follow" for "no-cors" request' + ) + } + + // 2. Set request’s response tainting to "opaque". + request.responseTainting = 'opaque' + + // 3. Return the result of running scheme fetch given fetchParams. + return await schemeFetch(fetchParams) + } + + // request’s current URL’s scheme is not an HTTP(S) scheme + if (!/^https?:/.test(requestCurrentURL(request).protocol)) { + // Return a network error. + return makeNetworkError('URL scheme must be a HTTP(S) scheme') + } + + // - request’s use-CORS-preflight flag is set + // - request’s unsafe-request flag is set and either request’s method is + // not a CORS-safelisted method or CORS-unsafe request-header names with + // request’s header list is not empty + // 1. Set request’s response tainting to "cors". + // 2. Let corsWithPreflightResponse be the result of running HTTP fetch + // given fetchParams and true. + // 3. If corsWithPreflightResponse is a network error, then clear cache + // entries using request. + // 4. Return corsWithPreflightResponse. + // TODO + + // Otherwise + // 1. Set request’s response tainting to "cors". + request.responseTainting = 'cors' + + // 2. Return the result of running HTTP fetch given fetchParams. + return await httpFetch(fetchParams) + })() + } + + // 12. If recursive is true, then return response. + if (recursive) { + return response + } + + // 13. If response is not a network error and response is not a filtered + // response, then: + if (response.status !== 0 && !response.internalResponse) { + // If request’s response tainting is "cors", then: + if (request.responseTainting === 'cors') { + // 1. Let headerNames be the result of extracting header list values + // given `Access-Control-Expose-Headers` and response’s header list. + // TODO + // 2. If request’s credentials mode is not "include" and headerNames + // contains `*`, then set response’s CORS-exposed header-name list to + // all unique header names in response’s header list. + // TODO + // 3. Otherwise, if headerNames is not null or failure, then set + // response’s CORS-exposed header-name list to headerNames. + // TODO + } + + // Set response to the following filtered response with response as its + // internal response, depending on request’s response tainting: + if (request.responseTainting === 'basic') { + response = filterResponse(response, 'basic') + } else if (request.responseTainting === 'cors') { + response = filterResponse(response, 'cors') + } else if (request.responseTainting === 'opaque') { + response = filterResponse(response, 'opaque') + } else { + assert(false) + } + } + + // 14. Let internalResponse be response, if response is a network error, + // and response’s internal response otherwise. + let internalResponse = + response.status === 0 ? response : response.internalResponse + + // 15. If internalResponse’s URL list is empty, then set it to a clone of + // request’s URL list. + if (internalResponse.urlList.length === 0) { + internalResponse.urlList.push(...request.urlList) + } + + // 16. If request’s timing allow failed flag is unset, then set + // internalResponse’s timing allow passed flag. + if (!request.timingAllowFailed) { + response.timingAllowPassed = true + } + + // 17. If response is not a network error and any of the following returns + // blocked + // - should internalResponse to request be blocked as mixed content + // - should internalResponse to request be blocked by Content Security Policy + // - should internalResponse to request be blocked due to its MIME type + // - should internalResponse to request be blocked due to nosniff + // TODO + + // 18. If response’s type is "opaque", internalResponse’s status is 206, + // internalResponse’s range-requested flag is set, and request’s header + // list does not contain `Range`, then set response and internalResponse + // to a network error. + if ( + response.type === 'opaque' && + internalResponse.status === 206 && + internalResponse.rangeRequested && + !request.headers.has('range') + ) { + response = internalResponse = makeNetworkError() + } + + // 19. If response is not a network error and either request’s method is + // `HEAD` or `CONNECT`, or internalResponse’s status is a null body status, + // set internalResponse’s body to null and disregard any enqueuing toward + // it (if any). + if ( + response.status !== 0 && + (request.method === 'HEAD' || + request.method === 'CONNECT' || + nullBodyStatus.includes(internalResponse.status)) + ) { + internalResponse.body = null + fetchParams.controller.dump = true + } + + // 20. If request’s integrity metadata is not the empty string, then: + if (request.integrity) { + // 1. Let processBodyError be this step: run fetch finale given fetchParams + // and a network error. + const processBodyError = (reason) => + fetchFinale(fetchParams, makeNetworkError(reason)) + + // 2. If request’s response tainting is "opaque", or response’s body is null, + // then run processBodyError and abort these steps. + if (request.responseTainting === 'opaque' || response.body == null) { + processBodyError(response.error) + return + } + + // 3. Let processBody given bytes be these steps: + const processBody = (bytes) => { + // 1. If bytes do not match request’s integrity metadata, + // then run processBodyError and abort these steps. [SRI] + if (!bytesMatch(bytes, request.integrity)) { + processBodyError('integrity mismatch') + return + } + + // 2. Set response’s body to bytes as a body. + response.body = safelyExtractBody(bytes)[0] + + // 3. Run fetch finale given fetchParams and response. + fetchFinale(fetchParams, response) + } + + // 4. Fully read response’s body given processBody and processBodyError. + await fullyReadBody(response.body, processBody, processBodyError) + } else { + // 21. Otherwise, run fetch finale given fetchParams and response. + fetchFinale(fetchParams, response) + } +} + +// https://fetch.spec.whatwg.org/#concept-scheme-fetch +// given a fetch params fetchParams +async function schemeFetch (fetchParams) { + // 1. If fetchParams is canceled, then return the appropriate network error for fetchParams. + if (isCancelled(fetchParams)) { + return makeAppropriateNetworkError(fetchParams) + } + + // 2. Let request be fetchParams’s request. + const { request } = fetchParams + + const { protocol: scheme } = requestCurrentURL(request) + + // 3. Switch on request’s current URL’s scheme and run the associated steps: + switch (scheme) { + case 'about:': { + // If request’s current URL’s path is the string "blank", then return a new response + // whose status message is `OK`, header list is « (`Content-Type`, `text/html;charset=utf-8`) », + // and body is the empty byte sequence as a body. + + // Otherwise, return a network error. + return makeNetworkError('about scheme is not supported') + } + case 'blob:': { + if (!resolveObjectURL) { + resolveObjectURL = require('buffer').resolveObjectURL + } + + // 1. Let blobURLEntry be request’s current URL’s blob URL entry. + const blobURLEntry = requestCurrentURL(request) + + // https://github.com/web-platform-tests/wpt/blob/7b0ebaccc62b566a1965396e5be7bb2bc06f841f/FileAPI/url/resources/fetch-tests.js#L52-L56 + // Buffer.resolveObjectURL does not ignore URL queries. + if (blobURLEntry.search.length !== 0) { + return makeNetworkError('NetworkError when attempting to fetch resource.') + } + + const blobURLEntryObject = resolveObjectURL(blobURLEntry.toString()) + + // 2. If request’s method is not `GET`, blobURLEntry is null, or blobURLEntry’s + // object is not a Blob object, then return a network error. + if (request.method !== 'GET' || !isBlobLike(blobURLEntryObject)) { + return makeNetworkError('invalid method') + } + + // 3. Let bodyWithType be the result of safely extracting blobURLEntry’s object. + const bodyWithType = safelyExtractBody(blobURLEntryObject) + + // 4. Let body be bodyWithType’s body. + const body = bodyWithType[0] + + // 5. Let length be body’s length, serialized and isomorphic encoded. + const length = `${body.length}` + + // 6. Let type be bodyWithType’s type if it is non-null; otherwise the empty byte sequence. + const type = bodyWithType[1] ?? '' + + // 7. Return a new response whose status message is `OK`, header list is + // « (`Content-Length`, length), (`Content-Type`, type) », and body is body. + const response = makeResponse({ + statusText: 'OK', + headersList: [ + ['content-length', length], + ['content-type', type] + ] + }) + + response.body = body + + return response + } + case 'data:': { + // 1. Let dataURLStruct be the result of running the + // data: URL processor on request’s current URL. + const currentURL = requestCurrentURL(request) + const dataURLStruct = dataURLProcessor(currentURL) + + // 2. If dataURLStruct is failure, then return a + // network error. + if (dataURLStruct === 'failure') { + return makeNetworkError('failed to fetch the data URL') + } + + // 3. Let mimeType be dataURLStruct’s MIME type, serialized. + const mimeType = serializeAMimeType(dataURLStruct.mimeType) + + // 4. Return a response whose status message is `OK`, + // header list is « (`Content-Type`, mimeType) », + // and body is dataURLStruct’s body as a body. + return makeResponse({ + statusText: 'OK', + headersList: [ + ['content-type', mimeType] + ], + body: safelyExtractBody(dataURLStruct.body)[0] + }) + } + case 'file:': { + // For now, unfortunate as it is, file URLs are left as an exercise for the reader. + // When in doubt, return a network error. + return makeNetworkError('not implemented... yet...') + } + case 'http:': + case 'https:': { + // Return the result of running HTTP fetch given fetchParams. + + return await httpFetch(fetchParams) + .catch((err) => makeNetworkError(err)) + } + default: { + return makeNetworkError('unknown scheme') + } + } +} + +// https://fetch.spec.whatwg.org/#finalize-response +function finalizeResponse (fetchParams, response) { + // 1. Set fetchParams’s request’s done flag. + fetchParams.request.done = true + + // 2, If fetchParams’s process response done is not null, then queue a fetch + // task to run fetchParams’s process response done given response, with + // fetchParams’s task destination. + if (fetchParams.processResponseDone != null) { + queueMicrotask(() => fetchParams.processResponseDone(response)) + } +} + +// https://fetch.spec.whatwg.org/#fetch-finale +async function fetchFinale (fetchParams, response) { + // 1. If response is a network error, then: + if (response.type === 'error') { + // 1. Set response’s URL list to « fetchParams’s request’s URL list[0] ». + response.urlList = [fetchParams.request.urlList[0]] + + // 2. Set response’s timing info to the result of creating an opaque timing + // info for fetchParams’s timing info. + response.timingInfo = createOpaqueTimingInfo({ + startTime: fetchParams.timingInfo.startTime + }) + } + + // 2. Let processResponseEndOfBody be the following steps: + const processResponseEndOfBody = () => { + // 1. Set fetchParams’s request’s done flag. + fetchParams.request.done = true + + // If fetchParams’s process response end-of-body is not null, + // then queue a fetch task to run fetchParams’s process response + // end-of-body given response with fetchParams’s task destination. + if (fetchParams.processResponseEndOfBody != null) { + queueMicrotask(() => fetchParams.processResponseEndOfBody(response)) + } + } + + // 3. If fetchParams’s process response is non-null, then queue a fetch task + // to run fetchParams’s process response given response, with fetchParams’s + // task destination. + if (fetchParams.processResponse != null) { + queueMicrotask(() => fetchParams.processResponse(response)) + } + + // 4. If response’s body is null, then run processResponseEndOfBody. + if (response.body == null) { + processResponseEndOfBody() + } else { + // 5. Otherwise: + + // 1. Let transformStream be a new a TransformStream. + + // 2. Let identityTransformAlgorithm be an algorithm which, given chunk, + // enqueues chunk in transformStream. + const identityTransformAlgorithm = (chunk, controller) => { + controller.enqueue(chunk) + } + + // 3. Set up transformStream with transformAlgorithm set to identityTransformAlgorithm + // and flushAlgorithm set to processResponseEndOfBody. + const transformStream = new TransformStream({ + start () {}, + transform: identityTransformAlgorithm, + flush: processResponseEndOfBody + }, { + size () { + return 1 + } + }, { + size () { + return 1 + } + }) + + // 4. Set response’s body to the result of piping response’s body through transformStream. + response.body = { stream: response.body.stream.pipeThrough(transformStream) } + } + + // 6. If fetchParams’s process response consume body is non-null, then: + if (fetchParams.processResponseConsumeBody != null) { + // 1. Let processBody given nullOrBytes be this step: run fetchParams’s + // process response consume body given response and nullOrBytes. + const processBody = (nullOrBytes) => fetchParams.processResponseConsumeBody(response, nullOrBytes) + + // 2. Let processBodyError be this step: run fetchParams’s process + // response consume body given response and failure. + const processBodyError = (failure) => fetchParams.processResponseConsumeBody(response, failure) + + // 3. If response’s body is null, then queue a fetch task to run processBody + // given null, with fetchParams’s task destination. + if (response.body == null) { + queueMicrotask(() => processBody(null)) + } else { + // 4. Otherwise, fully read response’s body given processBody, processBodyError, + // and fetchParams’s task destination. + await fullyReadBody(response.body, processBody, processBodyError) + } + } +} + +// https://fetch.spec.whatwg.org/#http-fetch +async function httpFetch (fetchParams) { + // 1. Let request be fetchParams’s request. + const request = fetchParams.request + + // 2. Let response be null. + let response = null + + // 3. Let actualResponse be null. + let actualResponse = null + + // 4. Let timingInfo be fetchParams’s timing info. + const timingInfo = fetchParams.timingInfo + + // 5. If request’s service-workers mode is "all", then: + if (request.serviceWorkers === 'all') { + // TODO + } + + // 6. If response is null, then: + if (response === null) { + // 1. If makeCORSPreflight is true and one of these conditions is true: + // TODO + + // 2. If request’s redirect mode is "follow", then set request’s + // service-workers mode to "none". + if (request.redirect === 'follow') { + request.serviceWorkers = 'none' + } + + // 3. Set response and actualResponse to the result of running + // HTTP-network-or-cache fetch given fetchParams. + actualResponse = response = await httpNetworkOrCacheFetch(fetchParams) + + // 4. If request’s response tainting is "cors" and a CORS check + // for request and response returns failure, then return a network error. + if ( + request.responseTainting === 'cors' && + corsCheck(request, response) === 'failure' + ) { + return makeNetworkError('cors failure') + } + + // 5. If the TAO check for request and response returns failure, then set + // request’s timing allow failed flag. + if (TAOCheck(request, response) === 'failure') { + request.timingAllowFailed = true + } + } + + // 7. If either request’s response tainting or response’s type + // is "opaque", and the cross-origin resource policy check with + // request’s origin, request’s client, request’s destination, + // and actualResponse returns blocked, then return a network error. + if ( + (request.responseTainting === 'opaque' || response.type === 'opaque') && + crossOriginResourcePolicyCheck( + request.origin, + request.client, + request.destination, + actualResponse + ) === 'blocked' + ) { + return makeNetworkError('blocked') + } + + // 8. If actualResponse’s status is a redirect status, then: + if (redirectStatus.includes(actualResponse.status)) { + // 1. If actualResponse’s status is not 303, request’s body is not null, + // and the connection uses HTTP/2, then user agents may, and are even + // encouraged to, transmit an RST_STREAM frame. + // See, https://github.com/whatwg/fetch/issues/1288 + if (request.redirect !== 'manual') { + fetchParams.controller.connection.destroy() + } + + // 2. Switch on request’s redirect mode: + if (request.redirect === 'error') { + // Set response to a network error. + response = makeNetworkError('unexpected redirect') + } else if (request.redirect === 'manual') { + // Set response to an opaque-redirect filtered response whose internal + // response is actualResponse. + // NOTE(spec): On the web this would return an `opaqueredirect` response, + // but that doesn't make sense server side. + // See https://github.com/nodejs/undici/issues/1193. + response = actualResponse + } else if (request.redirect === 'follow') { + // Set response to the result of running HTTP-redirect fetch given + // fetchParams and response. + response = await httpRedirectFetch(fetchParams, response) + } else { + assert(false) + } + } + + // 9. Set response’s timing info to timingInfo. + response.timingInfo = timingInfo + + // 10. Return response. + return response +} + +// https://fetch.spec.whatwg.org/#http-redirect-fetch +async function httpRedirectFetch (fetchParams, response) { + // 1. Let request be fetchParams’s request. + const request = fetchParams.request + + // 2. Let actualResponse be response, if response is not a filtered response, + // and response’s internal response otherwise. + const actualResponse = response.internalResponse + ? response.internalResponse + : response + + // 3. Let locationURL be actualResponse’s location URL given request’s current + // URL’s fragment. + let locationURL + + try { + locationURL = responseLocationURL( + actualResponse, + requestCurrentURL(request).hash + ) + + // 4. If locationURL is null, then return response. + if (locationURL == null) { + return response + } + } catch (err) { + // 5. If locationURL is failure, then return a network error. + return makeNetworkError(err) + } + + // 6. If locationURL’s scheme is not an HTTP(S) scheme, then return a network + // error. + if (!/^https?:/.test(locationURL.protocol)) { + return makeNetworkError('URL scheme must be a HTTP(S) scheme') + } + + // 7. If request’s redirect count is twenty, return a network error. + if (request.redirectCount === 20) { + return makeNetworkError('redirect count exceeded') + } + + // 8. Increase request’s redirect count by one. + request.redirectCount += 1 + + // 9. If request’s mode is "cors", locationURL includes credentials, and + // request’s origin is not same origin with locationURL’s origin, then return + // a network error. + if ( + request.mode === 'cors' && + (locationURL.username || locationURL.password) && + !sameOrigin(request, locationURL) + ) { + return makeNetworkError('cross origin not allowed for request mode "cors"') + } + + // 10. If request’s response tainting is "cors" and locationURL includes + // credentials, then return a network error. + if ( + request.responseTainting === 'cors' && + (locationURL.username || locationURL.password) + ) { + return makeNetworkError( + 'URL cannot contain credentials for request mode "cors"' + ) + } + + // 11. If actualResponse’s status is not 303, request’s body is non-null, + // and request’s body’s source is null, then return a network error. + if ( + actualResponse.status !== 303 && + request.body != null && + request.body.source == null + ) { + return makeNetworkError() + } + + // 12. If one of the following is true + // - actualResponse’s status is 301 or 302 and request’s method is `POST` + // - actualResponse’s status is 303 and request’s method is not `GET` or `HEAD` + if ( + ([301, 302].includes(actualResponse.status) && request.method === 'POST') || + (actualResponse.status === 303 && + !['GET', 'HEAD'].includes(request.method)) + ) { + // then: + // 1. Set request’s method to `GET` and request’s body to null. + request.method = 'GET' + request.body = null + + // 2. For each headerName of request-body-header name, delete headerName from + // request’s header list. + for (const headerName of requestBodyHeader) { + request.headersList.delete(headerName) + } + } + + // 13. If request’s body is non-null, then set request’s body to the first return + // value of safely extracting request’s body’s source. + if (request.body != null) { + assert(request.body.source) + request.body = safelyExtractBody(request.body.source)[0] + } + + // 14. Let timingInfo be fetchParams’s timing info. + const timingInfo = fetchParams.timingInfo + + // 15. Set timingInfo’s redirect end time and post-redirect start time to the + // coarsened shared current time given fetchParams’s cross-origin isolated + // capability. + timingInfo.redirectEndTime = timingInfo.postRedirectStartTime = + coarsenedSharedCurrentTime(fetchParams.crossOriginIsolatedCapability) + + // 16. If timingInfo’s redirect start time is 0, then set timingInfo’s + // redirect start time to timingInfo’s start time. + if (timingInfo.redirectStartTime === 0) { + timingInfo.redirectStartTime = timingInfo.startTime + } + + // 17. Append locationURL to request’s URL list. + request.urlList.push(locationURL) + + // 18. Invoke set request’s referrer policy on redirect on request and + // actualResponse. + setRequestReferrerPolicyOnRedirect(request, actualResponse) + + // 19. Return the result of running main fetch given fetchParams and true. + return mainFetch(fetchParams, true) +} + +// https://fetch.spec.whatwg.org/#http-network-or-cache-fetch +async function httpNetworkOrCacheFetch ( + fetchParams, + isAuthenticationFetch = false, + isNewConnectionFetch = false +) { + // 1. Let request be fetchParams’s request. + const request = fetchParams.request + + // 2. Let httpFetchParams be null. + let httpFetchParams = null + + // 3. Let httpRequest be null. + let httpRequest = null + + // 4. Let response be null. + let response = null + + // 5. Let storedResponse be null. + // TODO: cache + + // 6. Let httpCache be null. + const httpCache = null + + // 7. Let the revalidatingFlag be unset. + const revalidatingFlag = false + + // 8. Run these steps, but abort when the ongoing fetch is terminated: + + // 1. If request’s window is "no-window" and request’s redirect mode is + // "error", then set httpFetchParams to fetchParams and httpRequest to + // request. + if (request.window === 'no-window' && request.redirect === 'error') { + httpFetchParams = fetchParams + httpRequest = request + } else { + // Otherwise: + + // 1. Set httpRequest to a clone of request. + httpRequest = makeRequest(request) + + // 2. Set httpFetchParams to a copy of fetchParams. + httpFetchParams = { ...fetchParams } + + // 3. Set httpFetchParams’s request to httpRequest. + httpFetchParams.request = httpRequest + } + + // 3. Let includeCredentials be true if one of + const includeCredentials = + request.credentials === 'include' || + (request.credentials === 'same-origin' && + request.responseTainting === 'basic') + + // 4. Let contentLength be httpRequest’s body’s length, if httpRequest’s + // body is non-null; otherwise null. + const contentLength = httpRequest.body ? httpRequest.body.length : null + + // 5. Let contentLengthHeaderValue be null. + let contentLengthHeaderValue = null + + // 6. If httpRequest’s body is null and httpRequest’s method is `POST` or + // `PUT`, then set contentLengthHeaderValue to `0`. + if ( + httpRequest.body == null && + ['POST', 'PUT'].includes(httpRequest.method) + ) { + contentLengthHeaderValue = '0' + } + + // 7. If contentLength is non-null, then set contentLengthHeaderValue to + // contentLength, serialized and isomorphic encoded. + if (contentLength != null) { + // TODO: isomorphic encoded + contentLengthHeaderValue = String(contentLength) + } + + // 8. If contentLengthHeaderValue is non-null, then append + // `Content-Length`/contentLengthHeaderValue to httpRequest’s header + // list. + if (contentLengthHeaderValue != null) { + httpRequest.headersList.append('content-length', contentLengthHeaderValue) + } + + // 9. If contentLengthHeaderValue is non-null, then append (`Content-Length`, + // contentLengthHeaderValue) to httpRequest’s header list. + + // 10. If contentLength is non-null and httpRequest’s keepalive is true, + // then: + if (contentLength != null && httpRequest.keepalive) { + // NOTE: keepalive is a noop outside of browser context. + } + + // 11. If httpRequest’s referrer is a URL, then append + // `Referer`/httpRequest’s referrer, serialized and isomorphic encoded, + // to httpRequest’s header list. + if (httpRequest.referrer instanceof URL) { + // TODO: isomorphic encoded + httpRequest.headersList.append('referer', httpRequest.referrer.href) + } + + // 12. Append a request `Origin` header for httpRequest. + appendRequestOriginHeader(httpRequest) + + // 13. Append the Fetch metadata headers for httpRequest. [FETCH-METADATA] + appendFetchMetadata(httpRequest) + + // 14. If httpRequest’s header list does not contain `User-Agent`, then + // user agents should append `User-Agent`/default `User-Agent` value to + // httpRequest’s header list. + if (!httpRequest.headersList.has('user-agent')) { + httpRequest.headersList.append('user-agent', 'undici') + } + + // 15. If httpRequest’s cache mode is "default" and httpRequest’s header + // list contains `If-Modified-Since`, `If-None-Match`, + // `If-Unmodified-Since`, `If-Match`, or `If-Range`, then set + // httpRequest’s cache mode to "no-store". + if ( + httpRequest.cache === 'default' && + (httpRequest.headersList.has('if-modified-since') || + httpRequest.headersList.has('if-none-match') || + httpRequest.headersList.has('if-unmodified-since') || + httpRequest.headersList.has('if-match') || + httpRequest.headersList.has('if-range')) + ) { + httpRequest.cache = 'no-store' + } + + // 16. If httpRequest’s cache mode is "no-cache", httpRequest’s prevent + // no-cache cache-control header modification flag is unset, and + // httpRequest’s header list does not contain `Cache-Control`, then append + // `Cache-Control`/`max-age=0` to httpRequest’s header list. + if ( + httpRequest.cache === 'no-cache' && + !httpRequest.preventNoCacheCacheControlHeaderModification && + !httpRequest.headersList.has('cache-control') + ) { + httpRequest.headersList.append('cache-control', 'max-age=0') + } + + // 17. If httpRequest’s cache mode is "no-store" or "reload", then: + if (httpRequest.cache === 'no-store' || httpRequest.cache === 'reload') { + // 1. If httpRequest’s header list does not contain `Pragma`, then append + // `Pragma`/`no-cache` to httpRequest’s header list. + if (!httpRequest.headersList.has('pragma')) { + httpRequest.headersList.append('pragma', 'no-cache') + } + + // 2. If httpRequest’s header list does not contain `Cache-Control`, + // then append `Cache-Control`/`no-cache` to httpRequest’s header list. + if (!httpRequest.headersList.has('cache-control')) { + httpRequest.headersList.append('cache-control', 'no-cache') + } + } + + // 18. If httpRequest’s header list contains `Range`, then append + // `Accept-Encoding`/`identity` to httpRequest’s header list. + if (httpRequest.headersList.has('range')) { + httpRequest.headersList.append('accept-encoding', 'identity') + } + + // 19. Modify httpRequest’s header list per HTTP. Do not append a given + // header if httpRequest’s header list contains that header’s name. + // TODO: https://github.com/whatwg/fetch/issues/1285#issuecomment-896560129 + if (!httpRequest.headersList.has('accept-encoding')) { + if (/^https:/.test(requestCurrentURL(httpRequest).protocol)) { + httpRequest.headersList.append('accept-encoding', 'br, gzip, deflate') + } else { + httpRequest.headersList.append('accept-encoding', 'gzip, deflate') + } + } + + // 20. If includeCredentials is true, then: + if (includeCredentials) { + // 1. If the user agent is not configured to block cookies for httpRequest + // (see section 7 of [COOKIES]), then: + // TODO: credentials + // 2. If httpRequest’s header list does not contain `Authorization`, then: + // TODO: credentials + } + + // 21. If there’s a proxy-authentication entry, use it as appropriate. + // TODO: proxy-authentication + + // 22. Set httpCache to the result of determining the HTTP cache + // partition, given httpRequest. + // TODO: cache + + // 23. If httpCache is null, then set httpRequest’s cache mode to + // "no-store". + if (httpCache == null) { + httpRequest.cache = 'no-store' + } + + // 24. If httpRequest’s cache mode is neither "no-store" nor "reload", + // then: + if (httpRequest.mode !== 'no-store' && httpRequest.mode !== 'reload') { + // TODO: cache + } + + // 9. If aborted, then return the appropriate network error for fetchParams. + // TODO + + // 10. If response is null, then: + if (response == null) { + // 1. If httpRequest’s cache mode is "only-if-cached", then return a + // network error. + if (httpRequest.mode === 'only-if-cached') { + return makeNetworkError('only if cached') + } + + // 2. Let forwardResponse be the result of running HTTP-network fetch + // given httpFetchParams, includeCredentials, and isNewConnectionFetch. + const forwardResponse = await httpNetworkFetch( + httpFetchParams, + includeCredentials, + isNewConnectionFetch + ) + + // 3. If httpRequest’s method is unsafe and forwardResponse’s status is + // in the range 200 to 399, inclusive, invalidate appropriate stored + // responses in httpCache, as per the "Invalidation" chapter of HTTP + // Caching, and set storedResponse to null. [HTTP-CACHING] + if ( + !safeMethods.includes(httpRequest.method) && + forwardResponse.status >= 200 && + forwardResponse.status <= 399 + ) { + // TODO: cache + } + + // 4. If the revalidatingFlag is set and forwardResponse’s status is 304, + // then: + if (revalidatingFlag && forwardResponse.status === 304) { + // TODO: cache + } + + // 5. If response is null, then: + if (response == null) { + // 1. Set response to forwardResponse. + response = forwardResponse + + // 2. Store httpRequest and forwardResponse in httpCache, as per the + // "Storing Responses in Caches" chapter of HTTP Caching. [HTTP-CACHING] + // TODO: cache + } + } + + // 11. Set response’s URL list to a clone of httpRequest’s URL list. + response.urlList = [...httpRequest.urlList] + + // 12. If httpRequest’s header list contains `Range`, then set response’s + // range-requested flag. + if (httpRequest.headersList.has('range')) { + response.rangeRequested = true + } + + // 13. Set response’s request-includes-credentials to includeCredentials. + response.requestIncludesCredentials = includeCredentials + + // 14. If response’s status is 401, httpRequest’s response tainting is not + // "cors", includeCredentials is true, and request’s window is an environment + // settings object, then: + // TODO + + // 15. If response’s status is 407, then: + if (response.status === 407) { + // 1. If request’s window is "no-window", then return a network error. + if (request.window === 'no-window') { + return makeNetworkError() + } + + // 2. ??? + + // 3. If fetchParams is canceled, then return the appropriate network error for fetchParams. + if (isCancelled(fetchParams)) { + return makeAppropriateNetworkError(fetchParams) + } + + // 4. Prompt the end user as appropriate in request’s window and store + // the result as a proxy-authentication entry. [HTTP-AUTH] + // TODO: Invoke some kind of callback? + + // 5. Set response to the result of running HTTP-network-or-cache fetch given + // fetchParams. + // TODO + return makeNetworkError('proxy authentication required') + } + + // 16. If all of the following are true + if ( + // response’s status is 421 + response.status === 421 && + // isNewConnectionFetch is false + !isNewConnectionFetch && + // request’s body is null, or request’s body is non-null and request’s body’s source is non-null + (request.body == null || request.body.source != null) + ) { + // then: + + // 1. If fetchParams is canceled, then return the appropriate network error for fetchParams. + if (isCancelled(fetchParams)) { + return makeAppropriateNetworkError(fetchParams) + } + + // 2. Set response to the result of running HTTP-network-or-cache + // fetch given fetchParams, isAuthenticationFetch, and true. + + // TODO (spec): The spec doesn't specify this but we need to cancel + // the active response before we can start a new one. + // https://github.com/whatwg/fetch/issues/1293 + fetchParams.controller.connection.destroy() + + response = await httpNetworkOrCacheFetch( + fetchParams, + isAuthenticationFetch, + true + ) + } + + // 17. If isAuthenticationFetch is true, then create an authentication entry + if (isAuthenticationFetch) { + // TODO + } + + // 18. Return response. + return response +} + +// https://fetch.spec.whatwg.org/#http-network-fetch +async function httpNetworkFetch ( + fetchParams, + includeCredentials = false, + forceNewConnection = false +) { + assert(!fetchParams.controller.connection || fetchParams.controller.connection.destroyed) + + fetchParams.controller.connection = { + abort: null, + destroyed: false, + destroy (err) { + if (!this.destroyed) { + this.destroyed = true + this.abort?.(err ?? new DOMException('The operation was aborted.', 'AbortError')) + } + } + } + + // 1. Let request be fetchParams’s request. + const request = fetchParams.request + + // 2. Let response be null. + let response = null + + // 3. Let timingInfo be fetchParams’s timing info. + const timingInfo = fetchParams.timingInfo + + // 4. Let httpCache be the result of determining the HTTP cache partition, + // given request. + // TODO: cache + const httpCache = null + + // 5. If httpCache is null, then set request’s cache mode to "no-store". + if (httpCache == null) { + request.cache = 'no-store' + } + + // 6. Let networkPartitionKey be the result of determining the network + // partition key given request. + // TODO + + // 7. Let newConnection be "yes" if forceNewConnection is true; otherwise + // "no". + const newConnection = forceNewConnection ? 'yes' : 'no' // eslint-disable-line no-unused-vars + + // 8. Switch on request’s mode: + if (request.mode === 'websocket') { + // Let connection be the result of obtaining a WebSocket connection, + // given request’s current URL. + // TODO + } else { + // Let connection be the result of obtaining a connection, given + // networkPartitionKey, request’s current URL’s origin, + // includeCredentials, and forceNewConnection. + // TODO + } + + // 9. Run these steps, but abort when the ongoing fetch is terminated: + + // 1. If connection is failure, then return a network error. + + // 2. Set timingInfo’s final connection timing info to the result of + // calling clamp and coarsen connection timing info with connection’s + // timing info, timingInfo’s post-redirect start time, and fetchParams’s + // cross-origin isolated capability. + + // 3. If connection is not an HTTP/2 connection, request’s body is non-null, + // and request’s body’s source is null, then append (`Transfer-Encoding`, + // `chunked`) to request’s header list. + + // 4. Set timingInfo’s final network-request start time to the coarsened + // shared current time given fetchParams’s cross-origin isolated + // capability. + + // 5. Set response to the result of making an HTTP request over connection + // using request with the following caveats: + + // - Follow the relevant requirements from HTTP. [HTTP] [HTTP-SEMANTICS] + // [HTTP-COND] [HTTP-CACHING] [HTTP-AUTH] + + // - If request’s body is non-null, and request’s body’s source is null, + // then the user agent may have a buffer of up to 64 kibibytes and store + // a part of request’s body in that buffer. If the user agent reads from + // request’s body beyond that buffer’s size and the user agent needs to + // resend request, then instead return a network error. + + // - Set timingInfo’s final network-response start time to the coarsened + // shared current time given fetchParams’s cross-origin isolated capability, + // immediately after the user agent’s HTTP parser receives the first byte + // of the response (e.g., frame header bytes for HTTP/2 or response status + // line for HTTP/1.x). + + // - Wait until all the headers are transmitted. + + // - Any responses whose status is in the range 100 to 199, inclusive, + // and is not 101, are to be ignored, except for the purposes of setting + // timingInfo’s final network-response start time above. + + // - If request’s header list contains `Transfer-Encoding`/`chunked` and + // response is transferred via HTTP/1.0 or older, then return a network + // error. + + // - If the HTTP request results in a TLS client certificate dialog, then: + + // 1. If request’s window is an environment settings object, make the + // dialog available in request’s window. + + // 2. Otherwise, return a network error. + + // To transmit request’s body body, run these steps: + let requestBody = null + // 1. If body is null and fetchParams’s process request end-of-body is + // non-null, then queue a fetch task given fetchParams’s process request + // end-of-body and fetchParams’s task destination. + if (request.body == null && fetchParams.processRequestEndOfBody) { + queueMicrotask(() => fetchParams.processRequestEndOfBody()) + } else if (request.body != null) { + // 2. Otherwise, if body is non-null: + + // 1. Let processBodyChunk given bytes be these steps: + const processBodyChunk = async function * (bytes) { + // 1. If the ongoing fetch is terminated, then abort these steps. + if (isCancelled(fetchParams)) { + return + } + + // 2. Run this step in parallel: transmit bytes. + yield bytes + + // 3. If fetchParams’s process request body is non-null, then run + // fetchParams’s process request body given bytes’s length. + fetchParams.processRequestBodyChunkLength?.(bytes.byteLength) + } + + // 2. Let processEndOfBody be these steps: + const processEndOfBody = () => { + // 1. If fetchParams is canceled, then abort these steps. + if (isCancelled(fetchParams)) { + return + } + + // 2. If fetchParams’s process request end-of-body is non-null, + // then run fetchParams’s process request end-of-body. + if (fetchParams.processRequestEndOfBody) { + fetchParams.processRequestEndOfBody() + } + } + + // 3. Let processBodyError given e be these steps: + const processBodyError = (e) => { + // 1. If fetchParams is canceled, then abort these steps. + if (isCancelled(fetchParams)) { + return + } + + // 2. If e is an "AbortError" DOMException, then abort fetchParams’s controller. + if (e.name === 'AbortError') { + fetchParams.controller.abort() + } else { + fetchParams.controller.terminate(e) + } + } + + // 4. Incrementally read request’s body given processBodyChunk, processEndOfBody, + // processBodyError, and fetchParams’s task destination. + requestBody = (async function * () { + try { + for await (const bytes of request.body.stream) { + yield * processBodyChunk(bytes) + } + processEndOfBody() + } catch (err) { + processBodyError(err) + } + })() + } + + try { + const { body, status, statusText, headersList } = await dispatch({ body: requestBody }) + + const iterator = body[Symbol.asyncIterator]() + fetchParams.controller.next = () => iterator.next() + + response = makeResponse({ status, statusText, headersList }) + } catch (err) { + // 10. If aborted, then: + if (err.name === 'AbortError') { + // 1. If connection uses HTTP/2, then transmit an RST_STREAM frame. + fetchParams.controller.connection.destroy() + + // 2. Return the appropriate network error for fetchParams. + return makeAppropriateNetworkError(fetchParams) + } + + return makeNetworkError(err) + } + + // 11. Let pullAlgorithm be an action that resumes the ongoing fetch + // if it is suspended. + const pullAlgorithm = () => { + fetchParams.controller.resume() + } + + // 12. Let cancelAlgorithm be an algorithm that aborts fetchParams’s + // controller with reason, given reason. + const cancelAlgorithm = (reason) => { + fetchParams.controller.abort(reason) + } + + // 13. Let highWaterMark be a non-negative, non-NaN number, chosen by + // the user agent. + // TODO + + // 14. Let sizeAlgorithm be an algorithm that accepts a chunk object + // and returns a non-negative, non-NaN, non-infinite number, chosen by the user agent. + // TODO + + // 15. Let stream be a new ReadableStream. + // 16. Set up stream with pullAlgorithm set to pullAlgorithm, + // cancelAlgorithm set to cancelAlgorithm, highWaterMark set to + // highWaterMark, and sizeAlgorithm set to sizeAlgorithm. + if (!ReadableStream) { + ReadableStream = require('stream/web').ReadableStream + } + + const stream = new ReadableStream( + { + async start (controller) { + fetchParams.controller.controller = controller + }, + async pull (controller) { + await pullAlgorithm(controller) + }, + async cancel (reason) { + await cancelAlgorithm(reason) + } + }, + { + highWaterMark: 0, + size () { + return 1 + } + } + ) + + // 17. Run these steps, but abort when the ongoing fetch is terminated: + + // 1. Set response’s body to a new body whose stream is stream. + response.body = { stream } + + // 2. If response is not a network error and request’s cache mode is + // not "no-store", then update response in httpCache for request. + // TODO + + // 3. If includeCredentials is true and the user agent is not configured + // to block cookies for request (see section 7 of [COOKIES]), then run the + // "set-cookie-string" parsing algorithm (see section 5.2 of [COOKIES]) on + // the value of each header whose name is a byte-case-insensitive match for + // `Set-Cookie` in response’s header list, if any, and request’s current URL. + // TODO + + // 18. If aborted, then: + // TODO + + // 19. Run these steps in parallel: + + // 1. Run these steps, but abort when fetchParams is canceled: + fetchParams.controller.on('terminated', onAborted) + fetchParams.controller.resume = async () => { + // 1. While true + while (true) { + // 1-3. See onData... + + // 4. Set bytes to the result of handling content codings given + // codings and bytes. + let bytes + try { + const { done, value } = await fetchParams.controller.next() + + if (isAborted(fetchParams)) { + break + } + + bytes = done ? undefined : value + } catch (err) { + if (fetchParams.controller.ended && !timingInfo.encodedBodySize) { + // zlib doesn't like empty streams. + bytes = undefined + } else { + bytes = err + } + } + + if (bytes === undefined) { + // 2. Otherwise, if the bytes transmission for response’s message + // body is done normally and stream is readable, then close + // stream, finalize response for fetchParams and response, and + // abort these in-parallel steps. + readableStreamClose(fetchParams.controller.controller) + + finalizeResponse(fetchParams, response) + + return + } + + // 5. Increase timingInfo’s decoded body size by bytes’s length. + timingInfo.decodedBodySize += bytes?.byteLength ?? 0 + + // 6. If bytes is failure, then terminate fetchParams’s controller. + if (isErrorLike(bytes)) { + fetchParams.controller.terminate(bytes) + return + } + + // 7. Enqueue a Uint8Array wrapping an ArrayBuffer containing bytes + // into stream. + fetchParams.controller.controller.enqueue(new Uint8Array(bytes)) + + // 8. If stream is errored, then terminate the ongoing fetch. + if (isErrored(stream)) { + fetchParams.controller.terminate() + return + } + + // 9. If stream doesn’t need more data ask the user agent to suspend + // the ongoing fetch. + if (!fetchParams.controller.controller.desiredSize) { + return + } + } + } + + // 2. If aborted, then: + function onAborted (reason) { + // 2. If fetchParams is aborted, then: + if (isAborted(fetchParams)) { + // 1. Set response’s aborted flag. + response.aborted = true + + // 2. If stream is readable, then error stream with the result of + // deserialize a serialized abort reason given fetchParams’s + // controller’s serialized abort reason and an + // implementation-defined realm. + if (isReadable(stream)) { + fetchParams.controller.controller.error( + fetchParams.controller.serializedAbortReason + ) + } + } else { + // 3. Otherwise, if stream is readable, error stream with a TypeError. + if (isReadable(stream)) { + fetchParams.controller.controller.error(new TypeError('terminated', { + cause: isErrorLike(reason) ? reason : undefined + })) + } + } + + // 4. If connection uses HTTP/2, then transmit an RST_STREAM frame. + // 5. Otherwise, the user agent should close connection unless it would be bad for performance to do so. + fetchParams.controller.connection.destroy() + } + + // 20. Return response. + return response + + async function dispatch ({ body }) { + const url = requestCurrentURL(request) + return new Promise((resolve, reject) => fetchParams.controller.dispatcher.dispatch( + { + path: url.pathname + url.search, + origin: url.origin, + method: request.method, + body: fetchParams.controller.dispatcher.isMockActive ? request.body && request.body.source : body, + headers: [...request.headersList].flat(), + maxRedirections: 0, + bodyTimeout: 300_000, + headersTimeout: 300_000 + }, + { + body: null, + abort: null, + + onConnect (abort) { + // TODO (fix): Do we need connection here? + const { connection } = fetchParams.controller + + if (connection.destroyed) { + abort(new DOMException('The operation was aborted.', 'AbortError')) + } else { + fetchParams.controller.on('terminated', abort) + this.abort = connection.abort = abort + } + }, + + onHeaders (status, headersList, resume, statusText) { + if (status < 200) { + return + } + + let codings = [] + let location = '' + + const headers = new Headers() + for (let n = 0; n < headersList.length; n += 2) { + const key = headersList[n + 0].toString('latin1') + const val = headersList[n + 1].toString('latin1') + + if (key.toLowerCase() === 'content-encoding') { + codings = val.split(',').map((x) => x.trim()) + } else if (key.toLowerCase() === 'location') { + location = val + } + + headers.append(key, val) + } + + this.body = new Readable({ read: resume }) + + const decoders = [] + + const willFollow = request.redirect === 'follow' && + location && + redirectStatus.includes(status) + + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding + if (request.method !== 'HEAD' && request.method !== 'CONNECT' && !nullBodyStatus.includes(status) && !willFollow) { + for (const coding of codings) { + if (/(x-)?gzip/.test(coding)) { + decoders.push(zlib.createGunzip()) + } else if (/(x-)?deflate/.test(coding)) { + decoders.push(zlib.createInflate()) + } else if (coding === 'br') { + decoders.push(zlib.createBrotliDecompress()) + } else { + decoders.length = 0 + break + } + } + } + + resolve({ + status, + statusText, + headersList: headers[kHeadersList], + body: decoders.length + ? pipeline(this.body, ...decoders, () => { }) + : this.body.on('error', () => {}) + }) + + return true + }, + + onData (chunk) { + if (fetchParams.controller.dump) { + return + } + + // 1. If one or more bytes have been transmitted from response’s + // message body, then: + + // 1. Let bytes be the transmitted bytes. + const bytes = chunk + + // 2. Let codings be the result of extracting header list values + // given `Content-Encoding` and response’s header list. + // See pullAlgorithm. + + // 3. Increase timingInfo’s encoded body size by bytes’s length. + timingInfo.encodedBodySize += bytes.byteLength + + // 4. See pullAlgorithm... + + return this.body.push(bytes) + }, + + onComplete () { + if (this.abort) { + fetchParams.controller.off('terminated', this.abort) + } + + fetchParams.controller.ended = true + + this.body.push(null) + }, + + onError (error) { + if (this.abort) { + fetchParams.controller.off('terminated', this.abort) + } + + this.body?.destroy(error) + + fetchParams.controller.terminate(error) + + reject(error) + } + } + )) + } +} + +module.exports = { + fetch, + Fetch, + fetching, + finalizeAndReportTiming +} diff --git a/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/fetch/request.js b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/fetch/request.js new file mode 100644 index 000000000..7c3032bd5 --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/fetch/request.js @@ -0,0 +1,985 @@ +/* globals AbortController */ + +'use strict' + +const { extractBody, mixinBody, cloneBody } = require('./body') +const { Headers, fill: fillHeaders, HeadersList } = require('./headers') +const { FinalizationRegistry } = require('../compat/dispatcher-weakref')() +const util = require('../core/util') +const { + isValidHTTPToken, + sameOrigin, + normalizeMethod +} = require('./util') +const { + forbiddenMethods, + corsSafeListedMethods, + referrerPolicy, + requestRedirect, + requestMode, + requestCredentials, + requestCache +} = require('./constants') +const { kEnumerableProperty } = util +const { kHeaders, kSignal, kState, kGuard, kRealm } = require('./symbols') +const { webidl } = require('./webidl') +const { getGlobalOrigin } = require('./global') +const { URLSerializer } = require('./dataURL') +const { kHeadersList } = require('../core/symbols') +const assert = require('assert') + +let TransformStream + +const kInit = Symbol('init') + +const requestFinalizer = new FinalizationRegistry(({ signal, abort }) => { + signal.removeEventListener('abort', abort) +}) + +// https://fetch.spec.whatwg.org/#request-class +class Request { + // https://fetch.spec.whatwg.org/#dom-request + constructor (input, init = {}) { + if (input === kInit) { + return + } + + if (arguments.length < 1) { + throw new TypeError( + `Failed to construct 'Request': 1 argument required, but only ${arguments.length} present.` + ) + } + + input = webidl.converters.RequestInfo(input) + init = webidl.converters.RequestInit(init) + + // TODO + this[kRealm] = { + settingsObject: { + baseUrl: getGlobalOrigin() + } + } + + // 1. Let request be null. + let request = null + + // 2. Let fallbackMode be null. + let fallbackMode = null + + // 3. Let baseURL be this’s relevant settings object’s API base URL. + const baseUrl = this[kRealm].settingsObject.baseUrl + + // 4. Let signal be null. + let signal = null + + // 5. If input is a string, then: + if (typeof input === 'string') { + // 1. Let parsedURL be the result of parsing input with baseURL. + // 2. If parsedURL is failure, then throw a TypeError. + let parsedURL + try { + parsedURL = new URL(input, baseUrl) + } catch (err) { + throw new TypeError('Failed to parse URL from ' + input, { cause: err }) + } + + // 3. If parsedURL includes credentials, then throw a TypeError. + if (parsedURL.username || parsedURL.password) { + throw new TypeError( + 'Request cannot be constructed from a URL that includes credentials: ' + + input + ) + } + + // 4. Set request to a new request whose URL is parsedURL. + request = makeRequest({ urlList: [parsedURL] }) + + // 5. Set fallbackMode to "cors". + fallbackMode = 'cors' + } else { + // 6. Otherwise: + + // 7. Assert: input is a Request object. + assert(input instanceof Request) + + // 8. Set request to input’s request. + request = input[kState] + + // 9. Set signal to input’s signal. + signal = input[kSignal] + } + + // 7. Let origin be this’s relevant settings object’s origin. + const origin = this[kRealm].settingsObject.origin + + // 8. Let window be "client". + let window = 'client' + + // 9. If request’s window is an environment settings object and its origin + // is same origin with origin, then set window to request’s window. + if ( + request.window?.constructor?.name === 'EnvironmentSettingsObject' && + sameOrigin(request.window, origin) + ) { + window = request.window + } + + // 10. If init["window"] exists and is non-null, then throw a TypeError. + if (init.window !== undefined && init.window != null) { + throw new TypeError(`'window' option '${window}' must be null`) + } + + // 11. If init["window"] exists, then set window to "no-window". + if (init.window !== undefined) { + window = 'no-window' + } + + // 12. Set request to a new request with the following properties: + request = makeRequest({ + // URL request’s URL. + // undici implementation note: this is set as the first item in request's urlList in makeRequest + // method request’s method. + method: request.method, + // header list A copy of request’s header list. + // undici implementation note: headersList is cloned in makeRequest + headersList: request.headersList, + // unsafe-request flag Set. + unsafeRequest: request.unsafeRequest, + // client This’s relevant settings object. + client: this[kRealm].settingsObject, + // window window. + window, + // priority request’s priority. + priority: request.priority, + // origin request’s origin. The propagation of the origin is only significant for navigation requests + // being handled by a service worker. In this scenario a request can have an origin that is different + // from the current client. + origin: request.origin, + // referrer request’s referrer. + referrer: request.referrer, + // referrer policy request’s referrer policy. + referrerPolicy: request.referrerPolicy, + // mode request’s mode. + mode: request.mode, + // credentials mode request’s credentials mode. + credentials: request.credentials, + // cache mode request’s cache mode. + cache: request.cache, + // redirect mode request’s redirect mode. + redirect: request.redirect, + // integrity metadata request’s integrity metadata. + integrity: request.integrity, + // keepalive request’s keepalive. + keepalive: request.keepalive, + // reload-navigation flag request’s reload-navigation flag. + reloadNavigation: request.reloadNavigation, + // history-navigation flag request’s history-navigation flag. + historyNavigation: request.historyNavigation, + // URL list A clone of request’s URL list. + urlList: [...request.urlList] + }) + + // 13. If init is not empty, then: + if (Object.keys(init).length > 0) { + // 1. If request’s mode is "navigate", then set it to "same-origin". + if (request.mode === 'navigate') { + request.mode = 'same-origin' + } + + // 2. Unset request’s reload-navigation flag. + request.reloadNavigation = false + + // 3. Unset request’s history-navigation flag. + request.historyNavigation = false + + // 4. Set request’s origin to "client". + request.origin = 'client' + + // 5. Set request’s referrer to "client" + request.referrer = 'client' + + // 6. Set request’s referrer policy to the empty string. + request.referrerPolicy = '' + + // 7. Set request’s URL to request’s current URL. + request.url = request.urlList[request.urlList.length - 1] + + // 8. Set request’s URL list to « request’s URL ». + request.urlList = [request.url] + } + + // 14. If init["referrer"] exists, then: + if (init.referrer !== undefined) { + // 1. Let referrer be init["referrer"]. + const referrer = init.referrer + + // 2. If referrer is the empty string, then set request’s referrer to "no-referrer". + if (referrer === '') { + request.referrer = 'no-referrer' + } else { + // 1. Let parsedReferrer be the result of parsing referrer with + // baseURL. + // 2. If parsedReferrer is failure, then throw a TypeError. + let parsedReferrer + try { + parsedReferrer = new URL(referrer, baseUrl) + } catch (err) { + throw new TypeError(`Referrer "${referrer}" is not a valid URL.`, { cause: err }) + } + + // 3. If one of the following is true + // parsedReferrer’s cannot-be-a-base-URL is true, scheme is "about", + // and path contains a single string "client" + // parsedReferrer’s origin is not same origin with origin + // then set request’s referrer to "client". + // TODO + + // 4. Otherwise, set request’s referrer to parsedReferrer. + request.referrer = parsedReferrer + } + } + + // 15. If init["referrerPolicy"] exists, then set request’s referrer policy + // to it. + if (init.referrerPolicy !== undefined) { + request.referrerPolicy = init.referrerPolicy + if (!referrerPolicy.includes(request.referrerPolicy)) { + throw new TypeError( + `Failed to construct 'Request': The provided value '${request.referrerPolicy}' is not a valid enum value of type ReferrerPolicy.` + ) + } + } + + // 16. Let mode be init["mode"] if it exists, and fallbackMode otherwise. + let mode + if (init.mode !== undefined) { + mode = init.mode + if (!requestMode.includes(mode)) { + throw new TypeError( + `Failed to construct 'Request': The provided value '${request.mode}' is not a valid enum value of type RequestMode.` + ) + } + } else { + mode = fallbackMode + } + + // 17. If mode is "navigate", then throw a TypeError. + if (mode === 'navigate') { + webidl.errors.exception({ + header: 'Request constructor', + message: 'invalid request mode navigate.' + }) + } + + // 18. If mode is non-null, set request’s mode to mode. + if (mode != null) { + request.mode = mode + } + + // 19. If init["credentials"] exists, then set request’s credentials mode + // to it. + if (init.credentials !== undefined) { + request.credentials = init.credentials + if (!requestCredentials.includes(request.credentials)) { + throw new TypeError( + `Failed to construct 'Request': The provided value '${request.credentials}' is not a valid enum value of type RequestCredentials.` + ) + } + } + + // 18. If init["cache"] exists, then set request’s cache mode to it. + if (init.cache !== undefined) { + request.cache = init.cache + if (!requestCache.includes(request.cache)) { + throw new TypeError( + `Failed to construct 'Request': The provided value '${request.cache}' is not a valid enum value of type RequestCache.` + ) + } + } + + // 21. If request’s cache mode is "only-if-cached" and request’s mode is + // not "same-origin", then throw a TypeError. + if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') { + throw new TypeError( + "'only-if-cached' can be set only with 'same-origin' mode" + ) + } + + // 22. If init["redirect"] exists, then set request’s redirect mode to it. + if (init.redirect !== undefined) { + request.redirect = init.redirect + if (!requestRedirect.includes(request.redirect)) { + throw new TypeError( + `Failed to construct 'Request': The provided value '${request.redirect}' is not a valid enum value of type RequestRedirect.` + ) + } + } + + // 23. If init["integrity"] exists, then set request’s integrity metadata to it. + if (init.integrity !== undefined && init.integrity != null) { + request.integrity = String(init.integrity) + } + + // 24. If init["keepalive"] exists, then set request’s keepalive to it. + if (init.keepalive !== undefined) { + request.keepalive = Boolean(init.keepalive) + } + + // 25. If init["method"] exists, then: + if (init.method !== undefined) { + // 1. Let method be init["method"]. + let method = init.method + + // 2. If method is not a method or method is a forbidden method, then + // throw a TypeError. + if (!isValidHTTPToken(init.method)) { + throw TypeError(`'${init.method}' is not a valid HTTP method.`) + } + + if (forbiddenMethods.indexOf(method.toUpperCase()) !== -1) { + throw TypeError(`'${init.method}' HTTP method is unsupported.`) + } + + // 3. Normalize method. + method = normalizeMethod(init.method) + + // 4. Set request’s method to method. + request.method = method + } + + // 26. If init["signal"] exists, then set signal to it. + if (init.signal !== undefined) { + signal = init.signal + } + + // 27. Set this’s request to request. + this[kState] = request + + // 28. Set this’s signal to a new AbortSignal object with this’s relevant + // Realm. + const ac = new AbortController() + this[kSignal] = ac.signal + this[kSignal][kRealm] = this[kRealm] + + // 29. If signal is not null, then make this’s signal follow signal. + if (signal != null) { + if ( + !signal || + typeof signal.aborted !== 'boolean' || + typeof signal.addEventListener !== 'function' + ) { + throw new TypeError( + "Failed to construct 'Request': member signal is not of type AbortSignal." + ) + } + + if (signal.aborted) { + ac.abort(signal.reason) + } else { + const abort = () => ac.abort(signal.reason) + signal.addEventListener('abort', abort, { once: true }) + requestFinalizer.register(this, { signal, abort }) + } + } + + // 30. Set this’s headers to a new Headers object with this’s relevant + // Realm, whose header list is request’s header list and guard is + // "request". + this[kHeaders] = new Headers() + this[kHeaders][kHeadersList] = request.headersList + this[kHeaders][kGuard] = 'request' + this[kHeaders][kRealm] = this[kRealm] + + // 31. If this’s request’s mode is "no-cors", then: + if (mode === 'no-cors') { + // 1. If this’s request’s method is not a CORS-safelisted method, + // then throw a TypeError. + if (!corsSafeListedMethods.includes(request.method)) { + throw new TypeError( + `'${request.method} is unsupported in no-cors mode.` + ) + } + + // 2. Set this’s headers’s guard to "request-no-cors". + this[kHeaders][kGuard] = 'request-no-cors' + } + + // 32. If init is not empty, then: + if (Object.keys(init).length !== 0) { + // 1. Let headers be a copy of this’s headers and its associated header + // list. + let headers = new Headers(this[kHeaders]) + + // 2. If init["headers"] exists, then set headers to init["headers"]. + if (init.headers !== undefined) { + headers = init.headers + } + + // 3. Empty this’s headers’s header list. + this[kHeaders][kHeadersList].clear() + + // 4. If headers is a Headers object, then for each header in its header + // list, append header’s name/header’s value to this’s headers. + if (headers.constructor.name === 'Headers') { + for (const [key, val] of headers) { + this[kHeaders].append(key, val) + } + } else { + // 5. Otherwise, fill this’s headers with headers. + fillHeaders(this[kHeaders], headers) + } + } + + // 33. Let inputBody be input’s request’s body if input is a Request + // object; otherwise null. + const inputBody = input instanceof Request ? input[kState].body : null + + // 34. If either init["body"] exists and is non-null or inputBody is + // non-null, and request’s method is `GET` or `HEAD`, then throw a + // TypeError. + if ( + ((init.body !== undefined && init.body != null) || inputBody != null) && + (request.method === 'GET' || request.method === 'HEAD') + ) { + throw new TypeError('Request with GET/HEAD method cannot have body.') + } + + // 35. Let initBody be null. + let initBody = null + + // 36. If init["body"] exists and is non-null, then: + if (init.body !== undefined && init.body != null) { + // 1. Let Content-Type be null. + // 2. Set initBody and Content-Type to the result of extracting + // init["body"], with keepalive set to request’s keepalive. + const [extractedBody, contentType] = extractBody( + init.body, + request.keepalive + ) + initBody = extractedBody + + // 3, If Content-Type is non-null and this’s headers’s header list does + // not contain `Content-Type`, then append `Content-Type`/Content-Type to + // this’s headers. + if (contentType && !this[kHeaders].has('content-type')) { + this[kHeaders].append('content-type', contentType) + } + } + + // 37. Let inputOrInitBody be initBody if it is non-null; otherwise + // inputBody. + const inputOrInitBody = initBody ?? inputBody + + // 38. If inputOrInitBody is non-null and inputOrInitBody’s source is + // null, then: + if (inputOrInitBody != null && inputOrInitBody.source == null) { + // 1. If initBody is non-null and init["duplex"] does not exist, + // then throw a TypeError. + if (initBody != null && init.duplex == null) { + throw new TypeError('RequestInit: duplex option is required when sending a body.') + } + + // 2. If this’s request’s mode is neither "same-origin" nor "cors", + // then throw a TypeError. + if (request.mode !== 'same-origin' && request.mode !== 'cors') { + throw new TypeError( + 'If request is made from ReadableStream, mode should be "same-origin" or "cors"' + ) + } + + // 3. Set this’s request’s use-CORS-preflight flag. + request.useCORSPreflightFlag = true + } + + // 39. Let finalBody be inputOrInitBody. + let finalBody = inputOrInitBody + + // 40. If initBody is null and inputBody is non-null, then: + if (initBody == null && inputBody != null) { + // 1. If input is unusable, then throw a TypeError. + if (util.isDisturbed(inputBody.stream) || inputBody.stream.locked) { + throw new TypeError( + 'Cannot construct a Request with a Request object that has already been used.' + ) + } + + // 2. Set finalBody to the result of creating a proxy for inputBody. + if (!TransformStream) { + TransformStream = require('stream/web').TransformStream + } + + // https://streams.spec.whatwg.org/#readablestream-create-a-proxy + const identityTransform = new TransformStream() + inputBody.stream.pipeThrough(identityTransform) + finalBody = { + source: inputBody.source, + length: inputBody.length, + stream: identityTransform.readable + } + } + + // 41. Set this’s request’s body to finalBody. + this[kState].body = finalBody + } + + get [Symbol.toStringTag] () { + return this.constructor.name + } + + // Returns request’s HTTP method, which is "GET" by default. + get method () { + if (!(this instanceof Request)) { + throw new TypeError('Illegal invocation') + } + + // The method getter steps are to return this’s request’s method. + return this[kState].method + } + + // Returns the URL of request as a string. + get url () { + if (!(this instanceof Request)) { + throw new TypeError('Illegal invocation') + } + + // The url getter steps are to return this’s request’s URL, serialized. + return URLSerializer(this[kState].url) + } + + // Returns a Headers object consisting of the headers associated with request. + // Note that headers added in the network layer by the user agent will not + // be accounted for in this object, e.g., the "Host" header. + get headers () { + if (!(this instanceof Request)) { + throw new TypeError('Illegal invocation') + } + + // The headers getter steps are to return this’s headers. + return this[kHeaders] + } + + // Returns the kind of resource requested by request, e.g., "document" + // or "script". + get destination () { + if (!(this instanceof Request)) { + throw new TypeError('Illegal invocation') + } + + // The destination getter are to return this’s request’s destination. + return this[kState].destination + } + + // Returns the referrer of request. Its value can be a same-origin URL if + // explicitly set in init, the empty string to indicate no referrer, and + // "about:client" when defaulting to the global’s default. This is used + // during fetching to determine the value of the `Referer` header of the + // request being made. + get referrer () { + if (!(this instanceof Request)) { + throw new TypeError('Illegal invocation') + } + + // 1. If this’s request’s referrer is "no-referrer", then return the + // empty string. + if (this[kState].referrer === 'no-referrer') { + return '' + } + + // 2. If this’s request’s referrer is "client", then return + // "about:client". + if (this[kState].referrer === 'client') { + return 'about:client' + } + + // Return this’s request’s referrer, serialized. + return this[kState].referrer.toString() + } + + // Returns the referrer policy associated with request. + // This is used during fetching to compute the value of the request’s + // referrer. + get referrerPolicy () { + if (!(this instanceof Request)) { + throw new TypeError('Illegal invocation') + } + + // The referrerPolicy getter steps are to return this’s request’s referrer policy. + return this[kState].referrerPolicy + } + + // Returns the mode associated with request, which is a string indicating + // whether the request will use CORS, or will be restricted to same-origin + // URLs. + get mode () { + if (!(this instanceof Request)) { + throw new TypeError('Illegal invocation') + } + + // The mode getter steps are to return this’s request’s mode. + return this[kState].mode + } + + // Returns the credentials mode associated with request, + // which is a string indicating whether credentials will be sent with the + // request always, never, or only when sent to a same-origin URL. + get credentials () { + // The credentials getter steps are to return this’s request’s credentials mode. + return this[kState].credentials + } + + // Returns the cache mode associated with request, + // which is a string indicating how the request will + // interact with the browser’s cache when fetching. + get cache () { + if (!(this instanceof Request)) { + throw new TypeError('Illegal invocation') + } + + // The cache getter steps are to return this’s request’s cache mode. + return this[kState].cache + } + + // Returns the redirect mode associated with request, + // which is a string indicating how redirects for the + // request will be handled during fetching. A request + // will follow redirects by default. + get redirect () { + if (!(this instanceof Request)) { + throw new TypeError('Illegal invocation') + } + + // The redirect getter steps are to return this’s request’s redirect mode. + return this[kState].redirect + } + + // Returns request’s subresource integrity metadata, which is a + // cryptographic hash of the resource being fetched. Its value + // consists of multiple hashes separated by whitespace. [SRI] + get integrity () { + if (!(this instanceof Request)) { + throw new TypeError('Illegal invocation') + } + + // The integrity getter steps are to return this’s request’s integrity + // metadata. + return this[kState].integrity + } + + // Returns a boolean indicating whether or not request can outlive the + // global in which it was created. + get keepalive () { + if (!(this instanceof Request)) { + throw new TypeError('Illegal invocation') + } + + // The keepalive getter steps are to return this’s request’s keepalive. + return this[kState].keepalive + } + + // Returns a boolean indicating whether or not request is for a reload + // navigation. + get isReloadNavigation () { + if (!(this instanceof Request)) { + throw new TypeError('Illegal invocation') + } + + // The isReloadNavigation getter steps are to return true if this’s + // request’s reload-navigation flag is set; otherwise false. + return this[kState].reloadNavigation + } + + // Returns a boolean indicating whether or not request is for a history + // navigation (a.k.a. back-foward navigation). + get isHistoryNavigation () { + if (!(this instanceof Request)) { + throw new TypeError('Illegal invocation') + } + + // The isHistoryNavigation getter steps are to return true if this’s request’s + // history-navigation flag is set; otherwise false. + return this[kState].historyNavigation + } + + // Returns the signal associated with request, which is an AbortSignal + // object indicating whether or not request has been aborted, and its + // abort event handler. + get signal () { + if (!(this instanceof Request)) { + throw new TypeError('Illegal invocation') + } + + // The signal getter steps are to return this’s signal. + return this[kSignal] + } + + get body () { + if (!this || !this[kState]) { + throw new TypeError('Illegal invocation') + } + + return this[kState].body ? this[kState].body.stream : null + } + + get bodyUsed () { + if (!this || !this[kState]) { + throw new TypeError('Illegal invocation') + } + + return !!this[kState].body && util.isDisturbed(this[kState].body.stream) + } + + get duplex () { + if (!(this instanceof Request)) { + throw new TypeError('Illegal invocation') + } + + return 'half' + } + + // Returns a clone of request. + clone () { + if (!(this instanceof Request)) { + throw new TypeError('Illegal invocation') + } + + // 1. If this is unusable, then throw a TypeError. + if (this.bodyUsed || this.body?.locked) { + throw new TypeError('unusable') + } + + // 2. Let clonedRequest be the result of cloning this’s request. + const clonedRequest = cloneRequest(this[kState]) + + // 3. Let clonedRequestObject be the result of creating a Request object, + // given clonedRequest, this’s headers’s guard, and this’s relevant Realm. + const clonedRequestObject = new Request(kInit) + clonedRequestObject[kState] = clonedRequest + clonedRequestObject[kRealm] = this[kRealm] + clonedRequestObject[kHeaders] = new Headers() + clonedRequestObject[kHeaders][kHeadersList] = clonedRequest.headersList + clonedRequestObject[kHeaders][kGuard] = this[kHeaders][kGuard] + clonedRequestObject[kHeaders][kRealm] = this[kHeaders][kRealm] + + // 4. Make clonedRequestObject’s signal follow this’s signal. + const ac = new AbortController() + if (this.signal.aborted) { + ac.abort(this.signal.reason) + } else { + this.signal.addEventListener( + 'abort', + () => { + ac.abort(this.signal.reason) + }, + { once: true } + ) + } + clonedRequestObject[kSignal] = ac.signal + + // 4. Return clonedRequestObject. + return clonedRequestObject + } +} + +mixinBody(Request) + +function makeRequest (init) { + // https://fetch.spec.whatwg.org/#requests + const request = { + method: 'GET', + localURLsOnly: false, + unsafeRequest: false, + body: null, + client: null, + reservedClient: null, + replacesClientId: '', + window: 'client', + keepalive: false, + serviceWorkers: 'all', + initiator: '', + destination: '', + priority: null, + origin: 'client', + policyContainer: 'client', + referrer: 'client', + referrerPolicy: '', + mode: 'no-cors', + useCORSPreflightFlag: false, + credentials: 'same-origin', + useCredentials: false, + cache: 'default', + redirect: 'follow', + integrity: '', + cryptoGraphicsNonceMetadata: '', + parserMetadata: '', + reloadNavigation: false, + historyNavigation: false, + userActivation: false, + taintedOrigin: false, + redirectCount: 0, + responseTainting: 'basic', + preventNoCacheCacheControlHeaderModification: false, + done: false, + timingAllowFailed: false, + ...init, + headersList: init.headersList + ? new HeadersList(init.headersList) + : new HeadersList() + } + request.url = request.urlList[0] + return request +} + +// https://fetch.spec.whatwg.org/#concept-request-clone +function cloneRequest (request) { + // To clone a request request, run these steps: + + // 1. Let newRequest be a copy of request, except for its body. + const newRequest = makeRequest({ ...request, body: null }) + + // 2. If request’s body is non-null, set newRequest’s body to the + // result of cloning request’s body. + if (request.body != null) { + newRequest.body = cloneBody(request.body) + } + + // 3. Return newRequest. + return newRequest +} + +Object.defineProperties(Request.prototype, { + method: kEnumerableProperty, + url: kEnumerableProperty, + headers: kEnumerableProperty, + redirect: kEnumerableProperty, + clone: kEnumerableProperty, + signal: kEnumerableProperty, + duplex: kEnumerableProperty, + destination: kEnumerableProperty, + body: kEnumerableProperty, + bodyUsed: kEnumerableProperty, + isHistoryNavigation: kEnumerableProperty, + isReloadNavigation: kEnumerableProperty, + keepalive: kEnumerableProperty, + integrity: kEnumerableProperty, + cache: kEnumerableProperty, + credentials: kEnumerableProperty, + attribute: kEnumerableProperty, + referrerPolicy: kEnumerableProperty, + referrer: kEnumerableProperty, + mode: kEnumerableProperty +}) + +webidl.converters.Request = webidl.interfaceConverter( + Request +) + +// https://fetch.spec.whatwg.org/#requestinfo +webidl.converters.RequestInfo = function (V) { + if (typeof V === 'string') { + return webidl.converters.USVString(V) + } + + if (V instanceof Request) { + return webidl.converters.Request(V) + } + + return webidl.converters.USVString(V) +} + +webidl.converters.AbortSignal = webidl.interfaceConverter( + AbortSignal +) + +// https://fetch.spec.whatwg.org/#requestinit +webidl.converters.RequestInit = webidl.dictionaryConverter([ + { + key: 'method', + converter: webidl.converters.ByteString + }, + { + key: 'headers', + converter: webidl.converters.HeadersInit + }, + { + key: 'body', + converter: webidl.nullableConverter( + webidl.converters.BodyInit + ) + }, + { + key: 'referrer', + converter: webidl.converters.USVString + }, + { + key: 'referrerPolicy', + converter: webidl.converters.DOMString, + // https://w3c.github.io/webappsec-referrer-policy/#referrer-policy + allowedValues: [ + '', 'no-referrer', 'no-referrer-when-downgrade', + 'same-origin', 'origin', 'strict-origin', + 'origin-when-cross-origin', 'strict-origin-when-cross-origin', + 'unsafe-url' + ] + }, + { + key: 'mode', + converter: webidl.converters.DOMString, + // https://fetch.spec.whatwg.org/#concept-request-mode + allowedValues: [ + 'same-origin', 'cors', 'no-cors', 'navigate', 'websocket' + ] + }, + { + key: 'credentials', + converter: webidl.converters.DOMString, + // https://fetch.spec.whatwg.org/#requestcredentials + allowedValues: [ + 'omit', 'same-origin', 'include' + ] + }, + { + key: 'cache', + converter: webidl.converters.DOMString, + // https://fetch.spec.whatwg.org/#requestcache + allowedValues: [ + 'default', 'no-store', 'reload', 'no-cache', 'force-cache', + 'only-if-cached' + ] + }, + { + key: 'redirect', + converter: webidl.converters.DOMString, + // https://fetch.spec.whatwg.org/#requestredirect + allowedValues: [ + 'follow', 'error', 'manual' + ] + }, + { + key: 'integrity', + converter: webidl.converters.DOMString + }, + { + key: 'keepalive', + converter: webidl.converters.boolean + }, + { + key: 'signal', + converter: webidl.nullableConverter( + (signal) => webidl.converters.AbortSignal( + signal, + { strict: false } + ) + ) + }, + { + key: 'window', + converter: webidl.converters.any + }, + { + key: 'duplex', + converter: webidl.converters.DOMString, + allowedValues: ['half'] + } +]) + +module.exports = { Request, makeRequest } diff --git a/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/fetch/response.js b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/fetch/response.js new file mode 100644 index 000000000..be97d67ce --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/fetch/response.js @@ -0,0 +1,603 @@ +'use strict' + +const { Headers, HeadersList, fill } = require('./headers') +const { extractBody, cloneBody, mixinBody } = require('./body') +const util = require('../core/util') +const { kEnumerableProperty } = util +const { + isValidReasonPhrase, + isCancelled, + isAborted, + isBlobLike, + serializeJavascriptValueToJSONString, + isErrorLike +} = require('./util') +const { + redirectStatus, + nullBodyStatus, + DOMException +} = require('./constants') +const { kState, kHeaders, kGuard, kRealm } = require('./symbols') +const { webidl } = require('./webidl') +const { FormData } = require('./formdata') +const { getGlobalOrigin } = require('./global') +const { URLSerializer } = require('./dataURL') +const { kHeadersList } = require('../core/symbols') +const assert = require('assert') +const { types } = require('util') + +const ReadableStream = globalThis.ReadableStream || require('stream/web').ReadableStream + +// https://fetch.spec.whatwg.org/#response-class +class Response { + // Creates network error Response. + static error () { + // TODO + const relevantRealm = { settingsObject: {} } + + // The static error() method steps are to return the result of creating a + // Response object, given a new network error, "immutable", and this’s + // relevant Realm. + const responseObject = new Response() + responseObject[kState] = makeNetworkError() + responseObject[kRealm] = relevantRealm + responseObject[kHeaders][kHeadersList] = responseObject[kState].headersList + responseObject[kHeaders][kGuard] = 'immutable' + responseObject[kHeaders][kRealm] = relevantRealm + return responseObject + } + + // https://fetch.spec.whatwg.org/#dom-response-json + static json (data = undefined, init = {}) { + if (arguments.length === 0) { + throw new TypeError( + 'Failed to execute \'json\' on \'Response\': 1 argument required, but 0 present.' + ) + } + + if (init !== null) { + init = webidl.converters.ResponseInit(init) + } + + // 1. Let bytes the result of running serialize a JavaScript value to JSON bytes on data. + const bytes = new TextEncoder('utf-8').encode( + serializeJavascriptValueToJSONString(data) + ) + + // 2. Let body be the result of extracting bytes. + const body = extractBody(bytes) + + // 3. Let responseObject be the result of creating a Response object, given a new response, + // "response", and this’s relevant Realm. + const relevantRealm = { settingsObject: {} } + const responseObject = new Response() + responseObject[kRealm] = relevantRealm + responseObject[kHeaders][kGuard] = 'response' + responseObject[kHeaders][kRealm] = relevantRealm + + // 4. Perform initialize a response given responseObject, init, and (body, "application/json"). + initializeResponse(responseObject, init, { body: body[0], type: 'application/json' }) + + // 5. Return responseObject. + return responseObject + } + + // Creates a redirect Response that redirects to url with status status. + static redirect (url, status = 302) { + const relevantRealm = { settingsObject: {} } + + if (arguments.length < 1) { + throw new TypeError( + `Failed to execute 'redirect' on 'Response': 1 argument required, but only ${arguments.length} present.` + ) + } + + url = webidl.converters.USVString(url) + status = webidl.converters['unsigned short'](status) + + // 1. Let parsedURL be the result of parsing url with current settings + // object’s API base URL. + // 2. If parsedURL is failure, then throw a TypeError. + // TODO: base-URL? + let parsedURL + try { + parsedURL = new URL(url, getGlobalOrigin()) + } catch (err) { + throw Object.assign(new TypeError('Failed to parse URL from ' + url), { + cause: err + }) + } + + // 3. If status is not a redirect status, then throw a RangeError. + if (!redirectStatus.includes(status)) { + throw new RangeError('Invalid status code') + } + + // 4. Let responseObject be the result of creating a Response object, + // given a new response, "immutable", and this’s relevant Realm. + const responseObject = new Response() + responseObject[kRealm] = relevantRealm + responseObject[kHeaders][kGuard] = 'immutable' + responseObject[kHeaders][kRealm] = relevantRealm + + // 5. Set responseObject’s response’s status to status. + responseObject[kState].status = status + + // 6. Let value be parsedURL, serialized and isomorphic encoded. + // TODO: isomorphic encoded? + const value = parsedURL.toString() + + // 7. Append `Location`/value to responseObject’s response’s header list. + responseObject[kState].headersList.append('location', value) + + // 8. Return responseObject. + return responseObject + } + + // https://fetch.spec.whatwg.org/#dom-response + constructor (body = null, init = {}) { + if (body !== null) { + body = webidl.converters.BodyInit(body) + } + + init = webidl.converters.ResponseInit(init) + + // TODO + this[kRealm] = { settingsObject: {} } + + // 1. Set this’s response to a new response. + this[kState] = makeResponse({}) + + // 2. Set this’s headers to a new Headers object with this’s relevant + // Realm, whose header list is this’s response’s header list and guard + // is "response". + this[kHeaders] = new Headers() + this[kHeaders][kGuard] = 'response' + this[kHeaders][kHeadersList] = this[kState].headersList + this[kHeaders][kRealm] = this[kRealm] + + // 3. Let bodyWithType be null. + let bodyWithType = null + + // 4. If body is non-null, then set bodyWithType to the result of extracting body. + if (body != null) { + const [extractedBody, type] = extractBody(body) + bodyWithType = { body: extractedBody, type } + } + + // 5. Perform initialize a response given this, init, and bodyWithType. + initializeResponse(this, init, bodyWithType) + } + + get [Symbol.toStringTag] () { + return this.constructor.name + } + + // Returns response’s type, e.g., "cors". + get type () { + if (!(this instanceof Response)) { + throw new TypeError('Illegal invocation') + } + + // The type getter steps are to return this’s response’s type. + return this[kState].type + } + + // Returns response’s URL, if it has one; otherwise the empty string. + get url () { + if (!(this instanceof Response)) { + throw new TypeError('Illegal invocation') + } + + const urlList = this[kState].urlList + + // The url getter steps are to return the empty string if this’s + // response’s URL is null; otherwise this’s response’s URL, + // serialized with exclude fragment set to true. + const url = urlList[urlList.length - 1] ?? null + + if (url === null) { + return '' + } + + return URLSerializer(url, true) + } + + // Returns whether response was obtained through a redirect. + get redirected () { + if (!(this instanceof Response)) { + throw new TypeError('Illegal invocation') + } + + // The redirected getter steps are to return true if this’s response’s URL + // list has more than one item; otherwise false. + return this[kState].urlList.length > 1 + } + + // Returns response’s status. + get status () { + if (!(this instanceof Response)) { + throw new TypeError('Illegal invocation') + } + + // The status getter steps are to return this’s response’s status. + return this[kState].status + } + + // Returns whether response’s status is an ok status. + get ok () { + if (!(this instanceof Response)) { + throw new TypeError('Illegal invocation') + } + + // The ok getter steps are to return true if this’s response’s status is an + // ok status; otherwise false. + return this[kState].status >= 200 && this[kState].status <= 299 + } + + // Returns response’s status message. + get statusText () { + if (!(this instanceof Response)) { + throw new TypeError('Illegal invocation') + } + + // The statusText getter steps are to return this’s response’s status + // message. + return this[kState].statusText + } + + // Returns response’s headers as Headers. + get headers () { + if (!(this instanceof Response)) { + throw new TypeError('Illegal invocation') + } + + // The headers getter steps are to return this’s headers. + return this[kHeaders] + } + + get body () { + if (!this || !this[kState]) { + throw new TypeError('Illegal invocation') + } + + return this[kState].body ? this[kState].body.stream : null + } + + get bodyUsed () { + if (!this || !this[kState]) { + throw new TypeError('Illegal invocation') + } + + return !!this[kState].body && util.isDisturbed(this[kState].body.stream) + } + + // Returns a clone of response. + clone () { + if (!(this instanceof Response)) { + throw new TypeError('Illegal invocation') + } + + // 1. If this is unusable, then throw a TypeError. + if (this.bodyUsed || (this.body && this.body.locked)) { + webidl.errors.exception({ + header: 'Response.clone', + message: 'Body has already been consumed.' + }) + } + + // 2. Let clonedResponse be the result of cloning this’s response. + const clonedResponse = cloneResponse(this[kState]) + + // 3. Return the result of creating a Response object, given + // clonedResponse, this’s headers’s guard, and this’s relevant Realm. + const clonedResponseObject = new Response() + clonedResponseObject[kState] = clonedResponse + clonedResponseObject[kRealm] = this[kRealm] + clonedResponseObject[kHeaders][kHeadersList] = clonedResponse.headersList + clonedResponseObject[kHeaders][kGuard] = this[kHeaders][kGuard] + clonedResponseObject[kHeaders][kRealm] = this[kHeaders][kRealm] + + return clonedResponseObject + } +} + +mixinBody(Response) + +Object.defineProperties(Response.prototype, { + type: kEnumerableProperty, + url: kEnumerableProperty, + status: kEnumerableProperty, + ok: kEnumerableProperty, + redirected: kEnumerableProperty, + statusText: kEnumerableProperty, + headers: kEnumerableProperty, + clone: kEnumerableProperty, + body: kEnumerableProperty, + bodyUsed: kEnumerableProperty +}) + +Object.defineProperties(Response, { + json: kEnumerableProperty, + redirect: kEnumerableProperty, + error: kEnumerableProperty +}) + +// https://fetch.spec.whatwg.org/#concept-response-clone +function cloneResponse (response) { + // To clone a response response, run these steps: + + // 1. If response is a filtered response, then return a new identical + // filtered response whose internal response is a clone of response’s + // internal response. + if (response.internalResponse) { + return filterResponse( + cloneResponse(response.internalResponse), + response.type + ) + } + + // 2. Let newResponse be a copy of response, except for its body. + const newResponse = makeResponse({ ...response, body: null }) + + // 3. If response’s body is non-null, then set newResponse’s body to the + // result of cloning response’s body. + if (response.body != null) { + newResponse.body = cloneBody(response.body) + } + + // 4. Return newResponse. + return newResponse +} + +function makeResponse (init) { + return { + aborted: false, + rangeRequested: false, + timingAllowPassed: false, + requestIncludesCredentials: false, + type: 'default', + status: 200, + timingInfo: null, + cacheState: '', + statusText: '', + ...init, + headersList: init.headersList + ? new HeadersList(init.headersList) + : new HeadersList(), + urlList: init.urlList ? [...init.urlList] : [] + } +} + +function makeNetworkError (reason) { + const isError = isErrorLike(reason) + return makeResponse({ + type: 'error', + status: 0, + error: isError + ? reason + : new Error(reason ? String(reason) : reason, { + cause: isError ? reason : undefined + }), + aborted: reason && reason.name === 'AbortError' + }) +} + +function makeFilteredResponse (response, state) { + state = { + internalResponse: response, + ...state + } + + return new Proxy(response, { + get (target, p) { + return p in state ? state[p] : target[p] + }, + set (target, p, value) { + assert(!(p in state)) + target[p] = value + return true + } + }) +} + +// https://fetch.spec.whatwg.org/#concept-filtered-response +function filterResponse (response, type) { + // Set response to the following filtered response with response as its + // internal response, depending on request’s response tainting: + if (type === 'basic') { + // A basic filtered response is a filtered response whose type is "basic" + // and header list excludes any headers in internal response’s header list + // whose name is a forbidden response-header name. + + // Note: undici does not implement forbidden response-header names + return makeFilteredResponse(response, { + type: 'basic', + headersList: response.headersList + }) + } else if (type === 'cors') { + // A CORS filtered response is a filtered response whose type is "cors" + // and header list excludes any headers in internal response’s header + // list whose name is not a CORS-safelisted response-header name, given + // internal response’s CORS-exposed header-name list. + + // Note: undici does not implement CORS-safelisted response-header names + return makeFilteredResponse(response, { + type: 'cors', + headersList: response.headersList + }) + } else if (type === 'opaque') { + // An opaque filtered response is a filtered response whose type is + // "opaque", URL list is the empty list, status is 0, status message + // is the empty byte sequence, header list is empty, and body is null. + + return makeFilteredResponse(response, { + type: 'opaque', + urlList: Object.freeze([]), + status: 0, + statusText: '', + body: null + }) + } else if (type === 'opaqueredirect') { + // An opaque-redirect filtered response is a filtered response whose type + // is "opaqueredirect", status is 0, status message is the empty byte + // sequence, header list is empty, and body is null. + + return makeFilteredResponse(response, { + type: 'opaqueredirect', + status: 0, + statusText: '', + headersList: [], + body: null + }) + } else { + assert(false) + } +} + +// https://fetch.spec.whatwg.org/#appropriate-network-error +function makeAppropriateNetworkError (fetchParams) { + // 1. Assert: fetchParams is canceled. + assert(isCancelled(fetchParams)) + + // 2. Return an aborted network error if fetchParams is aborted; + // otherwise return a network error. + return isAborted(fetchParams) + ? makeNetworkError(new DOMException('The operation was aborted.', 'AbortError')) + : makeNetworkError(fetchParams.controller.terminated.reason) +} + +// https://whatpr.org/fetch/1392.html#initialize-a-response +function initializeResponse (response, init, body) { + // 1. If init["status"] is not in the range 200 to 599, inclusive, then + // throw a RangeError. + if (init.status !== null && (init.status < 200 || init.status > 599)) { + throw new RangeError('init["status"] must be in the range of 200 to 599, inclusive.') + } + + // 2. If init["statusText"] does not match the reason-phrase token production, + // then throw a TypeError. + if ('statusText' in init && init.statusText != null) { + // See, https://datatracker.ietf.org/doc/html/rfc7230#section-3.1.2: + // reason-phrase = *( HTAB / SP / VCHAR / obs-text ) + if (!isValidReasonPhrase(String(init.statusText))) { + throw new TypeError('Invalid statusText') + } + } + + // 3. Set response’s response’s status to init["status"]. + if ('status' in init && init.status != null) { + response[kState].status = init.status + } + + // 4. Set response’s response’s status message to init["statusText"]. + if ('statusText' in init && init.statusText != null) { + response[kState].statusText = init.statusText + } + + // 5. If init["headers"] exists, then fill response’s headers with init["headers"]. + if ('headers' in init && init.headers != null) { + fill(response[kState].headersList, init.headers) + } + + // 6. If body was given, then: + if (body) { + // 1. If response's status is a null body status, then throw a TypeError. + if (nullBodyStatus.includes(response.status)) { + webidl.errors.exception({ + header: 'Response constructor', + message: 'Invalid response status code.' + }) + } + + // 2. Set response's body to body's body. + response[kState].body = body.body + + // 3. If body's type is non-null and response's header list does not contain + // `Content-Type`, then append (`Content-Type`, body's type) to response's header list. + if (body.type != null && !response[kState].headersList.has('Content-Type')) { + response[kState].headersList.append('content-type', body.type) + } + } +} + +webidl.converters.ReadableStream = webidl.interfaceConverter( + ReadableStream +) + +webidl.converters.FormData = webidl.interfaceConverter( + FormData +) + +webidl.converters.URLSearchParams = webidl.interfaceConverter( + URLSearchParams +) + +// https://fetch.spec.whatwg.org/#typedefdef-xmlhttprequestbodyinit +webidl.converters.XMLHttpRequestBodyInit = function (V) { + if (typeof V === 'string') { + return webidl.converters.USVString(V) + } + + if (isBlobLike(V)) { + return webidl.converters.Blob(V, { strict: false }) + } + + if ( + types.isAnyArrayBuffer(V) || + types.isTypedArray(V) || + types.isDataView(V) + ) { + return webidl.converters.BufferSource(V) + } + + if (util.isFormDataLike(V)) { + return webidl.converters.FormData(V, { strict: false }) + } + + if (V instanceof URLSearchParams) { + return webidl.converters.URLSearchParams(V) + } + + return webidl.converters.DOMString(V) +} + +// https://fetch.spec.whatwg.org/#bodyinit +webidl.converters.BodyInit = function (V) { + if (V instanceof ReadableStream) { + return webidl.converters.ReadableStream(V) + } + + // Note: the spec doesn't include async iterables, + // this is an undici extension. + if (V?.[Symbol.asyncIterator]) { + return V + } + + return webidl.converters.XMLHttpRequestBodyInit(V) +} + +webidl.converters.ResponseInit = webidl.dictionaryConverter([ + { + key: 'status', + converter: webidl.converters['unsigned short'], + defaultValue: 200 + }, + { + key: 'statusText', + converter: webidl.converters.ByteString, + defaultValue: '' + }, + { + key: 'headers', + converter: webidl.converters.HeadersInit + } +]) + +module.exports = { + makeNetworkError, + makeResponse, + makeAppropriateNetworkError, + filterResponse, + Response +} diff --git a/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/fetch/symbols.js b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/fetch/symbols.js new file mode 100644 index 000000000..0b947d55b --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/fetch/symbols.js @@ -0,0 +1,10 @@ +'use strict' + +module.exports = { + kUrl: Symbol('url'), + kHeaders: Symbol('headers'), + kSignal: Symbol('signal'), + kState: Symbol('state'), + kGuard: Symbol('guard'), + kRealm: Symbol('realm') +} diff --git a/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/fetch/util.js b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/fetch/util.js new file mode 100644 index 000000000..c71ab16ee --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/fetch/util.js @@ -0,0 +1,916 @@ +'use strict' + +const { redirectStatus } = require('./constants') +const { performance } = require('perf_hooks') +const { isBlobLike, toUSVString, ReadableStreamFrom } = require('../core/util') +const assert = require('assert') +const { isUint8Array } = require('util/types') + +// https://nodejs.org/api/crypto.html#determining-if-crypto-support-is-unavailable +/** @type {import('crypto')|undefined} */ +let crypto + +try { + crypto = require('crypto') +} catch { + +} + +// https://fetch.spec.whatwg.org/#block-bad-port +const badPorts = [ + '1', '7', '9', '11', '13', '15', '17', '19', '20', '21', '22', '23', '25', '37', '42', '43', '53', '69', '77', '79', + '87', '95', '101', '102', '103', '104', '109', '110', '111', '113', '115', '117', '119', '123', '135', '137', + '139', '143', '161', '179', '389', '427', '465', '512', '513', '514', '515', '526', '530', '531', '532', + '540', '548', '554', '556', '563', '587', '601', '636', '989', '990', '993', '995', '1719', '1720', '1723', + '2049', '3659', '4045', '5060', '5061', '6000', '6566', '6665', '6666', '6667', '6668', '6669', '6697', + '10080' +] + +function responseURL (response) { + // https://fetch.spec.whatwg.org/#responses + // A response has an associated URL. It is a pointer to the last URL + // in response’s URL list and null if response’s URL list is empty. + const urlList = response.urlList + const length = urlList.length + return length === 0 ? null : urlList[length - 1].toString() +} + +// https://fetch.spec.whatwg.org/#concept-response-location-url +function responseLocationURL (response, requestFragment) { + // 1. If response’s status is not a redirect status, then return null. + if (!redirectStatus.includes(response.status)) { + return null + } + + // 2. Let location be the result of extracting header list values given + // `Location` and response’s header list. + let location = response.headersList.get('location') + + // 3. If location is a value, then set location to the result of parsing + // location with response’s URL. + location = location ? new URL(location, responseURL(response)) : null + + // 4. If location is a URL whose fragment is null, then set location’s + // fragment to requestFragment. + if (location && !location.hash) { + location.hash = requestFragment + } + + // 5. Return location. + return location +} + +/** @returns {URL} */ +function requestCurrentURL (request) { + return request.urlList[request.urlList.length - 1] +} + +function requestBadPort (request) { + // 1. Let url be request’s current URL. + const url = requestCurrentURL(request) + + // 2. If url’s scheme is an HTTP(S) scheme and url’s port is a bad port, + // then return blocked. + if (/^https?:/.test(url.protocol) && badPorts.includes(url.port)) { + return 'blocked' + } + + // 3. Return allowed. + return 'allowed' +} + +function isErrorLike (object) { + return object instanceof Error || ( + object?.constructor?.name === 'Error' || + object?.constructor?.name === 'DOMException' + ) +} + +// Check whether |statusText| is a ByteString and +// matches the Reason-Phrase token production. +// RFC 2616: https://tools.ietf.org/html/rfc2616 +// RFC 7230: https://tools.ietf.org/html/rfc7230 +// "reason-phrase = *( HTAB / SP / VCHAR / obs-text )" +// https://github.com/chromium/chromium/blob/94.0.4604.1/third_party/blink/renderer/core/fetch/response.cc#L116 +function isValidReasonPhrase (statusText) { + for (let i = 0; i < statusText.length; ++i) { + const c = statusText.charCodeAt(i) + if ( + !( + ( + c === 0x09 || // HTAB + (c >= 0x20 && c <= 0x7e) || // SP / VCHAR + (c >= 0x80 && c <= 0xff) + ) // obs-text + ) + ) { + return false + } + } + return true +} + +function isTokenChar (c) { + return !( + c >= 0x7f || + c <= 0x20 || + c === '(' || + c === ')' || + c === '<' || + c === '>' || + c === '@' || + c === ',' || + c === ';' || + c === ':' || + c === '\\' || + c === '"' || + c === '/' || + c === '[' || + c === ']' || + c === '?' || + c === '=' || + c === '{' || + c === '}' + ) +} + +// See RFC 7230, Section 3.2.6. +// https://github.com/chromium/chromium/blob/d7da0240cae77824d1eda25745c4022757499131/third_party/blink/renderer/platform/network/http_parsers.cc#L321 +function isValidHTTPToken (characters) { + if (!characters || typeof characters !== 'string') { + return false + } + for (let i = 0; i < characters.length; ++i) { + const c = characters.charCodeAt(i) + if (c > 0x7f || !isTokenChar(c)) { + return false + } + } + return true +} + +// https://fetch.spec.whatwg.org/#header-name +// https://github.com/chromium/chromium/blob/b3d37e6f94f87d59e44662d6078f6a12de845d17/net/http/http_util.cc#L342 +function isValidHeaderName (potentialValue) { + if (potentialValue.length === 0) { + return false + } + + for (const char of potentialValue) { + if (!isValidHTTPToken(char)) { + return false + } + } + + return true +} + +/** + * @see https://fetch.spec.whatwg.org/#header-value + * @param {string} potentialValue + */ +function isValidHeaderValue (potentialValue) { + // - Has no leading or trailing HTTP tab or space bytes. + // - Contains no 0x00 (NUL) or HTTP newline bytes. + if ( + potentialValue.startsWith('\t') || + potentialValue.startsWith(' ') || + potentialValue.endsWith('\t') || + potentialValue.endsWith(' ') + ) { + return false + } + + if ( + potentialValue.includes('\0') || + potentialValue.includes('\r') || + potentialValue.includes('\n') + ) { + return false + } + + return true +} + +// https://w3c.github.io/webappsec-referrer-policy/#set-requests-referrer-policy-on-redirect +function setRequestReferrerPolicyOnRedirect (request, actualResponse) { + // Given a request request and a response actualResponse, this algorithm + // updates request’s referrer policy according to the Referrer-Policy + // header (if any) in actualResponse. + + // 1. Let policy be the result of executing § 8.1 Parse a referrer policy + // from a Referrer-Policy header on actualResponse. + // TODO: https://w3c.github.io/webappsec-referrer-policy/#parse-referrer-policy-from-header + const policy = '' + + // 2. If policy is not the empty string, then set request’s referrer policy to policy. + if (policy !== '') { + request.referrerPolicy = policy + } +} + +// https://fetch.spec.whatwg.org/#cross-origin-resource-policy-check +function crossOriginResourcePolicyCheck () { + // TODO + return 'allowed' +} + +// https://fetch.spec.whatwg.org/#concept-cors-check +function corsCheck () { + // TODO + return 'success' +} + +// https://fetch.spec.whatwg.org/#concept-tao-check +function TAOCheck () { + // TODO + return 'success' +} + +function appendFetchMetadata (httpRequest) { + // https://w3c.github.io/webappsec-fetch-metadata/#sec-fetch-dest-header + // TODO + + // https://w3c.github.io/webappsec-fetch-metadata/#sec-fetch-mode-header + + // 1. Assert: r’s url is a potentially trustworthy URL. + // TODO + + // 2. Let header be a Structured Header whose value is a token. + let header = null + + // 3. Set header’s value to r’s mode. + header = httpRequest.mode + + // 4. Set a structured field value `Sec-Fetch-Mode`/header in r’s header list. + httpRequest.headersList.set('sec-fetch-mode', header) + + // https://w3c.github.io/webappsec-fetch-metadata/#sec-fetch-site-header + // TODO + + // https://w3c.github.io/webappsec-fetch-metadata/#sec-fetch-user-header + // TODO +} + +// https://fetch.spec.whatwg.org/#append-a-request-origin-header +function appendRequestOriginHeader (request) { + // 1. Let serializedOrigin be the result of byte-serializing a request origin with request. + let serializedOrigin = request.origin + + // 2. If request’s response tainting is "cors" or request’s mode is "websocket", then append (`Origin`, serializedOrigin) to request’s header list. + if (request.responseTainting === 'cors' || request.mode === 'websocket') { + if (serializedOrigin) { + request.headersList.append('Origin', serializedOrigin) + } + + // 3. Otherwise, if request’s method is neither `GET` nor `HEAD`, then: + } else if (request.method !== 'GET' && request.method !== 'HEAD') { + // 1. Switch on request’s referrer policy: + switch (request.referrerPolicy) { + case 'no-referrer': + // Set serializedOrigin to `null`. + serializedOrigin = null + break + case 'no-referrer-when-downgrade': + case 'strict-origin': + case 'strict-origin-when-cross-origin': + // If request’s origin is a tuple origin, its scheme is "https", and request’s current URL’s scheme is not "https", then set serializedOrigin to `null`. + if (/^https:/.test(request.origin) && !/^https:/.test(requestCurrentURL(request))) { + serializedOrigin = null + } + break + case 'same-origin': + // If request’s origin is not same origin with request’s current URL’s origin, then set serializedOrigin to `null`. + if (!sameOrigin(request, requestCurrentURL(request))) { + serializedOrigin = null + } + break + default: + // Do nothing. + } + + if (serializedOrigin) { + // 2. Append (`Origin`, serializedOrigin) to request’s header list. + request.headersList.append('Origin', serializedOrigin) + } + } +} + +function coarsenedSharedCurrentTime (crossOriginIsolatedCapability) { + // TODO + return performance.now() +} + +// https://fetch.spec.whatwg.org/#create-an-opaque-timing-info +function createOpaqueTimingInfo (timingInfo) { + return { + startTime: timingInfo.startTime ?? 0, + redirectStartTime: 0, + redirectEndTime: 0, + postRedirectStartTime: timingInfo.startTime ?? 0, + finalServiceWorkerStartTime: 0, + finalNetworkResponseStartTime: 0, + finalNetworkRequestStartTime: 0, + endTime: 0, + encodedBodySize: 0, + decodedBodySize: 0, + finalConnectionTimingInfo: null + } +} + +// https://html.spec.whatwg.org/multipage/origin.html#policy-container +function makePolicyContainer () { + // TODO + return {} +} + +// https://html.spec.whatwg.org/multipage/origin.html#clone-a-policy-container +function clonePolicyContainer () { + // TODO + return {} +} + +// https://w3c.github.io/webappsec-referrer-policy/#determine-requests-referrer +function determineRequestsReferrer (request) { + // 1. Let policy be request's referrer policy. + const policy = request.referrerPolicy + + // Return no-referrer when empty or policy says so + if (policy == null || policy === '' || policy === 'no-referrer') { + return 'no-referrer' + } + + // 2. Let environment be the request client + const environment = request.client + let referrerSource = null + + /** + * 3, Switch on request’s referrer: + "client" + If environment’s global object is a Window object, then + Let document be the associated Document of environment’s global object. + If document’s origin is an opaque origin, return no referrer. + While document is an iframe srcdoc document, + let document be document’s browsing context’s browsing context container’s node document. + Let referrerSource be document’s URL. + + Otherwise, let referrerSource be environment’s creation URL. + + a URL + Let referrerSource be request’s referrer. + */ + if (request.referrer === 'client') { + // Not defined in Node but part of the spec + if (request.client?.globalObject?.constructor?.name === 'Window' ) { // eslint-disable-line + const origin = environment.globalObject.self?.origin ?? environment.globalObject.location?.origin + + // If document’s origin is an opaque origin, return no referrer. + if (origin == null || origin === 'null') return 'no-referrer' + + // Let referrerSource be document’s URL. + referrerSource = new URL(environment.globalObject.location.href) + } else { + // 3(a)(II) If environment's global object is not Window, + // Let referrerSource be environments creationURL + if (environment?.globalObject?.location == null) { + return 'no-referrer' + } + + referrerSource = new URL(environment.globalObject.location.href) + } + } else if (request.referrer instanceof URL) { + // 3(b) If requests's referrer is a URL instance, then make + // referrerSource be requests's referrer. + referrerSource = request.referrer + } else { + // If referrerSource neither client nor instance of URL + // then return "no-referrer". + return 'no-referrer' + } + + const urlProtocol = referrerSource.protocol + + // If url's scheme is a local scheme (i.e. one of "about", "data", "javascript", "file") + // then return "no-referrer". + if ( + urlProtocol === 'about:' || urlProtocol === 'data:' || + urlProtocol === 'blob:' + ) { + return 'no-referrer' + } + + let temp + let referrerOrigin + // 4. Let requests's referrerURL be the result of stripping referrer + // source for use as referrer (using util function, without origin only) + const referrerUrl = (temp = stripURLForReferrer(referrerSource)).length > 4096 + // 5. Let referrerOrigin be the result of stripping referrer + // source for use as referrer (using util function, with originOnly true) + ? (referrerOrigin = stripURLForReferrer(referrerSource, true)) + // 6. If result of seralizing referrerUrl is a string whose length is greater than + // 4096, then set referrerURL to referrerOrigin + : temp + const areSameOrigin = sameOrigin(request, referrerUrl) + const isNonPotentiallyTrustWorthy = isURLPotentiallyTrustworthy(referrerUrl) && + !isURLPotentiallyTrustworthy(request.url) + + // NOTE: How to treat step 7? + // 8. Execute the switch statements corresponding to the value of policy: + switch (policy) { + case 'origin': return referrerOrigin != null ? referrerOrigin : stripURLForReferrer(referrerSource, true) + case 'unsafe-url': return referrerUrl + case 'same-origin': + return areSameOrigin ? referrerOrigin : 'no-referrer' + case 'origin-when-cross-origin': + return areSameOrigin ? referrerUrl : referrerOrigin + case 'strict-origin-when-cross-origin': + /** + * 1. If the origin of referrerURL and the origin of request’s current URL are the same, + * then return referrerURL. + * 2. If referrerURL is a potentially trustworthy URL and request’s current URL is not a + * potentially trustworthy URL, then return no referrer. + * 3. Return referrerOrigin + */ + if (areSameOrigin) return referrerOrigin + // else return isNonPotentiallyTrustWorthy ? 'no-referrer' : referrerOrigin + case 'strict-origin': // eslint-disable-line + /** + * 1. If referrerURL is a potentially trustworthy URL and + * request’s current URL is not a potentially trustworthy URL, + * then return no referrer. + * 2. Return referrerOrigin + */ + case 'no-referrer-when-downgrade': // eslint-disable-line + /** + * 1. If referrerURL is a potentially trustworthy URL and + * request’s current URL is not a potentially trustworthy URL, + * then return no referrer. + * 2. Return referrerOrigin + */ + + default: // eslint-disable-line + return isNonPotentiallyTrustWorthy ? 'no-referrer' : referrerOrigin + } + + function stripURLForReferrer (url, originOnly = false) { + const urlObject = new URL(url.href) + urlObject.username = '' + urlObject.password = '' + urlObject.hash = '' + + return originOnly ? urlObject.origin : urlObject.href + } +} + +function isURLPotentiallyTrustworthy (url) { + if (!(url instanceof URL)) { + return false + } + + // If child of about, return true + if (url.href === 'about:blank' || url.href === 'about:srcdoc') { + return true + } + + // If scheme is data, return true + if (url.protocol === 'data:') return true + + // If file, return true + if (url.protocol === 'file:') return true + + return isOriginPotentiallyTrustworthy(url.origin) + + function isOriginPotentiallyTrustworthy (origin) { + // If origin is explicitly null, return false + if (origin == null || origin === 'null') return false + + const originAsURL = new URL(origin) + + // If secure, return true + if (originAsURL.protocol === 'https:' || originAsURL.protocol === 'wss:') { + return true + } + + // If localhost or variants, return true + if (/^127(?:\.[0-9]+){0,2}\.[0-9]+$|^\[(?:0*:)*?:?0*1\]$/.test(originAsURL.hostname) || + (originAsURL.hostname === 'localhost' || originAsURL.hostname.includes('localhost.')) || + (originAsURL.hostname.endsWith('.localhost'))) { + return true + } + + // If any other, return false + return false + } +} + +/** + * @see https://w3c.github.io/webappsec-subresource-integrity/#does-response-match-metadatalist + * @param {Uint8Array} bytes + * @param {string} metadataList + */ +function bytesMatch (bytes, metadataList) { + // If node is not built with OpenSSL support, we cannot check + // a request's integrity, so allow it by default (the spec will + // allow requests if an invalid hash is given, as precedence). + /* istanbul ignore if: only if node is built with --without-ssl */ + if (crypto === undefined) { + return true + } + + // 1. Let parsedMetadata be the result of parsing metadataList. + const parsedMetadata = parseMetadata(metadataList) + + // 2. If parsedMetadata is no metadata, return true. + if (parsedMetadata === 'no metadata') { + return true + } + + // 3. If parsedMetadata is the empty set, return true. + if (parsedMetadata.length === 0) { + return true + } + + // 4. Let metadata be the result of getting the strongest + // metadata from parsedMetadata. + const list = parsedMetadata.sort((c, d) => d.algo.localeCompare(c.algo)) + // get the strongest algorithm + const strongest = list[0].algo + // get all entries that use the strongest algorithm; ignore weaker + const metadata = list.filter((item) => item.algo === strongest) + + // 5. For each item in metadata: + for (const item of metadata) { + // 1. Let algorithm be the alg component of item. + const algorithm = item.algo + + // 2. Let expectedValue be the val component of item. + const expectedValue = item.hash + + // 3. Let actualValue be the result of applying algorithm to bytes. + const actualValue = crypto.createHash(algorithm).update(bytes).digest('base64') + + // 4. If actualValue is a case-sensitive match for expectedValue, + // return true. + if (actualValue === expectedValue) { + return true + } + } + + // 6. Return false. + return false +} + +// https://w3c.github.io/webappsec-subresource-integrity/#grammardef-hash-with-options +// https://www.w3.org/TR/CSP2/#source-list-syntax +// https://www.rfc-editor.org/rfc/rfc5234#appendix-B.1 +const parseHashWithOptions = /((?sha256|sha384|sha512)-(?[A-z0-9+/]{1}.*={0,2}))( +[\x21-\x7e]?)?/i + +/** + * @see https://w3c.github.io/webappsec-subresource-integrity/#parse-metadata + * @param {string} metadata + */ +function parseMetadata (metadata) { + // 1. Let result be the empty set. + /** @type {{ algo: string, hash: string }[]} */ + const result = [] + + // 2. Let empty be equal to true. + let empty = true + + const supportedHashes = crypto.getHashes() + + // 3. For each token returned by splitting metadata on spaces: + for (const token of metadata.split(' ')) { + // 1. Set empty to false. + empty = false + + // 2. Parse token as a hash-with-options. + const parsedToken = parseHashWithOptions.exec(token) + + // 3. If token does not parse, continue to the next token. + if (parsedToken === null || parsedToken.groups === undefined) { + // Note: Chromium blocks the request at this point, but Firefox + // gives a warning that an invalid integrity was given. The + // correct behavior is to ignore these, and subsequently not + // check the integrity of the resource. + continue + } + + // 4. Let algorithm be the hash-algo component of token. + const algorithm = parsedToken.groups.algo + + // 5. If algorithm is a hash function recognized by the user + // agent, add the parsed token to result. + if (supportedHashes.includes(algorithm.toLowerCase())) { + result.push(parsedToken.groups) + } + } + + // 4. Return no metadata if empty is true, otherwise return result. + if (empty === true) { + return 'no metadata' + } + + return result +} + +// https://w3c.github.io/webappsec-upgrade-insecure-requests/#upgrade-request +function tryUpgradeRequestToAPotentiallyTrustworthyURL (request) { + // TODO +} + +/** + * @link {https://html.spec.whatwg.org/multipage/origin.html#same-origin} + * @param {URL} A + * @param {URL} B + */ +function sameOrigin (A, B) { + // 1. If A and B are the same opaque origin, then return true. + // "opaque origin" is an internal value we cannot access, ignore. + + // 2. If A and B are both tuple origins and their schemes, + // hosts, and port are identical, then return true. + if (A.protocol === B.protocol && A.hostname === B.hostname && A.port === B.port) { + return true + } + + // 3. Return false. + return false +} + +function createDeferredPromise () { + let res + let rej + const promise = new Promise((resolve, reject) => { + res = resolve + rej = reject + }) + + return { promise, resolve: res, reject: rej } +} + +function isAborted (fetchParams) { + return fetchParams.controller.state === 'aborted' +} + +function isCancelled (fetchParams) { + return fetchParams.controller.state === 'aborted' || + fetchParams.controller.state === 'terminated' +} + +// https://fetch.spec.whatwg.org/#concept-method-normalize +function normalizeMethod (method) { + return /^(DELETE|GET|HEAD|OPTIONS|POST|PUT)$/i.test(method) + ? method.toUpperCase() + : method +} + +// https://infra.spec.whatwg.org/#serialize-a-javascript-value-to-a-json-string +function serializeJavascriptValueToJSONString (value) { + // 1. Let result be ? Call(%JSON.stringify%, undefined, « value »). + const result = JSON.stringify(value) + + // 2. If result is undefined, then throw a TypeError. + if (result === undefined) { + throw new TypeError('Value is not JSON serializable') + } + + // 3. Assert: result is a string. + assert(typeof result === 'string') + + // 4. Return result. + return result +} + +// https://tc39.es/ecma262/#sec-%25iteratorprototype%25-object +const esIteratorPrototype = Object.getPrototypeOf(Object.getPrototypeOf([][Symbol.iterator]())) + +/** + * @see https://webidl.spec.whatwg.org/#dfn-iterator-prototype-object + * @param {() => unknown[]} iterator + * @param {string} name name of the instance + * @param {'key'|'value'|'key+value'} kind + */ +function makeIterator (iterator, name, kind) { + const object = { + index: 0, + kind, + target: iterator + } + + const i = { + next () { + // 1. Let interface be the interface for which the iterator prototype object exists. + + // 2. Let thisValue be the this value. + + // 3. Let object be ? ToObject(thisValue). + + // 4. If object is a platform object, then perform a security + // check, passing: + + // 5. If object is not a default iterator object for interface, + // then throw a TypeError. + if (Object.getPrototypeOf(this) !== i) { + throw new TypeError( + `'next' called on an object that does not implement interface ${name} Iterator.` + ) + } + + // 6. Let index be object’s index. + // 7. Let kind be object’s kind. + // 8. Let values be object’s target's value pairs to iterate over. + const { index, kind, target } = object + const values = target() + + // 9. Let len be the length of values. + const len = values.length + + // 10. If index is greater than or equal to len, then return + // CreateIterResultObject(undefined, true). + if (index >= len) { + return { value: undefined, done: true } + } + + // 11. Let pair be the entry in values at index index. + const pair = values[index] + + // 12. Set object’s index to index + 1. + object.index = index + 1 + + // 13. Return the iterator result for pair and kind. + return iteratorResult(pair, kind) + }, + // The class string of an iterator prototype object for a given interface is the + // result of concatenating the identifier of the interface and the string " Iterator". + [Symbol.toStringTag]: `${name} Iterator` + } + + // The [[Prototype]] internal slot of an iterator prototype object must be %IteratorPrototype%. + Object.setPrototypeOf(i, esIteratorPrototype) + // esIteratorPrototype needs to be the prototype of i + // which is the prototype of an empty object. Yes, it's confusing. + return Object.setPrototypeOf({}, i) +} + +// https://webidl.spec.whatwg.org/#iterator-result +function iteratorResult (pair, kind) { + let result + + // 1. Let result be a value determined by the value of kind: + switch (kind) { + case 'key': { + // 1. Let idlKey be pair’s key. + // 2. Let key be the result of converting idlKey to an + // ECMAScript value. + // 3. result is key. + result = pair[0] + break + } + case 'value': { + // 1. Let idlValue be pair’s value. + // 2. Let value be the result of converting idlValue to + // an ECMAScript value. + // 3. result is value. + result = pair[1] + break + } + case 'key+value': { + // 1. Let idlKey be pair’s key. + // 2. Let idlValue be pair’s value. + // 3. Let key be the result of converting idlKey to an + // ECMAScript value. + // 4. Let value be the result of converting idlValue to + // an ECMAScript value. + // 5. Let array be ! ArrayCreate(2). + // 6. Call ! CreateDataProperty(array, "0", key). + // 7. Call ! CreateDataProperty(array, "1", value). + // 8. result is array. + result = pair + break + } + } + + // 2. Return CreateIterResultObject(result, false). + return { value: result, done: false } +} + +/** + * @see https://fetch.spec.whatwg.org/#body-fully-read + */ +async function fullyReadBody (body, processBody, processBodyError) { + // 1. If taskDestination is null, then set taskDestination to + // the result of starting a new parallel queue. + + // 2. Let promise be the result of fully reading body as promise + // given body. + try { + /** @type {Uint8Array[]} */ + const chunks = [] + let length = 0 + + const reader = body.stream.getReader() + + while (true) { + const { done, value } = await reader.read() + + if (done === true) { + break + } + + // read-loop chunk steps + assert(isUint8Array(value)) + + chunks.push(value) + length += value.byteLength + } + + // 3. Let fulfilledSteps given a byte sequence bytes be to queue + // a fetch task to run processBody given bytes, with + // taskDestination. + const fulfilledSteps = (bytes) => queueMicrotask(() => { + processBody(bytes) + }) + + fulfilledSteps(Buffer.concat(chunks, length)) + } catch (err) { + // 4. Let rejectedSteps be to queue a fetch task to run + // processBodyError, with taskDestination. + queueMicrotask(() => processBodyError(err)) + } + + // 5. React to promise with fulfilledSteps and rejectedSteps. +} + +/** @type {ReadableStream} */ +let ReadableStream = globalThis.ReadableStream + +function isReadableStreamLike (stream) { + if (!ReadableStream) { + ReadableStream = require('stream/web').ReadableStream + } + + return stream instanceof ReadableStream || ( + stream[Symbol.toStringTag] === 'ReadableStream' && + typeof stream.tee === 'function' + ) +} + +/** + * @param {ReadableStreamController} controller + */ +function readableStreamClose (controller) { + try { + controller.close() + } catch (err) { + // TODO: add comment explaining why this error occurs. + if (!err.message.includes('Controller is already closed')) { + throw err + } + } +} + +/** + * Fetch supports node >= 16.8.0, but Object.hasOwn was added in v16.9.0. + */ +const hasOwn = Object.hasOwn || ((dict, key) => Object.prototype.hasOwnProperty.call(dict, key)) + +module.exports = { + isAborted, + isCancelled, + createDeferredPromise, + ReadableStreamFrom, + toUSVString, + tryUpgradeRequestToAPotentiallyTrustworthyURL, + coarsenedSharedCurrentTime, + determineRequestsReferrer, + makePolicyContainer, + clonePolicyContainer, + appendFetchMetadata, + appendRequestOriginHeader, + TAOCheck, + corsCheck, + crossOriginResourcePolicyCheck, + createOpaqueTimingInfo, + setRequestReferrerPolicyOnRedirect, + isValidHTTPToken, + requestBadPort, + requestCurrentURL, + responseURL, + responseLocationURL, + isBlobLike, + isURLPotentiallyTrustworthy, + isValidReasonPhrase, + sameOrigin, + normalizeMethod, + serializeJavascriptValueToJSONString, + makeIterator, + isValidHeaderName, + isValidHeaderValue, + hasOwn, + isErrorLike, + fullyReadBody, + bytesMatch, + isReadableStreamLike, + readableStreamClose +} diff --git a/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/fetch/webidl.js b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/fetch/webidl.js new file mode 100644 index 000000000..cd42de2ac --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/fetch/webidl.js @@ -0,0 +1,645 @@ +'use strict' + +const { types } = require('util') +const { hasOwn, toUSVString } = require('./util') + +const webidl = {} +webidl.converters = {} +webidl.util = {} +webidl.errors = {} + +/** + * + * @param {{ + * header: string + * message: string + * }} message + */ +webidl.errors.exception = function (message) { + throw new TypeError(`${message.header}: ${message.message}`) +} + +/** + * Throw an error when conversion from one type to another has failed + * @param {{ + * prefix: string + * argument: string + * types: string[] + * }} context + */ +webidl.errors.conversionFailed = function (context) { + const plural = context.types.length === 1 ? '' : ' one of' + const message = + `${context.argument} could not be converted to` + + `${plural}: ${context.types.join(', ')}.` + + return webidl.errors.exception({ + header: context.prefix, + message + }) +} + +/** + * Throw an error when an invalid argument is provided + * @param {{ + * prefix: string + * value: string + * type: string + * }} context + */ +webidl.errors.invalidArgument = function (context) { + return webidl.errors.exception({ + header: context.prefix, + message: `"${context.value}" is an invalid ${context.type}.` + }) +} + +// https://tc39.es/ecma262/#sec-ecmascript-data-types-and-values +webidl.util.Type = function (V) { + switch (typeof V) { + case 'undefined': return 'Undefined' + case 'boolean': return 'Boolean' + case 'string': return 'String' + case 'symbol': return 'Symbol' + case 'number': return 'Number' + case 'bigint': return 'BigInt' + case 'function': + case 'object': { + if (V === null) { + return 'Null' + } + + return 'Object' + } + } +} + +// https://webidl.spec.whatwg.org/#abstract-opdef-converttoint +webidl.util.ConvertToInt = function (V, bitLength, signedness, opts = {}) { + let upperBound + let lowerBound + + // 1. If bitLength is 64, then: + if (bitLength === 64) { + // 1. Let upperBound be 2^53 − 1. + upperBound = Math.pow(2, 53) - 1 + + // 2. If signedness is "unsigned", then let lowerBound be 0. + if (signedness === 'unsigned') { + lowerBound = 0 + } else { + // 3. Otherwise let lowerBound be −2^53 + 1. + lowerBound = Math.pow(-2, 53) + 1 + } + } else if (signedness === 'unsigned') { + // 2. Otherwise, if signedness is "unsigned", then: + + // 1. Let lowerBound be 0. + lowerBound = 0 + + // 2. Let upperBound be 2^bitLength − 1. + upperBound = Math.pow(2, bitLength) - 1 + } else { + // 3. Otherwise: + + // 1. Let lowerBound be -2^bitLength − 1. + lowerBound = Math.pow(-2, bitLength) - 1 + + // 2. Let upperBound be 2^bitLength − 1 − 1. + upperBound = Math.pow(2, bitLength - 1) - 1 + } + + // 4. Let x be ? ToNumber(V). + let x = Number(V) + + // 5. If x is −0, then set x to +0. + if (Object.is(-0, x)) { + x = 0 + } + + // 6. If the conversion is to an IDL type associated + // with the [EnforceRange] extended attribute, then: + if (opts.enforceRange === true) { + // 1. If x is NaN, +∞, or −∞, then throw a TypeError. + if ( + Number.isNaN(x) || + x === Number.POSITIVE_INFINITY || + x === Number.NEGATIVE_INFINITY + ) { + webidl.errors.exception({ + header: 'Integer conversion', + message: `Could not convert ${V} to an integer.` + }) + } + + // 2. Set x to IntegerPart(x). + x = webidl.util.IntegerPart(x) + + // 3. If x < lowerBound or x > upperBound, then + // throw a TypeError. + if (x < lowerBound || x > upperBound) { + webidl.errors.exception({ + header: 'Integer conversion', + message: `Value must be between ${lowerBound}-${upperBound}, got ${x}.` + }) + } + + // 4. Return x. + return x + } + + // 7. If x is not NaN and the conversion is to an IDL + // type associated with the [Clamp] extended + // attribute, then: + if (!Number.isNaN(x) && opts.clamp === true) { + // 1. Set x to min(max(x, lowerBound), upperBound). + x = Math.min(Math.max(x, lowerBound), upperBound) + + // 2. Round x to the nearest integer, choosing the + // even integer if it lies halfway between two, + // and choosing +0 rather than −0. + if (Math.floor(x) % 2 === 0) { + x = Math.floor(x) + } else { + x = Math.ceil(x) + } + + // 3. Return x. + return x + } + + // 8. If x is NaN, +0, +∞, or −∞, then return +0. + if ( + Number.isNaN(x) || + Object.is(0, x) || + x === Number.POSITIVE_INFINITY || + x === Number.NEGATIVE_INFINITY + ) { + return 0 + } + + // 9. Set x to IntegerPart(x). + x = webidl.util.IntegerPart(x) + + // 10. Set x to x modulo 2^bitLength. + x = x % Math.pow(2, bitLength) + + // 11. If signedness is "signed" and x ≥ 2^bitLength − 1, + // then return x − 2^bitLength. + if (signedness === 'signed' && x >= Math.pow(2, bitLength) - 1) { + return x - Math.pow(2, bitLength) + } + + // 12. Otherwise, return x. + return x +} + +// https://webidl.spec.whatwg.org/#abstract-opdef-integerpart +webidl.util.IntegerPart = function (n) { + // 1. Let r be floor(abs(n)). + const r = Math.floor(Math.abs(n)) + + // 2. If n < 0, then return -1 × r. + if (n < 0) { + return -1 * r + } + + // 3. Otherwise, return r. + return r +} + +// https://webidl.spec.whatwg.org/#es-sequence +webidl.sequenceConverter = function (converter) { + return (V) => { + // 1. If Type(V) is not Object, throw a TypeError. + if (webidl.util.Type(V) !== 'Object') { + webidl.errors.exception({ + header: 'Sequence', + message: `Value of type ${webidl.util.Type(V)} is not an Object.` + }) + } + + // 2. Let method be ? GetMethod(V, @@iterator). + /** @type {Generator} */ + const method = V?.[Symbol.iterator]?.() + const seq = [] + + // 3. If method is undefined, throw a TypeError. + if ( + method === undefined || + typeof method.next !== 'function' + ) { + webidl.errors.exception({ + header: 'Sequence', + message: 'Object is not an iterator.' + }) + } + + // https://webidl.spec.whatwg.org/#create-sequence-from-iterable + while (true) { + const { done, value } = method.next() + + if (done) { + break + } + + seq.push(converter(value)) + } + + return seq + } +} + +// https://webidl.spec.whatwg.org/#es-to-record +webidl.recordConverter = function (keyConverter, valueConverter) { + return (O) => { + // 1. If Type(O) is not Object, throw a TypeError. + if (webidl.util.Type(O) !== 'Object') { + webidl.errors.exception({ + header: 'Record', + message: `Value of type ${webidl.util.Type(O)} is not an Object.` + }) + } + + // 2. Let result be a new empty instance of record. + const result = {} + + if (!types.isProxy(O)) { + // Object.keys only returns enumerable properties + const keys = Object.keys(O) + + for (const key of keys) { + // 1. Let typedKey be key converted to an IDL value of type K. + const typedKey = keyConverter(key) + + // 2. Let value be ? Get(O, key). + // 3. Let typedValue be value converted to an IDL value of type V. + const typedValue = valueConverter(O[key]) + + // 4. Set result[typedKey] to typedValue. + result[typedKey] = typedValue + } + + // 5. Return result. + return result + } + + // 3. Let keys be ? O.[[OwnPropertyKeys]](). + const keys = Reflect.ownKeys(O) + + // 4. For each key of keys. + for (const key of keys) { + // 1. Let desc be ? O.[[GetOwnProperty]](key). + const desc = Reflect.getOwnPropertyDescriptor(O, key) + + // 2. If desc is not undefined and desc.[[Enumerable]] is true: + if (desc?.enumerable) { + // 1. Let typedKey be key converted to an IDL value of type K. + const typedKey = keyConverter(key) + + // 2. Let value be ? Get(O, key). + // 3. Let typedValue be value converted to an IDL value of type V. + const typedValue = valueConverter(O[key]) + + // 4. Set result[typedKey] to typedValue. + result[typedKey] = typedValue + } + } + + // 5. Return result. + return result + } +} + +webidl.interfaceConverter = function (i) { + return (V, opts = {}) => { + if (opts.strict !== false && !(V instanceof i)) { + webidl.errors.exception({ + header: i.name, + message: `Expected ${V} to be an instance of ${i.name}.` + }) + } + + return V + } +} + +/** + * @param {{ + * key: string, + * defaultValue?: any, + * required?: boolean, + * converter: (...args: unknown[]) => unknown, + * allowedValues?: any[] + * }[]} converters + * @returns + */ +webidl.dictionaryConverter = function (converters) { + return (dictionary) => { + const type = webidl.util.Type(dictionary) + const dict = {} + + if (type === 'Null' || type === 'Undefined') { + return dict + } else if (type !== 'Object') { + webidl.errors.exception({ + header: 'Dictionary', + message: `Expected ${dictionary} to be one of: Null, Undefined, Object.` + }) + } + + for (const options of converters) { + const { key, defaultValue, required, converter } = options + + if (required === true) { + if (!hasOwn(dictionary, key)) { + webidl.errors.exception({ + header: 'Dictionary', + message: `Missing required key "${key}".` + }) + } + } + + let value = dictionary[key] + const hasDefault = hasOwn(options, 'defaultValue') + + // Only use defaultValue if value is undefined and + // a defaultValue options was provided. + if (hasDefault && value !== null) { + value = value ?? defaultValue + } + + // A key can be optional and have no default value. + // When this happens, do not perform a conversion, + // and do not assign the key a value. + if (required || hasDefault || value !== undefined) { + value = converter(value) + + if ( + options.allowedValues && + !options.allowedValues.includes(value) + ) { + webidl.errors.exception({ + header: 'Dictionary', + message: `${value} is not an accepted type. Expected one of ${options.allowedValues.join(', ')}.` + }) + } + + dict[key] = value + } + } + + return dict + } +} + +webidl.nullableConverter = function (converter) { + return (V) => { + if (V === null) { + return V + } + + return converter(V) + } +} + +// https://webidl.spec.whatwg.org/#es-DOMString +webidl.converters.DOMString = function (V, opts = {}) { + // 1. If V is null and the conversion is to an IDL type + // associated with the [LegacyNullToEmptyString] + // extended attribute, then return the DOMString value + // that represents the empty string. + if (V === null && opts.legacyNullToEmptyString) { + return '' + } + + // 2. Let x be ? ToString(V). + if (typeof V === 'symbol') { + throw new TypeError('Could not convert argument of type symbol to string.') + } + + // 3. Return the IDL DOMString value that represents the + // same sequence of code units as the one the + // ECMAScript String value x represents. + return String(V) +} + +// https://webidl.spec.whatwg.org/#es-ByteString +webidl.converters.ByteString = function (V) { + // 1. Let x be ? ToString(V). + // Note: DOMString converter perform ? ToString(V) + const x = webidl.converters.DOMString(V) + + // 2. If the value of any element of x is greater than + // 255, then throw a TypeError. + for (let index = 0; index < x.length; index++) { + const charCode = x.charCodeAt(index) + + if (charCode > 255) { + throw new TypeError( + 'Cannot convert argument to a ByteString because the character at ' + + `index ${index} has a value of ${charCode} which is greater than 255.` + ) + } + } + + // 3. Return an IDL ByteString value whose length is the + // length of x, and where the value of each element is + // the value of the corresponding element of x. + return x +} + +// https://webidl.spec.whatwg.org/#es-USVString +// TODO: ensure that util.toUSVString follows webidl spec +webidl.converters.USVString = toUSVString + +// https://webidl.spec.whatwg.org/#es-boolean +webidl.converters.boolean = function (V) { + // 1. Let x be the result of computing ToBoolean(V). + const x = Boolean(V) + + // 2. Return the IDL boolean value that is the one that represents + // the same truth value as the ECMAScript Boolean value x. + return x +} + +// https://webidl.spec.whatwg.org/#es-any +webidl.converters.any = function (V) { + return V +} + +// https://webidl.spec.whatwg.org/#es-long-long +webidl.converters['long long'] = function (V, opts) { + // 1. Let x be ? ConvertToInt(V, 64, "signed"). + const x = webidl.util.ConvertToInt(V, 64, 'signed', opts) + + // 2. Return the IDL long long value that represents + // the same numeric value as x. + return x +} + +// https://webidl.spec.whatwg.org/#es-unsigned-long-long +webidl.converters['unsigned long long'] = function (V) { + // 1. Let x be ? ConvertToInt(V, 64, "unsigned"). + const x = webidl.util.ConvertToInt(V, 64, 'unsigned') + + // 2. Return the IDL unsigned long long value that + // represents the same numeric value as x. + return x +} + +// https://webidl.spec.whatwg.org/#es-unsigned-short +webidl.converters['unsigned short'] = function (V) { + // 1. Let x be ? ConvertToInt(V, 16, "unsigned"). + const x = webidl.util.ConvertToInt(V, 16, 'unsigned') + + // 2. Return the IDL unsigned short value that represents + // the same numeric value as x. + return x +} + +// https://webidl.spec.whatwg.org/#idl-ArrayBuffer +webidl.converters.ArrayBuffer = function (V, opts = {}) { + // 1. If Type(V) is not Object, or V does not have an + // [[ArrayBufferData]] internal slot, then throw a + // TypeError. + // see: https://tc39.es/ecma262/#sec-properties-of-the-arraybuffer-instances + // see: https://tc39.es/ecma262/#sec-properties-of-the-sharedarraybuffer-instances + if ( + webidl.util.Type(V) !== 'Object' || + !types.isAnyArrayBuffer(V) + ) { + webidl.errors.conversionFailed({ + prefix: `${V}`, + argument: `${V}`, + types: ['ArrayBuffer'] + }) + } + + // 2. If the conversion is not to an IDL type associated + // with the [AllowShared] extended attribute, and + // IsSharedArrayBuffer(V) is true, then throw a + // TypeError. + if (opts.allowShared === false && types.isSharedArrayBuffer(V)) { + webidl.errors.exception({ + header: 'ArrayBuffer', + message: 'SharedArrayBuffer is not allowed.' + }) + } + + // 3. If the conversion is not to an IDL type associated + // with the [AllowResizable] extended attribute, and + // IsResizableArrayBuffer(V) is true, then throw a + // TypeError. + // Note: resizable ArrayBuffers are currently a proposal. + + // 4. Return the IDL ArrayBuffer value that is a + // reference to the same object as V. + return V +} + +webidl.converters.TypedArray = function (V, T, opts = {}) { + // 1. Let T be the IDL type V is being converted to. + + // 2. If Type(V) is not Object, or V does not have a + // [[TypedArrayName]] internal slot with a value + // equal to T’s name, then throw a TypeError. + if ( + webidl.util.Type(V) !== 'Object' || + !types.isTypedArray(V) || + V.constructor.name !== T.name + ) { + webidl.errors.conversionFailed({ + prefix: `${T.name}`, + argument: `${V}`, + types: [T.name] + }) + } + + // 3. If the conversion is not to an IDL type associated + // with the [AllowShared] extended attribute, and + // IsSharedArrayBuffer(V.[[ViewedArrayBuffer]]) is + // true, then throw a TypeError. + if (opts.allowShared === false && types.isSharedArrayBuffer(V.buffer)) { + webidl.errors.exception({ + header: 'ArrayBuffer', + message: 'SharedArrayBuffer is not allowed.' + }) + } + + // 4. If the conversion is not to an IDL type associated + // with the [AllowResizable] extended attribute, and + // IsResizableArrayBuffer(V.[[ViewedArrayBuffer]]) is + // true, then throw a TypeError. + // Note: resizable array buffers are currently a proposal + + // 5. Return the IDL value of type T that is a reference + // to the same object as V. + return V +} + +webidl.converters.DataView = function (V, opts = {}) { + // 1. If Type(V) is not Object, or V does not have a + // [[DataView]] internal slot, then throw a TypeError. + if (webidl.util.Type(V) !== 'Object' || !types.isDataView(V)) { + webidl.errors.exception({ + header: 'DataView', + message: 'Object is not a DataView.' + }) + } + + // 2. If the conversion is not to an IDL type associated + // with the [AllowShared] extended attribute, and + // IsSharedArrayBuffer(V.[[ViewedArrayBuffer]]) is true, + // then throw a TypeError. + if (opts.allowShared === false && types.isSharedArrayBuffer(V.buffer)) { + webidl.errors.exception({ + header: 'ArrayBuffer', + message: 'SharedArrayBuffer is not allowed.' + }) + } + + // 3. If the conversion is not to an IDL type associated + // with the [AllowResizable] extended attribute, and + // IsResizableArrayBuffer(V.[[ViewedArrayBuffer]]) is + // true, then throw a TypeError. + // Note: resizable ArrayBuffers are currently a proposal + + // 4. Return the IDL DataView value that is a reference + // to the same object as V. + return V +} + +// https://webidl.spec.whatwg.org/#BufferSource +webidl.converters.BufferSource = function (V, opts = {}) { + if (types.isAnyArrayBuffer(V)) { + return webidl.converters.ArrayBuffer(V, opts) + } + + if (types.isTypedArray(V)) { + return webidl.converters.TypedArray(V, V.constructor) + } + + if (types.isDataView(V)) { + return webidl.converters.DataView(V, opts) + } + + throw new TypeError(`Could not convert ${V} to a BufferSource.`) +} + +webidl.converters['sequence'] = webidl.sequenceConverter( + webidl.converters.ByteString +) + +webidl.converters['sequence>'] = webidl.sequenceConverter( + webidl.converters['sequence'] +) + +webidl.converters['record'] = webidl.recordConverter( + webidl.converters.ByteString, + webidl.converters.ByteString +) + +module.exports = { + webidl +} diff --git a/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/fileapi/encoding.js b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/fileapi/encoding.js new file mode 100644 index 000000000..4aac20cce --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/fileapi/encoding.js @@ -0,0 +1,286 @@ +'use strict' + +/** + * @see https://encoding.spec.whatwg.org/#concept-encoding-get + * @param {string} label + */ +function getEncoding (label) { + // 1. Remove any leading and trailing ASCII whitespace from label. + // 2. If label is an ASCII case-insensitive match for any of the + // labels listed in the table below, then return the + // corresponding encoding; otherwise return failure. + switch (label.trim().toLowerCase()) { + case 'unicode-1-1-utf-8': + case 'unicode11utf8': + case 'unicode20utf8': + case 'utf-8': + case 'utf8': + case 'x-unicode20utf8': + return 'UTF-8' + case '866': + case 'cp866': + case 'csibm866': + case 'ibm866': + return 'IBM866' + case 'csisolatin2': + case 'iso-8859-2': + case 'iso-ir-101': + case 'iso8859-2': + case 'iso88592': + case 'iso_8859-2': + case 'iso_8859-2:1987': + case 'l2': + case 'latin2': + return 'ISO-8859-2' + case 'csisolatin3': + case 'iso-8859-3': + case 'iso-ir-109': + case 'iso8859-3': + case 'iso88593': + case 'iso_8859-3': + case 'iso_8859-3:1988': + case 'l3': + case 'latin3': + return 'ISO-8859-3' + case 'csisolatin4': + case 'iso-8859-4': + case 'iso-ir-110': + case 'iso8859-4': + case 'iso88594': + case 'iso_8859-4': + case 'iso_8859-4:1988': + case 'l4': + case 'latin4': + return 'ISO-8859-4' + case 'csisolatincyrillic': + case 'cyrillic': + case 'iso-8859-5': + case 'iso-ir-144': + case 'iso8859-5': + case 'iso88595': + case 'iso_8859-5': + case 'iso_8859-5:1988': + return 'ISO-8859-5' + case 'arabic': + case 'asmo-708': + case 'csiso88596e': + case 'csiso88596i': + case 'csisolatinarabic': + case 'ecma-114': + case 'iso-8859-6': + case 'iso-8859-6-e': + case 'iso-8859-6-i': + case 'iso-ir-127': + case 'iso8859-6': + case 'iso88596': + case 'iso_8859-6': + case 'iso_8859-6:1987': + return 'ISO-8859-6' + case 'csisolatingreek': + case 'ecma-118': + case 'elot_928': + case 'greek': + case 'greek8': + case 'iso-8859-7': + case 'iso-ir-126': + case 'iso8859-7': + case 'iso88597': + case 'iso_8859-7': + case 'iso_8859-7:1987': + case 'sun_eu_greek': + return 'ISO-8859-7' + case 'csiso88598e': + case 'csisolatinhebrew': + case 'hebrew': + case 'iso-8859-8': + case 'iso-8859-8-e': + case 'iso-ir-138': + case 'iso8859-8': + case 'iso88598': + case 'iso_8859-8': + case 'iso_8859-8:1988': + case 'visual': + return 'ISO-8859-8' + case 'csiso88598i': + case 'iso-8859-8-i': + case 'logical': + return 'ISO-8859-8-I' + case 'csisolatin6': + case 'iso-8859-10': + case 'iso-ir-157': + case 'iso8859-10': + case 'iso885910': + case 'l6': + case 'latin6': + return 'ISO-8859-10' + case 'iso-8859-13': + case 'iso8859-13': + case 'iso885913': + return 'ISO-8859-13' + case 'iso-8859-14': + case 'iso8859-14': + case 'iso885914': + return 'ISO-8859-14' + case 'csisolatin9': + case 'iso-8859-15': + case 'iso8859-15': + case 'iso885915': + case 'iso_8859-15': + case 'l9': + return 'ISO-8859-15' + case 'iso-8859-16': + return 'ISO-8859-16' + case 'cskoi8r': + case 'koi': + case 'koi8': + case 'koi8-r': + case 'koi8_r': + return 'KOI8-R' + case 'koi8-ru': + case 'koi8-u': + return 'KOI8-U' + case 'csmacintosh': + case 'mac': + case 'macintosh': + case 'x-mac-roman': + return 'macintosh' + case 'iso-8859-11': + case 'iso8859-11': + case 'iso885911': + case 'tis-620': + case 'windows-874': + return 'windows-874' + case 'cp1250': + case 'windows-1250': + case 'x-cp1250': + return 'windows-1250' + case 'cp1251': + case 'windows-1251': + case 'x-cp1251': + return 'windows-1251' + case 'ansi_x3.4-1968': + case 'ascii': + case 'cp1252': + case 'cp819': + case 'csisolatin1': + case 'ibm819': + case 'iso-8859-1': + case 'iso-ir-100': + case 'iso8859-1': + case 'iso88591': + case 'iso_8859-1': + case 'iso_8859-1:1987': + case 'l1': + case 'latin1': + case 'us-ascii': + case 'windows-1252': + case 'x-cp1252': + return 'windows-1252' + case 'cp1253': + case 'windows-1253': + case 'x-cp1253': + return 'windows-1253' + case 'cp1254': + case 'csisolatin5': + case 'iso-8859-9': + case 'iso-ir-148': + case 'iso8859-9': + case 'iso88599': + case 'iso_8859-9': + case 'iso_8859-9:1989': + case 'l5': + case 'latin5': + case 'windows-1254': + case 'x-cp1254': + return 'windows-1254' + case 'cp1255': + case 'windows-1255': + case 'x-cp1255': + return 'windows-1255' + case 'cp1256': + case 'windows-1256': + case 'x-cp1256': + return 'windows-1256' + case 'cp1257': + case 'windows-1257': + case 'x-cp1257': + return 'windows-1257' + case 'cp1258': + case 'windows-1258': + case 'x-cp1258': + return 'windows-1258' + case 'x-mac-cyrillic': + case 'x-mac-ukrainian': + return 'x-mac-cyrillic' + case 'chinese': + case 'csgb2312': + case 'csiso58gb231280': + case 'gb2312': + case 'gb_2312': + case 'gb_2312-80': + case 'gbk': + case 'iso-ir-58': + case 'x-gbk': + return 'GBK' + case 'gb18030': + return 'gb18030' + case 'big5': + case 'big5-hkscs': + case 'cn-big5': + case 'csbig5': + case 'x-x-big5': + return 'Big5' + case 'cseucpkdfmtjapanese': + case 'euc-jp': + case 'x-euc-jp': + return 'EUC-JP' + case 'csiso2022jp': + case 'iso-2022-jp': + return 'ISO-2022-JP' + case 'csshiftjis': + case 'ms932': + case 'ms_kanji': + case 'shift-jis': + case 'shift_jis': + case 'sjis': + case 'windows-31j': + case 'x-sjis': + return 'Shift_JIS' + case 'cseuckr': + case 'csksc56011987': + case 'euc-kr': + case 'iso-ir-149': + case 'korean': + case 'ks_c_5601-1987': + case 'ks_c_5601-1989': + case 'ksc5601': + case 'ksc_5601': + case 'windows-949': + return 'EUC-KR' + case 'csiso2022kr': + case 'hz-gb-2312': + case 'iso-2022-cn': + case 'iso-2022-cn-ext': + case 'iso-2022-kr': + case 'replacement': + return 'replacement' + case 'unicodefffe': + case 'utf-16be': + return 'UTF-16BE' + case 'csunicode': + case 'iso-10646-ucs-2': + case 'ucs-2': + case 'unicode': + case 'unicodefeff': + case 'utf-16': + case 'utf-16le': + return 'UTF-16LE' + case 'x-user-defined': + return 'x-user-defined' + default: return 'failure' + } +} + +module.exports = { + getEncoding +} diff --git a/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/fileapi/filereader.js b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/fileapi/filereader.js new file mode 100644 index 000000000..fa18e8eb5 --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/fileapi/filereader.js @@ -0,0 +1,368 @@ +'use strict' + +const { + staticPropertyDescriptors, + readOperation, + fireAProgressEvent +} = require('./util') +const { + kState, + kError, + kResult, + kEvents, + kAborted +} = require('./symbols') +const { webidl } = require('../fetch/webidl') +const { kEnumerableProperty } = require('../core/util') + +class FileReader extends EventTarget { + constructor () { + super() + + this[kState] = 'empty' + this[kResult] = null + this[kError] = null + this[kEvents] = { + loadend: null, + error: null, + abort: null, + load: null, + progress: null, + loadstart: null + } + } + + /** + * @see https://w3c.github.io/FileAPI/#dfn-readAsArrayBuffer + * @param {import('buffer').Blob} blob + */ + readAsArrayBuffer (blob) { + if (!(this instanceof FileReader)) { + throw new TypeError('Illegal invocation') + } + + if (arguments.length === 0) { + throw new TypeError( + 'Failed to execute \'readAsArrayBuffer\' on \'FileReader\': 1 argument required, but 0 present.' + ) + } + + blob = webidl.converters.Blob(blob, { strict: false }) + + // The readAsArrayBuffer(blob) method, when invoked, + // must initiate a read operation for blob with ArrayBuffer. + readOperation(this, blob, 'ArrayBuffer') + } + + /** + * @see https://w3c.github.io/FileAPI/#readAsBinaryString + * @param {import('buffer').Blob} blob + */ + readAsBinaryString (blob) { + if (!(this instanceof FileReader)) { + throw new TypeError('Illegal invocation') + } + + if (arguments.length === 0) { + throw new TypeError( + 'Failed to execute \'readAsBinaryString\' on \'FileReader\': 1 argument required, but 0 present.' + ) + } + + blob = webidl.converters.Blob(blob, { strict: false }) + + // The readAsBinaryString(blob) method, when invoked, + // must initiate a read operation for blob with BinaryString. + readOperation(this, blob, 'BinaryString') + } + + /** + * @see https://w3c.github.io/FileAPI/#readAsDataText + * @param {import('buffer').Blob} blob + * @param {string?} encoding + */ + readAsText (blob, encoding = undefined) { + if (!(this instanceof FileReader)) { + throw new TypeError('Illegal invocation') + } + + if (arguments.length === 0) { + throw new TypeError( + 'Failed to execute \'readAsText\' on \'FileReader\': 1 argument required, but 0 present.' + ) + } + + blob = webidl.converters.Blob(blob, { strict: false }) + + if (encoding !== undefined) { + encoding = webidl.converters.DOMString(encoding) + } + + // The readAsText(blob, encoding) method, when invoked, + // must initiate a read operation for blob with Text and encoding. + readOperation(this, blob, 'Text', encoding) + } + + /** + * @see https://w3c.github.io/FileAPI/#dfn-readAsDataURL + * @param {import('buffer').Blob} blob + */ + readAsDataURL (blob) { + if (!(this instanceof FileReader)) { + throw new TypeError('Illegal invocation') + } + + if (arguments.length === 0) { + throw new TypeError( + 'Failed to execute \'readAsDataURL\' on \'FileReader\': 1 argument required, but 0 present.' + ) + } + + blob = webidl.converters.Blob(blob, { strict: false }) + + // The readAsDataURL(blob) method, when invoked, must + // initiate a read operation for blob with DataURL. + readOperation(this, blob, 'DataURL') + } + + /** + * @see https://w3c.github.io/FileAPI/#dfn-abort + */ + abort () { + // 1. If this's state is "empty" or if this's state is + // "done" set this's result to null and terminate + // this algorithm. + if (this[kState] === 'empty' || this[kState] === 'done') { + this[kResult] = null + return + } + + // 2. If this's state is "loading" set this's state to + // "done" and set this's result to null. + if (this[kState] === 'loading') { + this[kState] = 'done' + this[kResult] = null + } + + // 3. If there are any tasks from this on the file reading + // task source in an affiliated task queue, then remove + // those tasks from that task queue. + this[kAborted] = true + + // 4. Terminate the algorithm for the read method being processed. + // TODO + + // 5. Fire a progress event called abort at this. + fireAProgressEvent('abort', this) + + // 6. If this's state is not "loading", fire a progress + // event called loadend at this. + if (this[kState] !== 'loading') { + fireAProgressEvent('loadend', this) + } + } + + /** + * @see https://w3c.github.io/FileAPI/#dom-filereader-readystate + */ + get readyState () { + if (!(this instanceof FileReader)) { + throw new TypeError('Illegal invocation') + } + + switch (this[kState]) { + case 'empty': return this.EMPTY + case 'loading': return this.LOADING + case 'done': return this.DONE + } + } + + /** + * @see https://w3c.github.io/FileAPI/#dom-filereader-result + */ + get result () { + if (!(this instanceof FileReader)) { + throw new TypeError('Illegal invocation') + } + + // The result attribute’s getter, when invoked, must return + // this's result. + return this[kResult] + } + + /** + * @see https://w3c.github.io/FileAPI/#dom-filereader-error + */ + get error () { + if (!(this instanceof FileReader)) { + throw new TypeError('Illegal invocation') + } + + // The error attribute’s getter, when invoked, must return + // this's error. + return this[kError] + } + + get onloadend () { + if (!(this instanceof FileReader)) { + throw new TypeError('Illegal invocation') + } + + return this[kEvents].loadend + } + + set onloadend (fn) { + if (!(this instanceof FileReader)) { + throw new TypeError('Illegal invocation') + } + + if (typeof fn === 'function') { + this[kEvents].loadend = fn + } else { + this[kEvents].loadend = null + } + } + + get onerror () { + if (!(this instanceof FileReader)) { + throw new TypeError('Illegal invocation') + } + + return this[kEvents].error + } + + set onerror (fn) { + if (!(this instanceof FileReader)) { + throw new TypeError('Illegal invocation') + } + + if (typeof fn === 'function') { + this[kEvents].error = fn + } else { + this[kEvents].error = null + } + } + + get onloadstart () { + if (!(this instanceof FileReader)) { + throw new TypeError('Illegal invocation') + } + + return this[kEvents].loadstart + } + + set onloadstart (fn) { + if (!(this instanceof FileReader)) { + throw new TypeError('Illegal invocation') + } + + if (typeof fn === 'function') { + this[kEvents].loadstart = fn + } else { + this[kEvents].loadstart = null + } + } + + get onprogress () { + if (!(this instanceof FileReader)) { + throw new TypeError('Illegal invocation') + } + + return this[kEvents].progress + } + + set onprogress (fn) { + if (!(this instanceof FileReader)) { + throw new TypeError('Illegal invocation') + } + + if (typeof fn === 'function') { + this[kEvents].progress = fn + } else { + this[kEvents].progress = null + } + } + + get onload () { + if (!(this instanceof FileReader)) { + throw new TypeError('Illegal invocation') + } + + return this[kEvents].load + } + + set onload (fn) { + if (!(this instanceof FileReader)) { + throw new TypeError('Illegal invocation') + } + + if (typeof fn === 'function') { + this[kEvents].load = fn + } else { + this[kEvents].load = null + } + } + + get onabort () { + if (!(this instanceof FileReader)) { + throw new TypeError('Illegal invocation') + } + + return this[kEvents].abort + } + + set onabort (fn) { + if (!(this instanceof FileReader)) { + throw new TypeError('Illegal invocation') + } + + if (typeof fn === 'function') { + this[kEvents].abort = fn + } else { + this[kEvents].abort = null + } + } +} + +// https://w3c.github.io/FileAPI/#dom-filereader-empty +FileReader.EMPTY = FileReader.prototype.EMPTY = 0 +// https://w3c.github.io/FileAPI/#dom-filereader-loading +FileReader.LOADING = FileReader.prototype.LOADING = 1 +// https://w3c.github.io/FileAPI/#dom-filereader-done +FileReader.DONE = FileReader.prototype.DONE = 2 + +Object.defineProperties(FileReader.prototype, { + EMPTY: staticPropertyDescriptors, + LOADING: staticPropertyDescriptors, + DONE: staticPropertyDescriptors, + readAsArrayBuffer: kEnumerableProperty, + readAsBinaryString: kEnumerableProperty, + readAsText: kEnumerableProperty, + readAsDataURL: kEnumerableProperty, + abort: kEnumerableProperty, + readyState: kEnumerableProperty, + result: kEnumerableProperty, + error: kEnumerableProperty, + onloadstart: kEnumerableProperty, + onprogress: kEnumerableProperty, + onload: kEnumerableProperty, + onabort: kEnumerableProperty, + onerror: kEnumerableProperty, + onloadend: kEnumerableProperty, + [Symbol.toStringTag]: { + value: 'FileReader', + writable: false, + enumerable: false, + configurable: true + } +}) + +Object.defineProperties(FileReader, { + EMPTY: staticPropertyDescriptors, + LOADING: staticPropertyDescriptors, + DONE: staticPropertyDescriptors +}) + +module.exports = { + FileReader +} diff --git a/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/fileapi/progressevent.js b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/fileapi/progressevent.js new file mode 100644 index 000000000..ab3c6191e --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/fileapi/progressevent.js @@ -0,0 +1,84 @@ +'use strict' + +const { webidl } = require('../fetch/webidl') + +const kState = Symbol('ProgressEvent state') + +/** + * @see https://xhr.spec.whatwg.org/#progressevent + */ +class ProgressEvent extends Event { + constructor (type, eventInitDict = {}) { + type = webidl.converters.DOMString(type) + eventInitDict = webidl.converters.ProgressEventInit(eventInitDict ?? {}) + + super(type, eventInitDict) + + this[kState] = { + lengthComputable: eventInitDict.lengthComputable, + loaded: eventInitDict.loaded, + total: eventInitDict.total + } + } + + get lengthComputable () { + if (!(this instanceof ProgressEvent)) { + throw new TypeError('Illegal invocation') + } + + return this[kState].lengthComputable + } + + get loaded () { + if (!(this instanceof ProgressEvent)) { + throw new TypeError('Illegal invocation') + } + + return this[kState].loaded + } + + get total () { + if (!(this instanceof ProgressEvent)) { + throw new TypeError('Illegal invocation') + } + + return this[kState].total + } +} + +webidl.converters.ProgressEventInit = webidl.dictionaryConverter([ + { + key: 'lengthComputable', + converter: webidl.converters.boolean, + defaultValue: false + }, + { + key: 'loaded', + converter: webidl.converters['unsigned long long'], + defaultValue: 0 + }, + { + key: 'total', + converter: webidl.converters['unsigned long long'], + defaultValue: 0 + }, + { + key: 'bubbles', + converter: webidl.converters.boolean, + defaultValue: false + }, + { + key: 'cancelable', + converter: webidl.converters.boolean, + defaultValue: false + }, + { + key: 'composed', + converter: webidl.converters.boolean, + defaultValue: false + } +]) + +module.exports = { + ProgressEvent +} diff --git a/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/fileapi/symbols.js b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/fileapi/symbols.js new file mode 100644 index 000000000..dd11746de --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/fileapi/symbols.js @@ -0,0 +1,10 @@ +'use strict' + +module.exports = { + kState: Symbol('FileReader state'), + kResult: Symbol('FileReader result'), + kError: Symbol('FileReader error'), + kLastProgressEventFired: Symbol('FileReader last progress event fired timestamp'), + kEvents: Symbol('FileReader events'), + kAborted: Symbol('FileReader aborted') +} diff --git a/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/fileapi/util.js b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/fileapi/util.js new file mode 100644 index 000000000..b37e0dd2b --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/fileapi/util.js @@ -0,0 +1,398 @@ +'use strict' + +const { + kState, + kError, + kResult, + kAborted, + kLastProgressEventFired +} = require('./symbols') +const { ProgressEvent } = require('./progressevent') +const { getEncoding } = require('./encoding') +const { DOMException } = require('../fetch/constants') +const { serializeAMimeType, parseMIMEType } = require('../fetch/dataURL') +const { types } = require('util') +const { StringDecoder } = require('string_decoder') +const { btoa } = require('buffer') + +/** @type {PropertyDescriptor} */ +const staticPropertyDescriptors = { + enumerable: true, + writable: false, + configurable: false +} + +/** + * @see https://w3c.github.io/FileAPI/#readOperation + * @param {import('./filereader').FileReader} fr + * @param {import('buffer').Blob} blob + * @param {string} type + * @param {string?} encodingName + */ +function readOperation (fr, blob, type, encodingName) { + // 1. If fr’s state is "loading", throw an InvalidStateError + // DOMException. + if (fr[kState] === 'loading') { + throw new DOMException('Invalid state', 'InvalidStateError') + } + + // 2. Set fr’s state to "loading". + fr[kState] = 'loading' + + // 3. Set fr’s result to null. + fr[kResult] = null + + // 4. Set fr’s error to null. + fr[kError] = null + + // 5. Let stream be the result of calling get stream on blob. + /** @type {import('stream/web').ReadableStream} */ + const stream = blob.stream() + + // 6. Let reader be the result of getting a reader from stream. + const reader = stream.getReader() + + // 7. Let bytes be an empty byte sequence. + /** @type {Uint8Array[]} */ + const bytes = [] + + // 8. Let chunkPromise be the result of reading a chunk from + // stream with reader. + let chunkPromise = reader.read() + + // 9. Let isFirstChunk be true. + let isFirstChunk = true + + // 10. In parallel, while true: + // Note: "In parallel" just means non-blocking + // Note 2: readOperation itself cannot be async as double + // reading the body would then reject the promise, instead + // of throwing an error. + ;(async () => { + while (!fr[kAborted]) { + // 1. Wait for chunkPromise to be fulfilled or rejected. + try { + const { done, value } = await chunkPromise + + // 2. If chunkPromise is fulfilled, and isFirstChunk is + // true, queue a task to fire a progress event called + // loadstart at fr. + if (isFirstChunk && !fr[kAborted]) { + queueMicrotask(() => { + fireAProgressEvent('loadstart', fr) + }) + } + + // 3. Set isFirstChunk to false. + isFirstChunk = false + + // 4. If chunkPromise is fulfilled with an object whose + // done property is false and whose value property is + // a Uint8Array object, run these steps: + if (!done && types.isUint8Array(value)) { + // 1. Let bs be the byte sequence represented by the + // Uint8Array object. + + // 2. Append bs to bytes. + bytes.push(value) + + // 3. If roughly 50ms have passed since these steps + // were last invoked, queue a task to fire a + // progress event called progress at fr. + if ( + ( + fr[kLastProgressEventFired] === undefined || + Date.now() - fr[kLastProgressEventFired] >= 50 + ) && + !fr[kAborted] + ) { + fr[kLastProgressEventFired] = Date.now() + queueMicrotask(() => { + fireAProgressEvent('progress', fr) + }) + } + + // 4. Set chunkPromise to the result of reading a + // chunk from stream with reader. + chunkPromise = reader.read() + } else if (done) { + // 5. Otherwise, if chunkPromise is fulfilled with an + // object whose done property is true, queue a task + // to run the following steps and abort this algorithm: + queueMicrotask(() => { + // 1. Set fr’s state to "done". + fr[kState] = 'done' + + // 2. Let result be the result of package data given + // bytes, type, blob’s type, and encodingName. + try { + const result = packageData(bytes, type, blob.type, encodingName) + + // 4. Else: + + if (fr[kAborted]) { + return + } + + // 1. Set fr’s result to result. + fr[kResult] = result + + // 2. Fire a progress event called load at the fr. + fireAProgressEvent('load', fr) + } catch (error) { + // 3. If package data threw an exception error: + + // 1. Set fr’s error to error. + fr[kError] = error + + // 2. Fire a progress event called error at fr. + fireAProgressEvent('error', fr) + } + + // 5. If fr’s state is not "loading", fire a progress + // event called loadend at the fr. + if (fr[kState] !== 'loading') { + fireAProgressEvent('loadend', fr) + } + }) + + break + } + } catch (error) { + if (fr[kAborted]) { + return + } + + // 6. Otherwise, if chunkPromise is rejected with an + // error error, queue a task to run the following + // steps and abort this algorithm: + queueMicrotask(() => { + // 1. Set fr’s state to "done". + fr[kState] = 'done' + + // 2. Set fr’s error to error. + fr[kError] = error + + // 3. Fire a progress event called error at fr. + fireAProgressEvent('error', fr) + + // 4. If fr’s state is not "loading", fire a progress + // event called loadend at fr. + if (fr[kState] !== 'loading') { + fireAProgressEvent('loadend', fr) + } + }) + + break + } + } + })() +} + +/** + * @see https://w3c.github.io/FileAPI/#fire-a-progress-event + * @param {string} e The name of the event + * @param {import('./filereader').FileReader} reader + */ +function fireAProgressEvent (e, reader) { + const event = new ProgressEvent(e, { + bubbles: false, + cancelable: false + }) + + reader.dispatchEvent(event) + try { + // eslint-disable-next-line no-useless-call + reader[`on${e}`]?.call(reader, event) + } catch (err) { + // Prevent the error from being swallowed + queueMicrotask(() => { + throw err + }) + } +} + +/** + * @see https://w3c.github.io/FileAPI/#blob-package-data + * @param {Uint8Array[]} bytes + * @param {string} type + * @param {string?} mimeType + * @param {string?} encodingName + */ +function packageData (bytes, type, mimeType, encodingName) { + // 1. A Blob has an associated package data algorithm, given + // bytes, a type, a optional mimeType, and a optional + // encodingName, which switches on type and runs the + // associated steps: + + switch (type) { + case 'DataURL': { + // 1. Return bytes as a DataURL [RFC2397] subject to + // the considerations below: + // * Use mimeType as part of the Data URL if it is + // available in keeping with the Data URL + // specification [RFC2397]. + // * If mimeType is not available return a Data URL + // without a media-type. [RFC2397]. + + // https://datatracker.ietf.org/doc/html/rfc2397#section-3 + // dataurl := "data:" [ mediatype ] [ ";base64" ] "," data + // mediatype := [ type "/" subtype ] *( ";" parameter ) + // data := *urlchar + // parameter := attribute "=" value + let dataURL = 'data:' + + const parsed = parseMIMEType(mimeType || 'application/octet-stream') + + if (parsed !== 'failure') { + dataURL += serializeAMimeType(parsed) + } + + dataURL += ';base64,' + + const decoder = new StringDecoder('latin1') + + for (const chunk of bytes) { + dataURL += btoa(decoder.write(chunk)) + } + + dataURL += btoa(decoder.end()) + + return dataURL + } + case 'Text': { + // 1. Let encoding be failure + let encoding = 'failure' + + // 2. If the encodingName is present, set encoding to the + // result of getting an encoding from encodingName. + if (encodingName) { + encoding = getEncoding(encodingName) + } + + // 3. If encoding is failure, and mimeType is present: + if (encoding === 'failure' && mimeType) { + // 1. Let type be the result of parse a MIME type + // given mimeType. + const type = parseMIMEType(mimeType) + + // 2. If type is not failure, set encoding to the result + // of getting an encoding from type’s parameters["charset"]. + if (type !== 'failure') { + encoding = getEncoding(type.parameters.get('charset')) + } + } + + // 4. If encoding is failure, then set encoding to UTF-8. + if (encoding === 'failure') { + encoding = 'UTF-8' + } + + // 5. Decode bytes using fallback encoding encoding, and + // return the result. + return decode(bytes, encoding) + } + case 'ArrayBuffer': { + // Return a new ArrayBuffer whose contents are bytes. + const sequence = combineByteSequences(bytes) + + return sequence.buffer + } + case 'BinaryString': { + // Return bytes as a binary string, in which every byte + // is represented by a code unit of equal value [0..255]. + let binaryString = '' + + const decoder = new StringDecoder('latin1') + + for (const chunk of bytes) { + binaryString += decoder.write(chunk) + } + + binaryString += decoder.end() + + return binaryString + } + } +} + +/** + * @see https://encoding.spec.whatwg.org/#decode + * @param {Uint8Array[]} ioQueue + * @param {string} encoding + */ +function decode (ioQueue, encoding) { + const bytes = combineByteSequences(ioQueue) + + // 1. Let BOMEncoding be the result of BOM sniffing ioQueue. + const BOMEncoding = BOMSniffing(bytes) + + let slice = 0 + + // 2. If BOMEncoding is non-null: + if (BOMEncoding !== null) { + // 1. Set encoding to BOMEncoding. + encoding = BOMEncoding + + // 2. Read three bytes from ioQueue, if BOMEncoding is + // UTF-8; otherwise read two bytes. + // (Do nothing with those bytes.) + slice = BOMEncoding === 'UTF-8' ? 3 : 2 + } + + // 3. Process a queue with an instance of encoding’s + // decoder, ioQueue, output, and "replacement". + + // 4. Return output. + + const sliced = bytes.slice(slice) + return new TextDecoder(encoding).decode(sliced) +} + +/** + * @see https://encoding.spec.whatwg.org/#bom-sniff + * @param {Uint8Array} ioQueue + */ +function BOMSniffing (ioQueue) { + // 1. Let BOM be the result of peeking 3 bytes from ioQueue, + // converted to a byte sequence. + const [a, b, c] = ioQueue + + // 2. For each of the rows in the table below, starting with + // the first one and going down, if BOM starts with the + // bytes given in the first column, then return the + // encoding given in the cell in the second column of that + // row. Otherwise, return null. + if (a === 0xEF && b === 0xBB && c === 0xBF) { + return 'UTF-8' + } else if (a === 0xFE && b === 0xFF) { + return 'UTF-16BE' + } else if (a === 0xFF && b === 0xFE) { + return 'UTF-16LE' + } + + return null +} + +/** + * @param {Uint8Array[]} sequences + */ +function combineByteSequences (sequences) { + const size = sequences.reduce((a, b) => { + return a + b.byteLength + }, 0) + + let offset = 0 + + return sequences.reduce((a, b) => { + a.set(b, offset) + offset += b.byteLength + return a + }, new Uint8Array(size)) +} + +module.exports = { + staticPropertyDescriptors, + readOperation, + fireAProgressEvent +} diff --git a/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/global.js b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/global.js new file mode 100644 index 000000000..18bfd73cc --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/global.js @@ -0,0 +1,32 @@ +'use strict' + +// We include a version number for the Dispatcher API. In case of breaking changes, +// this version number must be increased to avoid conflicts. +const globalDispatcher = Symbol.for('undici.globalDispatcher.1') +const { InvalidArgumentError } = require('./core/errors') +const Agent = require('./agent') + +if (getGlobalDispatcher() === undefined) { + setGlobalDispatcher(new Agent()) +} + +function setGlobalDispatcher (agent) { + if (!agent || typeof agent.dispatch !== 'function') { + throw new InvalidArgumentError('Argument agent must implement Agent') + } + Object.defineProperty(globalThis, globalDispatcher, { + value: agent, + writable: true, + enumerable: false, + configurable: false + }) +} + +function getGlobalDispatcher () { + return globalThis[globalDispatcher] +} + +module.exports = { + setGlobalDispatcher, + getGlobalDispatcher +} diff --git a/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/handler/DecoratorHandler.js b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/handler/DecoratorHandler.js new file mode 100644 index 000000000..9d70a767f --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/handler/DecoratorHandler.js @@ -0,0 +1,35 @@ +'use strict' + +module.exports = class DecoratorHandler { + constructor (handler) { + this.handler = handler + } + + onConnect (...args) { + return this.handler.onConnect(...args) + } + + onError (...args) { + return this.handler.onError(...args) + } + + onUpgrade (...args) { + return this.handler.onUpgrade(...args) + } + + onHeaders (...args) { + return this.handler.onHeaders(...args) + } + + onData (...args) { + return this.handler.onData(...args) + } + + onComplete (...args) { + return this.handler.onComplete(...args) + } + + onBodySent (...args) { + return this.handler.onBodySent(...args) + } +} diff --git a/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/handler/RedirectHandler.js b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/handler/RedirectHandler.js new file mode 100644 index 000000000..baca27ed1 --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/handler/RedirectHandler.js @@ -0,0 +1,216 @@ +'use strict' + +const util = require('../core/util') +const { kBodyUsed } = require('../core/symbols') +const assert = require('assert') +const { InvalidArgumentError } = require('../core/errors') +const EE = require('events') + +const redirectableStatusCodes = [300, 301, 302, 303, 307, 308] + +const kBody = Symbol('body') + +class BodyAsyncIterable { + constructor (body) { + this[kBody] = body + this[kBodyUsed] = false + } + + async * [Symbol.asyncIterator] () { + assert(!this[kBodyUsed], 'disturbed') + this[kBodyUsed] = true + yield * this[kBody] + } +} + +class RedirectHandler { + constructor (dispatch, maxRedirections, opts, handler) { + if (maxRedirections != null && (!Number.isInteger(maxRedirections) || maxRedirections < 0)) { + throw new InvalidArgumentError('maxRedirections must be a positive number') + } + + util.validateHandler(handler, opts.method, opts.upgrade) + + this.dispatch = dispatch + this.location = null + this.abort = null + this.opts = { ...opts, maxRedirections: 0 } // opts must be a copy + this.maxRedirections = maxRedirections + this.handler = handler + this.history = [] + + if (util.isStream(this.opts.body)) { + // TODO (fix): Provide some way for the user to cache the file to e.g. /tmp + // so that it can be dispatched again? + // TODO (fix): Do we need 100-expect support to provide a way to do this properly? + if (util.bodyLength(this.opts.body) === 0) { + this.opts.body + .on('data', function () { + assert(false) + }) + } + + if (typeof this.opts.body.readableDidRead !== 'boolean') { + this.opts.body[kBodyUsed] = false + EE.prototype.on.call(this.opts.body, 'data', function () { + this[kBodyUsed] = true + }) + } + } else if (this.opts.body && typeof this.opts.body.pipeTo === 'function') { + // TODO (fix): We can't access ReadableStream internal state + // to determine whether or not it has been disturbed. This is just + // a workaround. + this.opts.body = new BodyAsyncIterable(this.opts.body) + } else if ( + this.opts.body && + typeof this.opts.body !== 'string' && + !ArrayBuffer.isView(this.opts.body) && + util.isIterable(this.opts.body) + ) { + // TODO: Should we allow re-using iterable if !this.opts.idempotent + // or through some other flag? + this.opts.body = new BodyAsyncIterable(this.opts.body) + } + } + + onConnect (abort) { + this.abort = abort + this.handler.onConnect(abort, { history: this.history }) + } + + onUpgrade (statusCode, headers, socket) { + this.handler.onUpgrade(statusCode, headers, socket) + } + + onError (error) { + this.handler.onError(error) + } + + onHeaders (statusCode, headers, resume, statusText) { + this.location = this.history.length >= this.maxRedirections || util.isDisturbed(this.opts.body) + ? null + : parseLocation(statusCode, headers) + + if (this.opts.origin) { + this.history.push(new URL(this.opts.path, this.opts.origin)) + } + + if (!this.location) { + return this.handler.onHeaders(statusCode, headers, resume, statusText) + } + + const { origin, pathname, search } = util.parseURL(new URL(this.location, this.opts.origin && new URL(this.opts.path, this.opts.origin))) + const path = search ? `${pathname}${search}` : pathname + + // Remove headers referring to the original URL. + // By default it is Host only, unless it's a 303 (see below), which removes also all Content-* headers. + // https://tools.ietf.org/html/rfc7231#section-6.4 + this.opts.headers = cleanRequestHeaders(this.opts.headers, statusCode === 303, this.opts.origin !== origin) + this.opts.path = path + this.opts.origin = origin + this.opts.maxRedirections = 0 + this.opts.query = null + + // https://tools.ietf.org/html/rfc7231#section-6.4.4 + // In case of HTTP 303, always replace method to be either HEAD or GET + if (statusCode === 303 && this.opts.method !== 'HEAD') { + this.opts.method = 'GET' + this.opts.body = null + } + } + + onData (chunk) { + if (this.location) { + /* + https://tools.ietf.org/html/rfc7231#section-6.4 + + TLDR: undici always ignores 3xx response bodies. + + Redirection is used to serve the requested resource from another URL, so it is assumes that + no body is generated (and thus can be ignored). Even though generating a body is not prohibited. + + For status 301, 302, 303, 307 and 308 (the latter from RFC 7238), the specs mention that the body usually + (which means it's optional and not mandated) contain just an hyperlink to the value of + the Location response header, so the body can be ignored safely. + + For status 300, which is "Multiple Choices", the spec mentions both generating a Location + response header AND a response body with the other possible location to follow. + Since the spec explicitily chooses not to specify a format for such body and leave it to + servers and browsers implementors, we ignore the body as there is no specified way to eventually parse it. + */ + } else { + return this.handler.onData(chunk) + } + } + + onComplete (trailers) { + if (this.location) { + /* + https://tools.ietf.org/html/rfc7231#section-6.4 + + TLDR: undici always ignores 3xx response trailers as they are not expected in case of redirections + and neither are useful if present. + + See comment on onData method above for more detailed informations. + */ + + this.location = null + this.abort = null + + this.dispatch(this.opts, this) + } else { + this.handler.onComplete(trailers) + } + } + + onBodySent (chunk) { + if (this.handler.onBodySent) { + this.handler.onBodySent(chunk) + } + } +} + +function parseLocation (statusCode, headers) { + if (redirectableStatusCodes.indexOf(statusCode) === -1) { + return null + } + + for (let i = 0; i < headers.length; i += 2) { + if (headers[i].toString().toLowerCase() === 'location') { + return headers[i + 1] + } + } +} + +// https://tools.ietf.org/html/rfc7231#section-6.4.4 +function shouldRemoveHeader (header, removeContent, unknownOrigin) { + return ( + (header.length === 4 && header.toString().toLowerCase() === 'host') || + (removeContent && header.toString().toLowerCase().indexOf('content-') === 0) || + (unknownOrigin && header.length === 13 && header.toString().toLowerCase() === 'authorization') || + (unknownOrigin && header.length === 6 && header.toString().toLowerCase() === 'cookie') + ) +} + +// https://tools.ietf.org/html/rfc7231#section-6.4 +function cleanRequestHeaders (headers, removeContent, unknownOrigin) { + const ret = [] + if (Array.isArray(headers)) { + for (let i = 0; i < headers.length; i += 2) { + if (!shouldRemoveHeader(headers[i], removeContent, unknownOrigin)) { + ret.push(headers[i], headers[i + 1]) + } + } + } else if (headers && typeof headers === 'object') { + for (const key of Object.keys(headers)) { + if (!shouldRemoveHeader(key, removeContent, unknownOrigin)) { + ret.push(key, headers[key]) + } + } + } else { + assert(headers == null, 'headers must be an object or an array') + } + return ret +} + +module.exports = RedirectHandler diff --git a/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/interceptor/redirectInterceptor.js b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/interceptor/redirectInterceptor.js new file mode 100644 index 000000000..7cc035e09 --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/interceptor/redirectInterceptor.js @@ -0,0 +1,21 @@ +'use strict' + +const RedirectHandler = require('../handler/RedirectHandler') + +function createRedirectInterceptor ({ maxRedirections: defaultMaxRedirections }) { + return (dispatch) => { + return function Intercept (opts, handler) { + const { maxRedirections = defaultMaxRedirections } = opts + + if (!maxRedirections) { + return dispatch(opts, handler) + } + + const redirectHandler = new RedirectHandler(dispatch, maxRedirections, opts, handler) + opts = { ...opts, maxRedirections: 0 } // Stop sub dispatcher from also redirecting. + return dispatch(opts, redirectHandler) + } + } +} + +module.exports = createRedirectInterceptor diff --git a/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/llhttp/constants.d.ts b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/llhttp/constants.d.ts new file mode 100644 index 000000000..b75ab1bc2 --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/llhttp/constants.d.ts @@ -0,0 +1,199 @@ +import { IEnumMap } from './utils'; +export declare type HTTPMode = 'loose' | 'strict'; +export declare enum ERROR { + OK = 0, + INTERNAL = 1, + STRICT = 2, + LF_EXPECTED = 3, + UNEXPECTED_CONTENT_LENGTH = 4, + CLOSED_CONNECTION = 5, + INVALID_METHOD = 6, + INVALID_URL = 7, + INVALID_CONSTANT = 8, + INVALID_VERSION = 9, + INVALID_HEADER_TOKEN = 10, + INVALID_CONTENT_LENGTH = 11, + INVALID_CHUNK_SIZE = 12, + INVALID_STATUS = 13, + INVALID_EOF_STATE = 14, + INVALID_TRANSFER_ENCODING = 15, + CB_MESSAGE_BEGIN = 16, + CB_HEADERS_COMPLETE = 17, + CB_MESSAGE_COMPLETE = 18, + CB_CHUNK_HEADER = 19, + CB_CHUNK_COMPLETE = 20, + PAUSED = 21, + PAUSED_UPGRADE = 22, + PAUSED_H2_UPGRADE = 23, + USER = 24 +} +export declare enum TYPE { + BOTH = 0, + REQUEST = 1, + RESPONSE = 2 +} +export declare enum FLAGS { + CONNECTION_KEEP_ALIVE = 1, + CONNECTION_CLOSE = 2, + CONNECTION_UPGRADE = 4, + CHUNKED = 8, + UPGRADE = 16, + CONTENT_LENGTH = 32, + SKIPBODY = 64, + TRAILING = 128, + TRANSFER_ENCODING = 512 +} +export declare enum LENIENT_FLAGS { + HEADERS = 1, + CHUNKED_LENGTH = 2, + KEEP_ALIVE = 4 +} +export declare enum METHODS { + DELETE = 0, + GET = 1, + HEAD = 2, + POST = 3, + PUT = 4, + CONNECT = 5, + OPTIONS = 6, + TRACE = 7, + COPY = 8, + LOCK = 9, + MKCOL = 10, + MOVE = 11, + PROPFIND = 12, + PROPPATCH = 13, + SEARCH = 14, + UNLOCK = 15, + BIND = 16, + REBIND = 17, + UNBIND = 18, + ACL = 19, + REPORT = 20, + MKACTIVITY = 21, + CHECKOUT = 22, + MERGE = 23, + 'M-SEARCH' = 24, + NOTIFY = 25, + SUBSCRIBE = 26, + UNSUBSCRIBE = 27, + PATCH = 28, + PURGE = 29, + MKCALENDAR = 30, + LINK = 31, + UNLINK = 32, + SOURCE = 33, + PRI = 34, + DESCRIBE = 35, + ANNOUNCE = 36, + SETUP = 37, + PLAY = 38, + PAUSE = 39, + TEARDOWN = 40, + GET_PARAMETER = 41, + SET_PARAMETER = 42, + REDIRECT = 43, + RECORD = 44, + FLUSH = 45 +} +export declare const METHODS_HTTP: METHODS[]; +export declare const METHODS_ICE: METHODS[]; +export declare const METHODS_RTSP: METHODS[]; +export declare const METHOD_MAP: IEnumMap; +export declare const H_METHOD_MAP: IEnumMap; +export declare enum FINISH { + SAFE = 0, + SAFE_WITH_CB = 1, + UNSAFE = 2 +} +export declare type CharList = Array; +export declare const ALPHA: CharList; +export declare const NUM_MAP: { + 0: number; + 1: number; + 2: number; + 3: number; + 4: number; + 5: number; + 6: number; + 7: number; + 8: number; + 9: number; +}; +export declare const HEX_MAP: { + 0: number; + 1: number; + 2: number; + 3: number; + 4: number; + 5: number; + 6: number; + 7: number; + 8: number; + 9: number; + A: number; + B: number; + C: number; + D: number; + E: number; + F: number; + a: number; + b: number; + c: number; + d: number; + e: number; + f: number; +}; +export declare const NUM: CharList; +export declare const ALPHANUM: CharList; +export declare const MARK: CharList; +export declare const USERINFO_CHARS: CharList; +export declare const STRICT_URL_CHAR: CharList; +export declare const URL_CHAR: CharList; +export declare const HEX: CharList; +export declare const STRICT_TOKEN: CharList; +export declare const TOKEN: CharList; +export declare const HEADER_CHARS: CharList; +export declare const CONNECTION_TOKEN_CHARS: CharList; +export declare const MAJOR: { + 0: number; + 1: number; + 2: number; + 3: number; + 4: number; + 5: number; + 6: number; + 7: number; + 8: number; + 9: number; +}; +export declare const MINOR: { + 0: number; + 1: number; + 2: number; + 3: number; + 4: number; + 5: number; + 6: number; + 7: number; + 8: number; + 9: number; +}; +export declare enum HEADER_STATE { + GENERAL = 0, + CONNECTION = 1, + CONTENT_LENGTH = 2, + TRANSFER_ENCODING = 3, + UPGRADE = 4, + CONNECTION_KEEP_ALIVE = 5, + CONNECTION_CLOSE = 6, + CONNECTION_UPGRADE = 7, + TRANSFER_ENCODING_CHUNKED = 8 +} +export declare const SPECIAL_HEADERS: { + connection: HEADER_STATE; + 'content-length': HEADER_STATE; + 'proxy-connection': HEADER_STATE; + 'transfer-encoding': HEADER_STATE; + upgrade: HEADER_STATE; +}; diff --git a/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/llhttp/constants.js b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/llhttp/constants.js new file mode 100644 index 000000000..fb0b5a24c --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/llhttp/constants.js @@ -0,0 +1,278 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.SPECIAL_HEADERS = exports.HEADER_STATE = exports.MINOR = exports.MAJOR = exports.CONNECTION_TOKEN_CHARS = exports.HEADER_CHARS = exports.TOKEN = exports.STRICT_TOKEN = exports.HEX = exports.URL_CHAR = exports.STRICT_URL_CHAR = exports.USERINFO_CHARS = exports.MARK = exports.ALPHANUM = exports.NUM = exports.HEX_MAP = exports.NUM_MAP = exports.ALPHA = exports.FINISH = exports.H_METHOD_MAP = exports.METHOD_MAP = exports.METHODS_RTSP = exports.METHODS_ICE = exports.METHODS_HTTP = exports.METHODS = exports.LENIENT_FLAGS = exports.FLAGS = exports.TYPE = exports.ERROR = void 0; +const utils_1 = require("./utils"); +// C headers +var ERROR; +(function (ERROR) { + ERROR[ERROR["OK"] = 0] = "OK"; + ERROR[ERROR["INTERNAL"] = 1] = "INTERNAL"; + ERROR[ERROR["STRICT"] = 2] = "STRICT"; + ERROR[ERROR["LF_EXPECTED"] = 3] = "LF_EXPECTED"; + ERROR[ERROR["UNEXPECTED_CONTENT_LENGTH"] = 4] = "UNEXPECTED_CONTENT_LENGTH"; + ERROR[ERROR["CLOSED_CONNECTION"] = 5] = "CLOSED_CONNECTION"; + ERROR[ERROR["INVALID_METHOD"] = 6] = "INVALID_METHOD"; + ERROR[ERROR["INVALID_URL"] = 7] = "INVALID_URL"; + ERROR[ERROR["INVALID_CONSTANT"] = 8] = "INVALID_CONSTANT"; + ERROR[ERROR["INVALID_VERSION"] = 9] = "INVALID_VERSION"; + ERROR[ERROR["INVALID_HEADER_TOKEN"] = 10] = "INVALID_HEADER_TOKEN"; + ERROR[ERROR["INVALID_CONTENT_LENGTH"] = 11] = "INVALID_CONTENT_LENGTH"; + ERROR[ERROR["INVALID_CHUNK_SIZE"] = 12] = "INVALID_CHUNK_SIZE"; + ERROR[ERROR["INVALID_STATUS"] = 13] = "INVALID_STATUS"; + ERROR[ERROR["INVALID_EOF_STATE"] = 14] = "INVALID_EOF_STATE"; + ERROR[ERROR["INVALID_TRANSFER_ENCODING"] = 15] = "INVALID_TRANSFER_ENCODING"; + ERROR[ERROR["CB_MESSAGE_BEGIN"] = 16] = "CB_MESSAGE_BEGIN"; + ERROR[ERROR["CB_HEADERS_COMPLETE"] = 17] = "CB_HEADERS_COMPLETE"; + ERROR[ERROR["CB_MESSAGE_COMPLETE"] = 18] = "CB_MESSAGE_COMPLETE"; + ERROR[ERROR["CB_CHUNK_HEADER"] = 19] = "CB_CHUNK_HEADER"; + ERROR[ERROR["CB_CHUNK_COMPLETE"] = 20] = "CB_CHUNK_COMPLETE"; + ERROR[ERROR["PAUSED"] = 21] = "PAUSED"; + ERROR[ERROR["PAUSED_UPGRADE"] = 22] = "PAUSED_UPGRADE"; + ERROR[ERROR["PAUSED_H2_UPGRADE"] = 23] = "PAUSED_H2_UPGRADE"; + ERROR[ERROR["USER"] = 24] = "USER"; +})(ERROR = exports.ERROR || (exports.ERROR = {})); +var TYPE; +(function (TYPE) { + TYPE[TYPE["BOTH"] = 0] = "BOTH"; + TYPE[TYPE["REQUEST"] = 1] = "REQUEST"; + TYPE[TYPE["RESPONSE"] = 2] = "RESPONSE"; +})(TYPE = exports.TYPE || (exports.TYPE = {})); +var FLAGS; +(function (FLAGS) { + FLAGS[FLAGS["CONNECTION_KEEP_ALIVE"] = 1] = "CONNECTION_KEEP_ALIVE"; + FLAGS[FLAGS["CONNECTION_CLOSE"] = 2] = "CONNECTION_CLOSE"; + FLAGS[FLAGS["CONNECTION_UPGRADE"] = 4] = "CONNECTION_UPGRADE"; + FLAGS[FLAGS["CHUNKED"] = 8] = "CHUNKED"; + FLAGS[FLAGS["UPGRADE"] = 16] = "UPGRADE"; + FLAGS[FLAGS["CONTENT_LENGTH"] = 32] = "CONTENT_LENGTH"; + FLAGS[FLAGS["SKIPBODY"] = 64] = "SKIPBODY"; + FLAGS[FLAGS["TRAILING"] = 128] = "TRAILING"; + // 1 << 8 is unused + FLAGS[FLAGS["TRANSFER_ENCODING"] = 512] = "TRANSFER_ENCODING"; +})(FLAGS = exports.FLAGS || (exports.FLAGS = {})); +var LENIENT_FLAGS; +(function (LENIENT_FLAGS) { + LENIENT_FLAGS[LENIENT_FLAGS["HEADERS"] = 1] = "HEADERS"; + LENIENT_FLAGS[LENIENT_FLAGS["CHUNKED_LENGTH"] = 2] = "CHUNKED_LENGTH"; + LENIENT_FLAGS[LENIENT_FLAGS["KEEP_ALIVE"] = 4] = "KEEP_ALIVE"; +})(LENIENT_FLAGS = exports.LENIENT_FLAGS || (exports.LENIENT_FLAGS = {})); +var METHODS; +(function (METHODS) { + METHODS[METHODS["DELETE"] = 0] = "DELETE"; + METHODS[METHODS["GET"] = 1] = "GET"; + METHODS[METHODS["HEAD"] = 2] = "HEAD"; + METHODS[METHODS["POST"] = 3] = "POST"; + METHODS[METHODS["PUT"] = 4] = "PUT"; + /* pathological */ + METHODS[METHODS["CONNECT"] = 5] = "CONNECT"; + METHODS[METHODS["OPTIONS"] = 6] = "OPTIONS"; + METHODS[METHODS["TRACE"] = 7] = "TRACE"; + /* WebDAV */ + METHODS[METHODS["COPY"] = 8] = "COPY"; + METHODS[METHODS["LOCK"] = 9] = "LOCK"; + METHODS[METHODS["MKCOL"] = 10] = "MKCOL"; + METHODS[METHODS["MOVE"] = 11] = "MOVE"; + METHODS[METHODS["PROPFIND"] = 12] = "PROPFIND"; + METHODS[METHODS["PROPPATCH"] = 13] = "PROPPATCH"; + METHODS[METHODS["SEARCH"] = 14] = "SEARCH"; + METHODS[METHODS["UNLOCK"] = 15] = "UNLOCK"; + METHODS[METHODS["BIND"] = 16] = "BIND"; + METHODS[METHODS["REBIND"] = 17] = "REBIND"; + METHODS[METHODS["UNBIND"] = 18] = "UNBIND"; + METHODS[METHODS["ACL"] = 19] = "ACL"; + /* subversion */ + METHODS[METHODS["REPORT"] = 20] = "REPORT"; + METHODS[METHODS["MKACTIVITY"] = 21] = "MKACTIVITY"; + METHODS[METHODS["CHECKOUT"] = 22] = "CHECKOUT"; + METHODS[METHODS["MERGE"] = 23] = "MERGE"; + /* upnp */ + METHODS[METHODS["M-SEARCH"] = 24] = "M-SEARCH"; + METHODS[METHODS["NOTIFY"] = 25] = "NOTIFY"; + METHODS[METHODS["SUBSCRIBE"] = 26] = "SUBSCRIBE"; + METHODS[METHODS["UNSUBSCRIBE"] = 27] = "UNSUBSCRIBE"; + /* RFC-5789 */ + METHODS[METHODS["PATCH"] = 28] = "PATCH"; + METHODS[METHODS["PURGE"] = 29] = "PURGE"; + /* CalDAV */ + METHODS[METHODS["MKCALENDAR"] = 30] = "MKCALENDAR"; + /* RFC-2068, section 19.6.1.2 */ + METHODS[METHODS["LINK"] = 31] = "LINK"; + METHODS[METHODS["UNLINK"] = 32] = "UNLINK"; + /* icecast */ + METHODS[METHODS["SOURCE"] = 33] = "SOURCE"; + /* RFC-7540, section 11.6 */ + METHODS[METHODS["PRI"] = 34] = "PRI"; + /* RFC-2326 RTSP */ + METHODS[METHODS["DESCRIBE"] = 35] = "DESCRIBE"; + METHODS[METHODS["ANNOUNCE"] = 36] = "ANNOUNCE"; + METHODS[METHODS["SETUP"] = 37] = "SETUP"; + METHODS[METHODS["PLAY"] = 38] = "PLAY"; + METHODS[METHODS["PAUSE"] = 39] = "PAUSE"; + METHODS[METHODS["TEARDOWN"] = 40] = "TEARDOWN"; + METHODS[METHODS["GET_PARAMETER"] = 41] = "GET_PARAMETER"; + METHODS[METHODS["SET_PARAMETER"] = 42] = "SET_PARAMETER"; + METHODS[METHODS["REDIRECT"] = 43] = "REDIRECT"; + METHODS[METHODS["RECORD"] = 44] = "RECORD"; + /* RAOP */ + METHODS[METHODS["FLUSH"] = 45] = "FLUSH"; +})(METHODS = exports.METHODS || (exports.METHODS = {})); +exports.METHODS_HTTP = [ + METHODS.DELETE, + METHODS.GET, + METHODS.HEAD, + METHODS.POST, + METHODS.PUT, + METHODS.CONNECT, + METHODS.OPTIONS, + METHODS.TRACE, + METHODS.COPY, + METHODS.LOCK, + METHODS.MKCOL, + METHODS.MOVE, + METHODS.PROPFIND, + METHODS.PROPPATCH, + METHODS.SEARCH, + METHODS.UNLOCK, + METHODS.BIND, + METHODS.REBIND, + METHODS.UNBIND, + METHODS.ACL, + METHODS.REPORT, + METHODS.MKACTIVITY, + METHODS.CHECKOUT, + METHODS.MERGE, + METHODS['M-SEARCH'], + METHODS.NOTIFY, + METHODS.SUBSCRIBE, + METHODS.UNSUBSCRIBE, + METHODS.PATCH, + METHODS.PURGE, + METHODS.MKCALENDAR, + METHODS.LINK, + METHODS.UNLINK, + METHODS.PRI, + // TODO(indutny): should we allow it with HTTP? + METHODS.SOURCE, +]; +exports.METHODS_ICE = [ + METHODS.SOURCE, +]; +exports.METHODS_RTSP = [ + METHODS.OPTIONS, + METHODS.DESCRIBE, + METHODS.ANNOUNCE, + METHODS.SETUP, + METHODS.PLAY, + METHODS.PAUSE, + METHODS.TEARDOWN, + METHODS.GET_PARAMETER, + METHODS.SET_PARAMETER, + METHODS.REDIRECT, + METHODS.RECORD, + METHODS.FLUSH, + // For AirPlay + METHODS.GET, + METHODS.POST, +]; +exports.METHOD_MAP = utils_1.enumToMap(METHODS); +exports.H_METHOD_MAP = {}; +Object.keys(exports.METHOD_MAP).forEach((key) => { + if (/^H/.test(key)) { + exports.H_METHOD_MAP[key] = exports.METHOD_MAP[key]; + } +}); +var FINISH; +(function (FINISH) { + FINISH[FINISH["SAFE"] = 0] = "SAFE"; + FINISH[FINISH["SAFE_WITH_CB"] = 1] = "SAFE_WITH_CB"; + FINISH[FINISH["UNSAFE"] = 2] = "UNSAFE"; +})(FINISH = exports.FINISH || (exports.FINISH = {})); +exports.ALPHA = []; +for (let i = 'A'.charCodeAt(0); i <= 'Z'.charCodeAt(0); i++) { + // Upper case + exports.ALPHA.push(String.fromCharCode(i)); + // Lower case + exports.ALPHA.push(String.fromCharCode(i + 0x20)); +} +exports.NUM_MAP = { + 0: 0, 1: 1, 2: 2, 3: 3, 4: 4, + 5: 5, 6: 6, 7: 7, 8: 8, 9: 9, +}; +exports.HEX_MAP = { + 0: 0, 1: 1, 2: 2, 3: 3, 4: 4, + 5: 5, 6: 6, 7: 7, 8: 8, 9: 9, + A: 0XA, B: 0XB, C: 0XC, D: 0XD, E: 0XE, F: 0XF, + a: 0xa, b: 0xb, c: 0xc, d: 0xd, e: 0xe, f: 0xf, +}; +exports.NUM = [ + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', +]; +exports.ALPHANUM = exports.ALPHA.concat(exports.NUM); +exports.MARK = ['-', '_', '.', '!', '~', '*', '\'', '(', ')']; +exports.USERINFO_CHARS = exports.ALPHANUM + .concat(exports.MARK) + .concat(['%', ';', ':', '&', '=', '+', '$', ',']); +// TODO(indutny): use RFC +exports.STRICT_URL_CHAR = [ + '!', '"', '$', '%', '&', '\'', + '(', ')', '*', '+', ',', '-', '.', '/', + ':', ';', '<', '=', '>', + '@', '[', '\\', ']', '^', '_', + '`', + '{', '|', '}', '~', +].concat(exports.ALPHANUM); +exports.URL_CHAR = exports.STRICT_URL_CHAR + .concat(['\t', '\f']); +// All characters with 0x80 bit set to 1 +for (let i = 0x80; i <= 0xff; i++) { + exports.URL_CHAR.push(i); +} +exports.HEX = exports.NUM.concat(['a', 'b', 'c', 'd', 'e', 'f', 'A', 'B', 'C', 'D', 'E', 'F']); +/* Tokens as defined by rfc 2616. Also lowercases them. + * token = 1* + * separators = "(" | ")" | "<" | ">" | "@" + * | "," | ";" | ":" | "\" | <"> + * | "/" | "[" | "]" | "?" | "=" + * | "{" | "}" | SP | HT + */ +exports.STRICT_TOKEN = [ + '!', '#', '$', '%', '&', '\'', + '*', '+', '-', '.', + '^', '_', '`', + '|', '~', +].concat(exports.ALPHANUM); +exports.TOKEN = exports.STRICT_TOKEN.concat([' ']); +/* + * Verify that a char is a valid visible (printable) US-ASCII + * character or %x80-FF + */ +exports.HEADER_CHARS = ['\t']; +for (let i = 32; i <= 255; i++) { + if (i !== 127) { + exports.HEADER_CHARS.push(i); + } +} +// ',' = \x44 +exports.CONNECTION_TOKEN_CHARS = exports.HEADER_CHARS.filter((c) => c !== 44); +exports.MAJOR = exports.NUM_MAP; +exports.MINOR = exports.MAJOR; +var HEADER_STATE; +(function (HEADER_STATE) { + HEADER_STATE[HEADER_STATE["GENERAL"] = 0] = "GENERAL"; + HEADER_STATE[HEADER_STATE["CONNECTION"] = 1] = "CONNECTION"; + HEADER_STATE[HEADER_STATE["CONTENT_LENGTH"] = 2] = "CONTENT_LENGTH"; + HEADER_STATE[HEADER_STATE["TRANSFER_ENCODING"] = 3] = "TRANSFER_ENCODING"; + HEADER_STATE[HEADER_STATE["UPGRADE"] = 4] = "UPGRADE"; + HEADER_STATE[HEADER_STATE["CONNECTION_KEEP_ALIVE"] = 5] = "CONNECTION_KEEP_ALIVE"; + HEADER_STATE[HEADER_STATE["CONNECTION_CLOSE"] = 6] = "CONNECTION_CLOSE"; + HEADER_STATE[HEADER_STATE["CONNECTION_UPGRADE"] = 7] = "CONNECTION_UPGRADE"; + HEADER_STATE[HEADER_STATE["TRANSFER_ENCODING_CHUNKED"] = 8] = "TRANSFER_ENCODING_CHUNKED"; +})(HEADER_STATE = exports.HEADER_STATE || (exports.HEADER_STATE = {})); +exports.SPECIAL_HEADERS = { + 'connection': HEADER_STATE.CONNECTION, + 'content-length': HEADER_STATE.CONTENT_LENGTH, + 'proxy-connection': HEADER_STATE.CONNECTION, + 'transfer-encoding': HEADER_STATE.TRANSFER_ENCODING, + 'upgrade': HEADER_STATE.UPGRADE, +}; +//# sourceMappingURL=constants.js.map \ No newline at end of file diff --git a/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/llhttp/constants.js.map b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/llhttp/constants.js.map new file mode 100644 index 000000000..6ac54bc3a --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/llhttp/constants.js.map @@ -0,0 +1 @@ +{"version":3,"file":"constants.js","sourceRoot":"","sources":["../../src/llhttp/constants.ts"],"names":[],"mappings":";;;AAAA,mCAA8C;AAI9C,YAAY;AAEZ,IAAY,KA6BX;AA7BD,WAAY,KAAK;IACf,6BAAM,CAAA;IACN,yCAAQ,CAAA;IACR,qCAAM,CAAA;IACN,+CAAW,CAAA;IACX,2EAAyB,CAAA;IACzB,2DAAiB,CAAA;IACjB,qDAAc,CAAA;IACd,+CAAW,CAAA;IACX,yDAAgB,CAAA;IAChB,uDAAe,CAAA;IACf,kEAAoB,CAAA;IACpB,sEAAsB,CAAA;IACtB,8DAAkB,CAAA;IAClB,sDAAc,CAAA;IACd,4DAAiB,CAAA;IACjB,4EAAyB,CAAA;IAEzB,0DAAgB,CAAA;IAChB,gEAAmB,CAAA;IACnB,gEAAmB,CAAA;IACnB,wDAAe,CAAA;IACf,4DAAiB,CAAA;IAEjB,sCAAM,CAAA;IACN,sDAAc,CAAA;IACd,4DAAiB,CAAA;IAEjB,kCAAI,CAAA;AACN,CAAC,EA7BW,KAAK,GAAL,aAAK,KAAL,aAAK,QA6BhB;AAED,IAAY,IAIX;AAJD,WAAY,IAAI;IACd,+BAAQ,CAAA;IACR,qCAAO,CAAA;IACP,uCAAQ,CAAA;AACV,CAAC,EAJW,IAAI,GAAJ,YAAI,KAAJ,YAAI,QAIf;AAED,IAAY,KAWX;AAXD,WAAY,KAAK;IACf,mEAA8B,CAAA;IAC9B,yDAAyB,CAAA;IACzB,6DAA2B,CAAA;IAC3B,uCAAgB,CAAA;IAChB,wCAAgB,CAAA;IAChB,sDAAuB,CAAA;IACvB,0CAAiB,CAAA;IACjB,2CAAiB,CAAA;IACjB,mBAAmB;IACnB,6DAA0B,CAAA;AAC5B,CAAC,EAXW,KAAK,GAAL,aAAK,KAAL,aAAK,QAWhB;AAED,IAAY,aAIX;AAJD,WAAY,aAAa;IACvB,uDAAgB,CAAA;IAChB,qEAAuB,CAAA;IACvB,6DAAmB,CAAA;AACrB,CAAC,EAJW,aAAa,GAAb,qBAAa,KAAb,qBAAa,QAIxB;AAED,IAAY,OA0DX;AA1DD,WAAY,OAAO;IACjB,yCAAU,CAAA;IACV,mCAAO,CAAA;IACP,qCAAQ,CAAA;IACR,qCAAQ,CAAA;IACR,mCAAO,CAAA;IACP,kBAAkB;IAClB,2CAAW,CAAA;IACX,2CAAW,CAAA;IACX,uCAAS,CAAA;IACT,YAAY;IACZ,qCAAQ,CAAA;IACR,qCAAQ,CAAA;IACR,wCAAU,CAAA;IACV,sCAAS,CAAA;IACT,8CAAa,CAAA;IACb,gDAAc,CAAA;IACd,0CAAW,CAAA;IACX,0CAAW,CAAA;IACX,sCAAS,CAAA;IACT,0CAAW,CAAA;IACX,0CAAW,CAAA;IACX,oCAAQ,CAAA;IACR,gBAAgB;IAChB,0CAAW,CAAA;IACX,kDAAe,CAAA;IACf,8CAAa,CAAA;IACb,wCAAU,CAAA;IACV,UAAU;IACV,8CAAe,CAAA;IACf,0CAAW,CAAA;IACX,gDAAc,CAAA;IACd,oDAAgB,CAAA;IAChB,cAAc;IACd,wCAAU,CAAA;IACV,wCAAU,CAAA;IACV,YAAY;IACZ,kDAAe,CAAA;IACf,gCAAgC;IAChC,sCAAS,CAAA;IACT,0CAAW,CAAA;IACX,aAAa;IACb,0CAAW,CAAA;IACX,4BAA4B;IAC5B,oCAAQ,CAAA;IACR,mBAAmB;IACnB,8CAAa,CAAA;IACb,8CAAa,CAAA;IACb,wCAAU,CAAA;IACV,sCAAS,CAAA;IACT,wCAAU,CAAA;IACV,8CAAa,CAAA;IACb,wDAAkB,CAAA;IAClB,wDAAkB,CAAA;IAClB,8CAAa,CAAA;IACb,0CAAW,CAAA;IACX,UAAU;IACV,wCAAU,CAAA;AACZ,CAAC,EA1DW,OAAO,GAAP,eAAO,KAAP,eAAO,QA0DlB;AAEY,QAAA,YAAY,GAAG;IAC1B,OAAO,CAAC,MAAM;IACd,OAAO,CAAC,GAAG;IACX,OAAO,CAAC,IAAI;IACZ,OAAO,CAAC,IAAI;IACZ,OAAO,CAAC,GAAG;IACX,OAAO,CAAC,OAAO;IACf,OAAO,CAAC,OAAO;IACf,OAAO,CAAC,KAAK;IACb,OAAO,CAAC,IAAI;IACZ,OAAO,CAAC,IAAI;IACZ,OAAO,CAAC,KAAK;IACb,OAAO,CAAC,IAAI;IACZ,OAAO,CAAC,QAAQ;IAChB,OAAO,CAAC,SAAS;IACjB,OAAO,CAAC,MAAM;IACd,OAAO,CAAC,MAAM;IACd,OAAO,CAAC,IAAI;IACZ,OAAO,CAAC,MAAM;IACd,OAAO,CAAC,MAAM;IACd,OAAO,CAAC,GAAG;IACX,OAAO,CAAC,MAAM;IACd,OAAO,CAAC,UAAU;IAClB,OAAO,CAAC,QAAQ;IAChB,OAAO,CAAC,KAAK;IACb,OAAO,CAAC,UAAU,CAAC;IACnB,OAAO,CAAC,MAAM;IACd,OAAO,CAAC,SAAS;IACjB,OAAO,CAAC,WAAW;IACnB,OAAO,CAAC,KAAK;IACb,OAAO,CAAC,KAAK;IACb,OAAO,CAAC,UAAU;IAClB,OAAO,CAAC,IAAI;IACZ,OAAO,CAAC,MAAM;IACd,OAAO,CAAC,GAAG;IAEX,+CAA+C;IAC/C,OAAO,CAAC,MAAM;CACf,CAAC;AAEW,QAAA,WAAW,GAAG;IACzB,OAAO,CAAC,MAAM;CACf,CAAC;AAEW,QAAA,YAAY,GAAG;IAC1B,OAAO,CAAC,OAAO;IACf,OAAO,CAAC,QAAQ;IAChB,OAAO,CAAC,QAAQ;IAChB,OAAO,CAAC,KAAK;IACb,OAAO,CAAC,IAAI;IACZ,OAAO,CAAC,KAAK;IACb,OAAO,CAAC,QAAQ;IAChB,OAAO,CAAC,aAAa;IACrB,OAAO,CAAC,aAAa;IACrB,OAAO,CAAC,QAAQ;IAChB,OAAO,CAAC,MAAM;IACd,OAAO,CAAC,KAAK;IAEb,cAAc;IACd,OAAO,CAAC,GAAG;IACX,OAAO,CAAC,IAAI;CACb,CAAC;AAEW,QAAA,UAAU,GAAG,iBAAS,CAAC,OAAO,CAAC,CAAC;AAChC,QAAA,YAAY,GAAa,EAAE,CAAC;AAEzC,MAAM,CAAC,IAAI,CAAC,kBAAU,CAAC,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,EAAE;IACtC,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE;QAClB,oBAAY,CAAC,GAAG,CAAC,GAAG,kBAAU,CAAC,GAAG,CAAC,CAAC;KACrC;AACH,CAAC,CAAC,CAAC;AAEH,IAAY,MAIX;AAJD,WAAY,MAAM;IAChB,mCAAQ,CAAA;IACR,mDAAY,CAAA;IACZ,uCAAM,CAAA;AACR,CAAC,EAJW,MAAM,GAAN,cAAM,KAAN,cAAM,QAIjB;AAMY,QAAA,KAAK,GAAa,EAAE,CAAC;AAElC,KAAK,IAAI,CAAC,GAAG,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE;IAC3D,aAAa;IACb,aAAK,CAAC,IAAI,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC;IAEnC,aAAa;IACb,aAAK,CAAC,IAAI,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC;CAC3C;AAEY,QAAA,OAAO,GAAG;IACrB,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC;IAC5B,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC;CAC7B,CAAC;AAEW,QAAA,OAAO,GAAG;IACrB,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC;IAC5B,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC;IAC5B,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG;IAC9C,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG;CAC/C,CAAC;AAEW,QAAA,GAAG,GAAa;IAC3B,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG;CACjD,CAAC;AAEW,QAAA,QAAQ,GAAa,aAAK,CAAC,MAAM,CAAC,WAAG,CAAC,CAAC;AACvC,QAAA,IAAI,GAAa,CAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG,CAAE,CAAC;AAClE,QAAA,cAAc,GAAa,gBAAQ;KAC7C,MAAM,CAAC,YAAI,CAAC;KACZ,MAAM,CAAC,CAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAE,CAAC,CAAC;AAEtD,yBAAyB;AACZ,QAAA,eAAe,GAAc;IACxC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI;IAC7B,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG;IACtC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG;IACvB,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG;IAC7B,GAAG;IACH,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG;CACN,CAAC,MAAM,CAAC,gBAAQ,CAAC,CAAC;AAEnB,QAAA,QAAQ,GAAa,uBAAe;KAC9C,MAAM,CAAE,CAAE,IAAI,EAAE,IAAI,CAAe,CAAC,CAAC;AAExC,wCAAwC;AACxC,KAAK,IAAI,CAAC,GAAG,IAAI,EAAE,CAAC,IAAI,IAAI,EAAE,CAAC,EAAE,EAAE;IACjC,gBAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;CAClB;AAEY,QAAA,GAAG,GAAa,WAAG,CAAC,MAAM,CACrC,CAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAE,CAAC,CAAC;AAElE;;;;;;GAMG;AACU,QAAA,YAAY,GAAc;IACrC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI;IAC7B,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG;IAClB,GAAG,EAAE,GAAG,EAAE,GAAG;IACb,GAAG,EAAE,GAAG;CACI,CAAC,MAAM,CAAC,gBAAQ,CAAC,CAAC;AAEnB,QAAA,KAAK,GAAa,oBAAY,CAAC,MAAM,CAAC,CAAE,GAAG,CAAE,CAAC,CAAC;AAE5D;;;GAGG;AACU,QAAA,YAAY,GAAa,CAAE,IAAI,CAAE,CAAC;AAC/C,KAAK,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC,IAAI,GAAG,EAAE,CAAC,EAAE,EAAE;IAC9B,IAAI,CAAC,KAAK,GAAG,EAAE;QACb,oBAAY,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;KACtB;CACF;AAED,aAAa;AACA,QAAA,sBAAsB,GACjC,oBAAY,CAAC,MAAM,CAAC,CAAC,CAAkB,EAAE,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC;AAE3C,QAAA,KAAK,GAAG,eAAO,CAAC;AAChB,QAAA,KAAK,GAAG,aAAK,CAAC;AAE3B,IAAY,YAWX;AAXD,WAAY,YAAY;IACtB,qDAAW,CAAA;IACX,2DAAU,CAAA;IACV,mEAAc,CAAA;IACd,yEAAiB,CAAA;IACjB,qDAAO,CAAA;IAEP,iFAAqB,CAAA;IACrB,uEAAgB,CAAA;IAChB,2EAAkB,CAAA;IAClB,yFAAyB,CAAA;AAC3B,CAAC,EAXW,YAAY,GAAZ,oBAAY,KAAZ,oBAAY,QAWvB;AAEY,QAAA,eAAe,GAAG;IAC7B,YAAY,EAAE,YAAY,CAAC,UAAU;IACrC,gBAAgB,EAAE,YAAY,CAAC,cAAc;IAC7C,kBAAkB,EAAE,YAAY,CAAC,UAAU;IAC3C,mBAAmB,EAAE,YAAY,CAAC,iBAAiB;IACnD,SAAS,EAAE,YAAY,CAAC,OAAO;CAChC,CAAC"} \ No newline at end of file diff --git a/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/llhttp/llhttp.wasm b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/llhttp/llhttp.wasm new file mode 100755 index 000000000..633d00a08 Binary files /dev/null and b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/llhttp/llhttp.wasm differ diff --git a/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/llhttp/llhttp.wasm.js b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/llhttp/llhttp.wasm.js new file mode 100644 index 000000000..e176ce2cf --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/llhttp/llhttp.wasm.js @@ -0,0 +1 @@ +module.exports = 'AGFzbQEAAAABMAhgAX8Bf2ADf39/AX9gBH9/f38Bf2AAAGADf39/AGABfwBgAn9/AGAGf39/f39/AALLAQgDZW52GHdhc21fb25faGVhZGVyc19jb21wbGV0ZQACA2VudhV3YXNtX29uX21lc3NhZ2VfYmVnaW4AAANlbnYLd2FzbV9vbl91cmwAAQNlbnYOd2FzbV9vbl9zdGF0dXMAAQNlbnYUd2FzbV9vbl9oZWFkZXJfZmllbGQAAQNlbnYUd2FzbV9vbl9oZWFkZXJfdmFsdWUAAQNlbnYMd2FzbV9vbl9ib2R5AAEDZW52GHdhc21fb25fbWVzc2FnZV9jb21wbGV0ZQAAA0ZFAwMEAAAFAAAAAAAABQEFAAUFBQAABgAAAAAGBgYGAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQABAAABAQcAAAUFAAMBBAUBcAESEgUDAQACBggBfwFBgNQECwfRBSIGbWVtb3J5AgALX2luaXRpYWxpemUACRlfX2luZGlyZWN0X2Z1bmN0aW9uX3RhYmxlAQALbGxodHRwX2luaXQAChhsbGh0dHBfc2hvdWxkX2tlZXBfYWxpdmUAQQxsbGh0dHBfYWxsb2MADAZtYWxsb2MARgtsbGh0dHBfZnJlZQANBGZyZWUASA9sbGh0dHBfZ2V0X3R5cGUADhVsbGh0dHBfZ2V0X2h0dHBfbWFqb3IADxVsbGh0dHBfZ2V0X2h0dHBfbWlub3IAEBFsbGh0dHBfZ2V0X21ldGhvZAARFmxsaHR0cF9nZXRfc3RhdHVzX2NvZGUAEhJsbGh0dHBfZ2V0X3VwZ3JhZGUAEwxsbGh0dHBfcmVzZXQAFA5sbGh0dHBfZXhlY3V0ZQAVFGxsaHR0cF9zZXR0aW5nc19pbml0ABYNbGxodHRwX2ZpbmlzaAAXDGxsaHR0cF9wYXVzZQAYDWxsaHR0cF9yZXN1bWUAGRtsbGh0dHBfcmVzdW1lX2FmdGVyX3VwZ3JhZGUAGhBsbGh0dHBfZ2V0X2Vycm5vABsXbGxodHRwX2dldF9lcnJvcl9yZWFzb24AHBdsbGh0dHBfc2V0X2Vycm9yX3JlYXNvbgAdFGxsaHR0cF9nZXRfZXJyb3JfcG9zAB4RbGxodHRwX2Vycm5vX25hbWUAHxJsbGh0dHBfbWV0aG9kX25hbWUAIBJs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EGpCADcDACAAQQhqQgA3AwAgAEHdATYCHEEAC3sBAX8CQCAAKAIMIgMNAAJAIAAoAgRFDQAgACABNgIECwJAIAAgASACEMSAgIAAIgMNACAAKAIMDwsgACADNgIcQQAhAyAAKAIEIgFFDQAgACABIAIgACgCCBGBgICAAAAiAUUNACAAIAI2AhQgACABNgIMIAEhAwsgAwvc9wEDKH8DfgV/I4CAgIAAQRBrIgMkgICAgAAgASEEIAEhBSABIQYgASEHIAEhCCABIQkgASEKIAEhCyABIQwgASENIAEhDiABIQ8gASEQIAEhESABIRIgASETIAEhFCABIRUgASEWIAEhFyABIRggASEZIAEhGiABIRsgASEcIAEhHSABIR4gASEfIAEhICABISEgASEiIAEhIyABISQgASElIAEhJiABIScgASEoIAEhKQJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQCAAKAIcIipBf2oO3QHaAQHZAQIDBAUGBwgJCgsMDQ7YAQ8Q1wEREtYBExQVFhcYGRob4AHfARwdHtUBHyAhIiMkJdQBJicoKSorLNMB0gEtLtEB0AEvMDEyMzQ1Njc4OTo7PD0+P0BBQkNERUbbAUdISUrPAc4B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AQtBwQEhKgsDQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkAgKg7HAQABAgMEBQYHCAkKCwwNDg8QERITFBUWFxgZGhscHh8gISMlKD9AQURFRkdISUpLTE1PUFFSU+MDV1lbXF1gYmVmZ2hpamtsbW9wcXJzdHV2d3h5ent8fX6AAYIBhQGGAYcBiQGLAYwBjQGOAY8BkAGRAZQBlQGWAZcBmAGZAZoBmwGcAZ0BngGfAaABoQGiAaMBpAGlAaYBpwGoAakBqgGrAawBrQGuAa8BsAGxAbIBswG0AbUBtgG3AbgBuQG6AbsBvAG9Ab4BvwHAAcEBwgHDAcQBxQHGAccByAHJAcoBywHMAc0BzgHPAdAB0QHSAdMB1AHVAdYB1wHYAdkB2gHbAdwB3QHeAeAB4QHiAeMB5AHlAeYB5wHoAekB6gHrAewB7QHuAe8B8AHxAfIB8wGZAqQCsgKEA4QDCyABIgQgAkcN8wFB3QEhKgyGBAsgASIqIAJHDd0BQcMBISoMhQQLIAEiASACRw2QAUH3ACEqDIQECyABIgEgAkcNhgFB7wAhKgyDBAsgASIBIAJHDX9B6gAhKgyCBAsgASIBIAJHDXtB6AAhKgyBBAsgASIBIAJHDXhB5gAhKgyABAsgASIBIAJHDRpBGCEqDP8DCyABIgEgAkcNFEESISoM/gMLIAEiASACRw1ZQcUAISoM/QMLIAEiASACRw1KQT8hKgz8AwsgASIBIAJHDUhBPCEqDPsDCyABIgEgAkcNQUExISoM+gMLIAAtAC5BAUYN8gMMhwILIAAgASIBIAIQwICAgABBAUcN5gEgAEIANwMgDOcBCyAAIAEiASACELSAgIAAIioN5wEgASEBDPsCCwJAIAEiASACRw0AQQYhKgz3AwsgACABQQFqIgEgAhC7gICAACIqDegBIAEhAQwxCyAAQgA3AyBBEiEqDNwDCyABIiogAkcNK0EdISoM9AMLAkAgASIBIAJGDQAgAUEBaiEBQRAhKgzbAwtBByEqDPMDCyAAQgAgACkDICIrIAIgASIqa60iLH0iLSAtICtWGzcDICArICxWIi5FDeUBQQghKgzyAwsCQCABIgEgAkYNACAAQYmAgIAANgIIIAAgATYCBCABIQFBFCEqDNkDC0EJISoM8QMLIAEhASAAKQMgUA3kASABIQEM+AILAkAgASIBIAJHDQBBCyEqDPADCyAAIAFBAWoiASACELaAgIAAIioN5QEgASEBDPgCCyAAIAEiASACELiAgIAAIioN5QEgASEBDPgCCyAAIAEiASACELiAgIAAIioN5gEgASEBDA0LIAAgASIBIAIQuoCAgAAiKg3nASABIQEM9gILAkAgASIBIAJHDQBBDyEqDOwDCyABLQAAIipBO0YNCCAqQQ1HDegBIAFBAWohAQz1AgsgACABIgEgAhC6gICAACIqDegBIAEhAQz4AgsDQAJAIAEtAABB8LWAgABqLQAAIipBAUYNACAqQQJHDesBIAAoAgQhKiAAQQA2AgQgACAqIAFBAWoiARC5gICAACIqDeoBIAEhAQz6AgsgAUEBaiIBIAJHDQALQRIhKgzpAwsgACABIgEgAhC6gICAACIqDekBIAEhAQwKCyABIgEgAkcNBkEbISoM5wMLAkAgASIBIAJHDQBBFiEqDOcDCyAAQYqAgIAANgIIIAAgATYCBCAAIAEgAhC4gICAACIqDeoBIAEhAUEgISoMzQMLAkAgASIBIAJGDQADQAJAIAEtAABB8LeAgABqLQAAIipBAkYNAAJAICpBf2oOBOUB7AEA6wHsAQsgAUEBaiEBQQghKgzPAwsgAUEBaiIBIAJHDQALQRUhKgzmAwtBFSEqDOUDCwNAAkAgAS0AAEHwuYCAAGotAAAiKkECRg0AICpBf2oOBN4B7AHgAesB7AELIAFBAWoiASACRw0AC0EYISoM5AMLAkAgASIBIAJGDQAgAEGLgICAADYCCCAAIAE2AgQgASEBQQchKgzLAwtBGSEqDOMDCyABQQFqIQEMAgsCQCABIi4gAkcNAEEaISoM4gMLIC4hAQJAIC4tAABBc2oOFOMC9AL0AvQC9AL0AvQC9AL0AvQC9AL0AvQC9AL0AvQC9AL0AvQCAPQCC0EAISogAEEANgIcIABBr4uAgAA2AhAgAEECNgIMIAAgLkEBajYCFAzhAwsCQCABLQAAIipBO0YNACAqQQ1HDegBIAFBAWohAQzrAgsgAUEBaiEBC0EiISoMxgMLAkAgASIqIAJHDQBBHCEqDN8DC0IAISsgKiEBICotAABBUGoON+cB5gEBAgMEBQYHCAAAAAAAAAAJCgsMDQ4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8QERITFAALQR4hKgzEAwtCAiErDOUBC0IDISsM5AELQgQhKwzjAQtCBSErDOIBC0IGISsM4QELQgchKwzgAQtCCCErDN8BC0IJISsM3gELQgohKwzdAQtCCyErDNwBC0IMISsM2wELQg0hKwzaAQtCDiErDNkBC0IPISsM2AELQgohKwzXAQtCCyErDNYBC0IMISsM1QELQg0hKwzUAQtCDiErDNMBC0IPISsM0gELQgAhKwJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkAgKi0AAEFQag435QHkAQABAgMEBQYH5gHmAeYB5gHmAeYB5gEICQoLDA3mAeYB5gHmAeYB5gHmAeYB5gHmAeYB5gHmAeYB5gHmAeYB5gHmAeYB5gHmAeYB5gHmAeYBDg8QERIT5gELQgIhKwzkAQtCAyErDOMBC0IEISsM4gELQgUhKwzhAQtCBiErDOABC0IHISsM3wELQgghKwzeAQtCCSErDN0BC0IKISsM3AELQgshKwzbAQtCDCErDNoBC0INISsM2QELQg4hKwzYAQtCDyErDNcBC0IKISsM1gELQgshKwzVAQtCDCErDNQBC0INISsM0wELQg4hKwzSAQtCDyErDNEBCyAAQgAgACkDICIrIAIgASIqa60iLH0iLSAtICtWGzcDICArICxWIi5FDdIBQR8hKgzHAwsCQCABIgEgAkYNACAAQYmAgIAANgIIIAAgATYCBCABIQFBJCEqDK4DC0EgISoMxgMLIAAgASIqIAIQvoCAgABBf2oOBbYBAMsCAdEB0gELQREhKgyrAwsgAEEBOgAvICohAQzCAwsgASIBIAJHDdIBQSQhKgzCAwsgASInIAJHDR5BxgAhKgzBAwsgACABIgEgAhCygICAACIqDdQBIAEhAQy1AQsgASIqIAJHDSZB0AAhKgy/AwsCQCABIgEgAkcNAEEoISoMvwMLIABBADYCBCAAQYyAgIAANgIIIAAgASABELGAgIAAIioN0wEgASEBDNgBCwJAIAEiKiACRw0AQSkhKgy+AwsgKi0AACIBQSBGDRQgAUEJRw3TASAqQQFqIQEMFQsCQCABIgEgAkYNACABQQFq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AUEBaiEBIARBAWoiBCACRw0ACyAAICo2AgBBgwEhKgzoAgtBACEqIABBADYCACAuIDJrQQNqIQEMygELAkAgASIqIAJHDQBBhAEhKgznAgsgAiAqayAAKAIAIi5qITIgKiEEIC4hAQJAA0AgBC0AACABQYPPgIAAai0AAEcNzAEgAUEERg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAyNgIAQYQBISoM5wILIABBADYCACAqIC5rQQVqIQFBIyEqDMkBCwJAIAEiBCACRw0AQYUBISoM5gILAkACQCAELQAAQbR/ag4IAMwBzAHMAcwBzAHMAQHMAQsgBEEBaiEBQe8AISoMzQILIARBAWohAUHwACEqDMwCCwJAIAEiBCACRw0AQYYBISoM5QILIAQtAABBxQBHDckBIARBAWohAQyKAgsCQCABIiogAkcNAEGHASEqDOQCCyACICprIAAoAgAiLmohMiAqIQQgLiEBAkADQCAELQAAIAFBiM+AgABqLQAARw3JASABQQNGDQEgAUEBaiEBIARBAWoiBCACRw0ACyAAIDI2AgBBhwEhKgzkAgsgAEEANgIAICogLmtBBGohAUEtISoMxgELAkAgASIqIAJHDQBBiAEhKgzjAgsgAiAqayAAKAIAIi5qITIgKiEEIC4hAQJAA0AgBC0AACABQdDPgIAAai0AAEcNyAEgAUEIRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAyNgIAQYgBISoM4wILIABBADYCACAqIC5rQQlqIQFBKSEqDMUBCwJAIAEiASACRw0AQYkBISoM4gILQQEhKiABLQAAQd8ARw3EASABQQFqIQEMiAILAkAgASIqIAJHDQBBigEhKgzhAgsgAiAqayAAKAIAIi5qITIgKiEEIC4hAQNAIAQtAAAgAUGMz4CAAGotAABHDcUBIAFBAUYNtwIgAUEBaiEBIARBAWoiBCACRw0ACyAAIDI2AgBBigEhKgzgAgsCQCABIiogAkcNAEGLASEqDOACCyACICprIAAoAgAiLmohMiAqIQQgLiEBAkADQCAELQAAIAFBjs+AgABqLQAARw3FASABQQJGDQEgAUEBaiEBIARBAWoiBCACRw0ACyAAIDI2AgBBiwEhKgzgAgsgAEEANgIAICogLmtBA2ohAUECISoMwgELAkAgASIqIAJHDQBB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AABBt39qDgcBrgGuAa4BrgGuAQCuAQsgBEEBaiEEQY4BISoMrwILIARBAWohAUEiISoMqgELAkAgDiACRw0AQaQBISoMxwILIAIgDmsgACgCACIqaiEuIA4hBCAqIQECQANAIAQtAAAgAUHAz4CAAGotAABHDawBIAFBAUYNASABQQFqIQEgBEEBaiIEIAJHDQALIAAgLjYCAEGkASEqDMcCCyAAQQA2AgAgDiAqa0ECaiEBQR0hKgypAQsCQCAEIAJHDQBBpQEhKgzGAgsCQAJAIAQtAABBrn9qDgMArAEBrAELIARBAWohDkGQASEqDK0CCyAEQQFqIQFBBCEqDKgBCwJAIAQgAkcNAEGmASEqDMUCCwJAAkACQAJAAkAgBC0AAEG/f2oOFQCuAa4BrgGuAa4BrgGuAa4BrgGuAQGuAa4BAq4BrgEDrgGuAQSuAQsgBEEBaiEEQYgBISoMrwILIARBAWohCkGJASEqDK4CCyAEQQFqIQtBigEhKgytAgsgBEEBaiEEQY8BISoMrAILIARBAWohBEGRASEqDKsCCwJAIA8gAkcNAEGnASEqDMQCCyACIA9rIAAoAgAiKmohLiAPIQQgKiEBAkADQCAELQAAIAFB7c+AgABqLQAARw2pASABQQJGDQEgAUEBaiEBIARBAWoiBCACRw0ACyAAIC42AgBBpwEhKgzEAgsgAEEANgIAIA8gKmtBA2ohAUERISoMpgELAkAgECACRw0AQagBISoMwwILIAIgEGsgACgCACIqaiEuIBAhBCAqIQECQANAIAQtAAAgAUHCz4CAAGotAABHDagBIAFBAkYNASABQQFqIQEgBEEBaiIEIAJHDQALIAAgLjYCAEGoASEqDMMCCyAAQQA2AgAgECAqa0EDaiEBQSwhKgylAQsCQCARIAJHDQBBqQEhKgzCAgsgAiARayAAKAIAIipqIS4gESEEICohAQJAA0AgBC0AACABQcXPgIAAai0AAEcNpwEgAUEERg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAuNgIAQakBISoMwgILIABBADYCACARICprQQVqIQFBKyEqDKQBCwJAIBIgAkcNAEGqASEqDMECCyACIBJrIAAoAgAiKmohLiASIQQgKiEBAkADQCAELQAAIAFBys+AgABqLQAARw2mASABQQJGDQEgAUEBaiEB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8s+AgABqLQAARw2WASABQQFGDQEgAUEBaiEBIARBAWoiBCACRw0ACyAAIC42AgBBugEhKgyxAgsgAEEANgIAIBsgKmtBAmohAUEPISoMkwELAkAgBCACRw0AQbsBISoMsAILAkACQCAELQAAQbd/ag4HAJYBlgGWAZYBlgEBlgELIARBAWohGkGlASEqDJcCCyAEQQFqIRtBpgEhKgyWAgsCQCAcIAJHDQBBvAEhKgyvAgsgAiAcayAAKAIAIipqIS4gHCEEICohAQJAA0AgBC0AACABQfTPgIAAai0AAEcNlAEgAUEHRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAuNgIAQbwBISoMrwILIABBADYCACAcICprQQhqIQFBGyEqDJEBCwJAIAQgAkcNAEG9ASEqDK4CCwJAAkACQCAELQAAQb5/ag4SAJUBlQGVAZUBlQGVAZUBlQGVAQGVAZUBlQGVAZUBlQEClQELIARBAWohGUGkASEqDJYCCyAEQQFqIQRBpwEhKgyVAgsgBEEBaiEcQagBISoMlAILAkAgBCACRw0AQb4BISoMrQILIAQtAABBzgBHDZEBIARBAWohBAzWAQsCQCAEIAJHDQBBvwEhKgysAgsCQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQCAELQAAQb9/ag4VAAECA6ABBAUGoAGgAaABBwgJCgugAQwNDg+gAQsgBEEBaiEBQegAISoMoQILIARBAWohAUHpACEqDKACCyAEQQFqIQFB7gAhKgyfAgsgBEEBaiEBQfIAISoMngILIARBAWohAUHzACEqDJ0CCyAEQQFqIQFB9gAhKgycAgsgBEEBaiEBQfcAISoMmwILIARBAWohAUH6ACEqDJoCCyAEQQFqIQRBgwEhKgyZAgsgBEEBaiEGQYQBISoMmAILIARBAWohB0GFASEqDJcCCyAEQQFqIQRBkgEhKgyWAgsgBEEBaiEEQZgBISoMlQILIARBAWohBEGgASEqDJQCCyAEQQFqIQRBowEhKgyTAgsgBEEBaiEEQaoBISoMkgILAkAgBCACRg0AIABBkICAgAA2AgggACAENgIEQasBISoMkgILQcABISoMqgILIAAgHSACEKqAgIAAIgENjwEgHSEBDF4LAkAgHiACRg0AIB5BAWohHQyRAQtBwgEhKgyoAgsD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NgIUIABB2o2AgAA2AhAgAEEUNgIMQQAhKgyNAgsgACgCBCEyIABBADYCBCAqICunaiIvIQEgACAyICogLyAuGyIqELWAgIAAIi5FDZMBIABBBzYCHCAAICo2AhQgACAuNgIMQQAhKgyMAgsgACAALwEwQYABcjsBMCABIQELQSohKgzxAQsgKkEVRg3ZASAAQQA2AhwgACABNgIUIABBg4yAgAA2AhAgAEETNgIMQQAhKgyJAgsgKkEVRg3XASAAQQA2AhwgACABNgIUIABBmo+AgAA2AhAgAEEiNgIMQQAhKgyIAgsgACgCBCEqIABBADYCBAJAIAAgKiABELeAgIAAIioNACABQQFqIQEMkwELIABBDDYCHCAAICo2AgwgACABQQFqNgIUQQAhKgyHAgsgKkEVRg3UASAAQQA2AhwgACABNgIUIABBmo+AgAA2AhAgAEEiNgIMQQAhKgyGAgsgACgCBCEqIABBADYCBAJAIAAgKiABELeAgIAAIioNACABQQFqIQEMkgELIABBDTYCHCAAICo2AgwgACABQQFqNgIUQQAhKgyFAgsgKkEVRg3RASAAQQA2AhwgACABNgIUIABBxoyAgAA2AhAgAEEjNgIMQQAhKgyEAgsgACgCBCEqIABBADYCBAJAIAAgKiABELmAgIAAIioNACABQQFqIQEMkQELIABBDjYCHCAAICo2AgwgACABQQFqNgIUQQAhKgyDAgsgAEEANgIcIAAgATYCFCAAQcCVgIAANgIQIABBAjYCDEEAISoMggILICpBFUYNzQEgAEEANgIcIAAgATYCFCAAQcaMgIAANgIQIABBIzYCDEEAISoMgQILIABBEDYCHCAAIAE2AhQgACAqNgIMQQAhKgyAAgsgACgCBCEEIABBADYCBAJAIAAgBCABELmAgIAAIgQNACABQQFqIQEM+AELIABBETYCHCAAIAQ2AgwgACABQQFqNgIUQQAhKgz/AQsgKkEVRg3JASAAQQA2AhwgACABNgIUIABBxoyAgAA2AhAgAEEjNgIMQQAhKgz+AQsgACgCBCEqIABBADYCBAJAIAAgKiABELmAgIAAIioNACABQQFqIQEMjgELIABBEzYCHCAAICo2AgwgACABQQFqNgIUQQAhKgz9AQsgACgCBCEEIABBADYCBAJAIAAgBCABELmAgIAAIgQNACAB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AkAgAC0AKUEjTw0AIAEhAQxcCyAAQQA2AhwgACABNgIUIABB04mAgAA2AhAgAEEINgIMQQAhKgy8AQsgAEEANgIAC0EAISogAEEANgIcIAAgATYCFCAAQZCzgIAANgIQIABBCDYCDAy6AQsgAEEANgIAIDIgL2tBA2ohAQJAIAAtAClBIUcNACABIQEMWQsgAEEANgIcIAAgATYCFCAAQZuKgIAANgIQIABBCDYCDEEAISoMuQELIABBADYCACAyIC9rQQRqIQECQCAALQApIipBXWpBC08NACABIQEMWAsCQCAqQQZLDQBBASAqdEHKAHFFDQAgASEBDFgLQQAhKiAAQQA2AhwgACABNgIUIABB94mAgAA2AhAgAEEINgIMDLgBCyAqQRVGDXUgAEEANgIcIAAgATYCFCAAQbmNgIAANgIQIABBGjYCDEEAISoMtwELIAAoAgQhKiAAQQA2AgQCQCAAICogARCngICAACIqDQAgASEBDFcLIABB5QA2AhwgACABNgIUIAAgKjYCDEEAISoMtgELIAAoAgQhKiAAQQA2AgQCQCAAICogARCngICAACIqDQAgASEBDE8LIABB0gA2AhwgACABNgIUIAAgKjYCDEEAISoMtQELIAAoAgQhKiAAQQA2AgQCQCAAICogARCngICAACIqDQAgASEBDE8LIABB0wA2AhwgACABNgIUIAAgKjYCDEEAISoMtAELIAAoAgQhKiAAQQA2AgQCQCAAICogARCngICAACIqDQAgASEBDFQLIABB5QA2AhwgACABNgIUIAAgKjYCDEEAISoMswELIABBADYCHCAAIAE2AhQgAEHGioCAADYCECAAQQc2AgxBACEqDLIBCyAAKAIEISogAEEANgIEAkAgACAqIAEQp4CAgAAiKg0AIAEhAQxLCyAAQdIANgIcIAAgATYCFCAAICo2AgxBACEqDLEBCyAAKAIEISogAEEANgIEAkAgACAqIAEQp4CAgAAiKg0AIAEhAQxLCyAAQdMANgIcIAAgATYCFCAAICo2AgxBACEqDLABCyAAKAIEISogAEEANgIEAkAgACAqIAEQp4CAgAAiKg0AIAEhAQxQCyAAQeUANgIcIAAgATYCFCAAICo2AgxBACEqDK8BCyAAQQA2AhwgACABNgIUIABB3IiAgAA2AhAgAEEHNgIMQQAhKgyuAQsgKkE/Rw0BIAFBAWohAQtBBSEqDJMBC0EAISogAEEANgIcIAAgATYCFCAAQf2SgIAANgIQIABBBzYCDAyrAQsgACgCBCEqIABBADYCBAJAIAAgKiABEKeAgIAAIioNACABIQEMRAsgAEHSADYCHCAAIAE2AhQgACAqNgIMQQAhKgyqAQsgACgCBCEqIABBADYCBAJAIAAgKiABEKeAgIAAIioNACABIQEMRAsgAEHTADYCHCAAIAE2AhQgACAqNgIMQQAhKgypAQsgACgCBCEqIABBADYCBAJAIAAgKiABEKeAgIAAIioNACABIQEMSQsgAEHlADYCHCAAIAE2AhQgACAqNgIMQQAhKgyoAQsgACgCBCEBIABBADYCBAJAIAAgASAuEKeAgIAAIgENACAuIQEMQQsgAEHSADYCHCAAIC42AhQgACABNgIMQQAhKgynAQsgACgCBCEBIABBADYCBAJAIAAgASAuEKeAgIAAIgENACAuIQEMQQsgAEHTADYCHCAAIC42AhQgACABNgIMQQAhKgymAQsgACgCBCEBIABBADYCBAJAIAAgASAuEKeAgIAAIgENACAuIQEMRgsgAEHlADYCHCAAIC42AhQgACABNgIMQQAhKgylAQsgAEEANgIcIAAgLjYCFCAAQcOPgIAANgIQIABBBzYCDEEAISoMpAELIABBADYCHCAAIAE2AhQgAEHDj4CAADYCECAAQQc2AgxBACEqDKMBC0EAISogAEEANgIcIAAgLjYCFCAAQYycgIAANgIQIABBBzYCDAyiAQsgAEEANgIcIAAgLjYCFCAAQYycgIAANgIQIABBBzYCDEEAISoMoQELIABBADYCHCAAIC42AhQgAEH+kYCAADYCECAAQQc2AgxBACEqDKABCyAAQQA2AhwgACABNgIUIABBjpuAgAA2AhAgAEEGNgIMQQAhKgyfAQsgKkEVRg1bIABBADYCHCAAIAE2AhQgAEHMjoCAADYCECAAQSA2AgxBACEqDJ4BCyAAQQA2AgAgKiAua0EGaiEBQSQhKgsgACAqOgApIAAoAgQhKiAAQQA2AgQgACAqIAEQq4CAgAAiKg1YIAEhAQxBCyAAQQA2AgALQQAhKiAAQQA2AhwgACAENgIUIABB8ZuAgAA2AhAgAEEGNgIMDJoBCyABQRVGDVQgAEEANgIcIAAgHTYC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AgAgKiAua0ECaiEBQfUAISoMEAsgASEBAkAgAC0AKUEFRw0AQeMAISoMEAtB4gAhKgwPC0EAISogAEEANgIcIABB5JGAgAA2AhAgAEEHNgIMIAAgLkEBajYCFAwnCyAAQQA2AgAgMiAva0ECaiEBQcAAISoMDQsgASEBC0E4ISoMCwsCQCABIikgAkYNAANAAkAgKS0AAEGAvoCAAGotAAAiAUEBRg0AIAFBAkcNAyApQQFqIQEMBAsgKUEBaiIpIAJHDQALQT4hKgwkC0E+ISoMIwsgAEEAOgAsICkhAQwBC0ELISoMCAtBOiEqDAcLIAFBAWohAUEtISoMBgtBKCEqDAULIABBADYCACAvIDBrQQRqIQFBBiEqCyAAICo6ACwgASEBQQwhKgwDCyAAQQA2AgAgMiAva0EHaiEBQQohKgwCCyAAQQA2AgALIABBADoALCAnIQFBCSEqDAALC0EAISogAEEANgIcIAAgIzYCFCAAQc2QgIAANgIQIABBCTYCDAwXC0EAISogAEEANgIcIAAgIjYCFCAAQemKgIAANgIQIABBCTYCDAwWC0EAISogAEEANgIcIAAgITYCFCAAQbeQgIAANgIQIABBCTYCDAwVC0EAISogAEEANgIcIAAgIDYCFCAAQZyRgIAANgIQIABBCTYCDAwUC0EAISogAEEANgIcIAAgATYCFCAAQc2QgIAANgIQIABBCTYCDAwTC0EAISogAEEANgIcIAAgATYCFCAAQemKgIAANgIQIABBCTYCDAwSC0EAISogAEEANgIcIAAgATYCFCAAQbeQgIAANgIQIABBCTYCDAwRC0EAISogAEEANgIcIAAgATYCFCAAQZyRgIAANgIQIABBCTYCDAwQC0EAISogAEEANgIcIAAgATYCFCAAQZeVgIAANgIQIABBDzYCDAwPC0EAISogAEEANgIcIAAgATYCFCAAQZeVgIAANgIQIABBDzYCDAwOC0EAISogAEEANgIcIAAgATYCFCAAQcCSgIAANgIQIABBCzYCDAwNC0EAISogAEEANgIcIAAgATYCFCAAQZWJgIAANgIQIABBCzYCDAwMC0EAISogAEEANgIcIAAgATYCFCAAQeGPgIAANgIQIABBCjYCDAwLC0EAISogAEEANgIcIAAgATYCFCAAQfuPgIAANgIQIABBCjYCDAwKC0EA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AAAABQAAAAAAAAAAAAAABgAAAAcAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABJbnZhbGlkIGNoYXIgaW4gdXJsIHF1ZXJ5AFNwYW4gY2FsbGJhY2sgZXJyb3IgaW4gb25fYm9keQBDb250ZW50LUxlbmd0aCBvdmVyZmxvdwBDaHVuayBzaXplIG92ZXJmbG93AFJlc3BvbnNlIG92ZXJmbG93AEludmFsaWQgbWV0aG9kIGZvciBIVFRQL3gueCByZXF1ZXN0AEludmFsaWQgbWV0aG9kIGZvciBSVFNQL3gueCByZXF1ZXN0AEV4cGVjdGVkIFNPVVJDRSBtZXRob2QgZm9yIElDRS94LnggcmVxdWVzdABJbnZhbGlkIGNoYXIgaW4gdXJsIGZyYWdtZW50IHN0YXJ0AEV4cGVjdGVkIGRvdABTcGFuIGNhbGxiYWNrIGVycm9yIGluIG9uX3N0YXR1cwBJbnZhbGlkIHJlc3BvbnNlIHN0YXR1cwBJbnZhbGlkIGNoYXJhY3RlciBpbiBjaHVuayBleHRlbnNpb25zAFVzZXIgY2FsbGJhY2sgZXJyb3IAYG9uX3Jlc2V0YCBjYWxsYmFjayBlcnJvcgBgb25fY2h1bmtfaGVhZGVyYCBjYWxsYmFjayBlcnJvcgBgb25fbWVzc2FnZV9iZWdpbmAgY2FsbGJhY2sgZXJyb3IAYG9uX2NodW5rX2V4dGVuc2lvbl92YWx1ZWAgY2FsbGJhY2sgZXJyb3IAYG9uX3N0YXR1c19jb21wbGV0ZWAgY2FsbGJhY2sgZXJyb3IAYG9uX3ZlcnNpb25fY29tcGxldGVgIGNhbGxiYWNrIGVycm9yAGBvbl91cmxfY29tcGxldGVgIGNhbGxiYWNrIGVycm9yAGBvbl9jaHVua19jb21wbGV0ZWAgY2FsbGJhY2sgZXJyb3IAYG9uX2hlYWRlcl92YWx1ZV9jb21wbGV0ZWAgY2FsbGJhY2sgZXJyb3IAYG9uX21lc3NhZ2VfY29tcGxldGVgIGNhbGxiYWNrIGVycm9yAGBvbl9tZXRob2RfY29tcGxldGVgIGNhbGxiYWNrIGVycm9yAGBvbl9oZWFkZXJfZmllbGRfY29tcGxldGVgIGNhbGxiYWNrIGVycm9yAGBvbl9jaHVua19leHRlbnNpb25fbmFtZWAgY2FsbGJhY2sgZXJyb3IAVW5leHBlY3RlZCBjaGFyIGluIHVybCBzZXJ2ZXIASW52YWxpZCBoZWFkZXIgdmFsdWUgY2hhcgBJbnZhbGlkIGhlYWRlciBmaWVsZCBjaGFyAFNwYW4gY2FsbGJhY2sgZXJyb3IgaW4gb25fdmVyc2lvbgBJbnZhbGlkIG1pbm9yIHZlcnNpb24ASW52YWxpZCBtYWpvciB2ZXJzaW9uAEV4cGVjdGVkIHNwYWNlIGFmdGVyIHZlcnNpb24ARXhwZWN0ZWQgQ1JMRiBhZnRlciB2ZXJzaW9uAEludmFsaWQgSFRUUCB2ZXJzaW9uAEludmFsaWQgaGVhZGVyIHRva2VuAFNwYW4gY2FsbGJhY2sgZXJyb3IgaW4gb25fdXJsAEludmFsaWQgY2hhcmFjdGVycyBpbiB1cmwAVW5leHBlY3RlZCBzdGFydCBjaGFyIGluIHVybABEb3VibGUgQCBpbiB1cmwARW1wdHkgQ29udGVudC1MZW5ndGgASW52YWxpZCBjaGFyYWN0ZXIgaW4gQ29udGVudC1MZW5ndGgARHVwbGljYXRlIENvbnRlbnQtTGVuZ3RoAEludmFsaWQgY2hhciBpbiB1cmwgcGF0aABDb250ZW50LUxlbmd0aCBjYW4ndCBiZSBwcmVzZW50IHdpdGggVHJhbnNmZXItRW5jb2RpbmcASW52YWxpZCBjaGFyYWN0ZXIgaW4gY2h1bmsgc2l6ZQBTcGFuIGNhbGxiYWNrIGVycm9yIGluIG9uX2hlYWRlcl92YWx1ZQBTcGFuIGNhbGxiYWNrIGVycm9yIGluIG9uX2NodW5rX2V4dGVuc2lvbl92YWx1ZQBJbnZhbGlkIGNoYXJhY3RlciBpbiBjaHVuayBleHRlbnNpb25zIHZhbHVlAE1pc3NpbmcgZXhwZWN0ZWQgTEYgYWZ0ZXIgaGVhZGVyIHZhbHVlAEludmFsaWQgYFRyYW5zZmVyLUVuY29kaW5nYCBoZWFkZXIgdmFsdWUASW52YWxpZCBjaGFyYWN0ZXIgaW4gY2h1bmsgZXh0ZW5zaW9ucyBxdW90ZSB2YWx1ZQBJbnZhbGlkIGNoYXJhY3RlciBpbiBjaHVuayBleHRlbnNpb25zIHF1b3RlZCB2YWx1ZQBQYXVzZWQgYnkgb25faGVhZGVyc19jb21wbGV0ZQBJbnZhbGlkIEVPRiBzdGF0ZQBvbl9yZXNldCBwYXVzZQBvbl9jaHVua19oZWFkZXIgcGF1c2UAb25fbWVzc2FnZV9iZWdpbiBwYXVzZQBvbl9jaHVua19leHRlbnNpb25fdmFsdWUgcGF1c2UAb25fc3RhdHVzX2NvbXBsZXRlIHBhdXNlAG9uX3ZlcnNpb25fY29tcGxldGUgcGF1c2UAb25fdXJsX2NvbXBsZXRlIHBhdXNlAG9uX2NodW5rX2NvbXBsZXRlIHBhdXNlAG9uX2hlYWRlcl92YWx1ZV9jb21wbGV0ZSBwYXVzZQBvbl9tZXNzYWdlX2NvbXBsZXRlIHBhdXNlAG9uX21ldGhvZF9jb21wbGV0ZSBwYXVzZQBvbl9oZWFkZXJfZmllbGRfY29tcGxldGUgcGF1c2UAb25fY2h1bmtfZXh0ZW5zaW9uX25hbWUgcGF1c2UAVW5leHBlY3RlZCBzcGFjZSBhZnRlciBzdGFydCBsaW5lAFNwYW4gY2FsbGJhY2sgZXJyb3IgaW4gb25fY2h1bmtfZXh0ZW5zaW9uX25hbWUASW52YWxpZCBjaGFyYWN0ZXIgaW4gY2h1bmsgZXh0ZW5zaW9ucyBuYW1lAFBhdXNlIG9uIENPTk5FQ1QvVXBncmFkZQBQYXVzZSBvbiBQUkkvVXBncmFkZQBFeHBlY3RlZCBIVFRQLzIgQ29ubmVjdGlvbiBQcmVmYWNlAFNwYW4gY2FsbGJhY2sgZXJyb3IgaW4gb25fbWV0aG9kAEV4cGVjdGVkIHNwYWNlIGFmdGVyIG1ldGhvZABTcGFuIGNhbGxiYWNrIGVycm9yIGluIG9uX2hlYWRlcl9maWVsZABQYXVzZWQASW52YWxpZCB3b3JkIGVuY291bnRlcmVkAEludmFsaWQgbWV0aG9kIGVuY291bnRlcmVkAFVuZXhwZWN0ZWQgY2hhciBpbiB1cmwgc2NoZW1hAFJlcXVlc3QgaGFzIGludmFsaWQgYFRyYW5zZmVyLUVuY29kaW5nYABTV0lUQ0hfUFJPWFkAVVNFX1BST1hZAE1LQUNUSVZJVFkAVU5QUk9DRVNTQUJMRV9FTlRJVFkAQ09QWQBNT1ZFRF9QRVJNQU5FTlRMWQBUT09fRUFSTFkATk9USUZZAEZBSUxFRF9ERVBFTkRFTkNZAEJBRF9HQVRFV0FZAFBMQVkAUFVUAENIRUNLT1VUAEdBVEVXQVlfVElNRU9VVABSRVFVRVNUX1RJTUVPVVQATkVUV09SS19DT05ORUNUX1RJTUVPVVQAQ09OTkVDVElPTl9USU1FT1VUAExPR0lOX1RJTUVPVVQATkVUV09SS19SRUFEX1RJTUVPVVQAUE9TVABNSVNESVJFQ1RFRF9SRVFVRVNUAENMSUVOVF9DTE9TRURfUkVRVUVTVABDTElFTlRfQ0xPU0VEX0xPQURfQkFMQU5DRURfUkVRVUVTVABCQURfUkVRVUVTVABIVFRQX1JFUVVFU1RfU0VOVF9UT19IVFRQU19QT1JUAFJFUE9SVABJTV9BX1RFQVBPVABSRVNFVF9DT05URU5UAE5PX0NPTlRFTlQAUEFSVElBTF9DT05URU5UAEhQRV9JTlZBTElEX0NPTlNUQU5UAEhQRV9DQl9SRVNFVABHRVQASFBFX1NUUklDVABDT05GTElDVABURU1QT1JBUllfUkVESVJFQ1QAUEVSTUFORU5UX1JFRElSRUNUAENPTk5FQ1QATVVMVElfU1RBVFVTAEhQRV9JTlZBTElEX1NUQVRVUwBUT09fTUFOWV9SRVFVRVNUUwBFQVJMWV9ISU5UUwBVTkFWQUlMQUJMRV9GT1JfTEVHQUxfUkVBU09OUwBPUFRJT05TAFNXSVRDSElOR19QUk9UT0NPTFMAVkFSSUFOVF9BTFNPX05FR09USUFURVMATVVMVElQTEVfQ0hPSUNFUwBJTlRFUk5BTF9TRVJWRVJfRVJST1IAV0VCX1NFUlZFUl9VTktOT1dOX0VSUk9SAFJBSUxHVU5fRVJST1IASURFTlRJVFlfUFJPVklERVJfQVVUSEVOVElDQVRJT05fRVJST1IAU1NMX0NFUlRJRklDQVRFX0VSUk9SAElOVkFMSURfWF9GT1JXQVJERURfRk9SAFNFVF9QQVJBTUVURVIAR0VUX1BBUkFNRVRFUgBIUEVfVVNFUgBTRUVfT1RIRVIASFBFX0NCX0NIVU5LX0hFQURFUgBNS0NBTEVOREFSAFNFVFVQAFdFQl9TRVJWRVJfSVNfRE9XTgBURUFSRE9XTgBIUEVfQ0xPU0VEX0NPTk5FQ1RJT04ASEVVUklTVElDX0VYUElSQVRJT04ARElTQ09OTkVDVEVEX09QRVJBVElPTgBOT05fQVVUSE9SSVRBVElWRV9JTkZPUk1BVElPTgBIUEVfSU5WQUxJRF9WRVJTSU9OAEhQRV9DQl9NRVNTQUdFX0JFR0lOAFNJVEVfSVNfRlJPWkVOAEhQRV9JTlZBTElEX0hFQURFUl9UT0tFTgBJTlZBTElEX1RPS0VOAEZPUkJJRERFTgBFTkhBTkNFX1lPVVJfQ0FMTQBIUEVfSU5WQUxJRF9VUkwAQkxPQ0tFRF9CWV9QQVJFTlRBTF9DT05UUk9MAE1LQ09MAEFDTABIUEVfSU5URVJOQUwAUkVRVUVTVF9IRUFERVJfRklFTERTX1RPT19MQVJHRV9VTk9GRklDSUFMAEhQRV9PSwBVTkxJTksAVU5MT0NLAFBSSQBSRVRSWV9XSVRIAEhQRV9JTlZBTElEX0NPTlRFTlRfTEVOR1RIAEhQRV9VTkVYUEVDVEVEX0NPTlRFTlRfTEVOR1RIAEZMVVNIAFBST1BQQVRDSABNLVNFQVJDSABVUklfVE9PX0xPTkcAUFJPQ0VTU0lORwBNSVNDRUxMQU5FT1VTX1BFUlNJU1RFTlRfV0FSTklORwBNSVNDRUxMQU5FT1VTX1dBUk5JTkcASFBFX0lOVkFMSURfVFJBTlNGRVJfRU5DT0RJTkcARXhwZWN0ZWQgQ1JMRgBIUEVfSU5WQUxJRF9DSFVOS19TSVpFAE1PVkUAQ09OVElOVUUASFBFX0NCX1NUQVRVU19DT01QTEVURQBIUEVfQ0JfSEVBREVSU19DT01QTEVURQBIUEVfQ0JfVkVSU0lPTl9DT01QTEVURQBIUEVfQ0JfVVJMX0NPTVBMRVRFAEhQRV9DQl9DSFVOS19DT01QTEVURQBIUEVfQ0JfSEVBREVSX1ZBTFVFX0NPTVBMRVRFAEhQRV9DQl9DSFVOS19FWFRFTlNJT05fVkFMVUVfQ09NUExFVEUASFBFX0NCX0NIVU5LX0VYVEVOU0lPTl9OQU1FX0NPTVBMRVRFAEhQRV9DQl9NRVNTQUdFX0NPTVBMRVRFAEhQRV9DQl9NRVRIT0RfQ09NUExFVEUASFBFX0NCX0hFQURFUl9GSUVMRF9DT01QTEVURQBERUxFVEUASFBFX0lOVkFMSURfRU9GX1NUQVRFAElOVkFMSURfU1NMX0NFUlRJRklDQVRFAFBBVVNFAE5PX1JFU1BPTlNFAFVOU1VQUE9SVEVEX01FRElBX1RZUEUAR09ORQBOT1RfQUNDRVBUQUJMRQBTRVJWSUNFX1VOQVZBSUxBQkxFAFJBTkdFX05PVF9TQVRJU0ZJQUJMRQBPUklHSU5fSVNfVU5SRUFDSEFCTEUAUkVTUE9OU0VfSVNfU1RBTEUAUFVSR0UATUVSR0UAUkVRVUVTVF9IRUFERVJfRklFTERTX1RPT19MQVJHRQBSRVFVRVNUX0hFQURFUl9UT09fTEFSR0UAUEFZTE9BRF9UT09fTEFSR0UASU5TVUZGSUNJRU5UX1NUT1JBR0UASFBFX1BBVVNFRF9VUEdSQURFAEhQRV9QQVVTRURfSDJfVVBHUkFERQBTT1VSQ0UAQU5OT1VOQ0UAVFJBQ0UASFBFX1VORVhQRUNURURfU1BBQ0UAREVTQ1JJQkUAVU5TVUJTQ1JJQkUAUkVDT1JEAEhQRV9JTlZBTElEX01FVEhPRABOT1RfRk9VTkQAUFJPUEZJTkQAVU5CSU5EAFJFQklORABVTkFVVEhPUklaRUQATUVUSE9EX05PVF9BTExPV0VEAEhUVFBfVkVSU0lPTl9OT1RfU1VQUE9SVEVEAEFMUkVBRFlfUkVQT1JURUQAQUNDRVBURUQATk9UX0lNUExFTUVOVEVEAExPT1BfREVURUNURUQASFBFX0NSX0VYUEVDVEVEAEhQRV9MRl9FWFBFQ1RFRABDUkVBVEVEAElNX1VTRUQASFBFX1BBVVNFRABUSU1FT1VUX09DQ1VSRUQAUEFZTUVOVF9SRVFVSVJFRABQUkVDT05ESVRJT05fUkVRVUlSRUQAUFJPWFlfQVVUSEVOVElDQVRJT05fUkVRVUlSRUQATkVUV09SS19BVVRIRU5USUNBVElPTl9SRVFVSVJFRABMRU5HVEhfUkVRVUlSRUQAU1NMX0NFUlRJRklDQVRFX1JFUVVJUkVEAFVQR1JBREVfUkVRVUlSRUQAUEFHRV9FWFBJUkVEAFBSRUNPTkRJVElPTl9GQUlMRUQARVhQRUNUQVRJT05fRkFJTEVEAFJFVkFMSURBVElPTl9GQUlMRUQAU1NMX0hBTkRTSEFLRV9GQUlMRUQATE9DS0VEAFRSQU5TRk9STUFUSU9OX0FQUExJRUQATk9UX01PRElGSUVEAE5PVF9FWFRFTkRFRABCQU5EV0lEVEhfTElNSVRfRVhDRUVERUQAU0lURV9JU19PVkVSTE9BREVEAEhFQUQARXhwZWN0ZWQgSFRUUC8AAF4TAAAmEwAAMBAAAPAXAACdEwAAFRIAADkXAADwEgAAChAAAHUSAACtEgAAghMAAE8UAAB/EAAAoBUAACMUAACJEgAAixQAAE0VAADUEQAAzxQAABAYAADJFgAA3BYAAMERAADgFwAAuxQAAHQUAAB8FQAA5RQAAAgXAAAfEAAAZRUAAKMUAAAoFQAAAhUAAJkVAAAsEAAAixkAAE8PAADUDgAAahAAAM4QAAACFwAAiQ4AAG4TAAAcEwAAZhQAAFYXAADBEwAAzRMAAGwTAABoFwAAZhcAAF8XAAAiEwAAzg8AAGkOAADYDgAAYxYAAMsTAACqDgAAKBcAACYXAADFEwAAXRYAAOgRAABnEwAAZRMAAPIWAABzEwAAHRcAAPkWAADzEQAAzw4AAM4VAAAMEgAAsxEAAKURAABhEAAAMhcAALsTAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQECAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAACAwICAgICAAACAgACAgACAgICAgICAgICAAQAAAAAAAICAgICAgICAgICAgICAgICAgICAgICAgICAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAAgACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgACAgICAgAAAgIAAgIAAgICAgICAgICAgADAAQAAAACAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAICAgICAgICAgICAgICAgICAgICAgICAgICAgICAAIAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGxvc2VlZXAtYWxpdmUAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQECAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAWNodW5rZWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAAEBAQEBAAABAQABAQABAQEBAQEBAQEBAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAQABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAZWN0aW9uZW50LWxlbmd0aG9ucm94eS1jb25uZWN0aW9uAAAAAAAAAAAAAAAAAAAAcmFuc2Zlci1lbmNvZGluZ3BncmFkZQ0KDQoNClNNDQoNClRUUC9DRS9UU1AvAAAAAAAAAAAAAAAAAQIAAQMAAAAAAAAAAAAAAAAAAAAAAAAEAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAECAAEDAAAAAAAAAAAAAAAAAAAAAAAABAEBBQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAABAAABAAAAAAAAAAAAAAAAAAAAAAAAAAABAQABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAEAAAIAAAAAAAAAAAAAAAAAAAAAAAADBAAABAQEBAQEBAQEBAQFBAQEBAQEBAQEBAQEAAQABgcEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQABAAEAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAABAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMAAAAAAAADAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAQAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAACAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAAAAAAAAAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAATk9VTkNFRUNLT1VUTkVDVEVURUNSSUJFTFVTSEVURUFEU0VBUkNIUkdFQ1RJVklUWUxFTkRBUlZFT1RJRllQVElPTlNDSFNFQVlTVEFUQ0hHRU9SRElSRUNUT1JUUkNIUEFSQU1FVEVSVVJDRUJTQ1JJQkVBUkRPV05BQ0VJTkROS0NLVUJTQ1JJQkVIVFRQL0FEVFAv' diff --git a/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/llhttp/llhttp_simd.wasm b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/llhttp/llhttp_simd.wasm new file mode 100755 index 000000000..93cdf5680 Binary files /dev/null and b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/llhttp/llhttp_simd.wasm differ diff --git a/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/llhttp/llhttp_simd.wasm.js b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/llhttp/llhttp_simd.wasm.js new file mode 100644 index 000000000..0fb8a67a0 --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/llhttp/llhttp_simd.wasm.js @@ -0,0 +1 @@ +module.exports = 'AGFzbQEAAAABMAhgAX8Bf2ADf39/AX9gBH9/f38Bf2AAAGADf39/AGABfwBgAn9/AGAGf39/f39/AALLAQgDZW52GHdhc21fb25faGVhZGVyc19jb21wbGV0ZQACA2VudhV3YXNtX29uX21lc3NhZ2VfYmVnaW4AAANlbnYLd2FzbV9vbl91cmwAAQNlbnYOd2FzbV9vbl9zdGF0dXMAAQNlbnYUd2FzbV9vbl9oZWFkZXJfZmllbGQAAQNlbnYUd2FzbV9vbl9oZWFkZXJfdmFsdWUAAQNlbnYMd2FzbV9vbl9ib2R5AAEDZW52GHdhc21fb25fbWVzc2FnZV9jb21wbGV0ZQAAA0ZFAwMEAAAFAAAAAAAABQEFAAUFBQAABgAAAAAGBgYGAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQABAAABAQcAAAUFAAMBBAUBcAESEgUDAQACBggBfwFBgNQECwfRBSIGbWVtb3J5AgALX2luaXRpYWxpemUACRlfX2luZGlyZWN0X2Z1bmN0aW9uX3RhYmxlAQALbGxodHRwX2luaXQAChhsbGh0dHBfc2hvdWxkX2tlZXBfYWxpdmUAQQxsbGh0dHBfYWxsb2MADAZtYWxsb2MARgtsbGh0dHBfZnJlZQANBGZyZWUASA9sbGh0dHBfZ2V0X3R5cGUADhVsbGh0dHBfZ2V0X2h0dHBfbWFqb3IADxVsbGh0dHBfZ2V0X2h0dHBfbWlub3IAEBFsbGh0dHBfZ2V0X21ldGhvZAARFmxsaHR0cF9nZXRfc3RhdHVzX2NvZGUAEhJsbGh0dHBfZ2V0X3VwZ3JhZGUAEwxsbGh0dHBfcmVzZXQAFA5sbGh0dHBfZXhlY3V0ZQAVFGxsaHR0cF9zZXR0aW5nc19pbml0ABYNbGxodHRwX2ZpbmlzaAAXDGxsaHR0cF9wYXVzZQAYDWxsaHR0cF9yZXN1bWUAGRtsbGh0dHBfcmVzdW1lX2FmdGVyX3VwZ3JhZGUAGhBsbGh0dHBfZ2V0X2Vycm5vABsXbGxodHRwX2dldF9lcnJvcl9yZWFzb24AHBdsbGh0dHBfc2V0X2Vycm9yX3JlYXNvbgAdFGxsaHR0cF9nZXRfZXJyb3JfcG9zAB4RbGxodHRwX2Vycm5vX25hbWUAHxJsbGh0dHBfbWV0aG9kX25hbWUAIBJs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ACAAQd0BNgIcQQALewEBfwJAIAAoAgwiAw0AAkAgACgCBEUNACAAIAE2AgQLAkAgACABIAIQxICAgAAiAw0AIAAoAgwPCyAAIAM2AhxBACEDIAAoAgQiAUUNACAAIAEgAiAAKAIIEYGAgIAAACIBRQ0AIAAgAjYCFCAAIAE2AgwgASEDCyADC9z3AQMofwN+BX8jgICAgABBEGsiAySAgICAACABIQQgASEFIAEhBiABIQcgASEIIAEhCSABIQogASELIAEhDCABIQ0gASEOIAEhDyABIRAgASERIAEhEiABIRMgASEUIAEhFSABIRYgASEXIAEhGCABIRkgASEaIAEhGyABIRwgASEdIAEhHiABIR8gASEgIAEhISABISIgASEjIAEhJCABISUgASEmIAEhJyABISggASEpAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAIAAoAhwiKkF/ag7dAdoBAdkBAgMEBQYHCAkKCwwNDtgBDxDXARES1gETFBUWFxgZGhvgAd8BHB0e1QEfICEiIyQl1AEmJygpKiss0wHSAS0u0QHQAS8wMTIzNDU2Nzg5Ojs8PT4/QEFCQ0RFRtsBR0hJSs8BzgFLzQFMzAFNTk9QUVJTVFVW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AkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJA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AAAAAAAAAAkKCwwNDgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADxAREhMUAAtBHiEqDMQDC0ICISsM5QELQgMhKwzkAQtCBCErDOMBC0IFISsM4gELQgYhKwzhAQtCByErDOABC0IIISsM3wELQgkhKwzeAQtCCiErDN0BC0ILISsM3AELQgwhKwzbAQtCDSErDNoBC0IOISsM2QELQg8hKwzYAQtCCiErDNcBC0ILISsM1gELQgwhKwzVAQtCDSErDNQBC0IOISsM0wELQg8hKwzSAQtCACErAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQCAqLQAAQVBqDjflAeQBAAECAwQFBgfmAeYB5gHmAeYB5gHmAQgJCgsMDeYB5gHmAeYB5gHmAeYB5gHmAeYB5gHmAeYB5gHmAeYB5gHmAeYB5gHmAeYB5gHmAeYB5gEODxAREhPmAQtCAiErDOQBC0IDISsM4wELQgQhKwziAQtCBSErDOEBC0IGISsM4AELQgchKwzfAQtCCCErDN4BC0IJISsM3QELQgohKwzcAQtCCyErDNsBC0IMISsM2gELQg0hKwzZAQtCDiErDNgBC0IPISsM1wELQgohKwzWAQtCCyErDNUBC0IMISsM1AELQg0hKwzTAQtCDiErDNIBC0IPISsM0QELIABCACAAKQMgIisgAiABIiprrSIsfSItIC0gK1YbNwMgICsgLFYiLkUN0gFBHyEqDMcDCwJAIAEiASACRg0AIABBiYCAgAA2AgggACABNgIEIAEhAUEkISoMrgMLQSAhKgzGAwsgACABIiogAhC+gICAAEF/ag4FtgEAywIB0QHSAQtBESEqDKsDCyAAQQE6AC8gKiEBDMIDCyABIgEgAkcN0gFBJCEqDMIDCyABIicgAkcNHkHGACEqDMEDCyAAIAEiASACELKAgIAAIioN1AEgASEBDLUBCyABIiogAkcNJkHQACEqDL8DCwJAIAEiASACRw0AQSghKgy/AwsgAEEANgIEIABBjICAgAA2AgggACABIAEQsYCAgAAiKg3TASABIQEM2AELAkAgASIqIAJHDQBBKSEqDL4DCyAqLQAAIgFBIEYNFCABQQlHDdMBICpBAWohAQwVCwJAIAEiASACRg0AIAFBAWohAQwXC0EqISoMvAMLAkAg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DQALIAAgKjYCAEGDASEqDOgCC0EAISogAEEANgIAIC4gMmtBA2ohAQzKAQsCQCABIiogAkcNAEGEASEqDOcCCyACICprIAAoAgAiLmohMiAqIQQgLiEBAkADQCAELQAAIAFBg8+AgABqLQAARw3MASABQQRGDQEgAUEBaiEBIARBAWoiBCACRw0ACyAAIDI2AgBBhAEhKgznAgsgAEEANgIAICogLmtBBWohAUEjISoMyQELAkAgASIEIAJHDQBBhQEhKgzmAgsCQAJAIAQtAABBtH9qDggAzAHMAcwBzAHMAcwBAcwBCyAEQQFqIQFB7wAhKgzNAgsgBEEBaiEBQfAAISoMzAILAkAgASIEIAJHDQBBhgEhKgzlAgsgBC0AAEHFAEcNyQEgBEEBaiEBDIoCCwJAIAEiKiACRw0AQYcBISoM5AILIAIgKmsgACgCACIuaiEyICohBCAuIQECQANAIAQtAAAgAUGIz4CAAGotAABHDckBIAFBA0YNASABQQFqIQEgBEEBaiIEIAJHDQALIAAgMjYCAEGHASEqDOQCCyAAQQA2AgAgKiAua0EEaiEBQS0hKgzGAQsCQCABIiogAkcNAEGIASEqDOMCCyACICprIAAoAgAiLmohMiAqIQQgLiEBAkADQCAELQAAIAFB0M+AgABqLQAARw3IASABQQhGDQEgAUEBaiEBIARBAWoiBCACRw0ACyAAIDI2AgBBiAEhKgzjAgsgAEEANgIAICogLmtBCWohAUEpISoMxQELAkAgASIBIAJHDQBBiQEhKgziAgtBASEqIAEtAABB3wBHDcQBIAFBAWohAQyIAgsCQCABIiogAkcNAEGKASEqDOECCyACICprIAAoAgAiLmohMiAqIQQgLiEBA0AgBC0AACABQYzPgIAAai0AAEcNxQEgAUEBRg23AiABQQFqIQEgBEEBaiIEIAJHDQALIAAgMjYCAEGKASEqDOACCwJAIAEiKiACRw0AQYsBISoM4AILIAIgKmsgACgCACIuaiEyICohBCAuIQECQANAIAQtAAAgAUGOz4CAAGotAABHDcUBIAFBAkYNASABQQFqIQEgBEEBaiIEIAJHDQALIAAgMjYCAEGLASEqDOACCyAAQQA2AgAgKiAua0EDaiEBQQIhKgzCAQsCQCABIiogAkcNAEGMASEqDN8CCyACICprIAAoAgAiLmohMiAqIQQgLiEBAkADQCAELQAAIAFB8M+AgABqLQAARw3EASABQQFGDQEgAUEBaiEBIARBAWoiBCACRw0ACyAAIDI2AgBBjAEhKgzfAgsgAEEANgIAICogLmtBAmohAUEfISoMwQELAkAgASIqIAJHDQBBjQEhKgzeAgsgAiAqayAAKAIAIi5qITIgKiEEIC4hAQJAA0AgBC0AACABQfLPgIAAai0AAEcNwwEgAUEBRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAyNgIAQY0BISoM3gILIABBADYCACAqIC5rQQJqIQFBCSEqDMABCwJAIAEiBCACRw0AQY4BISoM3QILAkACQCAELQAAQbd/ag4HAMMBwwHDAcMBwwEBwwELIARBAWohAUH4ACEqDMQCCyAEQQFqIQFB+QAhKgzDAgsCQCABIiogAkcNAEGPASEqDNwCCyACICprIAAoAgAiLmohMiAqIQQgLiEBAkADQCAELQAAIAFBkc+AgABqLQAARw3BASABQQVGDQEgAUEBaiEBIARBAWoiBCACRw0ACyAAIDI2AgBBjwEhKgzcAgsgAEEANgIAICogLmtBBmohAUEYISoMvgELAkAgASIqIAJHDQBBkAEhKgzbAgsgAiAqayAAKAIAIi5qITIgKiEEIC4hAQJAA0AgBC0AACABQZfPgIAAai0AAEcNwAEgAUECRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAyNgIAQZABISoM2wILIABBADYCACAqIC5rQQNqIQFBFyEqDL0BCwJAIAEiKiACRw0AQZEBISoM2gILIAIgKmsgACgCACIuaiEyICohBCAuIQECQANAIAQtAAAgAUGaz4CAAGotAABHDb8BIAFBBkYNASABQQFqIQEgBEEBaiIEIAJHDQALIAAgMjYCAEGRASEqDNoCCyAAQQA2AgAgKiAua0EHaiEBQRUhKgy8AQsCQCABIiogAkcNAEGSASEqDNkCCyACICprIAAoAgAiLmohMiAqIQQgLiEBAkADQCAELQAAIAFBoc+AgABqLQAARw2+ASABQQVGDQEgAUEBaiEBIARBAWoiBCACRw0ACyAAIDI2AgBBkgEhKgzZAgsgAEEANgIAICogLmtBBmohAUEeISoMuwELAkAgASIEIAJHDQBBkwEhKgzYAgsgBC0AAEHMAEcN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LjYCAEGqASEqDMECCyAAQQA2AgAgEiAqa0EDaiEBQRQhKgyjAQsCQCAEIAJHDQBBqwEhKgzAAgsCQAJAAkACQCAELQAAQb5/ag4PAAECqAGoAagBqAGoAagBqAGoAagBqAGoAQOoAQsgBEEBaiEPQZMBISoMqQILIARBAWohEEGUASEqDKgCCyAEQQFqIRFBlQEhKgynAgsgBEEBaiESQZYBISoMpgILAkAgBCACRw0AQawBISoMvwILIAQtAABBxQBHDaMBIARBAWohBAznAQsCQCATIAJHDQBBrQEhKgy+AgsgAiATayAAKAIAIipqIS4gEyEEICohAQJAA0AgBC0AACABQc3PgIAAai0AAEcNowEgAUECRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAuNgIAQa0BISoMvgILIABBADYCACATICprQQNqIQFBDiEqDKABCwJAIAQgAkcNAEGuASEqDL0CCyAELQAAQdAARw2hASAEQQFqIQFBJSEqDJ8BCwJAIBQgAkcNAEGvASEqDLwCCyACIBRrIAAoAgAiKmohLiAUIQQgKiEBAkADQCAELQAAIAFB0M+AgABqLQAARw2hASABQQhGDQEgAUEBaiEBIARBAWoiBCACRw0ACyAAIC42AgBBrwEhKgy8AgsgAEEANgIAIBQgKmtBCWohAUEqISoMngELAkAgBCACRw0AQbABISoMuwILAkACQCAELQAAQat/ag4LAKEBoQGhAaEBoQGhAaEBoQGhAQGhAQsgBEEBaiEEQZoBISoMogILIARBAWohFEGbASEqDKECCwJAIAQgAkcNAEGxASEqDLoCCwJAAkAgBC0AAEG/f2oOFACgAaABoAGgAaABoAGgAaABoAGgAaABoAGgAaABoAGgAaABoAEBoAELIARBAWohE0GZASEqDKECCyAEQQFqIQRBnAEhKgygAgsCQCAVIAJHDQBBsgEhKgy5AgsgAiAVayAAKAIAIipqIS4gFSEEICohAQJAA0AgBC0AACABQdnPgIAAai0AAEcNngEgAUEDRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAuNgIAQbIBISoMuQILIABBADYCACAVICprQQRqIQFBISEqDJsBCwJAIBYgAkcNAEGzASEqDLgCCyACIBZrIAAoAgAiKmohLiAWIQQgKiEBAkADQCAELQAAIAFB3c+AgABqLQAARw2dASABQQZGDQEgAUEBaiEBIARBAWoiBCACRw0ACyAAIC42AgBBswEhKgy4AgsgAEEANgIAIBYgKmtBB2ohAUEaISoMmgELAkAgBCACRw0AQbQBISoMtwILAkACQAJAIAQtAABBu39qDhEAngGeAZ4BngGeAZ4BngGeAZ4BAZ4BngGeAZ4BngECngELIARBAWohBEGdASEqDJ8CCyAEQQFqIRVBngEhKgyeAgsgBEEBaiEWQZ8BISoMnQILAkAgFyACRw0AQbUBISoMtgILIAIgF2sgACgCACIqaiEuIBchBCAqIQECQANAIAQtAAAgAUHkz4CAAGotAABHDZsBIAFBBUYNASABQQFqIQEgBEEBaiIEIAJHDQALIAAgLjYCAEG1ASEqDLYCCyAAQQA2AgAgFyAqa0EGaiEBQSghKgyYAQsCQCAYIAJHDQBBtgEhKgy1AgsgAiAYayAAKAIAIipqIS4gGCEEICohAQJAA0AgBC0AACABQerPgIAAai0AAEcNmgEgAUECRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAuNgIAQbYBISoMtQILIABBADYCACAYICprQQNqIQFBByEqDJcBCwJAIAQgAkcNAEG3ASEqDLQCCwJAAkAgBC0AAEG7f2oODgCaAZoBmgGaAZoBmgGaAZoBmgGaAZoBmgEBmgELIARBAWohF0GhASEqDJsCCyAEQQFqIRhBogEhKgyaAgsCQCAZIAJHDQBBuAEhKgyzAgsgAiAZayAAKAIAIipqIS4gGSEEICohAQJAA0AgBC0AACABQe3PgIAAai0AAEcNmAEgAUECRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAuNgIAQbgBISoMswILIABBADYCACAZICprQQNqIQFBEiEqDJUBCwJAIBogAkcNAEG5ASEqDLICCyACIBprIAAoAgAiKmohLiAaIQQgKiEBAkADQCAELQAAIAFB8M+AgABqLQAARw2XASABQQFGDQEgAUEBaiEBIARBAWoiBCACRw0ACyAAIC42AgBBuQEhKgyyAgsgAEEANgIAIBogKmtBAmohAUEgISoMlAELAkAgGyACRw0AQboBISoMsQILIAIgG2sgACgCACIqaiEuIBshBCAqIQECQANAIAQtAAAgAUHyz4CAAGotAABHDZYBIAFB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DM4BCyAAQQA2AhwgACABNgIUIABBoJmAgAA2AhAgAEEeNgIMQQAhKgzNAQsgAUEBaiEBCyAAQYASOwEqIAAgASACEKiAgIAAIioNASABIQELQccAISoMsQELICpBFUcNiQEgAEHRADYCHCAAIAE2AhQgAEHjl4CAADYCECAAQRU2AgxBACEqDMkBCyAAKAIEISogAEEANgIEAkAgACAqIAEQp4CAgAAiKg0AIAEhAQxiCyAAQdIANgIcIAAgATYCFCAAICo2AgxBACEqDMgBCyAAQQA2AhwgACAuNgIUIABBwaiAgAA2AhAgAEEHNgIMIABBADYCAEEAISoMxwELIAAoAgQhKiAAQQA2AgQCQCAAICogARCngICAACIqDQAgASEBDGELIABB0wA2AhwgACABNgIUIAAgKjYCDEEAISoMxgELQQAhKiAAQQA2AhwgACABNgIUIABBgJGAgAA2AhAgAEEJNgIMDMUBCyAqQRVGDYMBIABBADYCHCAAIAE2AhQgAEGUjYCAADYCECAAQSE2AgxBACEqDMQBC0EBIS9BACEyQQAhLkEBISoLIAAgKjoAKyABQQFqIQECQAJAIAAtAC1BEHENAAJAAkACQCAALQAqDgMBAAIECyAvRQ0DDAILIC4NAQwCCyAyRQ0BCyAAKAIEISogAEEANgIEAkAgACAqIAEQrYCAgAAiKg0AIAEhAQxgCyAAQdgANgIcIAAgATYCFCAAICo2AgxBACEqDMMBCyAAKAIEIQQgAEEANgIEAkAgACAEIAEQrYCAgAAiBA0AIAEhAQyyAQsgAEHZADYCHCAAIAE2AhQgACAENgIMQQAhKgzCAQsgACgCBCEEIABBADYCBAJAIAAgBCABEK2AgIAAIgQNACABIQEMsAELIABB2gA2AhwgACABNgIUIAAgBDYCDEEAISoMwQELIAAoAgQhBCAAQQA2AgQCQCAAIAQgARCtgICAACIEDQAgASEBDK4BCyAAQdwANgIcIAAgATYCFCAAIAQ2AgxBACEqDMABC0EBISoLIAAgKjoAKiABQQFqIQEMXAsgACgCBCEEIABBADYCBAJAIAAgBCABEK2AgIAAIgQNACABIQEMqgELIABB3gA2AhwgACABNgIUIAAgBDYCDEEAISoMvQELIABBADYCACAyIC9rQQRqIQECQCAALQApQSNPDQAgASEBDFwLIABBADYCHCAAIAE2AhQgAEHTiYCAADYCECAAQQg2AgxBACEqDLwBCyAAQQA2AgALQQAhKiAAQQA2AhwgACABNgIUIABBkLOAgAA2AhAgAEEINgIMDLoBCyAAQQA2AgAgMiAva0EDaiEBAkAgAC0AKUEhRw0AIAEhAQxZCyAAQQA2AhwgACABNgIUIABBm4qAgAA2AhAgAEEINgIMQQAhKgy5AQsgAEEANgIAIDIgL2tBBGohAQJAIAAtACkiKkFdakELTw0AIAEhAQxYCwJAICpBBksNAEEBICp0QcoAcUUNACABIQEMWAtBACEqIABBADYCHCAAIAE2AhQgAEH3iYCAADYCECAAQQg2AgwMuAELICpBFUYNdSAAQQA2AhwgACABNgIUIABBuY2AgAA2AhAgAEEaNgIMQQAhKgy3AQsgACgCBCEqIABBADYCBAJAIAAgKiABEKeAgIAAIioNACABIQEMVwsgAEHlADYCHCAAIAE2AhQgACAqNgIMQQAhKgy2AQsgACgCBCEqIABBADYCBAJAIAAgKiABEKeAgIAAIioNACABIQEMTwsgAEHSADYCHCAAIAE2AhQgACAqNgIMQQAhKgy1AQsgACgCBCEqIABBADYCBAJAIAAgKiABEKeAgIAAIioNACABIQEMTwsgAEHTADYCHCAAIAE2AhQgACAqNgIMQQAhKgy0AQsgACgCBCEqIABBADYCBAJAIAAgKiABEKeAgIAAIioNACABIQEMVAsgAEHlADYCHCAAIAE2AhQgACAqNgIMQQAhKgyzAQsgAEEANgIcIAAgATYCFCAAQcaKgIAANgIQIABBBzYCDEEAISoMsgELIAAoAgQhKiAAQQA2AgQCQCAAICogARCngICAACIqDQAgASEBDEsLIABB0gA2AhwgACABNgIUIAAgKjYCDEEAISoMsQELIAAoAgQhKiAAQQA2AgQCQCAAICogARCngICAACIqDQAgASEBDEsLIABB0wA2AhwgACABNgIUIAAgKjYCDEEAISoMsAELIAAoAgQhKiAAQQA2AgQCQCAAICogARCngICAACIqDQAgASEBDFALIABB5QA2AhwgACABNgIUIAAgKjYCDEEAISoMrwELIABBADYCHCAAIAE2AhQgAEHciICAADYCECAAQQc2AgxBACEqDK4BCyAqQT9HDQEgAUEBaiEB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AAAABwAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEludmFsaWQgY2hhciBpbiB1cmwgcXVlcnkAU3BhbiBjYWxsYmFjayBlcnJvciBpbiBvbl9ib2R5AENvbnRlbnQtTGVuZ3RoIG92ZXJmbG93AENodW5rIHNpemUgb3ZlcmZsb3cAUmVzcG9uc2Ugb3ZlcmZsb3cASW52YWxpZCBtZXRob2QgZm9yIEhUVFAveC54IHJlcXVlc3QASW52YWxpZCBtZXRob2QgZm9yIFJUU1AveC54IHJlcXVlc3QARXhwZWN0ZWQgU09VUkNFIG1ldGhvZCBmb3IgSUNFL3gueCByZXF1ZXN0AEludmFsaWQgY2hhciBpbiB1cmwgZnJhZ21lbnQgc3RhcnQARXhwZWN0ZWQgZG90AFNwYW4gY2FsbGJhY2sgZXJyb3IgaW4gb25fc3RhdHVzAEludmFsaWQgcmVzcG9uc2Ugc3RhdHVzAEludmFsaWQgY2hhcmFjdGVyIGluIGNodW5rIGV4dGVuc2lvbnMAVXNlciBjYWxsYmFjayBlcnJvcgBgb25fcmVzZXRgIGNhbGxiYWNrIGVycm9yAGBvbl9jaHVua19oZWFkZXJgIGNhbGxiYWNrIGVycm9yAGBvbl9tZXNzYWdlX2JlZ2luYCBjYWxsYmFjayBlcnJvcgBgb25fY2h1bmtfZXh0ZW5zaW9uX3ZhbHVlYCBjYWxsYmFjayBlcnJvcgBgb25fc3RhdHVzX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25fdmVyc2lvbl9jb21wbGV0ZWAgY2FsbGJhY2sgZXJyb3IAYG9uX3VybF9jb21wbGV0ZWAgY2FsbGJhY2sgZXJyb3IAYG9uX2NodW5rX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25faGVhZGVyX3ZhbHVlX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25fbWVzc2FnZV9jb21wbGV0ZWAgY2FsbGJhY2sgZXJyb3IAYG9uX21ldGhvZF9jb21wbGV0ZWAgY2FsbGJhY2sgZXJyb3IAYG9uX2hlYWRlcl9maWVsZF9jb21wbGV0ZWAgY2FsbGJhY2sgZXJyb3IAYG9uX2NodW5rX2V4dGVuc2lvbl9uYW1lYCBjYWxsYmFjayBlcnJvcgBVbmV4cGVjdGVkIGNoYXIgaW4gdXJsIHNlcnZlcgBJbnZhbGlkIGhlYWRlciB2YWx1ZSBjaGFyAEludmFsaWQgaGVhZGVyIGZpZWxkIGNoYXIAU3BhbiBjYWxsYmFjayBlcnJvciBpbiBvbl92ZXJzaW9uAEludmFsaWQgbWlub3IgdmVyc2lvbgBJbnZhbGlkIG1ham9yIHZlcnNpb24ARXhwZWN0ZWQgc3BhY2UgYWZ0ZXIgdmVyc2lvbgBFeHBlY3RlZCBDUkxGIGFmdGVyIHZlcnNpb24ASW52YWxpZCBIVFRQIHZlcnNpb24ASW52YWxpZCBoZWFkZXIgdG9rZW4AU3BhbiBjYWxsYmFjayBlcnJvciBpbiBvbl91cmwASW52YWxpZCBjaGFyYWN0ZXJzIGluIHVybABVbmV4cGVjdGVkIHN0YXJ0IGNoYXIgaW4gdXJsAERvdWJsZSBAIGluIHVybABFbXB0eSBDb250ZW50LUxlbmd0aABJbnZhbGlkIGNoYXJhY3RlciBpbiBDb250ZW50LUxlbmd0aABEdXBsaWNhdGUgQ29udGVudC1MZW5ndGgASW52YWxpZCBjaGFyIGluIHVybCBwYXRoAENvbnRlbnQtTGVuZ3RoIGNhbid0IGJlIHByZXNlbnQgd2l0aCBUcmFuc2Zlci1FbmNvZGluZwBJbnZhbGlkIGNoYXJhY3RlciBpbiBjaHVuayBzaXplAFNwYW4gY2FsbGJhY2sgZXJyb3IgaW4gb25faGVhZGVyX3ZhbHVlAFNwYW4gY2FsbGJhY2sgZXJyb3IgaW4gb25fY2h1bmtfZXh0ZW5zaW9uX3ZhbHVlAEludmFsaWQgY2hhcmFjdGVyIGluIGNodW5rIGV4dGVuc2lvbnMgdmFsdWUATWlzc2luZyBleHBlY3RlZCBMRiBhZnRlciBoZWFkZXIgdmFsdWUASW52YWxpZCBgVHJhbnNmZXItRW5jb2RpbmdgIGhlYWRlciB2YWx1ZQBJbnZhbGlkIGNoYXJhY3RlciBpbiBjaHVuayBleHRlbnNpb25zIHF1b3RlIHZhbHVlAEludmFsaWQgY2hhcmFjdGVyIGluIGNodW5rIGV4dGVuc2lvbnMgcXVvdGVkIHZhbHVlAFBhdXNlZCBieSBvbl9oZWFkZXJzX2NvbXBsZXRlAEludmFsaWQgRU9GIHN0YXRlAG9uX3Jlc2V0IHBhdXNlAG9uX2NodW5rX2hlYWRlciBwYXVzZQBvbl9tZXNzYWdlX2JlZ2luIHBhdXNlAG9uX2NodW5rX2V4dGVuc2lvbl92YWx1ZSBwYXVzZQBvbl9zdGF0dXNfY29tcGxldGUgcGF1c2UAb25fdmVyc2lvbl9jb21wbGV0ZSBwYXVzZQBvbl91cmxfY29tcGxldGUgcGF1c2UAb25fY2h1bmtfY29tcGxldGUgcGF1c2UAb25faGVhZGVyX3ZhbHVlX2NvbXBsZXRlIHBhdXNlAG9uX21lc3NhZ2VfY29tcGxldGUgcGF1c2UAb25fbWV0aG9kX2NvbXBsZXRlIHBhdXNlAG9uX2hlYWRlcl9maWVsZF9jb21wbGV0ZSBwYXVzZQBvbl9jaHVua19leHRlbnNpb25fbmFtZSBwYXVzZQBVbmV4cGVjdGVkIHNwYWNlIGFmdGVyIHN0YXJ0IGxpbmUAU3BhbiBjYWxsYmFjayBlcnJvciBpbiBvbl9jaHVua19leHRlbnNpb25fbmFtZQBJbnZhbGlkIGNoYXJhY3RlciBpbiBjaHVuayBleHRlbnNpb25zIG5hbWUAUGF1c2Ugb24gQ09OTkVDVC9VcGdyYWRlAFBhdXNlIG9uIFBSSS9VcGdyYWRlAEV4cGVjdGVkIEhUVFAvMiBDb25uZWN0aW9uIFByZWZhY2UAU3BhbiBjYWxsYmFjayBlcnJvciBpbiBvbl9tZXRob2QARXhwZWN0ZWQgc3BhY2UgYWZ0ZXIgbWV0aG9kAFNwYW4gY2FsbGJhY2sgZXJyb3IgaW4gb25faGVhZGVyX2ZpZWxkAFBhdXNlZABJbnZhbGlkIHdvcmQgZW5jb3VudGVyZWQASW52YWxpZCBtZXRob2QgZW5jb3VudGVyZWQAVW5leHBlY3RlZCBjaGFyIGluIHVybCBzY2hlbWEAUmVxdWVzdCBoYXMgaW52YWxpZCBgVHJhbnNmZXItRW5jb2RpbmdgAFNXSVRDSF9QUk9YWQBVU0VfUFJPWFkATUtBQ1RJVklUWQBVTlBST0NFU1NBQkxFX0VOVElUWQBDT1BZAE1PVkVEX1BFUk1BTkVOVExZAFRPT19FQVJMWQBOT1RJRlkARkFJTEVEX0RFUEVOREVOQ1kAQkFEX0dBVEVXQVkAUExBWQBQVVQAQ0hFQ0tPVVQAR0FURVdBWV9USU1FT1VUAFJFUVVFU1RfVElNRU9VVABORVRXT1JLX0NPTk5FQ1RfVElNRU9VVABDT05ORUNUSU9OX1RJTUVPVVQATE9HSU5fVElNRU9VVABORVRXT1JLX1JFQURfVElNRU9VVABQT1NUAE1JU0RJUkVDVEVEX1JFUVVFU1QAQ0xJRU5UX0NMT1NFRF9SRVFVRVNUAENMSUVOVF9DTE9TRURfTE9BRF9CQUxBTkNFRF9SRVFVRVNUAEJBRF9SRVFVRVNUAEhUVFBfUkVRVUVTVF9TRU5UX1RPX0hUVFBTX1BPUlQAUkVQT1JUAElNX0FfVEVBUE9UAFJFU0VUX0NPTlRFTlQATk9fQ09OVEVOVABQQVJUSUFMX0NPTlRFTlQASFBFX0lOVkFMSURfQ09OU1RBTlQASFBFX0NCX1JFU0VUAEdFVABIUEVfU1RSSUNUAENPTkZMSUNUAFRFTVBPUkFSWV9SRURJUkVDVABQRVJNQU5FTlRfUkVESVJFQ1QAQ09OTkVDVABNVUxUSV9TVEFUVVMASFBFX0lOVkFMSURfU1RBVFVTAFRPT19NQU5ZX1JFUVVFU1RTAEVBUkxZX0hJTlRTAFVOQVZBSUxBQkxFX0ZPUl9MRUdBTF9SRUFTT05TAE9QVElPTlMAU1dJVENISU5HX1BST1RPQ09MUwBWQVJJQU5UX0FMU09fTkVHT1RJQVRFUwBNVUxUSVBMRV9DSE9JQ0VTAElOVEVSTkFMX1NFUlZFUl9FUlJPUgBXRUJfU0VSVkVSX1VOS05PV05fRVJST1IAUkFJTEdVTl9FUlJPUgBJREVOVElUWV9QUk9WSURFUl9BVVRIRU5USUNBVElPTl9FUlJPUgBTU0xfQ0VSVElGSUNBVEVfRVJST1IASU5WQUxJRF9YX0ZPUldBUkRFRF9GT1IAU0VUX1BBUkFNRVRFUgBHRVRfUEFSQU1FVEVSAEhQRV9VU0VSAFNFRV9PVEhFUgBIUEVfQ0JfQ0hVTktfSEVBREVSAE1LQ0FMRU5EQVIAU0VUVVAAV0VCX1NFUlZFUl9JU19ET1dOAFRFQVJET1dOAEhQRV9DTE9TRURfQ09OTkVDVElPTgBIRVVSSVNUSUNfRVhQSVJBVElPTgBESVNDT05ORUNURURfT1BFUkFUSU9OAE5PTl9BVVRIT1JJVEFUSVZFX0lORk9STUFUSU9OAEhQRV9JTlZBTElEX1ZFUlNJT04ASFBFX0NCX01FU1NBR0VfQkVHSU4AU0lURV9JU19GUk9aRU4ASFBFX0lOVkFMSURfSEVBREVSX1RPS0VOAElOVkFMSURfVE9LRU4ARk9SQklEREVOAEVOSEFOQ0VfWU9VUl9DQUxNAEhQRV9JTlZBTElEX1VSTABCTE9DS0VEX0JZX1BBUkVOVEFMX0NPTlRST0wATUtDT0wAQUNMAEhQRV9JTlRFUk5BTABSRVFVRVNUX0hFQURFUl9GSUVMRFNfVE9PX0xBUkdFX1VOT0ZGSUNJQUwASFBFX09LAFVOTElOSwBVTkxPQ0sAUFJJAFJFVFJZX1dJVEgASFBFX0lOVkFMSURfQ09OVEVOVF9MRU5HVEgASFBFX1VORVhQRUNURURfQ09OVEVOVF9MRU5HVEgARkxVU0gAUFJPUFBBVENIAE0tU0VBUkNIAFVSSV9UT09fTE9ORwBQUk9DRVNTSU5HAE1JU0NFTExBTkVPVVNfUEVSU0lTVEVOVF9XQVJOSU5HAE1JU0NFTExBTkVPVVNfV0FSTklORwBIUEVfSU5WQUxJRF9UUkFOU0ZFUl9FTkNPRElORwBFeHBlY3RlZCBDUkxGAEhQRV9JTlZBTElEX0NIVU5LX1NJWkUATU9WRQBDT05USU5VRQBIUEVfQ0JfU1RBVFVTX0NPTVBMRVRFAEhQRV9DQl9IRUFERVJTX0NPTVBMRVRFAEhQRV9DQl9WRVJTSU9OX0NPTVBMRVRFAEhQRV9DQl9VUkxfQ09NUExFVEUASFBFX0NCX0NIVU5LX0NPTVBMRVRFAEhQRV9DQl9IRUFERVJfVkFMVUVfQ09NUExFVEUASFBFX0NCX0NIVU5LX0VYVEVOU0lPTl9WQUxVRV9DT01QTEVURQBIUEVfQ0JfQ0hVTktfRVhURU5TSU9OX05BTUVfQ09NUExFVEUASFBFX0NCX01FU1NBR0VfQ09NUExFVEUASFBFX0NCX01FVEhPRF9DT01QTEVURQBIUEVfQ0JfSEVBREVSX0ZJRUxEX0NPTVBMRVRFAERFTEVURQBIUEVfSU5WQUxJRF9FT0ZfU1RBVEUASU5WQUxJRF9TU0xfQ0VSVElGSUNBVEUAUEFVU0UATk9fUkVTUE9OU0UAVU5TVVBQT1JURURfTUVESUFfVFlQRQBHT05FAE5PVF9BQ0NFUFRBQkxFAFNFUlZJQ0VfVU5BVkFJTEFCTEUAUkFOR0VfTk9UX1NBVElTRklBQkxFAE9SSUdJTl9JU19VTlJFQUNIQUJMRQBSRVNQT05TRV9JU19TVEFMRQBQVVJHRQBNRVJHRQBSRVFVRVNUX0hFQURFUl9GSUVMRFNfVE9PX0xBUkdFAFJFUVVFU1RfSEVBREVSX1RPT19MQVJHRQBQQVlMT0FEX1RPT19MQVJHRQBJTlNVRkZJQ0lFTlRfU1RPUkFHRQBIUEVfUEFVU0VEX1VQR1JBREUASFBFX1BBVVNFRF9IMl9VUEdSQURFAFNPVVJDRQBBTk5PVU5DRQBUUkFDRQBIUEVfVU5FWFBFQ1RFRF9TUEFDRQBERVNDUklCRQBVTlNVQlNDUklCRQBSRUNPUkQASFBFX0lOVkFMSURfTUVUSE9EAE5PVF9GT1VORABQUk9QRklORABVTkJJTkQAUkVCSU5EAFVOQVVUSE9SSVpFRABNRVRIT0RfTk9UX0FMTE9XRUQASFRUUF9WRVJTSU9OX05PVF9TVVBQT1JURUQAQUxSRUFEWV9SRVBPUlRFRABBQ0NFUFRFRABOT1RfSU1QTEVNRU5URUQATE9PUF9ERVRFQ1RFRABIUEVfQ1JfRVhQRUNURUQASFBFX0xGX0VYUEVDVEVEAENSRUFURUQASU1fVVNFRABIUEVfUEFVU0VEAFRJTUVPVVRfT0NDVVJFRABQQVlNRU5UX1JFUVVJUkVEAFBSRUNPTkRJVElPTl9SRVFVSVJFRABQUk9YWV9BVVRIRU5USUNBVElPTl9SRVFVSVJFRABORVRXT1JLX0FVVEhFTlRJQ0FUSU9OX1JFUVVJUkVEAExFTkdUSF9SRVFVSVJFRABTU0xfQ0VSVElGSUNBVEVfUkVRVUlSRUQAVVBHUkFERV9SRVFVSVJFRABQQUdFX0VYUElSRUQAUFJFQ09ORElUSU9OX0ZBSUxFRABFWFBFQ1RBVElPTl9GQUlMRUQAUkVWQUxJREFUSU9OX0ZBSUxFRABTU0xfSEFORFNIQUtFX0ZBSUxFRABMT0NLRUQAVFJBTlNGT1JNQVRJT05fQVBQTElFRABOT1RfTU9ESUZJRUQATk9UX0VYVEVOREVEAEJBTkRXSURUSF9MSU1JVF9FWENFRURFRABTSVRFX0lTX09WRVJMT0FERUQASEVBRABFeHBlY3RlZCBIVFRQLwAAXhMAACYTAAAwEAAA8BcAAJ0TAAAVEgAAORcAAPASAAAKEAAAdRIAAK0SAACCEwAATxQAAH8QAACgFQAAIxQAAIkSAACLFAAATRUAANQRAADPFAAAEBgAAMkWAADcFgAAwREAAOAXAAC7FAAAdBQAAHwVAADlFAAACBcAAB8QAABlFQAAoxQAACgVAAACFQAAmRUAACwQAACLGQAATw8AANQOAABqEAAAzhAAAAIXAACJDgAAbhMAABwTAABmFAAAVhcAAMETAADNEwAAbBMAAGgXAABmFwAAXxcAACITAADODwAAaQ4AANgOAABjFgAAyxMAAKoOAAAoFwAAJhcAAMUTAABdFgAA6BEAAGcTAABlEwAA8hYAAHMTAAAdFwAA+RYAAPMRAADPDgAAzhUAAAwSAACzEQAApREAAGEQAAAyFwAAuxMAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQIBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIDAgICAgIAAAICAAICAAICAgICAgICAgIABAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgIAAAACAgICAgICAgICAgICAgICAgICAgICAgICAgICAgACAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAACAAICAgICAAACAgACAgACAgICAgICAgICAAMABAAAAAICAgICAgICAgICAgICAgICAgICAgICAgICAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAAgACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAbG9zZWVlcC1hbGl2ZQAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQIBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBY2h1bmtlZAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEAAQEBAQEAAAEBAAEBAAEBAQEBAQEBAQEAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQABAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABlY3Rpb25lbnQtbGVuZ3Rob25yb3h5LWNvbm5lY3Rpb24AAAAAAAAAAAAAAAAAAAByYW5zZmVyLWVuY29kaW5ncGdyYWRlDQoNCg0KU00NCg0KVFRQL0NFL1RTUC8AAAAAAAAAAAAAAAABAgABAwAAAAAAAAAAAAAAAAAAAAAAAAQBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAQIAAQMAAAAAAAAAAAAAAAAAAAAAAAAEAQEFAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAEAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAQAAAgAAAAAAAAAAAAAAAAAAAAAAAAMEAAAEBAQEBAQEBAQEBAUEBAQEBAQEBAQEBAQABAAGBwQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEAAQABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAEAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAADAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwAAAAAAAAMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAABAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAIAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMAAAAAAAADAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABOT1VOQ0VFQ0tPVVRORUNURVRFQ1JJQkVMVVNIRVRFQURTRUFSQ0hSR0VDVElWSVRZTEVOREFSVkVPVElGWVBUSU9OU0NIU0VBWVNUQVRDSEdFT1JESVJFQ1RPUlRSQ0hQQVJBTUVURVJVUkNFQlNDUklCRUFSRE9XTkFDRUlORE5LQ0tVQlNDUklCRUhUVFAvQURUUC8=' diff --git a/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/llhttp/utils.d.ts b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/llhttp/utils.d.ts new file mode 100644 index 000000000..15497f356 --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/llhttp/utils.d.ts @@ -0,0 +1,4 @@ +export interface IEnumMap { + [key: string]: number; +} +export declare function enumToMap(obj: any): IEnumMap; diff --git a/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/llhttp/utils.js b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/llhttp/utils.js new file mode 100644 index 000000000..8a32e564e --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/llhttp/utils.js @@ -0,0 +1,15 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.enumToMap = void 0; +function enumToMap(obj) { + const res = {}; + Object.keys(obj).forEach((key) => { + const value = obj[key]; + if (typeof value === 'number') { + res[key] = value; + } + }); + return res; +} +exports.enumToMap = enumToMap; +//# sourceMappingURL=utils.js.map \ No newline at end of file diff --git a/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/llhttp/utils.js.map b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/llhttp/utils.js.map new file mode 100644 index 000000000..2d7c35623 --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/llhttp/utils.js.map @@ -0,0 +1 @@ +{"version":3,"file":"utils.js","sourceRoot":"","sources":["../../src/llhttp/utils.ts"],"names":[],"mappings":";;;AAIA,SAAgB,SAAS,CAAC,GAAQ;IAChC,MAAM,GAAG,GAAa,EAAE,CAAC;IAEzB,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,EAAE;QAC/B,MAAM,KAAK,GAAG,GAAG,CAAC,GAAG,CAAC,CAAC;QACvB,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE;YAC7B,GAAG,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;SAClB;IACH,CAAC,CAAC,CAAC;IAEH,OAAO,GAAG,CAAC;AACb,CAAC;AAXD,8BAWC"} \ No newline at end of file diff --git a/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/mock/mock-agent.js b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/mock/mock-agent.js new file mode 100644 index 000000000..828e8af17 --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/mock/mock-agent.js @@ -0,0 +1,171 @@ +'use strict' + +const { kClients } = require('../core/symbols') +const Agent = require('../agent') +const { + kAgent, + kMockAgentSet, + kMockAgentGet, + kDispatches, + kIsMockActive, + kNetConnect, + kGetNetConnect, + kOptions, + kFactory +} = require('./mock-symbols') +const MockClient = require('./mock-client') +const MockPool = require('./mock-pool') +const { matchValue, buildMockOptions } = require('./mock-utils') +const { InvalidArgumentError, UndiciError } = require('../core/errors') +const Dispatcher = require('../dispatcher') +const Pluralizer = require('./pluralizer') +const PendingInterceptorsFormatter = require('./pending-interceptors-formatter') + +class FakeWeakRef { + constructor (value) { + this.value = value + } + + deref () { + return this.value + } +} + +class MockAgent extends Dispatcher { + constructor (opts) { + super(opts) + + this[kNetConnect] = true + this[kIsMockActive] = true + + // Instantiate Agent and encapsulate + if ((opts && opts.agent && typeof opts.agent.dispatch !== 'function')) { + throw new InvalidArgumentError('Argument opts.agent must implement Agent') + } + const agent = opts && opts.agent ? opts.agent : new Agent(opts) + this[kAgent] = agent + + this[kClients] = agent[kClients] + this[kOptions] = buildMockOptions(opts) + } + + get (origin) { + let dispatcher = this[kMockAgentGet](origin) + + if (!dispatcher) { + dispatcher = this[kFactory](origin) + this[kMockAgentSet](origin, dispatcher) + } + return dispatcher + } + + dispatch (opts, handler) { + // Call MockAgent.get to perform additional setup before dispatching as normal + this.get(opts.origin) + return this[kAgent].dispatch(opts, handler) + } + + async close () { + await this[kAgent].close() + this[kClients].clear() + } + + deactivate () { + this[kIsMockActive] = false + } + + activate () { + this[kIsMockActive] = true + } + + enableNetConnect (matcher) { + if (typeof matcher === 'string' || typeof matcher === 'function' || matcher instanceof RegExp) { + if (Array.isArray(this[kNetConnect])) { + this[kNetConnect].push(matcher) + } else { + this[kNetConnect] = [matcher] + } + } else if (typeof matcher === 'undefined') { + this[kNetConnect] = true + } else { + throw new InvalidArgumentError('Unsupported matcher. Must be one of String|Function|RegExp.') + } + } + + disableNetConnect () { + this[kNetConnect] = false + } + + // This is required to bypass issues caused by using global symbols - see: + // https://github.com/nodejs/undici/issues/1447 + get isMockActive () { + return this[kIsMockActive] + } + + [kMockAgentSet] (origin, dispatcher) { + this[kClients].set(origin, new FakeWeakRef(dispatcher)) + } + + [kFactory] (origin) { + const mockOptions = Object.assign({ agent: this }, this[kOptions]) + return this[kOptions] && this[kOptions].connections === 1 + ? new MockClient(origin, mockOptions) + : new MockPool(origin, mockOptions) + } + + [kMockAgentGet] (origin) { + // First check if we can immediately find it + const ref = this[kClients].get(origin) + if (ref) { + return ref.deref() + } + + // If the origin is not a string create a dummy parent pool and return to user + if (typeof origin !== 'string') { + const dispatcher = this[kFactory]('http://localhost:9999') + this[kMockAgentSet](origin, dispatcher) + return dispatcher + } + + // If we match, create a pool and assign the same dispatches + for (const [keyMatcher, nonExplicitRef] of Array.from(this[kClients])) { + const nonExplicitDispatcher = nonExplicitRef.deref() + if (nonExplicitDispatcher && typeof keyMatcher !== 'string' && matchValue(keyMatcher, origin)) { + const dispatcher = this[kFactory](origin) + this[kMockAgentSet](origin, dispatcher) + dispatcher[kDispatches] = nonExplicitDispatcher[kDispatches] + return dispatcher + } + } + } + + [kGetNetConnect] () { + return this[kNetConnect] + } + + pendingInterceptors () { + const mockAgentClients = this[kClients] + + return Array.from(mockAgentClients.entries()) + .flatMap(([origin, scope]) => scope.deref()[kDispatches].map(dispatch => ({ ...dispatch, origin }))) + .filter(({ pending }) => pending) + } + + assertNoPendingInterceptors ({ pendingInterceptorsFormatter = new PendingInterceptorsFormatter() } = {}) { + const pending = this.pendingInterceptors() + + if (pending.length === 0) { + return + } + + const pluralizer = new Pluralizer('interceptor', 'interceptors').pluralize(pending.length) + + throw new UndiciError(` +${pluralizer.count} ${pluralizer.noun} ${pluralizer.is} pending: + +${pendingInterceptorsFormatter.format(pending)} +`.trim()) + } +} + +module.exports = MockAgent diff --git a/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/mock/mock-client.js b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/mock/mock-client.js new file mode 100644 index 000000000..5f3121596 --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/mock/mock-client.js @@ -0,0 +1,59 @@ +'use strict' + +const { promisify } = require('util') +const Client = require('../client') +const { buildMockDispatch } = require('./mock-utils') +const { + kDispatches, + kMockAgent, + kClose, + kOriginalClose, + kOrigin, + kOriginalDispatch, + kConnected +} = require('./mock-symbols') +const { MockInterceptor } = require('./mock-interceptor') +const Symbols = require('../core/symbols') +const { InvalidArgumentError } = require('../core/errors') + +/** + * MockClient provides an API that extends the Client to influence the mockDispatches. + */ +class MockClient extends Client { + constructor (origin, opts) { + super(origin, opts) + + if (!opts || !opts.agent || typeof opts.agent.dispatch !== 'function') { + throw new InvalidArgumentError('Argument opts.agent must implement Agent') + } + + this[kMockAgent] = opts.agent + this[kOrigin] = origin + this[kDispatches] = [] + this[kConnected] = 1 + this[kOriginalDispatch] = this.dispatch + this[kOriginalClose] = this.close.bind(this) + + this.dispatch = buildMockDispatch.call(this) + this.close = this[kClose] + } + + get [Symbols.kConnected] () { + return this[kConnected] + } + + /** + * Sets up the base interceptor for mocking replies from undici. + */ + intercept (opts) { + return new MockInterceptor(opts, this[kDispatches]) + } + + async [kClose] () { + await promisify(this[kOriginalClose])() + this[kConnected] = 0 + this[kMockAgent][Symbols.kClients].delete(this[kOrigin]) + } +} + +module.exports = MockClient diff --git a/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/mock/mock-errors.js b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/mock/mock-errors.js new file mode 100644 index 000000000..5442c0e8d --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/mock/mock-errors.js @@ -0,0 +1,17 @@ +'use strict' + +const { UndiciError } = require('../core/errors') + +class MockNotMatchedError extends UndiciError { + constructor (message) { + super(message) + Error.captureStackTrace(this, MockNotMatchedError) + this.name = 'MockNotMatchedError' + this.message = message || 'The request does not match any registered mock dispatches' + this.code = 'UND_MOCK_ERR_MOCK_NOT_MATCHED' + } +} + +module.exports = { + MockNotMatchedError +} diff --git a/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/mock/mock-interceptor.js b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/mock/mock-interceptor.js new file mode 100644 index 000000000..781e47750 --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/mock/mock-interceptor.js @@ -0,0 +1,206 @@ +'use strict' + +const { getResponseData, buildKey, addMockDispatch } = require('./mock-utils') +const { + kDispatches, + kDispatchKey, + kDefaultHeaders, + kDefaultTrailers, + kContentLength, + kMockDispatch +} = require('./mock-symbols') +const { InvalidArgumentError } = require('../core/errors') +const { buildURL } = require('../core/util') + +/** + * Defines the scope API for an interceptor reply + */ +class MockScope { + constructor (mockDispatch) { + this[kMockDispatch] = mockDispatch + } + + /** + * Delay a reply by a set amount in ms. + */ + delay (waitInMs) { + if (typeof waitInMs !== 'number' || !Number.isInteger(waitInMs) || waitInMs <= 0) { + throw new InvalidArgumentError('waitInMs must be a valid integer > 0') + } + + this[kMockDispatch].delay = waitInMs + return this + } + + /** + * For a defined reply, never mark as consumed. + */ + persist () { + this[kMockDispatch].persist = true + return this + } + + /** + * Allow one to define a reply for a set amount of matching requests. + */ + times (repeatTimes) { + if (typeof repeatTimes !== 'number' || !Number.isInteger(repeatTimes) || repeatTimes <= 0) { + throw new InvalidArgumentError('repeatTimes must be a valid integer > 0') + } + + this[kMockDispatch].times = repeatTimes + return this + } +} + +/** + * Defines an interceptor for a Mock + */ +class MockInterceptor { + constructor (opts, mockDispatches) { + if (typeof opts !== 'object') { + throw new InvalidArgumentError('opts must be an object') + } + if (typeof opts.path === 'undefined') { + throw new InvalidArgumentError('opts.path must be defined') + } + if (typeof opts.method === 'undefined') { + opts.method = 'GET' + } + // See https://github.com/nodejs/undici/issues/1245 + // As per RFC 3986, clients are not supposed to send URI + // fragments to servers when they retrieve a document, + if (typeof opts.path === 'string') { + if (opts.query) { + opts.path = buildURL(opts.path, opts.query) + } else { + // Matches https://github.com/nodejs/undici/blob/main/lib/fetch/index.js#L1811 + const parsedURL = new URL(opts.path, 'data://') + opts.path = parsedURL.pathname + parsedURL.search + } + } + if (typeof opts.method === 'string') { + opts.method = opts.method.toUpperCase() + } + + this[kDispatchKey] = buildKey(opts) + this[kDispatches] = mockDispatches + this[kDefaultHeaders] = {} + this[kDefaultTrailers] = {} + this[kContentLength] = false + } + + createMockScopeDispatchData (statusCode, data, responseOptions = {}) { + const responseData = getResponseData(data) + const contentLength = this[kContentLength] ? { 'content-length': responseData.length } : {} + const headers = { ...this[kDefaultHeaders], ...contentLength, ...responseOptions.headers } + const trailers = { ...this[kDefaultTrailers], ...responseOptions.trailers } + + return { statusCode, data, headers, trailers } + } + + validateReplyParameters (statusCode, data, responseOptions) { + if (typeof statusCode === 'undefined') { + throw new InvalidArgumentError('statusCode must be defined') + } + if (typeof data === 'undefined') { + throw new InvalidArgumentError('data must be defined') + } + if (typeof responseOptions !== 'object') { + throw new InvalidArgumentError('responseOptions must be an object') + } + } + + /** + * Mock an undici request with a defined reply. + */ + reply (replyData) { + // Values of reply aren't available right now as they + // can only be available when the reply callback is invoked. + if (typeof replyData === 'function') { + // We'll first wrap the provided callback in another function, + // this function will properly resolve the data from the callback + // when invoked. + const wrappedDefaultsCallback = (opts) => { + // Our reply options callback contains the parameter for statusCode, data and options. + const resolvedData = replyData(opts) + + // Check if it is in the right format + if (typeof resolvedData !== 'object') { + throw new InvalidArgumentError('reply options callback must return an object') + } + + const { statusCode, data = '', responseOptions = {} } = resolvedData + this.validateReplyParameters(statusCode, data, responseOptions) + // Since the values can be obtained immediately we return them + // from this higher order function that will be resolved later. + return { + ...this.createMockScopeDispatchData(statusCode, data, responseOptions) + } + } + + // Add usual dispatch data, but this time set the data parameter to function that will eventually provide data. + const newMockDispatch = addMockDispatch(this[kDispatches], this[kDispatchKey], wrappedDefaultsCallback) + return new MockScope(newMockDispatch) + } + + // We can have either one or three parameters, if we get here, + // we should have 1-3 parameters. So we spread the arguments of + // this function to obtain the parameters, since replyData will always + // just be the statusCode. + const [statusCode, data = '', responseOptions = {}] = [...arguments] + this.validateReplyParameters(statusCode, data, responseOptions) + + // Send in-already provided data like usual + const dispatchData = this.createMockScopeDispatchData(statusCode, data, responseOptions) + const newMockDispatch = addMockDispatch(this[kDispatches], this[kDispatchKey], dispatchData) + return new MockScope(newMockDispatch) + } + + /** + * Mock an undici request with a defined error. + */ + replyWithError (error) { + if (typeof error === 'undefined') { + throw new InvalidArgumentError('error must be defined') + } + + const newMockDispatch = addMockDispatch(this[kDispatches], this[kDispatchKey], { error }) + return new MockScope(newMockDispatch) + } + + /** + * Set default reply headers on the interceptor for subsequent replies + */ + defaultReplyHeaders (headers) { + if (typeof headers === 'undefined') { + throw new InvalidArgumentError('headers must be defined') + } + + this[kDefaultHeaders] = headers + return this + } + + /** + * Set default reply trailers on the interceptor for subsequent replies + */ + defaultReplyTrailers (trailers) { + if (typeof trailers === 'undefined') { + throw new InvalidArgumentError('trailers must be defined') + } + + this[kDefaultTrailers] = trailers + return this + } + + /** + * Set reply content length header for replies on the interceptor + */ + replyContentLength () { + this[kContentLength] = true + return this + } +} + +module.exports.MockInterceptor = MockInterceptor +module.exports.MockScope = MockScope diff --git a/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/mock/mock-pool.js b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/mock/mock-pool.js new file mode 100644 index 000000000..0a3a7cd35 --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/mock/mock-pool.js @@ -0,0 +1,59 @@ +'use strict' + +const { promisify } = require('util') +const Pool = require('../pool') +const { buildMockDispatch } = require('./mock-utils') +const { + kDispatches, + kMockAgent, + kClose, + kOriginalClose, + kOrigin, + kOriginalDispatch, + kConnected +} = require('./mock-symbols') +const { MockInterceptor } = require('./mock-interceptor') +const Symbols = require('../core/symbols') +const { InvalidArgumentError } = require('../core/errors') + +/** + * MockPool provides an API that extends the Pool to influence the mockDispatches. + */ +class MockPool extends Pool { + constructor (origin, opts) { + super(origin, opts) + + if (!opts || !opts.agent || typeof opts.agent.dispatch !== 'function') { + throw new InvalidArgumentError('Argument opts.agent must implement Agent') + } + + this[kMockAgent] = opts.agent + this[kOrigin] = origin + this[kDispatches] = [] + this[kConnected] = 1 + this[kOriginalDispatch] = this.dispatch + this[kOriginalClose] = this.close.bind(this) + + this.dispatch = buildMockDispatch.call(this) + this.close = this[kClose] + } + + get [Symbols.kConnected] () { + return this[kConnected] + } + + /** + * Sets up the base interceptor for mocking replies from undici. + */ + intercept (opts) { + return new MockInterceptor(opts, this[kDispatches]) + } + + async [kClose] () { + await promisify(this[kOriginalClose])() + this[kConnected] = 0 + this[kMockAgent][Symbols.kClients].delete(this[kOrigin]) + } +} + +module.exports = MockPool diff --git a/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/mock/mock-symbols.js b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/mock/mock-symbols.js new file mode 100644 index 000000000..8c4cbb60e --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/mock/mock-symbols.js @@ -0,0 +1,23 @@ +'use strict' + +module.exports = { + kAgent: Symbol('agent'), + kOptions: Symbol('options'), + kFactory: Symbol('factory'), + kDispatches: Symbol('dispatches'), + kDispatchKey: Symbol('dispatch key'), + kDefaultHeaders: Symbol('default headers'), + kDefaultTrailers: Symbol('default trailers'), + kContentLength: Symbol('content length'), + kMockAgent: Symbol('mock agent'), + kMockAgentSet: Symbol('mock agent set'), + kMockAgentGet: Symbol('mock agent get'), + kMockDispatch: Symbol('mock dispatch'), + kClose: Symbol('close'), + kOriginalClose: Symbol('original agent close'), + kOrigin: Symbol('origin'), + kIsMockActive: Symbol('is mock active'), + kNetConnect: Symbol('net connect'), + kGetNetConnect: Symbol('get net connect'), + kConnected: Symbol('connected') +} diff --git a/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/mock/mock-utils.js b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/mock/mock-utils.js new file mode 100644 index 000000000..7d5ca5071 --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/mock/mock-utils.js @@ -0,0 +1,329 @@ +'use strict' + +const { MockNotMatchedError } = require('./mock-errors') +const { + kDispatches, + kMockAgent, + kOriginalDispatch, + kOrigin, + kGetNetConnect +} = require('./mock-symbols') +const { buildURL, nop } = require('../core/util') +const { STATUS_CODES } = require('http') + +function matchValue (match, value) { + if (typeof match === 'string') { + return match === value + } + if (match instanceof RegExp) { + return match.test(value) + } + if (typeof match === 'function') { + return match(value) === true + } + return false +} + +function lowerCaseEntries (headers) { + return Object.fromEntries( + Object.entries(headers).map(([headerName, headerValue]) => { + return [headerName.toLocaleLowerCase(), headerValue] + }) + ) +} + +/** + * @param {import('../../index').Headers|string[]|Record} headers + * @param {string} key + */ +function getHeaderByName (headers, key) { + if (Array.isArray(headers)) { + for (let i = 0; i < headers.length; i += 2) { + if (headers[i].toLocaleLowerCase() === key.toLocaleLowerCase()) { + return headers[i + 1] + } + } + + return undefined + } else if (typeof headers.get === 'function') { + return headers.get(key) + } else { + return lowerCaseEntries(headers)[key.toLocaleLowerCase()] + } +} + +/** @param {string[]} headers */ +function buildHeadersFromArray (headers) { // fetch HeadersList + const clone = headers.slice() + const entries = [] + for (let index = 0; index < clone.length; index += 2) { + entries.push([clone[index], clone[index + 1]]) + } + return Object.fromEntries(entries) +} + +function matchHeaders (mockDispatch, headers) { + if (typeof mockDispatch.headers === 'function') { + if (Array.isArray(headers)) { // fetch HeadersList + headers = buildHeadersFromArray(headers) + } + return mockDispatch.headers(headers ? lowerCaseEntries(headers) : {}) + } + if (typeof mockDispatch.headers === 'undefined') { + return true + } + if (typeof headers !== 'object' || typeof mockDispatch.headers !== 'object') { + return false + } + + for (const [matchHeaderName, matchHeaderValue] of Object.entries(mockDispatch.headers)) { + const headerValue = getHeaderByName(headers, matchHeaderName) + + if (!matchValue(matchHeaderValue, headerValue)) { + return false + } + } + return true +} + +function safeUrl (path) { + if (typeof path !== 'string') { + return path + } + + const pathSegments = path.split('?') + + if (pathSegments.length !== 2) { + return path + } + + const qp = new URLSearchParams(pathSegments.pop()) + qp.sort() + return [...pathSegments, qp.toString()].join('?') +} + +function matchKey (mockDispatch, { path, method, body, headers }) { + const pathMatch = matchValue(mockDispatch.path, path) + const methodMatch = matchValue(mockDispatch.method, method) + const bodyMatch = typeof mockDispatch.body !== 'undefined' ? matchValue(mockDispatch.body, body) : true + const headersMatch = matchHeaders(mockDispatch, headers) + return pathMatch && methodMatch && bodyMatch && headersMatch +} + +function getResponseData (data) { + if (Buffer.isBuffer(data)) { + return data + } else if (typeof data === 'object') { + return JSON.stringify(data) + } else { + return data.toString() + } +} + +function getMockDispatch (mockDispatches, key) { + const basePath = key.query ? buildURL(key.path, key.query) : key.path + const resolvedPath = typeof basePath === 'string' ? safeUrl(basePath) : basePath + + // Match path + let matchedMockDispatches = mockDispatches.filter(({ consumed }) => !consumed).filter(({ path }) => matchValue(safeUrl(path), resolvedPath)) + if (matchedMockDispatches.length === 0) { + throw new MockNotMatchedError(`Mock dispatch not matched for path '${resolvedPath}'`) + } + + // Match method + matchedMockDispatches = matchedMockDispatches.filter(({ method }) => matchValue(method, key.method)) + if (matchedMockDispatches.length === 0) { + throw new MockNotMatchedError(`Mock dispatch not matched for method '${key.method}'`) + } + + // Match body + matchedMockDispatches = matchedMockDispatches.filter(({ body }) => typeof body !== 'undefined' ? matchValue(body, key.body) : true) + if (matchedMockDispatches.length === 0) { + throw new MockNotMatchedError(`Mock dispatch not matched for body '${key.body}'`) + } + + // Match headers + matchedMockDispatches = matchedMockDispatches.filter((mockDispatch) => matchHeaders(mockDispatch, key.headers)) + if (matchedMockDispatches.length === 0) { + throw new MockNotMatchedError(`Mock dispatch not matched for headers '${typeof key.headers === 'object' ? JSON.stringify(key.headers) : key.headers}'`) + } + + return matchedMockDispatches[0] +} + +function addMockDispatch (mockDispatches, key, data) { + const baseData = { timesInvoked: 0, times: 1, persist: false, consumed: false } + const replyData = typeof data === 'function' ? { callback: data } : { ...data } + const newMockDispatch = { ...baseData, ...key, pending: true, data: { error: null, ...replyData } } + mockDispatches.push(newMockDispatch) + return newMockDispatch +} + +function deleteMockDispatch (mockDispatches, key) { + const index = mockDispatches.findIndex(dispatch => { + if (!dispatch.consumed) { + return false + } + return matchKey(dispatch, key) + }) + if (index !== -1) { + mockDispatches.splice(index, 1) + } +} + +function buildKey (opts) { + const { path, method, body, headers, query } = opts + return { + path, + method, + body, + headers, + query + } +} + +function generateKeyValues (data) { + return Object.entries(data).reduce((keyValuePairs, [key, value]) => [...keyValuePairs, key, value], []) +} + +/** + * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Status + * @param {number} statusCode + */ +function getStatusText (statusCode) { + return STATUS_CODES[statusCode] || 'unknown' +} + +async function getResponse (body) { + const buffers = [] + for await (const data of body) { + buffers.push(data) + } + return Buffer.concat(buffers).toString('utf8') +} + +/** + * Mock dispatch function used to simulate undici dispatches + */ +function mockDispatch (opts, handler) { + // Get mock dispatch from built key + const key = buildKey(opts) + const mockDispatch = getMockDispatch(this[kDispatches], key) + + mockDispatch.timesInvoked++ + + // Here's where we resolve a callback if a callback is present for the dispatch data. + if (mockDispatch.data.callback) { + mockDispatch.data = { ...mockDispatch.data, ...mockDispatch.data.callback(opts) } + } + + // Parse mockDispatch data + const { data: { statusCode, data, headers, trailers, error }, delay, persist } = mockDispatch + const { timesInvoked, times } = mockDispatch + + // If it's used up and not persistent, mark as consumed + mockDispatch.consumed = !persist && timesInvoked >= times + mockDispatch.pending = timesInvoked < times + + // If specified, trigger dispatch error + if (error !== null) { + deleteMockDispatch(this[kDispatches], key) + handler.onError(error) + return true + } + + // Handle the request with a delay if necessary + if (typeof delay === 'number' && delay > 0) { + setTimeout(() => { + handleReply(this[kDispatches]) + }, delay) + } else { + handleReply(this[kDispatches]) + } + + function handleReply (mockDispatches) { + // fetch's HeadersList is a 1D string array + const optsHeaders = Array.isArray(opts.headers) + ? buildHeadersFromArray(opts.headers) + : opts.headers + const responseData = getResponseData( + typeof data === 'function' ? data({ ...opts, headers: optsHeaders }) : data + ) + const responseHeaders = generateKeyValues(headers) + const responseTrailers = generateKeyValues(trailers) + + handler.abort = nop + handler.onHeaders(statusCode, responseHeaders, resume, getStatusText(statusCode)) + handler.onData(Buffer.from(responseData)) + handler.onComplete(responseTrailers) + deleteMockDispatch(mockDispatches, key) + } + + function resume () {} + + return true +} + +function buildMockDispatch () { + const agent = this[kMockAgent] + const origin = this[kOrigin] + const originalDispatch = this[kOriginalDispatch] + + return function dispatch (opts, handler) { + if (agent.isMockActive) { + try { + mockDispatch.call(this, opts, handler) + } catch (error) { + if (error instanceof MockNotMatchedError) { + const netConnect = agent[kGetNetConnect]() + if (netConnect === false) { + throw new MockNotMatchedError(`${error.message}: subsequent request to origin ${origin} was not allowed (net.connect disabled)`) + } + if (checkNetConnect(netConnect, origin)) { + originalDispatch.call(this, opts, handler) + } else { + throw new MockNotMatchedError(`${error.message}: subsequent request to origin ${origin} was not allowed (net.connect is not enabled for this origin)`) + } + } else { + throw error + } + } + } else { + originalDispatch.call(this, opts, handler) + } + } +} + +function checkNetConnect (netConnect, origin) { + const url = new URL(origin) + if (netConnect === true) { + return true + } else if (Array.isArray(netConnect) && netConnect.some((matcher) => matchValue(matcher, url.host))) { + return true + } + return false +} + +function buildMockOptions (opts) { + if (opts) { + const { agent, ...mockOptions } = opts + return mockOptions + } +} + +module.exports = { + getResponseData, + getMockDispatch, + addMockDispatch, + deleteMockDispatch, + buildKey, + generateKeyValues, + matchValue, + getResponse, + getStatusText, + mockDispatch, + buildMockDispatch, + checkNetConnect, + buildMockOptions, + getHeaderByName +} diff --git a/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/mock/pending-interceptors-formatter.js b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/mock/pending-interceptors-formatter.js new file mode 100644 index 000000000..1bc7539fd --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/mock/pending-interceptors-formatter.js @@ -0,0 +1,40 @@ +'use strict' + +const { Transform } = require('stream') +const { Console } = require('console') + +/** + * Gets the output of `console.table(…)` as a string. + */ +module.exports = class PendingInterceptorsFormatter { + constructor ({ disableColors } = {}) { + this.transform = new Transform({ + transform (chunk, _enc, cb) { + cb(null, chunk) + } + }) + + this.logger = new Console({ + stdout: this.transform, + inspectOptions: { + colors: !disableColors && !process.env.CI + } + }) + } + + format (pendingInterceptors) { + const withPrettyHeaders = pendingInterceptors.map( + ({ method, path, data: { statusCode }, persist, times, timesInvoked, origin }) => ({ + Method: method, + Origin: origin, + Path: path, + 'Status code': statusCode, + Persistent: persist ? '✅' : '❌', + Invocations: timesInvoked, + Remaining: persist ? Infinity : times - timesInvoked + })) + + this.logger.table(withPrettyHeaders) + return this.transform.read().toString() + } +} diff --git a/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/mock/pluralizer.js b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/mock/pluralizer.js new file mode 100644 index 000000000..47f150bc2 --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/mock/pluralizer.js @@ -0,0 +1,29 @@ +'use strict' + +const singulars = { + pronoun: 'it', + is: 'is', + was: 'was', + this: 'this' +} + +const plurals = { + pronoun: 'they', + is: 'are', + was: 'were', + this: 'these' +} + +module.exports = class Pluralizer { + constructor (singular, plural) { + this.singular = singular + this.plural = plural + } + + pluralize (count) { + const one = count === 1 + const keys = one ? singulars : plurals + const noun = one ? this.singular : this.plural + return { ...keys, count, noun } + } +} diff --git a/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/node/fixed-queue.js b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/node/fixed-queue.js new file mode 100644 index 000000000..35726819e --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/node/fixed-queue.js @@ -0,0 +1,117 @@ +/* eslint-disable */ + +'use strict' + +// Extracted from node/lib/internal/fixed_queue.js + +// Currently optimal queue size, tested on V8 6.0 - 6.6. Must be power of two. +const kSize = 2048; +const kMask = kSize - 1; + +// The FixedQueue is implemented as a singly-linked list of fixed-size +// circular buffers. It looks something like this: +// +// head tail +// | | +// v v +// +-----------+ <-----\ +-----------+ <------\ +-----------+ +// | [null] | \----- | next | \------- | next | +// +-----------+ +-----------+ +-----------+ +// | item | <-- bottom | item | <-- bottom | [empty] | +// | item | | item | | [empty] | +// | item | | item | | [empty] | +// | item | | item | | [empty] | +// | item | | item | bottom --> | item | +// | item | | item | | item | +// | ... | | ... | | ... | +// | item | | item | | item | +// | item | | item | | item | +// | [empty] | <-- top | item | | item | +// | [empty] | | item | | item | +// | [empty] | | [empty] | <-- top top --> | [empty] | +// +-----------+ +-----------+ +-----------+ +// +// Or, if there is only one circular buffer, it looks something +// like either of these: +// +// head tail head tail +// | | | | +// v v v v +// +-----------+ +-----------+ +// | [null] | | [null] | +// +-----------+ +-----------+ +// | [empty] | | item | +// | [empty] | | item | +// | item | <-- bottom top --> | [empty] | +// | item | | [empty] | +// | [empty] | <-- top bottom --> | item | +// | [empty] | | item | +// +-----------+ +-----------+ +// +// Adding a value means moving `top` forward by one, removing means +// moving `bottom` forward by one. After reaching the end, the queue +// wraps around. +// +// When `top === bottom` the current queue is empty and when +// `top + 1 === bottom` it's full. This wastes a single space of storage +// but allows much quicker checks. + +class FixedCircularBuffer { + constructor() { + this.bottom = 0; + this.top = 0; + this.list = new Array(kSize); + this.next = null; + } + + isEmpty() { + return this.top === this.bottom; + } + + isFull() { + return ((this.top + 1) & kMask) === this.bottom; + } + + push(data) { + this.list[this.top] = data; + this.top = (this.top + 1) & kMask; + } + + shift() { + const nextItem = this.list[this.bottom]; + if (nextItem === undefined) + return null; + this.list[this.bottom] = undefined; + this.bottom = (this.bottom + 1) & kMask; + return nextItem; + } +} + +module.exports = class FixedQueue { + constructor() { + this.head = this.tail = new FixedCircularBuffer(); + } + + isEmpty() { + return this.head.isEmpty(); + } + + push(data) { + if (this.head.isFull()) { + // Head is full: Creates a new queue, sets the old queue's `.next` to it, + // and sets it as the new main queue. + this.head = this.head.next = new FixedCircularBuffer(); + } + this.head.push(data); + } + + shift() { + const tail = this.tail; + const next = tail.shift(); + if (tail.isEmpty() && tail.next !== null) { + // If there is another queue, it forms the new tail. + this.tail = tail.next; + } + return next; + } +}; diff --git a/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/pool-base.js b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/pool-base.js new file mode 100644 index 000000000..2a909eee0 --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/pool-base.js @@ -0,0 +1,194 @@ +'use strict' + +const DispatcherBase = require('./dispatcher-base') +const FixedQueue = require('./node/fixed-queue') +const { kConnected, kSize, kRunning, kPending, kQueued, kBusy, kFree, kUrl, kClose, kDestroy, kDispatch } = require('./core/symbols') +const PoolStats = require('./pool-stats') + +const kClients = Symbol('clients') +const kNeedDrain = Symbol('needDrain') +const kQueue = Symbol('queue') +const kClosedResolve = Symbol('closed resolve') +const kOnDrain = Symbol('onDrain') +const kOnConnect = Symbol('onConnect') +const kOnDisconnect = Symbol('onDisconnect') +const kOnConnectionError = Symbol('onConnectionError') +const kGetDispatcher = Symbol('get dispatcher') +const kAddClient = Symbol('add client') +const kRemoveClient = Symbol('remove client') +const kStats = Symbol('stats') + +class PoolBase extends DispatcherBase { + constructor () { + super() + + this[kQueue] = new FixedQueue() + this[kClients] = [] + this[kQueued] = 0 + + const pool = this + + this[kOnDrain] = function onDrain (origin, targets) { + const queue = pool[kQueue] + + let needDrain = false + + while (!needDrain) { + const item = queue.shift() + if (!item) { + break + } + pool[kQueued]-- + needDrain = !this.dispatch(item.opts, item.handler) + } + + this[kNeedDrain] = needDrain + + if (!this[kNeedDrain] && pool[kNeedDrain]) { + pool[kNeedDrain] = false + pool.emit('drain', origin, [pool, ...targets]) + } + + if (pool[kClosedResolve] && queue.isEmpty()) { + Promise + .all(pool[kClients].map(c => c.close())) + .then(pool[kClosedResolve]) + } + } + + this[kOnConnect] = (origin, targets) => { + pool.emit('connect', origin, [pool, ...targets]) + } + + this[kOnDisconnect] = (origin, targets, err) => { + pool.emit('disconnect', origin, [pool, ...targets], err) + } + + this[kOnConnectionError] = (origin, targets, err) => { + pool.emit('connectionError', origin, [pool, ...targets], err) + } + + this[kStats] = new PoolStats(this) + } + + get [kBusy] () { + return this[kNeedDrain] + } + + get [kConnected] () { + return this[kClients].filter(client => client[kConnected]).length + } + + get [kFree] () { + return this[kClients].filter(client => client[kConnected] && !client[kNeedDrain]).length + } + + get [kPending] () { + let ret = this[kQueued] + for (const { [kPending]: pending } of this[kClients]) { + ret += pending + } + return ret + } + + get [kRunning] () { + let ret = 0 + for (const { [kRunning]: running } of this[kClients]) { + ret += running + } + return ret + } + + get [kSize] () { + let ret = this[kQueued] + for (const { [kSize]: size } of this[kClients]) { + ret += size + } + return ret + } + + get stats () { + return this[kStats] + } + + async [kClose] () { + if (this[kQueue].isEmpty()) { + return Promise.all(this[kClients].map(c => c.close())) + } else { + return new Promise((resolve) => { + this[kClosedResolve] = resolve + }) + } + } + + async [kDestroy] (err) { + while (true) { + const item = this[kQueue].shift() + if (!item) { + break + } + item.handler.onError(err) + } + + return Promise.all(this[kClients].map(c => c.destroy(err))) + } + + [kDispatch] (opts, handler) { + const dispatcher = this[kGetDispatcher]() + + if (!dispatcher) { + this[kNeedDrain] = true + this[kQueue].push({ opts, handler }) + this[kQueued]++ + } else if (!dispatcher.dispatch(opts, handler)) { + dispatcher[kNeedDrain] = true + this[kNeedDrain] = !this[kGetDispatcher]() + } + + return !this[kNeedDrain] + } + + [kAddClient] (client) { + client + .on('drain', this[kOnDrain]) + .on('connect', this[kOnConnect]) + .on('disconnect', this[kOnDisconnect]) + .on('connectionError', this[kOnConnectionError]) + + this[kClients].push(client) + + if (this[kNeedDrain]) { + process.nextTick(() => { + if (this[kNeedDrain]) { + this[kOnDrain](client[kUrl], [this, client]) + } + }) + } + + return this + } + + [kRemoveClient] (client) { + client.close(() => { + const idx = this[kClients].indexOf(client) + if (idx !== -1) { + this[kClients].splice(idx, 1) + } + }) + + this[kNeedDrain] = this[kClients].some(dispatcher => ( + !dispatcher[kNeedDrain] && + dispatcher.closed !== true && + dispatcher.destroyed !== true + )) + } +} + +module.exports = { + PoolBase, + kClients, + kNeedDrain, + kAddClient, + kRemoveClient, + kGetDispatcher +} diff --git a/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/pool-stats.js b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/pool-stats.js new file mode 100644 index 000000000..b4af8aeed --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/pool-stats.js @@ -0,0 +1,34 @@ +const { kFree, kConnected, kPending, kQueued, kRunning, kSize } = require('./core/symbols') +const kPool = Symbol('pool') + +class PoolStats { + constructor (pool) { + this[kPool] = pool + } + + get connected () { + return this[kPool][kConnected] + } + + get free () { + return this[kPool][kFree] + } + + get pending () { + return this[kPool][kPending] + } + + get queued () { + return this[kPool][kQueued] + } + + get running () { + return this[kPool][kRunning] + } + + get size () { + return this[kPool][kSize] + } +} + +module.exports = PoolStats diff --git a/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/pool.js b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/pool.js new file mode 100644 index 000000000..c1c20dd6b --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/pool.js @@ -0,0 +1,89 @@ +'use strict' + +const { + PoolBase, + kClients, + kNeedDrain, + kAddClient, + kGetDispatcher +} = require('./pool-base') +const Client = require('./client') +const { + InvalidArgumentError +} = require('./core/errors') +const util = require('./core/util') +const { kUrl, kInterceptors } = require('./core/symbols') +const buildConnector = require('./core/connect') + +const kOptions = Symbol('options') +const kConnections = Symbol('connections') +const kFactory = Symbol('factory') + +function defaultFactory (origin, opts) { + return new Client(origin, opts) +} + +class Pool extends PoolBase { + constructor (origin, { + connections, + factory = defaultFactory, + connect, + connectTimeout, + tls, + maxCachedSessions, + socketPath, + ...options + } = {}) { + super() + + if (connections != null && (!Number.isFinite(connections) || connections < 0)) { + throw new InvalidArgumentError('invalid connections') + } + + if (typeof factory !== 'function') { + throw new InvalidArgumentError('factory must be a function.') + } + + if (connect != null && typeof connect !== 'function' && typeof connect !== 'object') { + throw new InvalidArgumentError('connect must be a function or an object') + } + + if (typeof connect !== 'function') { + connect = buildConnector({ + ...tls, + maxCachedSessions, + socketPath, + timeout: connectTimeout == null ? 10e3 : connectTimeout, + ...connect + }) + } + + this[kInterceptors] = options.interceptors && options.interceptors.Pool && Array.isArray(options.interceptors.Pool) + ? options.interceptors.Pool + : [] + this[kConnections] = connections || null + this[kUrl] = util.parseOrigin(origin) + this[kOptions] = { ...util.deepClone(options), connect } + this[kOptions].interceptors = options.interceptors + ? { ...options.interceptors } + : undefined + this[kFactory] = factory + } + + [kGetDispatcher] () { + let dispatcher = this[kClients].find(dispatcher => !dispatcher[kNeedDrain]) + + if (dispatcher) { + return dispatcher + } + + if (!this[kConnections] || this[kClients].length < this[kConnections]) { + dispatcher = this[kFactory](this[kUrl], this[kOptions]) + this[kAddClient](dispatcher) + } + + return dispatcher + } +} + +module.exports = Pool diff --git a/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/proxy-agent.js b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/proxy-agent.js new file mode 100644 index 000000000..c2e7f268b --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/undici/lib/proxy-agent.js @@ -0,0 +1,177 @@ +'use strict' + +const { kProxy, kClose, kDestroy, kInterceptors } = require('./core/symbols') +const { URL } = require('url') +const Agent = require('./agent') +const Client = require('./client') +const DispatcherBase = require('./dispatcher-base') +const { InvalidArgumentError, RequestAbortedError } = require('./core/errors') +const buildConnector = require('./core/connect') + +const kAgent = Symbol('proxy agent') +const kClient = Symbol('proxy client') +const kProxyHeaders = Symbol('proxy headers') +const kRequestTls = Symbol('request tls settings') +const kProxyTls = Symbol('proxy tls settings') +const kConnectEndpoint = Symbol('connect endpoint function') + +function defaultProtocolPort (protocol) { + return protocol === 'https:' ? 443 : 80 +} + +function buildProxyOptions (opts) { + if (typeof opts === 'string') { + opts = { uri: opts } + } + + if (!opts || !opts.uri) { + throw new InvalidArgumentError('Proxy opts.uri is mandatory') + } + + return { + uri: opts.uri, + protocol: opts.protocol || 'https' + } +} + +class ProxyAgent extends DispatcherBase { + constructor (opts) { + super(opts) + this[kProxy] = buildProxyOptions(opts) + this[kAgent] = new Agent(opts) + this[kInterceptors] = opts.interceptors && opts.interceptors.ProxyAgent && Array.isArray(opts.interceptors.ProxyAgent) + ? opts.interceptors.ProxyAgent + : [] + + if (typeof opts === 'string') { + opts = { uri: opts } + } + + if (!opts || !opts.uri) { + throw new InvalidArgumentError('Proxy opts.uri is mandatory') + } + + this[kRequestTls] = opts.requestTls + this[kProxyTls] = opts.proxyTls + this[kProxyHeaders] = {} + + if (opts.auth && opts.token) { + throw new InvalidArgumentError('opts.auth cannot be used in combination with opts.token') + } else if (opts.auth) { + /* @deprecated in favour of opts.token */ + this[kProxyHeaders]['proxy-authorization'] = `Basic ${opts.auth}` + } else if (opts.token) { + this[kProxyHeaders]['proxy-authorization'] = opts.token + } + + const resolvedUrl = new URL(opts.uri) + const { origin, port, host } = resolvedUrl + + const connect = buildConnector({ ...opts.proxyTls }) + this[kConnectEndpoint] = buildConnector({ ...opts.requestTls }) + this[kClient] = new Client(resolvedUrl, { connect }) + this[kAgent] = new Agent({ + ...opts, + connect: async (opts, callback) => { + let requestedHost = opts.host + if (!opts.port) { + requestedHost += `:${defaultProtocolPort(opts.protocol)}` + } + try { + const { socket, statusCode } = await this[kClient].connect({ + origin, + port, + path: requestedHost, + signal: opts.signal, + headers: { + ...this[kProxyHeaders], + host + } + }) + if (statusCode !== 200) { + socket.on('error', () => {}).destroy() + callback(new RequestAbortedError('Proxy response !== 200 when HTTP Tunneling')) + } + if (opts.protocol !== 'https:') { + callback(null, socket) + return + } + let servername + if (this[kRequestTls]) { + servername = this[kRequestTls].servername + } else { + servername = opts.servername + } + this[kConnectEndpoint]({ ...opts, servername, httpSocket: socket }, callback) + } catch (err) { + callback(err) + } + } + }) + } + + dispatch (opts, handler) { + const { host } = new URL(opts.origin) + const headers = buildHeaders(opts.headers) + throwIfProxyAuthIsSent(headers) + return this[kAgent].dispatch( + { + ...opts, + headers: { + ...headers, + host + } + }, + handler + ) + } + + async [kClose] () { + await this[kAgent].close() + await this[kClient].close() + } + + async [kDestroy] () { + await this[kAgent].destroy() + await this[kClient].destroy() + } +} + +/** + * @param {string[] | Record} headers + * @returns {Record} + */ +function buildHeaders (headers) { + // When using undici.fetch, the headers list is stored + // as an array. + if (Array.isArray(headers)) { + /** @type {Record} */ + const headersPair = {} + + for (let i = 0; i < headers.length; i += 2) { + headersPair[headers[i]] = headers[i + 1] + } + + return headersPair + } + + return headers +} + +/** + * @param {Record} headers + * + * Previous versions of ProxyAgent suggests the Proxy-Authorization in request headers + * Nevertheless, it was changed and to avoid a security vulnerability by end users + * this check was created. + * It should be removed in the next major version for performance reasons + */ +function throwIfProxyAuthIsSent (headers) { + const existProxyAuth = headers && Object.keys(headers) + .find((key) => key.toLowerCase() === 'proxy-authorization') + if (existProxyAuth) { + throw new InvalidArgumentError('Proxy-Authorization should be sent in ProxyAgent constructor') + } +} + +module.exports = ProxyAgent diff --git a/tests/fixtures/npm/cases/undici5/node_modules/undici/package.json b/tests/fixtures/npm/cases/undici5/node_modules/undici/package.json new file mode 100644 index 000000000..c82f1a6e5 --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/undici/package.json @@ -0,0 +1,132 @@ +{ + "name": "undici", + "version": "5.12.0", + "description": "An HTTP/1.1 client, written from scratch for Node.js", + "homepage": "https://undici.nodejs.org", + "bugs": { + "url": "https://github.com/nodejs/undici/issues" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/nodejs/undici.git" + }, + "license": "MIT", + "author": "Matteo Collina ", + "contributors": [ + { + "name": "Robert Nagy", + "url": "https://github.com/ronag", + "author": true + } + ], + "keywords": [ + "fetch", + "http", + "https", + "promise", + "request", + "curl", + "wget", + "xhr", + "whatwg" + ], + "main": "index.js", + "types": "index.d.ts", + "files": [ + "*.d.ts", + "index.js", + "index-fetch.js", + "lib", + "types", + "docs" + ], + "scripts": { + "build:node": "npx esbuild@0.14.38 index-fetch.js --bundle --platform=node --outfile=undici-fetch.js", + "prebuild:wasm": "docker build -t llhttp_wasm_builder -f build/Dockerfile .", + "build:wasm": "node build/wasm.js --docker", + "lint": "standard | snazzy", + "lint:fix": "standard --fix | snazzy", + "test": "npm run test:tap && npm run test:node-fetch && npm run test:fetch && npm run test:wpt && npm run test:jest && tsd", + "test:node-fetch": "node scripts/verifyVersion.js 16 || mocha test/node-fetch", + "test:fetch": "node scripts/verifyVersion.js 16 || (npm run build:node && tap test/fetch/*.js && tap test/webidl/*.js)", + "test:jest": "node scripts/verifyVersion.js 14 || jest", + "test:tap": "tap test/*.js test/diagnostics-channel/*.js", + "test:tdd": "tap test/*.js test/diagnostics-channel/*.js -w", + "test:typescript": "tsd", + "test:wpt": "node scripts/verifyVersion 18 || (node test/wpt/start-fetch.mjs && node test/wpt/start-FileAPI.mjs && node test/wpt/start-mimesniff.mjs && node test/wpt/start-xhr.mjs)", + "coverage": "nyc --reporter=text --reporter=html npm run test", + "coverage:ci": "nyc --reporter=lcov npm run test", + "bench": "PORT=3042 concurrently -k -s first npm:bench:server npm:bench:run", + "bench:server": "node benchmarks/server.js", + "prebench:run": "node benchmarks/wait.js", + "bench:run": "CONNECTIONS=1 node --experimental-wasm-simd benchmarks/benchmark.js; CONNECTIONS=50 node --experimental-wasm-simd benchmarks/benchmark.js", + "serve:website": "docsify serve .", + "prepare": "husky install", + "fuzz": "jsfuzz test/fuzzing/fuzz.js corpus" + }, + "devDependencies": { + "@sinonjs/fake-timers": "^9.1.2", + "@types/node": "^18.0.3", + "abort-controller": "^3.0.0", + "atomic-sleep": "^1.0.0", + "chai": "^4.3.4", + "chai-as-promised": "^7.1.1", + "chai-iterator": "^3.0.2", + "chai-string": "^1.5.0", + "concurrently": "^7.1.0", + "cronometro": "^1.0.5", + "delay": "^5.0.0", + "docsify-cli": "^4.4.3", + "formdata-node": "^4.3.1", + "https-pem": "^3.0.0", + "husky": "^8.0.1", + "import-fresh": "^3.3.0", + "jest": "^29.0.2", + "jsfuzz": "^1.0.15", + "mocha": "^10.0.0", + "p-timeout": "^3.2.0", + "pre-commit": "^1.2.2", + "proxy": "^1.0.2", + "proxyquire": "^2.1.3", + "semver": "^7.3.5", + "sinon": "^14.0.0", + "snazzy": "^9.0.0", + "standard": "^17.0.0", + "table": "^6.8.0", + "tap": "^16.1.0", + "tsd": "^0.24.1", + "wait-on": "^6.0.0" + }, + "engines": { + "node": ">=12.18" + }, + "standard": { + "env": [ + "mocha" + ], + "ignore": [ + "lib/llhttp/constants.js", + "lib/llhttp/utils.js", + "test/wpt/tests", + "test/wpt/runner/resources" + ] + }, + "tsd": { + "directory": "test/types", + "compilerOptions": { + "esModuleInterop": true, + "lib": [ + "esnext", + "DOM" + ] + } + }, + "jest": { + "testMatch": [ + "/test/jest/**" + ] + }, + "dependencies": { + "busboy": "^1.6.0" + } +} diff --git a/tests/fixtures/npm/cases/undici5/node_modules/undici/types/agent.d.ts b/tests/fixtures/npm/cases/undici5/node_modules/undici/types/agent.d.ts new file mode 100644 index 000000000..c09260b29 --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/undici/types/agent.d.ts @@ -0,0 +1,32 @@ +import { URL } from 'url' +import Dispatcher = require('./dispatcher') +import Pool = require('./pool') +import {DispatchInterceptor} from "./dispatcher"; + +export = Agent + +declare class Agent extends Dispatcher{ + constructor(opts?: Agent.Options) + /** `true` after `dispatcher.close()` has been called. */ + closed: boolean; + /** `true` after `dispatcher.destroyed()` has been called or `dispatcher.close()` has been called and the dispatcher shutdown has completed. */ + destroyed: boolean; + /** Dispatches a request. */ + dispatch(options: Agent.DispatchOptions, handler: Dispatcher.DispatchHandlers): boolean; +} + +declare namespace Agent { + export interface Options extends Pool.Options { + /** Default: `(origin, opts) => new Pool(origin, opts)`. */ + factory?(origin: URL, opts: Object): Dispatcher; + /** Integer. Default: `0` */ + maxRedirections?: number; + + interceptors?: { Agent?: readonly DispatchInterceptor[] } & Pool.Options["interceptors"] + } + + export interface DispatchOptions extends Dispatcher.DispatchOptions { + /** Integer. */ + maxRedirections?: number; + } +} diff --git a/tests/fixtures/npm/cases/undici5/node_modules/undici/types/api.d.ts b/tests/fixtures/npm/cases/undici5/node_modules/undici/types/api.d.ts new file mode 100644 index 000000000..4bc3183e8 --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/undici/types/api.d.ts @@ -0,0 +1,43 @@ +import { URL, UrlObject } from 'url' +import { Duplex } from 'stream' +import Dispatcher = require('./dispatcher') + +export { + request, + stream, + pipeline, + connect, + upgrade, +} + +/** Performs an HTTP request. */ +declare function request( + url: string | URL | UrlObject, + options?: { dispatcher?: Dispatcher } & Omit & Partial>, +): Promise; + +/** A faster version of `request`. */ +declare function stream( + url: string | URL | UrlObject, + options: { dispatcher?: Dispatcher } & Omit, + factory: Dispatcher.StreamFactory +): Promise; + +/** For easy use with `stream.pipeline`. */ +declare function pipeline( + url: string | URL | UrlObject, + options: { dispatcher?: Dispatcher } & Omit, + handler: Dispatcher.PipelineHandler +): Duplex; + +/** Starts two-way communications with the requested resource. */ +declare function connect( + url: string | URL | UrlObject, + options?: { dispatcher?: Dispatcher } & Omit +): Promise; + +/** Upgrade to a different protocol. */ +declare function upgrade( + url: string | URL | UrlObject, + options?: { dispatcher?: Dispatcher } & Omit +): Promise; diff --git a/tests/fixtures/npm/cases/undici5/node_modules/undici/types/balanced-pool.d.ts b/tests/fixtures/npm/cases/undici5/node_modules/undici/types/balanced-pool.d.ts new file mode 100644 index 000000000..5a765e15e --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/undici/types/balanced-pool.d.ts @@ -0,0 +1,19 @@ +import Client = require('./client') +import Pool = require('./pool') +import Dispatcher = require('./dispatcher') +import { URL } from 'url' + +export = BalancedPool + +declare class BalancedPool extends Dispatcher { + constructor(url: string | URL | string[], options?: Pool.Options); + + addUpstream(upstream: string): BalancedPool; + removeUpstream(upstream: string): BalancedPool; + upstreams: Array; + + /** `true` after `pool.close()` has been called. */ + closed: boolean; + /** `true` after `pool.destroyed()` has been called or `pool.close()` has been called and the pool shutdown has completed. */ + destroyed: boolean; +} diff --git a/tests/fixtures/npm/cases/undici5/node_modules/undici/types/client.d.ts b/tests/fixtures/npm/cases/undici5/node_modules/undici/types/client.d.ts new file mode 100644 index 000000000..d87f08f1a --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/undici/types/client.d.ts @@ -0,0 +1,62 @@ +import { URL } from 'url' +import { TlsOptions } from 'tls' +import Dispatcher = require('./dispatcher') +import {DispatchInterceptor} from './dispatcher' +import buildConnector, {connector} from "./connector"; + +export = Client + +/** A basic HTTP/1.1 client, mapped on top a single TCP/TLS connection. Pipelining is disabled by default. */ +declare class Client extends Dispatcher { + constructor(url: string | URL, options?: Client.Options); + /** Property to get and set the pipelining factor. */ + pipelining: number; + /** `true` after `client.close()` has been called. */ + closed: boolean; + /** `true` after `client.destroyed()` has been called or `client.close()` has been called and the client shutdown has completed. */ + destroyed: boolean; +} + +declare namespace Client { + export interface Options { + /** the timeout after which a socket without active requests will time out. Monitors time between activity on a connected socket. This value may be overridden by *keep-alive* hints from the server. Default: `4e3` milliseconds (4s). */ + keepAliveTimeout?: number | null; + /** the maximum allowed `idleTimeout` when overridden by *keep-alive* hints from the server. Default: `600e3` milliseconds (10min). */ + keepAliveMaxTimeout?: number | null; + /** A number subtracted from server *keep-alive* hints when overriding `idleTimeout` to account for timing inaccuracies caused by e.g. transport latency. Default: `1e3` milliseconds (1s). */ + keepAliveTimeoutThreshold?: number | null; + /** The amount of concurrent requests to be sent over the single TCP/TLS connection according to [RFC7230](https://tools.ietf.org/html/rfc7230#section-6.3.2). Default: `1`. */ + pipelining?: number | null; + /** **/ + connect?: buildConnector.BuildOptions | connector | null; + /** The maximum length of request headers in bytes. Default: `16384` (16KiB). */ + maxHeaderSize?: number | null; + /** The timeout after which a request will time out, in milliseconds. Monitors time between receiving body data. Use `0` to disable it entirely. Default: `30e3` milliseconds (30s). */ + bodyTimeout?: number | null; + /** The amount of time the parser will wait to receive the complete HTTP headers (Node 14 and above only). Default: `30e3` milliseconds (30s). */ + headersTimeout?: number | null; + /** If `true`, an error is thrown when the request content-length header doesn't match the length of the request body. Default: `true`. */ + strictContentLength?: boolean; + /** @deprecated use the connect option instead */ + tls?: TlsOptions | null; + /** */ + maxRequestsPerClient?: number; + /** Max response body size in bytes, -1 is disabled */ + maxResponseSize?: number | null; + + interceptors?: {Client: readonly DispatchInterceptor[] | undefined} + } + + export interface SocketInfo { + localAddress?: string + localPort?: number + remoteAddress?: string + remotePort?: number + remoteFamily?: string + timeout?: number + bytesWritten?: number + bytesRead?: number + } + + +} diff --git a/tests/fixtures/npm/cases/undici5/node_modules/undici/types/connector.d.ts b/tests/fixtures/npm/cases/undici5/node_modules/undici/types/connector.d.ts new file mode 100644 index 000000000..9a47f87e5 --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/undici/types/connector.d.ts @@ -0,0 +1,35 @@ +import {TLSSocket, ConnectionOptions} from 'tls' +import {IpcNetConnectOpts, Socket, TcpNetConnectOpts} from 'net' + +export = buildConnector +declare function buildConnector (options?: buildConnector.BuildOptions): buildConnector.connector + +declare namespace buildConnector { + export type BuildOptions = (ConnectionOptions | TcpNetConnectOpts | IpcNetConnectOpts) & { + maxCachedSessions?: number | null; + socketPath?: string | null; + timeout?: number | null; + port?: number; + } + + export interface Options { + hostname: string + host?: string + protocol: string + port: number + servername?: string + } + + export type Callback = (...args: CallbackArgs) => void + type CallbackArgs = [null, Socket | TLSSocket] | [Error, null] + + export type connector = connectorAsync | connectorSync + + interface connectorSync { + (options: buildConnector.Options): Socket | TLSSocket + } + + interface connectorAsync { + (options: buildConnector.Options, callback: buildConnector.Callback): void + } +} diff --git a/tests/fixtures/npm/cases/undici5/node_modules/undici/types/diagnostics-channel.d.ts b/tests/fixtures/npm/cases/undici5/node_modules/undici/types/diagnostics-channel.d.ts new file mode 100644 index 000000000..6c754491b --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/undici/types/diagnostics-channel.d.ts @@ -0,0 +1,67 @@ +import { Socket } from "net"; +import { URL } from "url"; +import { connector } from "./connector"; +import { HttpMethod } from "./dispatcher"; + +declare namespace DiagnosticsChannel { + interface Request { + origin?: string | URL; + completed: boolean; + method?: HttpMethod; + path: string; + headers: string; + addHeader(key: string, value: string): Request; + } + interface Response { + statusCode: number; + statusText: string; + headers: Array; + } + type Error = unknown; + interface ConnectParams { + host: URL["host"]; + hostname: URL["hostname"]; + protocol: URL["protocol"]; + port: URL["port"]; + servername: string | null; + } + type Connector = connector; + export interface RequestCreateMessage { + request: Request; + } + export interface RequestBodySentMessage { + request: Request; + } + export interface RequestHeadersMessage { + request: Request; + response: Response; + } + export interface RequestTrailersMessage { + request: Request; + trailers: Array; + } + export interface RequestErrorMessage { + request: Request; + error: Error; + } + export interface ClientSendHeadersMessage { + request: Request; + headers: string; + socket: Socket; + } + export interface ClientBeforeConnectMessage { + connectParams: ConnectParams; + connector: Connector; + } + export interface ClientConnectedMessage { + socket: Socket; + connectParams: ConnectParams; + connector: Connector; + } + export interface ClientConnectErrorMessage { + error: Error; + socket: Socket; + connectParams: ConnectParams; + connector: Connector; + } +} diff --git a/tests/fixtures/npm/cases/undici5/node_modules/undici/types/dispatcher.d.ts b/tests/fixtures/npm/cases/undici5/node_modules/undici/types/dispatcher.d.ts new file mode 100644 index 000000000..cbd558a93 --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/undici/types/dispatcher.d.ts @@ -0,0 +1,233 @@ +import { URL } from 'url' +import { Duplex, Readable, Writable } from 'stream' +import { EventEmitter } from 'events' +import { IncomingHttpHeaders } from 'http' +import { Blob } from 'buffer' +import type BodyReadable from './readable' +import { FormData } from './formdata' +import { UndiciError } from './errors' + +type AbortSignal = unknown; + +export = Dispatcher; + +/** Dispatcher is the core API used to dispatch requests. */ +declare class Dispatcher extends EventEmitter { + /** Dispatches a request. This API is expected to evolve through semver-major versions and is less stable than the preceding higher level APIs. It is primarily intended for library developers who implement higher level APIs on top of this. */ + dispatch(options: Dispatcher.DispatchOptions, handler: Dispatcher.DispatchHandlers): boolean; + /** Starts two-way communications with the requested resource. */ + connect(options: Dispatcher.ConnectOptions): Promise; + connect(options: Dispatcher.ConnectOptions, callback: (err: Error | null, data: Dispatcher.ConnectData) => void): void; + /** Performs an HTTP request. */ + request(options: Dispatcher.RequestOptions): Promise; + request(options: Dispatcher.RequestOptions, callback: (err: Error | null, data: Dispatcher.ResponseData) => void): void; + /** For easy use with `stream.pipeline`. */ + pipeline(options: Dispatcher.PipelineOptions, handler: Dispatcher.PipelineHandler): Duplex; + /** A faster version of `Dispatcher.request`. */ + stream(options: Dispatcher.RequestOptions, factory: Dispatcher.StreamFactory): Promise; + stream(options: Dispatcher.RequestOptions, factory: Dispatcher.StreamFactory, callback: (err: Error | null, data: Dispatcher.StreamData) => void): void; + /** Upgrade to a different protocol. */ + upgrade(options: Dispatcher.UpgradeOptions): Promise; + upgrade(options: Dispatcher.UpgradeOptions, callback: (err: Error | null, data: Dispatcher.UpgradeData) => void): void; + /** Closes the client and gracefully waits for enqueued requests to complete before invoking the callback (or returning a promise if no callback is provided). */ + close(): Promise; + close(callback: () => void): void; + /** Destroy the client abruptly with the given err. All the pending and running requests will be asynchronously aborted and error. Waits until socket is closed before invoking the callback (or returning a promise if no callback is provided). Since this operation is asynchronously dispatched there might still be some progress on dispatched requests. */ + destroy(): Promise; + destroy(err: Error | null): Promise; + destroy(callback: () => void): void; + destroy(err: Error | null, callback: () => void): void; + + on(eventName: 'connect', callback: (origin: URL, targets: readonly Dispatcher[]) => void): this; + on(eventName: 'disconnect', callback: (origin: URL, targets: readonly Dispatcher[], error: UndiciError) => void): this; + on(eventName: 'connectionError', callback: (origin: URL, targets: readonly Dispatcher[], error: UndiciError) => void): this; + on(eventName: 'drain', callback: (origin: URL) => void): this; + + + once(eventName: 'connect', callback: (origin: URL, targets: readonly Dispatcher[]) => void): this; + once(eventName: 'disconnect', callback: (origin: URL, targets: readonly Dispatcher[], error: UndiciError) => void): this; + once(eventName: 'connectionError', callback: (origin: URL, targets: readonly Dispatcher[], error: UndiciError) => void): this; + once(eventName: 'drain', callback: (origin: URL) => void): this; + + + off(eventName: 'connect', callback: (origin: URL, targets: readonly Dispatcher[]) => void): this; + off(eventName: 'disconnect', callback: (origin: URL, targets: readonly Dispatcher[], error: UndiciError) => void): this; + off(eventName: 'connectionError', callback: (origin: URL, targets: readonly Dispatcher[], error: UndiciError) => void): this; + off(eventName: 'drain', callback: (origin: URL) => void): this; + + + addListener(eventName: 'connect', callback: (origin: URL, targets: readonly Dispatcher[]) => void): this; + addListener(eventName: 'disconnect', callback: (origin: URL, targets: readonly Dispatcher[], error: UndiciError) => void): this; + addListener(eventName: 'connectionError', callback: (origin: URL, targets: readonly Dispatcher[], error: UndiciError) => void): this; + addListener(eventName: 'drain', callback: (origin: URL) => void): this; + + removeListener(eventName: 'connect', callback: (origin: URL, targets: readonly Dispatcher[]) => void): this; + removeListener(eventName: 'disconnect', callback: (origin: URL, targets: readonly Dispatcher[], error: UndiciError) => void): this; + removeListener(eventName: 'connectionError', callback: (origin: URL, targets: readonly Dispatcher[], error: UndiciError) => void): this; + removeListener(eventName: 'drain', callback: (origin: URL) => void): this; + + prependListener(eventName: 'connect', callback: (origin: URL, targets: readonly Dispatcher[]) => void): this; + prependListener(eventName: 'disconnect', callback: (origin: URL, targets: readonly Dispatcher[], error: UndiciError) => void): this; + prependListener(eventName: 'connectionError', callback: (origin: URL, targets: readonly Dispatcher[], error: UndiciError) => void): this; + prependListener(eventName: 'drain', callback: (origin: URL) => void): this; + + prependOnceListener(eventName: 'connect', callback: (origin: URL, targets: readonly Dispatcher[]) => void): this; + prependOnceListener(eventName: 'disconnect', callback: (origin: URL, targets: readonly Dispatcher[], error: UndiciError) => void): this; + prependOnceListener(eventName: 'connectionError', callback: (origin: URL, targets: readonly Dispatcher[], error: UndiciError) => void): this; + prependOnceListener(eventName: 'drain', callback: (origin: URL) => void): this; + + listeners(eventName: 'connect'): ((origin: URL, targets: readonly Dispatcher[]) => void)[] + listeners(eventName: 'disconnect'): ((origin: URL, targets: readonly Dispatcher[], error: UndiciError) => void)[]; + listeners(eventName: 'connectionError'): ((origin: URL, targets: readonly Dispatcher[], error: UndiciError) => void)[]; + listeners(eventName: 'drain'): ((origin: URL) => void)[]; + + rawListeners(eventName: 'connect'): ((origin: URL, targets: readonly Dispatcher[]) => void)[] + rawListeners(eventName: 'disconnect'): ((origin: URL, targets: readonly Dispatcher[], error: UndiciError) => void)[]; + rawListeners(eventName: 'connectionError'): ((origin: URL, targets: readonly Dispatcher[], error: UndiciError) => void)[]; + rawListeners(eventName: 'drain'): ((origin: URL) => void)[]; + + emit(eventName: 'connect', origin: URL, targets: readonly Dispatcher[]): boolean; + emit(eventName: 'disconnect', origin: URL, targets: readonly Dispatcher[], error: UndiciError): boolean; + emit(eventName: 'connectionError', origin: URL, targets: readonly Dispatcher[], error: UndiciError): boolean; + emit(eventName: 'drain', origin: URL): boolean; +} + +declare namespace Dispatcher { + export interface DispatchOptions { + origin?: string | URL; + path: string; + method: HttpMethod; + /** Default: `null` */ + body?: string | Buffer | Uint8Array | Readable | null | FormData; + /** Default: `null` */ + headers?: IncomingHttpHeaders | string[] | null; + /** Query string params to be embedded in the request URL. Default: `null` */ + query?: Record; + /** Whether the requests can be safely retried or not. If `false` the request won't be sent until all preceding requests in the pipeline have completed. Default: `true` if `method` is `HEAD` or `GET`. */ + idempotent?: boolean; + /** Upgrade the request. Should be used to specify the kind of upgrade i.e. `'Websocket'`. Default: `method === 'CONNECT' || null`. */ + upgrade?: boolean | string | null; + /** The amount of time the parser will wait to receive the complete HTTP headers. Defaults to 30 seconds. */ + headersTimeout?: number | null; + /** The timeout after which a request will time out, in milliseconds. Monitors time between receiving body data. Use 0 to disable it entirely. Defaults to 30 seconds. */ + bodyTimeout?: number | null; + /** Whether Undici should throw an error upon receiving a 4xx or 5xx response from the server. Defaults to false */ + throwOnError?: boolean; + } + export interface ConnectOptions { + path: string; + /** Default: `null` */ + headers?: IncomingHttpHeaders | string[] | null; + /** Default: `null` */ + signal?: AbortSignal | EventEmitter | null; + /** This argument parameter is passed through to `ConnectData` */ + opaque?: unknown; + /** Default: 0 */ + maxRedirections?: number; + /** Default: `null` */ + responseHeader?: 'raw' | null; + } + export interface RequestOptions extends DispatchOptions { + /** Default: `null` */ + opaque?: unknown; + /** Default: `null` */ + signal?: AbortSignal | EventEmitter | null; + /** Default: 0 */ + maxRedirections?: number; + /** Default: `null` */ + onInfo?: (info: { statusCode: number, headers: Record }) => void; + /** Default: `null` */ + responseHeader?: 'raw' | null; + } + export interface PipelineOptions extends RequestOptions { + /** `true` if the `handler` will return an object stream. Default: `false` */ + objectMode?: boolean; + } + export interface UpgradeOptions { + path: string; + /** Default: `'GET'` */ + method?: string; + /** Default: `null` */ + headers?: IncomingHttpHeaders | string[] | null; + /** A string of comma separated protocols, in descending preference order. Default: `'Websocket'` */ + protocol?: string; + /** Default: `null` */ + signal?: AbortSignal | EventEmitter | null; + /** Default: 0 */ + maxRedirections?: number; + /** Default: `null` */ + responseHeader?: 'raw' | null; + } + export interface ConnectData { + statusCode: number; + headers: IncomingHttpHeaders; + socket: Duplex; + opaque: unknown; + } + export interface ResponseData { + statusCode: number; + headers: IncomingHttpHeaders; + body: BodyReadable & BodyMixin; + trailers: Record; + opaque: unknown; + context: object; + } + export interface PipelineHandlerData { + statusCode: number; + headers: IncomingHttpHeaders; + opaque: unknown; + body: BodyReadable; + context: object; + } + export interface StreamData { + opaque: unknown; + trailers: Record; + } + export interface UpgradeData { + headers: IncomingHttpHeaders; + socket: Duplex; + opaque: unknown; + } + export interface StreamFactoryData { + statusCode: number; + headers: IncomingHttpHeaders; + opaque: unknown; + context: object; + } + export type StreamFactory = (data: StreamFactoryData) => Writable; + export interface DispatchHandlers { + /** Invoked before request is dispatched on socket. May be invoked multiple times when a request is retried when the request at the head of the pipeline fails. */ + onConnect?(abort: () => void): void; + /** Invoked when an error has occurred. */ + onError?(err: Error): void; + /** Invoked when request is upgraded either due to a `Upgrade` header or `CONNECT` method. */ + onUpgrade?(statusCode: number, headers: Buffer[] | string[] | null, socket: Duplex): void; + /** Invoked when statusCode and headers have been received. May be invoked multiple times due to 1xx informational headers. */ + onHeaders?(statusCode: number, headers: Buffer[] | string[] | null, resume: () => void): boolean; + /** Invoked when response payload data is received. */ + onData?(chunk: Buffer): boolean; + /** Invoked when response payload and trailers have been received and the request has completed. */ + onComplete?(trailers: string[] | null): void; + /** Invoked when a body chunk is sent to the server. May be invoked multiple times for chunked requests */ + onBodySent?(chunkSize: number, totalBytesSent: number): void; + } + export type PipelineHandler = (data: PipelineHandlerData) => Readable; + export type HttpMethod = 'GET' | 'HEAD' | 'POST' | 'PUT' | 'DELETE' | 'CONNECT' | 'OPTIONS' | 'TRACE' | 'PATCH'; + + /** + * @link https://fetch.spec.whatwg.org/#body-mixin + */ + interface BodyMixin { + readonly body?: never; // throws on node v16.6.0 + readonly bodyUsed: boolean; + arrayBuffer(): Promise; + blob(): Promise; + formData(): Promise; + json(): Promise; + text(): Promise; + } + + export interface DispatchInterceptor { + (dispatch: Dispatcher['dispatch']): Dispatcher['dispatch'] + } +} diff --git a/tests/fixtures/npm/cases/undici5/node_modules/undici/types/errors.d.ts b/tests/fixtures/npm/cases/undici5/node_modules/undici/types/errors.d.ts new file mode 100644 index 000000000..492e31eb2 --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/undici/types/errors.d.ts @@ -0,0 +1,96 @@ +import {IncomingHttpHeaders} from "http"; + +export = Errors +import { SocketInfo } from './client' + +declare namespace Errors { + export class UndiciError extends Error { } + + /** A header exceeds the `headersTimeout` option. */ + export class HeadersTimeoutError extends UndiciError { + name: 'HeadersTimeoutError'; + code: 'UND_ERR_HEADERS_TIMEOUT'; + } + + /** A body exceeds the `bodyTimeout` option. */ + export class BodyTimeoutError extends UndiciError { + name: 'BodyTimeoutError'; + code: 'UND_ERR_BODY_TIMEOUT'; + } + + export class ResponseStatusCodeError extends UndiciError { + name: 'ResponseStatusCodeError'; + code: 'UND_ERR_RESPONSE_STATUS_CODE'; + body: null | Record | string + status: number + statusCode: number + headers: IncomingHttpHeaders | string[] | null; + } + + /** A socket exceeds the `socketTimeout` option. */ + export class SocketTimeoutError extends UndiciError { + name: 'SocketTimeoutError'; + code: 'UND_ERR_SOCKET_TIMEOUT'; + } + + /** Passed an invalid argument. */ + export class InvalidArgumentError extends UndiciError { + name: 'InvalidArgumentError'; + code: 'UND_ERR_INVALID_ARG'; + } + + /** Returned an invalid value. */ + export class InvalidReturnError extends UndiciError { + name: 'InvalidReturnError'; + code: 'UND_ERR_INVALID_RETURN_VALUE'; + } + + /** The request has been aborted by the user. */ + export class RequestAbortedError extends UndiciError { + name: 'RequestAbortedError'; + code: 'UND_ERR_ABORTED'; + } + + /** Expected error with reason. */ + export class InformationalError extends UndiciError { + name: 'InformationalError'; + code: 'UND_ERR_INFO'; + } + + /** Body does not match content-length header. */ + export class RequestContentLengthMismatchError extends UndiciError { + name: 'RequestContentLengthMismatchError'; + code: 'UND_ERR_REQ_CONTENT_LENGTH_MISMATCH'; + } + + /** Trying to use a destroyed client. */ + export class ClientDestroyedError extends UndiciError { + name: 'ClientDestroyedError'; + code: 'UND_ERR_DESTROYED'; + } + + /** Trying to use a closed client. */ + export class ClientClosedError extends UndiciError { + name: 'ClientClosedError'; + code: 'UND_ERR_CLOSED'; + } + + /** There is an error with the socket. */ + export class SocketError extends UndiciError { + name: 'SocketError'; + code: 'UND_ERR_SOCKET'; + socket: SocketInfo | null + } + + /** Encountered unsupported functionality. */ + export class NotSupportedError extends UndiciError { + name: 'NotSupportedError'; + code: 'UND_ERR_NOT_SUPPORTED'; + } + + /** The response exceed the length allowed */ + export class ResponseExceededMaxSizeError extends UndiciError { + name: 'ResponseExceededMaxSizeError'; + code: 'UND_ERR_RES_EXCEEDED_MAX_SIZE'; + } +} diff --git a/tests/fixtures/npm/cases/undici5/node_modules/undici/types/fetch.d.ts b/tests/fixtures/npm/cases/undici5/node_modules/undici/types/fetch.d.ts new file mode 100644 index 000000000..d38e0d612 --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/undici/types/fetch.d.ts @@ -0,0 +1,208 @@ +// based on https://github.com/Ethan-Arrowood/undici-fetch/blob/249269714db874351589d2d364a0645d5160ae71/index.d.ts (MIT license) +// and https://github.com/node-fetch/node-fetch/blob/914ce6be5ec67a8bab63d68510aabf07cb818b6d/index.d.ts (MIT license) +/// + +import { Blob } from 'buffer' +import { URL, URLSearchParams } from 'url' +import { ReadableStream } from 'stream/web' +import { FormData } from './formdata' + +import Dispatcher = require('./dispatcher') + +export type RequestInfo = string | URL | Request + +export declare function fetch ( + input: RequestInfo, + init?: RequestInit +): Promise + +export type BodyInit = + | ArrayBuffer + | AsyncIterable + | Blob + | FormData + | Iterable + | NodeJS.ArrayBufferView + | URLSearchParams + | null + | string + +export interface BodyMixin { + readonly body: ReadableStream | null + readonly bodyUsed: boolean + + readonly arrayBuffer: () => Promise + readonly blob: () => Promise + readonly formData: () => Promise + readonly json: () => Promise + readonly text: () => Promise +} + +export interface SpecIterator { + next(...args: [] | [TNext]): IteratorResult; +} + +export interface SpecIterableIterator extends SpecIterator { + [Symbol.iterator](): SpecIterableIterator; +} + +export interface SpecIterable { + [Symbol.iterator](): SpecIterator; +} + +export type HeadersInit = string[][] | Record> | Headers + +export declare class Headers implements SpecIterable<[string, string]> { + constructor (init?: HeadersInit) + readonly append: (name: string, value: string) => void + readonly delete: (name: string) => void + readonly get: (name: string) => string | null + readonly has: (name: string) => boolean + readonly set: (name: string, value: string) => void + readonly forEach: ( + callbackfn: (value: string, key: string, iterable: Headers) => void, + thisArg?: unknown + ) => void + + readonly keys: () => SpecIterableIterator + readonly values: () => SpecIterableIterator + readonly entries: () => SpecIterableIterator<[string, string]> + readonly [Symbol.iterator]: () => SpecIterator<[string, string]> +} + +export type RequestCache = + | 'default' + | 'force-cache' + | 'no-cache' + | 'no-store' + | 'only-if-cached' + | 'reload' + +export type RequestCredentials = 'omit' | 'include' | 'same-origin' + +type RequestDestination = + | '' + | 'audio' + | 'audioworklet' + | 'document' + | 'embed' + | 'font' + | 'image' + | 'manifest' + | 'object' + | 'paintworklet' + | 'report' + | 'script' + | 'sharedworker' + | 'style' + | 'track' + | 'video' + | 'worker' + | 'xslt' + +export interface RequestInit { + method?: string + keepalive?: boolean + headers?: HeadersInit + body?: BodyInit + redirect?: RequestRedirect + integrity?: string + signal?: AbortSignal + credentials?: RequestCredentials + mode?: RequestMode + referrer?: string + referrerPolicy?: ReferrerPolicy + window?: null + dispatcher?: Dispatcher + duplex?: RequestDuplex +} + +export type ReferrerPolicy = + | '' + | 'no-referrer' + | 'no-referrer-when-downgrade' + | 'origin' + | 'origin-when-cross-origin' + | 'same-origin' + | 'strict-origin' + | 'strict-origin-when-cross-origin' + | 'unsafe-url'; + +export type RequestMode = 'cors' | 'navigate' | 'no-cors' | 'same-origin' + +export type RequestRedirect = 'error' | 'follow' | 'manual' + +export type RequestDuplex = 'half' + +export declare class Request implements BodyMixin { + constructor (input: RequestInfo, init?: RequestInit) + + readonly cache: RequestCache + readonly credentials: RequestCredentials + readonly destination: RequestDestination + readonly headers: Headers + readonly integrity: string + readonly method: string + readonly mode: RequestMode + readonly redirect: RequestRedirect + readonly referrerPolicy: string + readonly url: string + + readonly keepalive: boolean + readonly signal: AbortSignal + readonly duplex: RequestDuplex + + readonly body: ReadableStream | null + readonly bodyUsed: boolean + + readonly arrayBuffer: () => Promise + readonly blob: () => Promise + readonly formData: () => Promise + readonly json: () => Promise + readonly text: () => Promise + + readonly clone: () => Request +} + +export interface ResponseInit { + readonly status?: number + readonly statusText?: string + readonly headers?: HeadersInit +} + +export type ResponseType = + | 'basic' + | 'cors' + | 'default' + | 'error' + | 'opaque' + | 'opaqueredirect' + +export type ResponseRedirectStatus = 301 | 302 | 303 | 307 | 308 + +export declare class Response implements BodyMixin { + constructor (body?: BodyInit, init?: ResponseInit) + + readonly headers: Headers + readonly ok: boolean + readonly status: number + readonly statusText: string + readonly type: ResponseType + readonly url: string + readonly redirected: boolean + + readonly body: ReadableStream | null + readonly bodyUsed: boolean + + readonly arrayBuffer: () => Promise + readonly blob: () => Promise + readonly formData: () => Promise + readonly json: () => Promise + readonly text: () => Promise + + readonly clone: () => Response + + static error (): Response + static json(data: any, init?: ResponseInit): Response + static redirect (url: string | URL, status: ResponseRedirectStatus): Response +} diff --git a/tests/fixtures/npm/cases/undici5/node_modules/undici/types/file.d.ts b/tests/fixtures/npm/cases/undici5/node_modules/undici/types/file.d.ts new file mode 100644 index 000000000..c695b7ab0 --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/undici/types/file.d.ts @@ -0,0 +1,39 @@ +// Based on https://github.com/octet-stream/form-data/blob/2d0f0dc371517444ce1f22cdde13f51995d0953a/lib/File.ts (MIT) +/// + +import { Blob } from 'buffer' + +export interface BlobPropertyBag { + type?: string + endings?: 'native' | 'transparent' +} + +export interface FilePropertyBag extends BlobPropertyBag { + /** + * The last modified date of the file as the number of milliseconds since the Unix epoch (January 1, 1970 at midnight). Files without a known last modified date return the current date. + */ + lastModified?: number +} + +export declare class File extends Blob { + /** + * Creates a new File instance. + * + * @param fileBits An `Array` strings, or [`ArrayBuffer`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer), [`ArrayBufferView`](https://developer.mozilla.org/en-US/docs/Web/API/ArrayBufferView), [`Blob`](https://developer.mozilla.org/en-US/docs/Web/API/Blob) objects, or a mix of any of such objects, that will be put inside the [`File`](https://developer.mozilla.org/en-US/docs/Web/API/File). + * @param fileName The name of the file. + * @param options An options object containing optional attributes for the file. + */ + constructor(fileBits: ReadonlyArray, fileName: string, options?: FilePropertyBag) + + /** + * Name of the file referenced by the File object. + */ + readonly name: string + + /** + * The last modified date of the file as the number of milliseconds since the Unix epoch (January 1, 1970 at midnight). Files without a known last modified date return the current date. + */ + readonly lastModified: number + + readonly [Symbol.toStringTag]: string +} diff --git a/tests/fixtures/npm/cases/undici5/node_modules/undici/types/filereader.d.ts b/tests/fixtures/npm/cases/undici5/node_modules/undici/types/filereader.d.ts new file mode 100644 index 000000000..8bd7e15c2 --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/undici/types/filereader.d.ts @@ -0,0 +1,49 @@ +/// + +import { Blob } from 'buffer' + +export declare class FileReader extends EventTarget { + constructor () + + readAsArrayBuffer (blob: Blob): void + readAsBinaryString (blob: Blob): void + readAsText (blob: Blob, encoding?: string): void + readAsDataURL (blob: Blob): void + + abort (): void + + static readonly EMPTY = 0 + static readonly LOADING = 1 + static readonly DONE = 2 + + readonly EMPTY = 0 + readonly LOADING = 1 + readonly DONE = 2 + + readonly readyState: number + + readonly result: string | ArrayBuffer | null + + readonly error: DOMException | null + + onloadstart: null | ((this: FileReader, event: ProgressEvent) => void) + onprogress: null | ((this: FileReader, event: ProgressEvent) => void) + onload: null | ((this: FileReader, event: ProgressEvent) => void) + onabort: null | ((this: FileReader, event: ProgressEvent) => void) + onerror: null | ((this: FileReader, event: ProgressEvent) => void) + onloadend: null | ((this: FileReader, event: ProgressEvent) => void) +} + +export interface ProgressEventInit extends EventInit { + lengthComputable?: boolean + loaded?: number + total?: number +} + +export declare class ProgressEvent extends Event { + constructor (type: string, eventInitDict?: ProgressEventInit) + + readonly lengthComputable: boolean + readonly loaded: number + readonly total: number +} \ No newline at end of file diff --git a/tests/fixtures/npm/cases/undici5/node_modules/undici/types/formdata.d.ts b/tests/fixtures/npm/cases/undici5/node_modules/undici/types/formdata.d.ts new file mode 100644 index 000000000..df29a572d --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/undici/types/formdata.d.ts @@ -0,0 +1,108 @@ +// Based on https://github.com/octet-stream/form-data/blob/2d0f0dc371517444ce1f22cdde13f51995d0953a/lib/FormData.ts (MIT) +/// + +import { File } from './file' +import { SpecIterator, SpecIterableIterator } from './fetch' + +/** + * A `string` or `File` that represents a single value from a set of `FormData` key-value pairs. + */ +declare type FormDataEntryValue = string | File + +/** + * Provides a way to easily construct a set of key/value pairs representing form fields and their values, which can then be easily sent using fetch(). + */ +export declare class FormData { + /** + * Appends a new value onto an existing key inside a FormData object, + * or adds the key if it does not already exist. + * + * The difference between `set()` and `append()` is that if the specified key already exists, `set()` will overwrite all existing values with the new one, whereas `append()` will append the new value onto the end of the existing set of values. + * + * @param name The name of the field whose data is contained in `value`. + * @param value The field's value. This can be [`Blob`](https://developer.mozilla.org/en-US/docs/Web/API/Blob) + or [`File`](https://developer.mozilla.org/en-US/docs/Web/API/File). If none of these are specified the value is converted to a string. + * @param fileName The filename reported to the server, when a Blob or File is passed as the second parameter. The default filename for Blob objects is "blob". The default filename for File objects is the file's filename. + */ + append(name: string, value: unknown, fileName?: string): void + + /** + * Set a new value for an existing key inside FormData, + * or add the new field if it does not already exist. + * + * @param name The name of the field whose data is contained in `value`. + * @param value The field's value. This can be [`Blob`](https://developer.mozilla.org/en-US/docs/Web/API/Blob) + or [`File`](https://developer.mozilla.org/en-US/docs/Web/API/File). If none of these are specified the value is converted to a string. + * @param fileName The filename reported to the server, when a Blob or File is passed as the second parameter. The default filename for Blob objects is "blob". The default filename for File objects is the file's filename. + * + */ + set(name: string, value: unknown, fileName?: string): void + + /** + * Returns the first value associated with a given key from within a `FormData` object. + * If you expect multiple values and want all of them, use the `getAll()` method instead. + * + * @param {string} name A name of the value you want to retrieve. + * + * @returns A `FormDataEntryValue` containing the value. If the key doesn't exist, the method returns null. + */ + get(name: string): FormDataEntryValue | null + + /** + * Returns all the values associated with a given key from within a `FormData` object. + * + * @param {string} name A name of the value you want to retrieve. + * + * @returns An array of `FormDataEntryValue` whose key matches the value passed in the `name` parameter. If the key doesn't exist, the method returns an empty list. + */ + getAll(name: string): FormDataEntryValue[] + + /** + * Returns a boolean stating whether a `FormData` object contains a certain key. + * + * @param name A string representing the name of the key you want to test for. + * + * @return A boolean value. + */ + has(name: string): boolean + + /** + * Deletes a key and its value(s) from a `FormData` object. + * + * @param name The name of the key you want to delete. + */ + delete(name: string): void + + /** + * Executes given callback function for each field of the FormData instance + */ + forEach: ( + callbackfn: (value: FormDataEntryValue, key: string, iterable: FormData) => void, + thisArg?: unknown + ) => void + + /** + * Returns an [`iterator`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols) allowing to go through all keys contained in this `FormData` object. + * Each key is a `string`. + */ + keys: () => SpecIterableIterator + + /** + * Returns an [`iterator`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols) allowing to go through all values contained in this object `FormData` object. + * Each value is a [`FormDataValue`](https://developer.mozilla.org/en-US/docs/Web/API/FormDataEntryValue). + */ + values: () => SpecIterableIterator + + /** + * Returns an [`iterator`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols) allowing to go through the `FormData` key/value pairs. + * The key of each pair is a string; the value is a [`FormDataValue`](https://developer.mozilla.org/en-US/docs/Web/API/FormDataEntryValue). + */ + entries: () => SpecIterableIterator<[string, FormDataEntryValue]> + + /** + * An alias for FormData#entries() + */ + [Symbol.iterator]: () => SpecIterableIterator<[string, FormDataEntryValue]> + + readonly [Symbol.toStringTag]: string +} diff --git a/tests/fixtures/npm/cases/undici5/node_modules/undici/types/global-dispatcher.d.ts b/tests/fixtures/npm/cases/undici5/node_modules/undici/types/global-dispatcher.d.ts new file mode 100644 index 000000000..56abd53d6 --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/undici/types/global-dispatcher.d.ts @@ -0,0 +1,9 @@ +import Dispatcher = require("./dispatcher"); + +export { + getGlobalDispatcher, + setGlobalDispatcher +} + +declare function setGlobalDispatcher(dispatcher: DispatcherImplementation): void; +declare function getGlobalDispatcher(): Dispatcher; diff --git a/tests/fixtures/npm/cases/undici5/node_modules/undici/types/global-origin.d.ts b/tests/fixtures/npm/cases/undici5/node_modules/undici/types/global-origin.d.ts new file mode 100644 index 000000000..322542d66 --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/undici/types/global-origin.d.ts @@ -0,0 +1,7 @@ +export { + setGlobalOrigin, + getGlobalOrigin +} + +declare function setGlobalOrigin(origin: string | URL | undefined): void; +declare function getGlobalOrigin(): URL | undefined; \ No newline at end of file diff --git a/tests/fixtures/npm/cases/undici5/node_modules/undici/types/handlers.d.ts b/tests/fixtures/npm/cases/undici5/node_modules/undici/types/handlers.d.ts new file mode 100644 index 000000000..eb4f5a9e8 --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/undici/types/handlers.d.ts @@ -0,0 +1,9 @@ +import Dispatcher from "./dispatcher"; + +export declare class RedirectHandler implements Dispatcher.DispatchHandlers{ + constructor (dispatch: Dispatcher, maxRedirections: number, opts: Dispatcher.DispatchOptions, handler: Dispatcher.DispatchHandlers) +} + +export declare class DecoratorHandler implements Dispatcher.DispatchHandlers{ + constructor (handler: Dispatcher.DispatchHandlers) +} diff --git a/tests/fixtures/npm/cases/undici5/node_modules/undici/types/interceptors.d.ts b/tests/fixtures/npm/cases/undici5/node_modules/undici/types/interceptors.d.ts new file mode 100644 index 000000000..a920ea982 --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/undici/types/interceptors.d.ts @@ -0,0 +1,5 @@ +import {DispatchInterceptor} from "./dispatcher"; + +type RedirectInterceptorOpts = { maxRedirections?: number } + +export declare function createRedirectInterceptor (opts: RedirectInterceptorOpts): DispatchInterceptor diff --git a/tests/fixtures/npm/cases/undici5/node_modules/undici/types/mock-agent.d.ts b/tests/fixtures/npm/cases/undici5/node_modules/undici/types/mock-agent.d.ts new file mode 100644 index 000000000..825d2aeff --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/undici/types/mock-agent.d.ts @@ -0,0 +1,50 @@ +import Agent = require('./agent') +import Dispatcher = require('./dispatcher') +import { Interceptable, MockInterceptor } from './mock-interceptor' +import MockDispatch = MockInterceptor.MockDispatch; + +export = MockAgent + +interface PendingInterceptor extends MockDispatch { + origin: string; +} + +/** A mocked Agent class that implements the Agent API. It allows one to intercept HTTP requests made through undici and return mocked responses instead. */ +declare class MockAgent extends Dispatcher { + constructor(options?: MockAgent.Options) + /** Creates and retrieves mock Dispatcher instances which can then be used to intercept HTTP requests. If the number of connections on the mock agent is set to 1, a MockClient instance is returned. Otherwise a MockPool instance is returned. */ + get(origin: string): TInterceptable; + get(origin: RegExp): TInterceptable; + get(origin: ((origin: string) => boolean)): TInterceptable; + /** Dispatches a mocked request. */ + dispatch(options: Agent.DispatchOptions, handler: Dispatcher.DispatchHandlers): boolean; + /** Closes the mock agent and waits for registered mock pools and clients to also close before resolving. */ + close(): Promise; + /** Disables mocking in MockAgent. */ + deactivate(): void; + /** Enables mocking in a MockAgent instance. When instantiated, a MockAgent is automatically activated. Therefore, this method is only effective after `MockAgent.deactivate` has been called. */ + activate(): void; + /** Define host matchers so only matching requests that aren't intercepted by the mock dispatchers will be attempted. */ + enableNetConnect(): void; + enableNetConnect(host: string): void; + enableNetConnect(host: RegExp): void; + enableNetConnect(host: ((host: string) => boolean)): void; + /** Causes all requests to throw when requests are not matched in a MockAgent intercept. */ + disableNetConnect(): void; + pendingInterceptors(): PendingInterceptor[]; + assertNoPendingInterceptors(options?: { + pendingInterceptorsFormatter?: PendingInterceptorsFormatter; + }): void; +} + +interface PendingInterceptorsFormatter { + format(pendingInterceptors: readonly PendingInterceptor[]): string; +} + +declare namespace MockAgent { + /** MockAgent options. */ + export interface Options extends Agent.Options { + /** A custom agent to be encapsulated by the MockAgent. */ + agent?: Agent; + } +} diff --git a/tests/fixtures/npm/cases/undici5/node_modules/undici/types/mock-client.d.ts b/tests/fixtures/npm/cases/undici5/node_modules/undici/types/mock-client.d.ts new file mode 100644 index 000000000..9e751f42f --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/undici/types/mock-client.d.ts @@ -0,0 +1,25 @@ +import Client = require('./client') +import Dispatcher = require('./dispatcher') +import MockAgent = require('./mock-agent') +import { MockInterceptor, Interceptable } from './mock-interceptor' + +export = MockClient + +/** MockClient extends the Client API and allows one to mock requests. */ +declare class MockClient extends Client implements Interceptable { + constructor(origin: string, options: MockClient.Options); + /** Intercepts any matching requests that use the same origin as this mock client. */ + intercept(options: MockInterceptor.Options): MockInterceptor; + /** Dispatches a mocked request. */ + dispatch(options: Dispatcher.DispatchOptions, handlers: Dispatcher.DispatchHandlers): boolean; + /** Closes the mock client and gracefully waits for enqueued requests to complete. */ + close(): Promise; +} + +declare namespace MockClient { + /** MockClient options. */ + export interface Options extends Client.Options { + /** The agent to associate this MockClient with. */ + agent: MockAgent; + } +} diff --git a/tests/fixtures/npm/cases/undici5/node_modules/undici/types/mock-errors.d.ts b/tests/fixtures/npm/cases/undici5/node_modules/undici/types/mock-errors.d.ts new file mode 100644 index 000000000..31e7ba23c --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/undici/types/mock-errors.d.ts @@ -0,0 +1,12 @@ +import { UndiciError } from './errors' + +export = MockErrors + +declare namespace MockErrors { + /** The request does not match any registered mock dispatches. */ + export class MockNotMatchedError extends UndiciError { + constructor(message?: string); + name: 'MockNotMatchedError'; + code: 'UND_MOCK_ERR_MOCK_NOT_MATCHED'; + } +} diff --git a/tests/fixtures/npm/cases/undici5/node_modules/undici/types/mock-interceptor.d.ts b/tests/fixtures/npm/cases/undici5/node_modules/undici/types/mock-interceptor.d.ts new file mode 100644 index 000000000..87eedcd40 --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/undici/types/mock-interceptor.d.ts @@ -0,0 +1,93 @@ +import { IncomingHttpHeaders } from 'http' +import Dispatcher = require('./dispatcher'); +import { BodyInit, Headers } from './fetch' + +export { + Interceptable, + MockInterceptor, + MockScope +} + +/** The scope associated with a mock dispatch. */ +declare class MockScope { + constructor(mockDispatch: MockInterceptor.MockDispatch); + /** Delay a reply by a set amount of time in ms. */ + delay(waitInMs: number): MockScope; + /** Persist the defined mock data for the associated reply. It will return the defined mock data indefinitely. */ + persist(): MockScope; + /** Define a reply for a set amount of matching requests. */ + times(repeatTimes: number): MockScope; +} + +/** The interceptor for a Mock. */ +declare class MockInterceptor { + constructor(options: MockInterceptor.Options, mockDispatches: MockInterceptor.MockDispatch[]); + /** Mock an undici request with the defined reply. */ + reply(replyOptionsCallback: MockInterceptor.MockReplyOptionsCallback): MockScope; + reply( + statusCode: number, + data?: TData | Buffer | string | MockInterceptor.MockResponseDataHandler, + responseOptions?: MockInterceptor.MockResponseOptions + ): MockScope; + /** Mock an undici request by throwing the defined reply error. */ + replyWithError(error: TError): MockScope; + /** Set default reply headers on the interceptor for subsequent mocked replies. */ + defaultReplyHeaders(headers: IncomingHttpHeaders): MockInterceptor; + /** Set default reply trailers on the interceptor for subsequent mocked replies. */ + defaultReplyTrailers(trailers: Record): MockInterceptor; + /** Set automatically calculated content-length header on subsequent mocked replies. */ + replyContentLength(): MockInterceptor; +} + +declare namespace MockInterceptor { + /** MockInterceptor options. */ + export interface Options { + /** Path to intercept on. */ + path: string | RegExp | ((path: string) => boolean); + /** Method to intercept on. Defaults to GET. */ + method?: string | RegExp | ((method: string) => boolean); + /** Body to intercept on. */ + body?: string | RegExp | ((body: string) => boolean); + /** Headers to intercept on. */ + headers?: Record boolean)> | ((headers: Record) => boolean); + /** Query params to intercept on */ + query?: Record; + } + export interface MockDispatch extends Options { + times: number | null; + persist: boolean; + consumed: boolean; + data: MockDispatchData; + } + export interface MockDispatchData extends MockResponseOptions { + error: TError | null; + statusCode?: number; + data?: TData | string; + } + export interface MockResponseOptions { + headers?: IncomingHttpHeaders; + trailers?: Record; + } + + export interface MockResponseCallbackOptions { + path: string; + origin: string; + method: string; + body?: BodyInit | Dispatcher.DispatchOptions['body']; + headers: Headers | Record; + maxRedirections: number; + } + + export type MockResponseDataHandler = ( + opts: MockResponseCallbackOptions + ) => TData | Buffer | string; + + export type MockReplyOptionsCallback = ( + opts: MockResponseCallbackOptions + ) => { statusCode: number, data?: TData | Buffer | string, responseOptions?: MockResponseOptions } +} + +interface Interceptable extends Dispatcher { + /** Intercepts any matching requests that use the same origin as this mock client. */ + intercept(options: MockInterceptor.Options): MockInterceptor; +} diff --git a/tests/fixtures/npm/cases/undici5/node_modules/undici/types/mock-pool.d.ts b/tests/fixtures/npm/cases/undici5/node_modules/undici/types/mock-pool.d.ts new file mode 100644 index 000000000..d0c905152 --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/undici/types/mock-pool.d.ts @@ -0,0 +1,25 @@ +import Pool = require('./pool') +import MockAgent = require('./mock-agent') +import { Interceptable, MockInterceptor } from './mock-interceptor' +import Dispatcher = require('./dispatcher') + +export = MockPool + +/** MockPool extends the Pool API and allows one to mock requests. */ +declare class MockPool extends Pool implements Interceptable { + constructor(origin: string, options: MockPool.Options); + /** Intercepts any matching requests that use the same origin as this mock pool. */ + intercept(options: MockInterceptor.Options): MockInterceptor; + /** Dispatches a mocked request. */ + dispatch(options: Dispatcher.DispatchOptions, handlers: Dispatcher.DispatchHandlers): boolean; + /** Closes the mock pool and gracefully waits for enqueued requests to complete. */ + close(): Promise; +} + +declare namespace MockPool { + /** MockPool options. */ + export interface Options extends Pool.Options { + /** The agent to associate this MockPool with. */ + agent: MockAgent; + } +} diff --git a/tests/fixtures/npm/cases/undici5/node_modules/undici/types/pool-stats.d.ts b/tests/fixtures/npm/cases/undici5/node_modules/undici/types/pool-stats.d.ts new file mode 100644 index 000000000..807e68f1b --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/undici/types/pool-stats.d.ts @@ -0,0 +1,19 @@ +import Pool = require("./pool") + +export = PoolStats + +declare class PoolStats { + constructor(pool: Pool); + /** Number of open socket connections in this pool. */ + connected: number; + /** Number of open socket connections in this pool that do not have an active request. */ + free: number; + /** Number of pending requests across all clients in this pool. */ + pending: number; + /** Number of queued requests across all clients in this pool. */ + queued: number; + /** Number of currently active requests across all clients in this pool. */ + running: number; + /** Number of active, pending, or queued requests across all clients in this pool. */ + size: number; +} diff --git a/tests/fixtures/npm/cases/undici5/node_modules/undici/types/pool.d.ts b/tests/fixtures/npm/cases/undici5/node_modules/undici/types/pool.d.ts new file mode 100644 index 000000000..0ef0bc398 --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/undici/types/pool.d.ts @@ -0,0 +1,29 @@ +import Client = require('./client') +import Dispatcher = require('./dispatcher') +import TPoolStats = require('./pool-stats') +import { URL } from 'url' +import {DispatchInterceptor} from "./dispatcher"; + +export = Pool + +declare class Pool extends Dispatcher { + constructor(url: string | URL, options?: Pool.Options) + /** `true` after `pool.close()` has been called. */ + closed: boolean; + /** `true` after `pool.destroyed()` has been called or `pool.close()` has been called and the pool shutdown has completed. */ + destroyed: boolean; + /** Aggregate stats for a Pool. */ + readonly stats: TPoolStats; +} + +declare namespace Pool { + export type PoolStats = TPoolStats; + export interface Options extends Client.Options { + /** Default: `(origin, opts) => new Client(origin, opts)`. */ + factory?(origin: URL, opts: object): Dispatcher; + /** The max number of clients to create. `null` if no limit. Default `null`. */ + connections?: number | null; + + interceptors?: { Pool?: readonly DispatchInterceptor[] } & Client.Options["interceptors"] + } +} diff --git a/tests/fixtures/npm/cases/undici5/node_modules/undici/types/proxy-agent.d.ts b/tests/fixtures/npm/cases/undici5/node_modules/undici/types/proxy-agent.d.ts new file mode 100644 index 000000000..118165d00 --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/undici/types/proxy-agent.d.ts @@ -0,0 +1,25 @@ +import { TlsOptions } from 'tls' +import Agent = require('./agent') +import Dispatcher = require('./dispatcher') + +export = ProxyAgent + +declare class ProxyAgent extends Dispatcher { + constructor(options: ProxyAgent.Options | string) + + dispatch(options: Agent.DispatchOptions, handler: Dispatcher.DispatchHandlers): boolean; + close(): Promise; +} + +declare namespace ProxyAgent { + export interface Options extends Agent.Options { + uri: string; + /** + * @deprecated use opts.token + */ + auth?: string; + token?: string; + requestTls?: TlsOptions & { servername?: string }; + proxyTls?: TlsOptions & { servername?: string }; + } +} diff --git a/tests/fixtures/npm/cases/undici5/node_modules/undici/types/readable.d.ts b/tests/fixtures/npm/cases/undici5/node_modules/undici/types/readable.d.ts new file mode 100644 index 000000000..0d82ba867 --- /dev/null +++ b/tests/fixtures/npm/cases/undici5/node_modules/undici/types/readable.d.ts @@ -0,0 +1,61 @@ +import { Readable } from "stream"; +import { Blob } from 'buffer' + +export = BodyReadable + +declare class BodyReadable extends Readable { + constructor( + resume?: (this: Readable, size: number) => void | null, + abort?: () => void | null, + contentType?: string + ) + + /** Consumes and returns the body as a string + * https://fetch.spec.whatwg.org/#dom-body-text + */ + text(): Promise + + /** Consumes and returns the body as a JavaScript Object + * https://fetch.spec.whatwg.org/#dom-body-json + */ + json(): Promise + + /** Consumes and returns the body as a Blob + * https://fetch.spec.whatwg.org/#dom-body-blob + */ + blob(): Promise + + /** Consumes and returns the body as an ArrayBuffer + * https://fetch.spec.whatwg.org/#dom-body-arraybuffer + */ + arrayBuffer(): Promise + + /** Not implemented + * + * https://fetch.spec.whatwg.org/#dom-body-formdata + */ + formData(): Promise + + /** Returns true if the body is not null and the body has been consumed + * + * Otherwise, returns false + * + * https://fetch.spec.whatwg.org/#dom-body-bodyused + */ + readonly bodyUsed: boolean + + /** Throws on node 16.6.0 + * + * If body is null, it should return null as the body + * + * If body is not null, should return the body as a ReadableStream + * + * https://fetch.spec.whatwg.org/#dom-body-body + */ + readonly body: never | undefined + + /** Dumps the response body by reading `limit` number of bytes. + * @param opts.limit Number of bytes to read (optional) - Default: 262144 + */ + dump(opts?: { limit: number }): Promise +} diff --git a/tests/fixtures/npm/cases/undici7/main.ts b/tests/fixtures/npm/cases/undici7/main.ts new file mode 100644 index 000000000..24b608e61 --- /dev/null +++ b/tests/fixtures/npm/cases/undici7/main.ts @@ -0,0 +1,361 @@ +// undici 7.x through scriptc's built-in implementation: the native build +// compiles the package's shim in place of the vendored undici the Node side +// loads. Covers request(), Agent/Pool/Client, a transport-style Agent +// subclass (per-origin pool factory, wrapped connector, client +// interceptors, pool stats), dispatch overrides, keep-alive reuse, error +// classes and codes, and the diagnostics_channel request lifecycle. +import { channel } from "node:diagnostics_channel"; +import * as http from "node:http"; +import type { Socket } from "node:net"; +import { + Agent, + Client, + Pool, + Headers, + buildConnector, + errors, + fetch, + getGlobalDispatcher, + request, + setGlobalDispatcher, +} from "undici"; +import type { Dispatcher } from "undici"; + +const server = http.createServer((req, res) => { + const url = req.url ?? "/"; + if (url === "/json") { + res.setHeader("content-type", "application/json"); + res.end(JSON.stringify({ ok: true, n: 3 })); + } else if (url === "/cookies") { + res.setHeader("set-cookie", ["a=1", "b=2"]); + res.end("cookies"); + } else if (url === "/slow") { + setTimeout(() => res.end("late"), 1500); + } else if (url === "/missing") { + res.statusCode = 404; + res.setHeader("content-type", "application/json"); + res.end(JSON.stringify({ error: "not found" })); + } else if (url === "/chunked") { + res.write("part1,"); + setTimeout(() => res.end("part2"), 10); + } else if (url === "/redirect") { + res.statusCode = 302; + res.setHeader("location", "/hello"); + res.end(); + } else if (url === "/echo") { + let body = ""; + req.on("data", (chunk: Buffer) => { + body += chunk.toString(); + }); + req.on("end", () => { + const test = req.headers["x-test"]; + const length = req.headers["content-length"]; + const shown = (v: string | string[] | undefined): string => (typeof v === "string" ? v : "-"); + res.end(`${req.method} ${shown(test)} ${shown(length)} ${body}`); + }); + } else { + res.end(`hello ${req.method} ${url}`); + } +}); + +const turn = (): Promise => new Promise((resolve) => setImmediate(resolve)); + +interface Telemetry { + label: string; + dispatched: number; +} + +let current: Telemetry | null = null; +const byRequest = new WeakMap(); + +channel("undici:request:create").subscribe((message) => { + const { request } = message as { request: object }; + if (current !== null) byRequest.set(request, current); +}); +channel("undici:request:headers").subscribe((message) => { + const { request, response } = message as { + request: object; + response: { statusCode: number }; + }; + const t = byRequest.get(request); + if (t !== undefined) console.log(` [diag] ${t.label} headers ${response.statusCode}`); +}); +channel("undici:request:trailers").subscribe((message) => { + const { request } = message as { request: object }; + const t = byRequest.get(request); + if (t !== undefined) console.log(` [diag] ${t.label} done after ${t.dispatched} dispatch`); +}); + +/** A transport-style Agent: one Pool per origin built by the factory, a + * wrapped connector, and a Client interceptor that marks dispatches. */ +class TracingAgent extends Agent { + readonly origins: Map; + connections = 0; + + constructor(maxConnections: number) { + const origins = new Map(); + const connect = buildConnector({}) as ( + options: buildConnector.Options, + callback: buildConnector.Callback, + ) => Socket; + let opened = 0; + super({ + factory(origin) { + const pool = new Pool(origin, { + connections: maxConnections, + keepAliveTimeout: 10_000, + pipelining: 1, + connect(options, callback) { + opened++; + return connect(options, callback); + }, + interceptors: { + Client: [ + (dispatch) => (params, handler) => { + const t = (params as Dispatcher.RequestOptions).opaque; + if (t !== null && typeof t === "object") { + const tel = t as Telemetry; + tel.dispatched++; + const previous = current; + current = tel; + try { + return dispatch(params, handler); + } finally { + current = previous; + } + } + return dispatch(params, handler); + }, + ], + }, + }); + origins.set(String(origin), pool); + return pool; + }, + }); + this.origins = origins; + this.on("connect", () => { + this.connections = opened; + }); + } + + statsFor(origin: string): string { + const pool = this.origins.get(origin); + if (pool === undefined) return "no pool"; + const s = pool.stats; + return `connected=${s.connected} free=${s.free} pending=${s.pending} queued=${s.queued} running=${s.running} size=${s.size}`; + } +} + +/** A subclass that overrides dispatch and forwards to the base Agent. */ +class CountingAgent extends Agent { + count = 0; + paths: string[] = []; + dispatch(options: Dispatcher.DispatchOptions, handler: Dispatcher.DispatchHandlers): boolean { + this.count++; + this.paths.push(`${options.method} ${options.path}`); + return super.dispatch(options, handler); + } +} + +function describe(err: unknown): string { + if (err instanceof errors.UndiciError) return `${err.name} ${err.code} ${err.message}`; + if (err instanceof DOMException) return `${err.name} ${err.message}`; + if (err instanceof Error) { + const code = (err as NodeJS.ErrnoException).code ?? ""; + return `${err.name} ${code} ${err.message}`; + } + return String(err); +} + +server.listen(0, "127.0.0.1", async () => { + const address = server.address(); + const port = address !== null && typeof address === "object" ? address.port : 0; + const origin = `http://127.0.0.1:${port}`; + + console.log("== request() through a TracingAgent"); + const agent = new TracingAgent(2); + const tel: Telemetry = { label: "hello", dispatched: 0 }; + const res = await request(`${origin}/hello`, { dispatcher: agent, opaque: tel }); + console.log(res.statusCode, res.statusText, res.headers["content-length"], await res.body.text()); + console.log("opaque passthrough:", res.opaque === tel); + const json = await request(`${origin}/json`, { dispatcher: agent }); + const data = (await json.body.json()) as { ok: boolean; n: number }; + console.log("json:", data.ok, data.n, json.headers["content-type"]); + const cookies = await request(`${origin}/cookies`, { dispatcher: agent }); + const setCookie = cookies.headers["set-cookie"]; + console.log("set-cookie:", Array.isArray(setCookie) ? setCookie.join("|") : setCookie); + await cookies.body.text(); + const chunked = await request(`${origin}/chunked`, { dispatcher: agent }); + console.log("chunked:", chunked.headers["transfer-encoding"], await chunked.body.text()); + const echo = await request(`${origin}/echo`, { + dispatcher: agent, + body: "payload", + headers: { "x-test": "yes" }, + }); + console.log("default method with a body:", await echo.body.text()); + const ab = await request(`${origin}/hello`, { dispatcher: agent, method: "POST" }); + console.log("arrayBuffer bytes:", (await ab.body.arrayBuffer()).byteLength); + + console.log("== keep-alive reuse"); + for (let i = 0; i < 4; i++) { + const r = await request(`${origin}/r${i}`, { dispatcher: agent }); + await r.body.text(); + await turn(); + } + console.log("connections opened:", agent.connections); + console.log("stats:", agent.statsFor(origin)); + + console.log("== Pool with a connection limit"); + const pool = new Pool(origin, { connections: 2 }); + const inflight: Promise[] = []; + for (let i = 0; i < 4; i++) { + inflight.push( + pool.request({ path: `/p${i}`, method: "GET" }).then((r) => r.body.text()), + ); + } + console.log("queued while busy:", pool.stats.queued, "size:", pool.stats.size); + console.log((await Promise.all(inflight)).join(", ")); + await pool.close(); + console.log("pool closed:", pool.closed, pool.destroyed); + + console.log("== Client"); + const client = new Client(origin); + const cr = await client.request({ path: "/client", method: "GET" }); + console.log(cr.statusCode, await cr.body.text()); + await client.close(); + + console.log("== connect.lookup"); + const looked: string[] = []; + const pinned = new Agent({ + connect: { + lookup(hostname, _options, callback) { + looked.push(hostname); + callback(null, [{ address: "127.0.0.1", family: 4 }]); + }, + }, + }); + const viaLookup = await request(`http://pinned.invalid:${port}/lookup`, { dispatcher: pinned }); + console.log(viaLookup.statusCode, await viaLookup.body.text(), looked.join(",")); + await pinned.close(); + + console.log("== dispatch override"); + const counting = new CountingAgent(); + for (const path of ["/a", "/b"]) { + const r = await request(`${origin}${path}`, { dispatcher: counting }); + await r.body.text(); + } + console.log("dispatched:", counting.count, counting.paths.join(", ")); + await counting.close(); + + console.log("== diagnostics channel lifecycle"); + const traced: Telemetry = { label: "traced", dispatched: 0 }; + const tr = await request(`${origin}/traced`, { dispatcher: agent, opaque: traced }); + await tr.body.text(); + + console.log("== errors"); + try { + await request("http://127.0.0.1:1/refused", {}); + } catch (err) { + console.log("refused:", describe(err)); + } + try { + await request(`${origin}/slow`, { dispatcher: agent, headersTimeout: 100 }); + } catch (err) { + console.log("headers timeout:", describe(err)); + } + const controller = new AbortController(); + const pending = request(`${origin}/slow`, { dispatcher: agent, signal: controller.signal }); + setTimeout(() => controller.abort(), 50); + try { + await pending; + } catch (err) { + console.log("abort:", describe(err)); + } + try { + await request(`${origin}/missing`, { dispatcher: agent, throwOnError: true }); + } catch (err) { + console.log("throwOnError:", describe(err)); + } + const notFound = await request(`${origin}/missing`, { dispatcher: agent }); + console.log("404 without throwOnError:", notFound.statusCode, await notFound.body.text()); + try { + await request("ftp://127.0.0.1/x", {}); + } catch (err) { + console.log("protocol:", describe(err)); + } + const early = await request(`${origin}/hello`, { dispatcher: agent }); + early.body.on("error", (err: Error) => console.log("destroyed body:", describe(err))); + early.body.destroy(); + await turn(); + console.log("instanceof:", new errors.HeadersTimeoutError() instanceof errors.UndiciError); + + console.log("== controller-API handler"); + await new Promise((resolve) => { + agent.dispatch( + { origin, path: "/controller", method: "GET" }, + { + onRequestStart(controller) { + console.log("start, aborted:", controller.aborted); + }, + onResponseStart(_controller, statusCode, headers) { + console.log("response", statusCode, headers["content-length"]); + }, + onResponseData(_controller, chunk) { + console.log("data", chunk.toString()); + }, + onResponseEnd() { + console.log("end"); + resolve(); + }, + onResponseError(_controller, err) { + console.log("error", describe(err)); + resolve(); + }, + }, + ); + }); + + console.log("== fetch"); + const viaAgent = await fetch(`${origin}/json`, { dispatcher: agent }); + console.log("through agent:", viaAgent.status, viaAgent.statusText, viaAgent.headers.get("content-type"), await viaAgent.text()); + const echoed = await fetch(`${origin}/echo`, { + dispatcher: agent, + method: "POST", + body: "fetched", + headers: new Headers({ "x-test": "fetch" }), + }); + console.log("post through agent:", await echoed.text()); + const direct = await fetch(`${origin}/hello`); + console.log("without dispatcher:", direct.status, await direct.text()); + const manual = await fetch(`${origin}/redirect`, { dispatcher: agent, redirect: "manual" }); + console.log("manual redirect:", manual.status, manual.headers.get("location")); + await manual.text(); + const followed = await fetch(`${origin}/redirect`, { dispatcher: agent }); + console.log("followed redirect:", followed.status, await followed.text()); + try { + await fetch("http://127.0.0.1:1/refused", { dispatcher: agent }); + } catch (err) { + const cause = (err as Error).cause; + console.log("fetch refused:", describe(err), "cause:", describe(cause)); + } + const fetchAbort = new AbortController(); + const slowFetch = fetch(`${origin}/slow`, { dispatcher: agent, signal: fetchAbort.signal }); + setTimeout(() => fetchAbort.abort(), 50); + try { + await slowFetch; + } catch (err) { + console.log("fetch abort:", describe(err)); + } + const h = new Headers([["a", "1"]]); + h.append("A", "2"); + console.log("Headers:", h.get("a")); + + console.log("== global dispatcher"); + setGlobalDispatcher(counting); + console.log("global is counting:", getGlobalDispatcher() === counting); + + await agent.close(); + server.close(); + console.log("done"); +}); diff --git a/tests/fixtures/npm/cases/undici7/node_modules/undici/LICENSE b/tests/fixtures/npm/cases/undici7/node_modules/undici/LICENSE new file mode 100644 index 000000000..e7323bb52 --- /dev/null +++ b/tests/fixtures/npm/cases/undici7/node_modules/undici/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) Matteo Collina and Undici contributors + +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/fixtures/npm/cases/undici7/node_modules/undici/index-fetch.js b/tests/fixtures/npm/cases/undici7/node_modules/undici/index-fetch.js new file mode 100644 index 000000000..3d1b811da --- /dev/null +++ b/tests/fixtures/npm/cases/undici7/node_modules/undici/index-fetch.js @@ -0,0 +1,65 @@ +'use strict' + +const { getGlobalDispatcher, setGlobalDispatcher } = require('./lib/global') +const EnvHttpProxyAgent = require('./lib/dispatcher/env-http-proxy-agent') +const fetchImpl = require('./lib/web/fetch').fetch + +// Capture __filename at module load time for stack trace augmentation. +// This may be undefined when bundled in environments like Node.js internals. +const currentFilename = typeof __filename !== 'undefined' ? __filename : undefined + +function appendFetchStackTrace (err, filename) { + if (!err || typeof err !== 'object') { + return + } + + const stack = typeof err.stack === 'string' ? err.stack : '' + const normalizedFilename = filename.replace(/\\/g, '/') + + if (stack && (stack.includes(filename) || stack.includes(normalizedFilename))) { + return + } + + const capture = {} + Error.captureStackTrace(capture, appendFetchStackTrace) + + if (!capture.stack) { + return + } + + const captureLines = capture.stack.split('\n').slice(1).join('\n') + + err.stack = stack ? `${stack}\n${captureLines}` : capture.stack +} + +module.exports.fetch = function fetch (init, options = undefined) { + return fetchImpl(init, options).catch(err => { + if (currentFilename) { + appendFetchStackTrace(err, currentFilename) + } else if (err && typeof err === 'object') { + Error.captureStackTrace(err, module.exports.fetch) + } + throw err + }) +} +module.exports.FormData = require('./lib/web/fetch/formdata').FormData +module.exports.Headers = require('./lib/web/fetch/headers').Headers +module.exports.Response = require('./lib/web/fetch/response').Response +module.exports.Request = require('./lib/web/fetch/request').Request + +const { CloseEvent, ErrorEvent, MessageEvent, createFastMessageEvent } = require('./lib/web/websocket/events') +module.exports.WebSocket = require('./lib/web/websocket/websocket').WebSocket +module.exports.CloseEvent = CloseEvent +module.exports.ErrorEvent = ErrorEvent +module.exports.MessageEvent = MessageEvent +module.exports.createFastMessageEvent = createFastMessageEvent + +module.exports.EventSource = require('./lib/web/eventsource/eventsource').EventSource + +const api = require('./lib/api') +const Dispatcher = require('./lib/dispatcher/dispatcher') +Object.assign(Dispatcher.prototype, api) +// Expose the fetch implementation to be enabled in Node.js core via a flag +module.exports.EnvHttpProxyAgent = EnvHttpProxyAgent +module.exports.getGlobalDispatcher = getGlobalDispatcher +module.exports.setGlobalDispatcher = setGlobalDispatcher diff --git a/tests/fixtures/npm/cases/undici7/node_modules/undici/index.d.ts b/tests/fixtures/npm/cases/undici7/node_modules/undici/index.d.ts new file mode 100644 index 000000000..0883fc7cc --- /dev/null +++ b/tests/fixtures/npm/cases/undici7/node_modules/undici/index.d.ts @@ -0,0 +1,3 @@ +import Undici from './types/index' +export default Undici +export * from './types/index' diff --git a/tests/fixtures/npm/cases/undici7/node_modules/undici/index.js b/tests/fixtures/npm/cases/undici7/node_modules/undici/index.js new file mode 100644 index 000000000..708a8ee80 --- /dev/null +++ b/tests/fixtures/npm/cases/undici7/node_modules/undici/index.js @@ -0,0 +1,234 @@ +'use strict' + +const Client = require('./lib/dispatcher/client') +const Dispatcher = require('./lib/dispatcher/dispatcher') +const Pool = require('./lib/dispatcher/pool') +const BalancedPool = require('./lib/dispatcher/balanced-pool') +const RoundRobinPool = require('./lib/dispatcher/round-robin-pool') +const Agent = require('./lib/dispatcher/agent') +const ProxyAgent = require('./lib/dispatcher/proxy-agent') +const Socks5ProxyAgent = require('./lib/dispatcher/socks5-proxy-agent') +const EnvHttpProxyAgent = require('./lib/dispatcher/env-http-proxy-agent') +const RetryAgent = require('./lib/dispatcher/retry-agent') +const H2CClient = require('./lib/dispatcher/h2c-client') +const errors = require('./lib/core/errors') +const util = require('./lib/core/util') +const { InvalidArgumentError } = errors +const api = require('./lib/api') +const buildConnector = require('./lib/core/connect') +const MockClient = require('./lib/mock/mock-client') +const { MockCallHistory, MockCallHistoryLog } = require('./lib/mock/mock-call-history') +const MockAgent = require('./lib/mock/mock-agent') +const MockPool = require('./lib/mock/mock-pool') +const SnapshotAgent = require('./lib/mock/snapshot-agent') +const mockErrors = require('./lib/mock/mock-errors') +const RetryHandler = require('./lib/handler/retry-handler') +const { getGlobalDispatcher, setGlobalDispatcher } = require('./lib/global') +const DecoratorHandler = require('./lib/handler/decorator-handler') +const RedirectHandler = require('./lib/handler/redirect-handler') + +Object.assign(Dispatcher.prototype, api) + +module.exports.Dispatcher = Dispatcher +module.exports.Client = Client +module.exports.Pool = Pool +module.exports.BalancedPool = BalancedPool +module.exports.RoundRobinPool = RoundRobinPool +module.exports.Agent = Agent +module.exports.ProxyAgent = ProxyAgent +module.exports.Socks5ProxyAgent = Socks5ProxyAgent +module.exports.EnvHttpProxyAgent = EnvHttpProxyAgent +module.exports.RetryAgent = RetryAgent +module.exports.H2CClient = H2CClient +module.exports.RetryHandler = RetryHandler + +module.exports.DecoratorHandler = DecoratorHandler +module.exports.RedirectHandler = RedirectHandler +module.exports.interceptors = { + redirect: require('./lib/interceptor/redirect'), + responseError: require('./lib/interceptor/response-error'), + retry: require('./lib/interceptor/retry'), + dump: require('./lib/interceptor/dump'), + dns: require('./lib/interceptor/dns'), + cache: require('./lib/interceptor/cache'), + decompress: require('./lib/interceptor/decompress'), + deduplicate: require('./lib/interceptor/deduplicate') +} + +module.exports.cacheStores = { + MemoryCacheStore: require('./lib/cache/memory-cache-store') +} + +const SqliteCacheStore = require('./lib/cache/sqlite-cache-store') +module.exports.cacheStores.SqliteCacheStore = SqliteCacheStore + +module.exports.buildConnector = buildConnector +module.exports.errors = errors +module.exports.util = { + parseHeaders: util.parseHeaders, + headerNameToString: util.headerNameToString +} + +function makeDispatcher (fn) { + return (url, opts, handler) => { + if (typeof opts === 'function') { + handler = opts + opts = null + } + + if (!url || (typeof url !== 'string' && typeof url !== 'object' && !(url instanceof URL))) { + throw new InvalidArgumentError('invalid url') + } + + if (opts != null && typeof opts !== 'object') { + throw new InvalidArgumentError('invalid opts') + } + + if (opts && opts.path != null) { + if (typeof opts.path !== 'string') { + throw new InvalidArgumentError('invalid opts.path') + } + + let path = opts.path + if (!opts.path.startsWith('/')) { + path = `/${path}` + } + + url = new URL(util.parseOrigin(url).origin + path) + } else { + if (!opts) { + opts = typeof url === 'object' ? url : {} + } + + url = util.parseURL(url) + } + + const { agent, dispatcher = getGlobalDispatcher() } = opts + + if (agent) { + throw new InvalidArgumentError('unsupported opts.agent. Did you mean opts.client?') + } + + return fn.call(dispatcher, { + ...opts, + origin: url.origin, + path: url.search ? `${url.pathname}${url.search}` : url.pathname, + method: opts.method || (opts.body ? 'PUT' : 'GET') + }, handler) + } +} + +module.exports.setGlobalDispatcher = setGlobalDispatcher +module.exports.getGlobalDispatcher = getGlobalDispatcher + +const fetchImpl = require('./lib/web/fetch').fetch + +// Capture __filename at module load time for stack trace augmentation. +// This may be undefined when bundled in environments like Node.js internals. +const currentFilename = typeof __filename !== 'undefined' ? __filename : undefined + +function appendFetchStackTrace (err, filename) { + if (!err || typeof err !== 'object') { + return + } + + const stack = typeof err.stack === 'string' ? err.stack : '' + const normalizedFilename = filename.replace(/\\/g, '/') + + if (stack && (stack.includes(filename) || stack.includes(normalizedFilename))) { + return + } + + const capture = {} + Error.captureStackTrace(capture, appendFetchStackTrace) + + if (!capture.stack) { + return + } + + const captureLines = capture.stack.split('\n').slice(1).join('\n') + + err.stack = stack ? `${stack}\n${captureLines}` : capture.stack +} + +module.exports.fetch = function fetch (init, options = undefined) { + return fetchImpl(init, options).catch(err => { + if (currentFilename) { + appendFetchStackTrace(err, currentFilename) + } else if (err && typeof err === 'object') { + Error.captureStackTrace(err, module.exports.fetch) + } + throw err + }) +} +module.exports.Headers = require('./lib/web/fetch/headers').Headers +module.exports.Response = require('./lib/web/fetch/response').Response +module.exports.Request = require('./lib/web/fetch/request').Request +module.exports.FormData = require('./lib/web/fetch/formdata').FormData + +const { setGlobalOrigin, getGlobalOrigin } = require('./lib/web/fetch/global') + +module.exports.setGlobalOrigin = setGlobalOrigin +module.exports.getGlobalOrigin = getGlobalOrigin + +const { CacheStorage } = require('./lib/web/cache/cachestorage') +const { kConstruct } = require('./lib/core/symbols') + +module.exports.caches = new CacheStorage(kConstruct) + +const { deleteCookie, getCookies, getSetCookies, setCookie, parseCookie } = require('./lib/web/cookies') + +module.exports.deleteCookie = deleteCookie +module.exports.getCookies = getCookies +module.exports.getSetCookies = getSetCookies +module.exports.setCookie = setCookie +module.exports.parseCookie = parseCookie + +const { parseMIMEType, serializeAMimeType } = require('./lib/web/fetch/data-url') + +module.exports.parseMIMEType = parseMIMEType +module.exports.serializeAMimeType = serializeAMimeType + +const { CloseEvent, ErrorEvent, MessageEvent } = require('./lib/web/websocket/events') +const { WebSocket, ping } = require('./lib/web/websocket/websocket') +module.exports.WebSocket = WebSocket +module.exports.CloseEvent = CloseEvent +module.exports.ErrorEvent = ErrorEvent +module.exports.MessageEvent = MessageEvent +module.exports.ping = ping + +module.exports.WebSocketStream = require('./lib/web/websocket/stream/websocketstream').WebSocketStream +module.exports.WebSocketError = require('./lib/web/websocket/stream/websocketerror').WebSocketError + +module.exports.request = makeDispatcher(api.request) +module.exports.stream = makeDispatcher(api.stream) +module.exports.pipeline = makeDispatcher(api.pipeline) +module.exports.connect = makeDispatcher(api.connect) +module.exports.upgrade = makeDispatcher(api.upgrade) + +module.exports.MockClient = MockClient +module.exports.MockCallHistory = MockCallHistory +module.exports.MockCallHistoryLog = MockCallHistoryLog +module.exports.MockPool = MockPool +module.exports.MockAgent = MockAgent +module.exports.SnapshotAgent = SnapshotAgent +module.exports.mockErrors = mockErrors + +const { EventSource } = require('./lib/web/eventsource/eventsource') + +module.exports.EventSource = EventSource + +function install () { + globalThis.fetch = module.exports.fetch + globalThis.Headers = module.exports.Headers + globalThis.Response = module.exports.Response + globalThis.Request = module.exports.Request + globalThis.FormData = module.exports.FormData + globalThis.WebSocket = module.exports.WebSocket + globalThis.CloseEvent = module.exports.CloseEvent + globalThis.ErrorEvent = module.exports.ErrorEvent + globalThis.MessageEvent = module.exports.MessageEvent + globalThis.EventSource = module.exports.EventSource +} + +module.exports.install = install diff --git a/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/api/abort-signal.js b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/api/abort-signal.js new file mode 100644 index 000000000..608170b43 --- /dev/null +++ b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/api/abort-signal.js @@ -0,0 +1,59 @@ +'use strict' + +const { addAbortListener } = require('../core/util') +const { RequestAbortedError } = require('../core/errors') + +const kListener = Symbol('kListener') +const kSignal = Symbol('kSignal') + +function abort (self) { + if (self.abort) { + self.abort(self[kSignal]?.reason) + } else { + self.reason = self[kSignal]?.reason ?? new RequestAbortedError() + } + removeSignal(self) +} + +function addSignal (self, signal) { + self.reason = null + + self[kSignal] = null + self[kListener] = null + + if (!signal) { + return + } + + if (signal.aborted) { + abort(self) + return + } + + self[kSignal] = signal + self[kListener] = () => { + abort(self) + } + + addAbortListener(self[kSignal], self[kListener]) +} + +function removeSignal (self) { + if (!self[kSignal]) { + return + } + + if ('removeEventListener' in self[kSignal]) { + self[kSignal].removeEventListener('abort', self[kListener]) + } else { + self[kSignal].removeListener('abort', self[kListener]) + } + + self[kSignal] = null + self[kListener] = null +} + +module.exports = { + addSignal, + removeSignal +} diff --git a/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/api/api-connect.js b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/api/api-connect.js new file mode 100644 index 000000000..c8b86dd7d --- /dev/null +++ b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/api/api-connect.js @@ -0,0 +1,110 @@ +'use strict' + +const assert = require('node:assert') +const { AsyncResource } = require('node:async_hooks') +const { InvalidArgumentError, SocketError } = require('../core/errors') +const util = require('../core/util') +const { addSignal, removeSignal } = require('./abort-signal') + +class ConnectHandler extends AsyncResource { + constructor (opts, callback) { + if (!opts || typeof opts !== 'object') { + throw new InvalidArgumentError('invalid opts') + } + + if (typeof callback !== 'function') { + throw new InvalidArgumentError('invalid callback') + } + + const { signal, opaque, responseHeaders } = opts + + if (signal && typeof signal.on !== 'function' && typeof signal.addEventListener !== 'function') { + throw new InvalidArgumentError('signal must be an EventEmitter or EventTarget') + } + + super('UNDICI_CONNECT') + + this.opaque = opaque || null + this.responseHeaders = responseHeaders || null + this.callback = callback + this.abort = null + + addSignal(this, signal) + } + + onConnect (abort, context) { + if (this.reason) { + abort(this.reason) + return + } + + assert(this.callback) + + this.abort = abort + this.context = context + } + + onHeaders () { + throw new SocketError('bad connect', null) + } + + onUpgrade (statusCode, rawHeaders, socket) { + const { callback, opaque, context } = this + + removeSignal(this) + + this.callback = null + + let headers = rawHeaders + // Indicates is an HTTP2Session + if (headers != null) { + headers = this.responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders) + } + + this.runInAsyncScope(callback, null, null, { + statusCode, + headers, + socket, + opaque, + context + }) + } + + onError (err) { + const { callback, opaque } = this + + removeSignal(this) + + if (callback) { + this.callback = null + queueMicrotask(() => { + this.runInAsyncScope(callback, null, err, { opaque }) + }) + } + } +} + +function connect (opts, callback) { + if (callback === undefined) { + return new Promise((resolve, reject) => { + connect.call(this, opts, (err, data) => { + return err ? reject(err) : resolve(data) + }) + }) + } + + try { + const connectHandler = new ConnectHandler(opts, callback) + const connectOptions = { ...opts, method: 'CONNECT' } + + this.dispatch(connectOptions, connectHandler) + } catch (err) { + if (typeof callback !== 'function') { + throw err + } + const opaque = opts?.opaque + queueMicrotask(() => callback(err, { opaque })) + } +} + +module.exports = connect diff --git a/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/api/api-pipeline.js b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/api/api-pipeline.js new file mode 100644 index 000000000..77f3520a8 --- /dev/null +++ b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/api/api-pipeline.js @@ -0,0 +1,252 @@ +'use strict' + +const { + Readable, + Duplex, + PassThrough +} = require('node:stream') +const assert = require('node:assert') +const { AsyncResource } = require('node:async_hooks') +const { + InvalidArgumentError, + InvalidReturnValueError, + RequestAbortedError +} = require('../core/errors') +const util = require('../core/util') +const { addSignal, removeSignal } = require('./abort-signal') + +function noop () {} + +const kResume = Symbol('resume') + +class PipelineRequest extends Readable { + constructor () { + super({ autoDestroy: true }) + + this[kResume] = null + } + + _read () { + const { [kResume]: resume } = this + + if (resume) { + this[kResume] = null + resume() + } + } + + _destroy (err, callback) { + this._read() + + callback(err) + } +} + +class PipelineResponse extends Readable { + constructor (resume) { + super({ autoDestroy: true }) + this[kResume] = resume + } + + _read () { + this[kResume]() + } + + _destroy (err, callback) { + if (!err && !this._readableState.endEmitted) { + err = new RequestAbortedError() + } + + callback(err) + } +} + +class PipelineHandler extends AsyncResource { + constructor (opts, handler) { + if (!opts || typeof opts !== 'object') { + throw new InvalidArgumentError('invalid opts') + } + + if (typeof handler !== 'function') { + throw new InvalidArgumentError('invalid handler') + } + + const { signal, method, opaque, onInfo, responseHeaders } = opts + + if (signal && typeof signal.on !== 'function' && typeof signal.addEventListener !== 'function') { + throw new InvalidArgumentError('signal must be an EventEmitter or EventTarget') + } + + if (method === 'CONNECT') { + throw new InvalidArgumentError('invalid method') + } + + if (onInfo && typeof onInfo !== 'function') { + throw new InvalidArgumentError('invalid onInfo callback') + } + + super('UNDICI_PIPELINE') + + this.opaque = opaque || null + this.responseHeaders = responseHeaders || null + this.handler = handler + this.abort = null + this.context = null + this.onInfo = onInfo || null + + this.req = new PipelineRequest().on('error', noop) + + this.ret = new Duplex({ + readableObjectMode: opts.objectMode, + autoDestroy: true, + read: () => { + const { body } = this + + if (body?.resume) { + body.resume() + } + }, + write: (chunk, encoding, callback) => { + const { req } = this + + if (req.push(chunk, encoding) || req._readableState.destroyed) { + callback() + } else { + req[kResume] = callback + } + }, + destroy: (err, callback) => { + const { body, req, res, ret, abort } = this + + if (!err && !ret._readableState.endEmitted) { + err = new RequestAbortedError() + } + + if (abort && err) { + abort() + } + + util.destroy(body, err) + util.destroy(req, err) + util.destroy(res, err) + + removeSignal(this) + + callback(err) + } + }).on('prefinish', () => { + const { req } = this + + // Node < 15 does not call _final in same tick. + req.push(null) + }) + + this.res = null + + addSignal(this, signal) + } + + onConnect (abort, context) { + const { res } = this + + if (this.reason) { + abort(this.reason) + return + } + + assert(!res, 'pipeline cannot be retried') + + this.abort = abort + this.context = context + } + + onHeaders (statusCode, rawHeaders, resume) { + const { opaque, handler, context } = this + + if (statusCode < 200) { + if (this.onInfo) { + const headers = this.responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders) + this.onInfo({ statusCode, headers }) + } + return + } + + this.res = new PipelineResponse(resume) + + let body + try { + this.handler = null + const headers = this.responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders) + body = this.runInAsyncScope(handler, null, { + statusCode, + headers, + opaque, + body: this.res, + context + }) + } catch (err) { + this.res.on('error', noop) + throw err + } + + if (!body || typeof body.on !== 'function') { + throw new InvalidReturnValueError('expected Readable') + } + + body + .on('data', (chunk) => { + const { ret, body } = this + + if (!ret.push(chunk) && body.pause) { + body.pause() + } + }) + .on('error', (err) => { + const { ret } = this + + util.destroy(ret, err) + }) + .on('end', () => { + const { ret } = this + + ret.push(null) + }) + .on('close', () => { + const { ret } = this + + if (!ret._readableState.ended) { + util.destroy(ret, new RequestAbortedError()) + } + }) + + this.body = body + } + + onData (chunk) { + const { res } = this + return res.push(chunk) + } + + onComplete (trailers) { + const { res } = this + res.push(null) + } + + onError (err) { + const { ret } = this + this.handler = null + util.destroy(ret, err) + } +} + +function pipeline (opts, handler) { + try { + const pipelineHandler = new PipelineHandler(opts, handler) + this.dispatch({ ...opts, body: pipelineHandler.req }, pipelineHandler) + return pipelineHandler.ret + } catch (err) { + return new PassThrough().destroy(err) + } +} + +module.exports = pipeline diff --git a/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/api/api-request.js b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/api/api-request.js new file mode 100644 index 000000000..a6341af53 --- /dev/null +++ b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/api/api-request.js @@ -0,0 +1,214 @@ +'use strict' + +const assert = require('node:assert') +const { AsyncResource } = require('node:async_hooks') +const { Readable } = require('./readable') +const { InvalidArgumentError, RequestAbortedError } = require('../core/errors') +const util = require('../core/util') + +function noop () {} + +class RequestHandler extends AsyncResource { + constructor (opts, callback) { + if (!opts || typeof opts !== 'object') { + throw new InvalidArgumentError('invalid opts') + } + + const { signal, method, opaque, body, onInfo, responseHeaders, highWaterMark } = opts + + try { + if (typeof callback !== 'function') { + throw new InvalidArgumentError('invalid callback') + } + + if (highWaterMark != null && (!Number.isFinite(highWaterMark) || highWaterMark < 0)) { + throw new InvalidArgumentError('invalid highWaterMark') + } + + if (signal && typeof signal.on !== 'function' && typeof signal.addEventListener !== 'function') { + throw new InvalidArgumentError('signal must be an EventEmitter or EventTarget') + } + + if (method === 'CONNECT') { + throw new InvalidArgumentError('invalid method') + } + + if (onInfo && typeof onInfo !== 'function') { + throw new InvalidArgumentError('invalid onInfo callback') + } + + super('UNDICI_REQUEST') + } catch (err) { + if (util.isStream(body)) { + util.destroy(body.on('error', noop), err) + } + throw err + } + + this.method = method + this.responseHeaders = responseHeaders || null + this.opaque = opaque || null + this.callback = callback + this.res = null + this.abort = null + this.body = body + this.trailers = {} + this.context = null + this.onInfo = onInfo || null + this.highWaterMark = highWaterMark + this.reason = null + this.removeAbortListener = null + + if (signal?.aborted) { + this.reason = signal.reason ?? new RequestAbortedError() + } else if (signal) { + this.removeAbortListener = util.addAbortListener(signal, () => { + this.reason = signal.reason ?? new RequestAbortedError() + if (this.res) { + util.destroy(this.res.on('error', noop), this.reason) + } else if (this.abort) { + this.abort(this.reason) + } + }) + } + } + + onConnect (abort, context) { + if (this.reason) { + abort(this.reason) + return + } + + assert(this.callback) + + this.abort = abort + this.context = context + } + + onHeaders (statusCode, rawHeaders, resume, statusMessage) { + const { callback, opaque, abort, context, responseHeaders, highWaterMark } = this + + const headers = responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders) + + if (statusCode < 200) { + if (this.onInfo) { + this.onInfo({ statusCode, headers }) + } + return + } + + const parsedHeaders = responseHeaders === 'raw' ? util.parseHeaders(rawHeaders) : headers + const contentType = parsedHeaders['content-type'] + const contentLength = parsedHeaders['content-length'] + const res = new Readable({ + resume, + abort, + contentType, + contentLength: this.method !== 'HEAD' && contentLength + ? Number(contentLength) + : null, + highWaterMark + }) + + if (this.removeAbortListener) { + res.on('close', this.removeAbortListener) + this.removeAbortListener = null + } + + this.callback = null + this.res = res + if (callback !== null) { + try { + this.runInAsyncScope(callback, null, null, { + statusCode, + statusText: statusMessage, + headers, + trailers: this.trailers, + opaque, + body: res, + context + }) + } catch (err) { + // If the callback throws synchronously, we need to handle it + // Remove reference to res to allow res being garbage collected + this.res = null + + // Destroy the response stream + util.destroy(res.on('error', noop), err) + + // Use queueMicrotask to re-throw the error so it reaches uncaughtException + queueMicrotask(() => { + throw err + }) + } + } + } + + onData (chunk) { + return this.res.push(chunk) + } + + onComplete (trailers) { + util.parseHeaders(trailers, this.trailers) + this.res.push(null) + } + + onError (err) { + const { res, callback, body, opaque } = this + + if (callback) { + // TODO: Does this need queueMicrotask? + this.callback = null + queueMicrotask(() => { + this.runInAsyncScope(callback, null, err, { opaque }) + }) + } + + if (res) { + this.res = null + // Ensure all queued handlers are invoked before destroying res. + queueMicrotask(() => { + util.destroy(res.on('error', noop), err) + }) + } + + if (body) { + this.body = null + + if (util.isStream(body)) { + body.on('error', noop) + util.destroy(body, err) + } + } + + if (this.removeAbortListener) { + this.removeAbortListener() + this.removeAbortListener = null + } + } +} + +function request (opts, callback) { + if (callback === undefined) { + return new Promise((resolve, reject) => { + request.call(this, opts, (err, data) => { + return err ? reject(err) : resolve(data) + }) + }) + } + + try { + const handler = new RequestHandler(opts, callback) + + this.dispatch(opts, handler) + } catch (err) { + if (typeof callback !== 'function') { + throw err + } + const opaque = opts?.opaque + queueMicrotask(() => callback(err, { opaque })) + } +} + +module.exports = request +module.exports.RequestHandler = RequestHandler diff --git a/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/api/api-stream.js b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/api/api-stream.js new file mode 100644 index 000000000..5d0b3fbe6 --- /dev/null +++ b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/api/api-stream.js @@ -0,0 +1,209 @@ +'use strict' + +const assert = require('node:assert') +const { finished } = require('node:stream') +const { AsyncResource } = require('node:async_hooks') +const { InvalidArgumentError, InvalidReturnValueError } = require('../core/errors') +const util = require('../core/util') +const { addSignal, removeSignal } = require('./abort-signal') + +function noop () {} + +class StreamHandler extends AsyncResource { + constructor (opts, factory, callback) { + if (!opts || typeof opts !== 'object') { + throw new InvalidArgumentError('invalid opts') + } + + const { signal, method, opaque, body, onInfo, responseHeaders } = opts + + try { + if (typeof callback !== 'function') { + throw new InvalidArgumentError('invalid callback') + } + + if (typeof factory !== 'function') { + throw new InvalidArgumentError('invalid factory') + } + + if (signal && typeof signal.on !== 'function' && typeof signal.addEventListener !== 'function') { + throw new InvalidArgumentError('signal must be an EventEmitter or EventTarget') + } + + if (method === 'CONNECT') { + throw new InvalidArgumentError('invalid method') + } + + if (onInfo && typeof onInfo !== 'function') { + throw new InvalidArgumentError('invalid onInfo callback') + } + + super('UNDICI_STREAM') + } catch (err) { + if (util.isStream(body)) { + util.destroy(body.on('error', noop), err) + } + throw err + } + + this.responseHeaders = responseHeaders || null + this.opaque = opaque || null + this.factory = factory + this.callback = callback + this.res = null + this.abort = null + this.context = null + this.trailers = null + this.body = body + this.onInfo = onInfo || null + + if (util.isStream(body)) { + body.on('error', (err) => { + this.onError(err) + }) + } + + addSignal(this, signal) + } + + onConnect (abort, context) { + if (this.reason) { + abort(this.reason) + return + } + + assert(this.callback) + + this.abort = abort + this.context = context + } + + onHeaders (statusCode, rawHeaders, resume, statusMessage) { + const { factory, opaque, context, responseHeaders } = this + + const headers = responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders) + + if (statusCode < 200) { + if (this.onInfo) { + this.onInfo({ statusCode, headers }) + } + return + } + + this.factory = null + + if (factory === null) { + return + } + + const res = this.runInAsyncScope(factory, null, { + statusCode, + headers, + opaque, + context + }) + + if ( + !res || + typeof res.write !== 'function' || + typeof res.end !== 'function' || + typeof res.on !== 'function' + ) { + throw new InvalidReturnValueError('expected Writable') + } + + // TODO: Avoid finished. It registers an unnecessary amount of listeners. + finished(res, { readable: false }, (err) => { + const { callback, res, opaque, trailers, abort } = this + + this.res = null + if (err || !res?.readable) { + util.destroy(res, err) + } + + this.callback = null + this.runInAsyncScope(callback, null, err || null, { opaque, trailers }) + + if (err) { + abort() + } + }) + + res.on('drain', resume) + + this.res = res + + const needDrain = res.writableNeedDrain !== undefined + ? res.writableNeedDrain + : res._writableState?.needDrain + + return needDrain !== true + } + + onData (chunk) { + const { res } = this + + return res ? res.write(chunk) : true + } + + onComplete (trailers) { + const { res } = this + + removeSignal(this) + + if (!res) { + return + } + + this.trailers = util.parseHeaders(trailers) + + res.end() + } + + onError (err) { + const { res, callback, opaque, body } = this + + removeSignal(this) + + this.factory = null + + if (res) { + this.res = null + util.destroy(res, err) + } else if (callback) { + this.callback = null + queueMicrotask(() => { + this.runInAsyncScope(callback, null, err, { opaque }) + }) + } + + if (body) { + this.body = null + util.destroy(body, err) + } + } +} + +function stream (opts, factory, callback) { + if (callback === undefined) { + return new Promise((resolve, reject) => { + stream.call(this, opts, factory, (err, data) => { + return err ? reject(err) : resolve(data) + }) + }) + } + + try { + const handler = new StreamHandler(opts, factory, callback) + + this.dispatch(opts, handler) + } catch (err) { + if (typeof callback !== 'function') { + throw err + } + const opaque = opts?.opaque + queueMicrotask(() => callback(err, { opaque })) + } +} + +module.exports = stream diff --git a/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/api/api-upgrade.js b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/api/api-upgrade.js new file mode 100644 index 000000000..2b03f2075 --- /dev/null +++ b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/api/api-upgrade.js @@ -0,0 +1,111 @@ +'use strict' + +const { InvalidArgumentError, SocketError } = require('../core/errors') +const { AsyncResource } = require('node:async_hooks') +const assert = require('node:assert') +const util = require('../core/util') +const { kHTTP2Stream } = require('../core/symbols') +const { addSignal, removeSignal } = require('./abort-signal') + +class UpgradeHandler extends AsyncResource { + constructor (opts, callback) { + if (!opts || typeof opts !== 'object') { + throw new InvalidArgumentError('invalid opts') + } + + if (typeof callback !== 'function') { + throw new InvalidArgumentError('invalid callback') + } + + const { signal, opaque, responseHeaders } = opts + + if (signal && typeof signal.on !== 'function' && typeof signal.addEventListener !== 'function') { + throw new InvalidArgumentError('signal must be an EventEmitter or EventTarget') + } + + super('UNDICI_UPGRADE') + + this.responseHeaders = responseHeaders || null + this.opaque = opaque || null + this.callback = callback + this.abort = null + this.context = null + + addSignal(this, signal) + } + + onConnect (abort, context) { + if (this.reason) { + abort(this.reason) + return + } + + assert(this.callback) + + this.abort = abort + this.context = null + } + + onHeaders () { + throw new SocketError('bad upgrade', null) + } + + onUpgrade (statusCode, rawHeaders, socket) { + assert(socket[kHTTP2Stream] === true ? statusCode === 200 : statusCode === 101) + + const { callback, opaque, context } = this + + removeSignal(this) + + this.callback = null + const headers = this.responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders) + this.runInAsyncScope(callback, null, null, { + headers, + socket, + opaque, + context + }) + } + + onError (err) { + const { callback, opaque } = this + + removeSignal(this) + + if (callback) { + this.callback = null + queueMicrotask(() => { + this.runInAsyncScope(callback, null, err, { opaque }) + }) + } + } +} + +function upgrade (opts, callback) { + if (callback === undefined) { + return new Promise((resolve, reject) => { + upgrade.call(this, opts, (err, data) => { + return err ? reject(err) : resolve(data) + }) + }) + } + + try { + const upgradeHandler = new UpgradeHandler(opts, callback) + const upgradeOpts = { + ...opts, + method: opts.method || 'GET', + upgrade: opts.protocol || 'Websocket' + } + + this.dispatch(upgradeOpts, upgradeHandler) + } catch (err) { + if (typeof callback !== 'function') { + throw err + } + const opaque = opts?.opaque + queueMicrotask(() => callback(err, { opaque })) + } +} + +module.exports = upgrade diff --git a/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/api/index.js b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/api/index.js new file mode 100644 index 000000000..8983a5e74 --- /dev/null +++ b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/api/index.js @@ -0,0 +1,7 @@ +'use strict' + +module.exports.request = require('./api-request') +module.exports.stream = require('./api-stream') +module.exports.pipeline = require('./api-pipeline') +module.exports.upgrade = require('./api-upgrade') +module.exports.connect = require('./api-connect') diff --git a/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/api/readable.js b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/api/readable.js new file mode 100644 index 000000000..cdede9599 --- /dev/null +++ b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/api/readable.js @@ -0,0 +1,580 @@ +'use strict' + +const assert = require('node:assert') +const { Readable } = require('node:stream') +const { RequestAbortedError, NotSupportedError, InvalidArgumentError, AbortError } = require('../core/errors') +const util = require('../core/util') +const { ReadableStreamFrom } = require('../core/util') + +const kConsume = Symbol('kConsume') +const kReading = Symbol('kReading') +const kBody = Symbol('kBody') +const kAbort = Symbol('kAbort') +const kContentType = Symbol('kContentType') +const kContentLength = Symbol('kContentLength') +const kUsed = Symbol('kUsed') +const kBytesRead = Symbol('kBytesRead') + +const noop = () => {} + +/** + * @class + * @extends {Readable} + * @see https://fetch.spec.whatwg.org/#body + */ +class BodyReadable extends Readable { + /** + * @param {object} opts + * @param {(this: Readable, size: number) => void} opts.resume + * @param {() => (void | null)} opts.abort + * @param {string} [opts.contentType = ''] + * @param {number} [opts.contentLength] + * @param {number} [opts.highWaterMark = 64 * 1024] + */ + constructor ({ + resume, + abort, + contentType = '', + contentLength, + highWaterMark = 64 * 1024 // Same as nodejs fs streams. + }) { + super({ + autoDestroy: true, + read: resume, + highWaterMark + }) + + this._readableState.dataEmitted = false + + this[kAbort] = abort + + /** @type {Consume | null} */ + this[kConsume] = null + + /** @type {number} */ + this[kBytesRead] = 0 + + /** @type {ReadableStream|null} */ + this[kBody] = null + + /** @type {boolean} */ + this[kUsed] = false + + /** @type {string} */ + this[kContentType] = contentType + + /** @type {number|null} */ + this[kContentLength] = Number.isFinite(contentLength) ? contentLength : null + + /** + * Is stream being consumed through Readable API? + * This is an optimization so that we avoid checking + * for 'data' and 'readable' listeners in the hot path + * inside push(). + * + * @type {boolean} + */ + this[kReading] = false + } + + /** + * @param {Error|null} err + * @param {(error:(Error|null)) => void} callback + * @returns {void} + */ + _destroy (err, callback) { + if (!err && !this._readableState.endEmitted) { + err = new RequestAbortedError() + } + + if (err) { + this[kAbort]() + } + + // Workaround for Node "bug". If the stream is destroyed in same + // tick as it is created, then a user who is waiting for a + // promise (i.e micro tick) for installing an 'error' listener will + // never get a chance and will always encounter an unhandled exception. + if (!this[kUsed]) { + setImmediate(callback, err) + } else { + callback(err) + } + } + + /** + * @param {string|symbol} event + * @param {(...args: any[]) => void} listener + * @returns {this} + */ + on (event, listener) { + if (event === 'data' || event === 'readable') { + this[kReading] = true + this[kUsed] = true + } + return super.on(event, listener) + } + + /** + * @param {string|symbol} event + * @param {(...args: any[]) => void} listener + * @returns {this} + */ + addListener (event, listener) { + return this.on(event, listener) + } + + /** + * @param {string|symbol} event + * @param {(...args: any[]) => void} listener + * @returns {this} + */ + off (event, listener) { + const ret = super.off(event, listener) + if (event === 'data' || event === 'readable') { + this[kReading] = ( + this.listenerCount('data') > 0 || + this.listenerCount('readable') > 0 + ) + } + return ret + } + + /** + * @param {string|symbol} event + * @param {(...args: any[]) => void} listener + * @returns {this} + */ + removeListener (event, listener) { + return this.off(event, listener) + } + + /** + * @param {Buffer|null} chunk + * @returns {boolean} + */ + push (chunk) { + if (chunk) { + this[kBytesRead] += chunk.length + if (this[kConsume]) { + consumePush(this[kConsume], chunk) + return this[kReading] ? super.push(chunk) : true + } + } + + return super.push(chunk) + } + + /** + * Consumes and returns the body as a string. + * + * @see https://fetch.spec.whatwg.org/#dom-body-text + * @returns {Promise} + */ + text () { + return consume(this, 'text') + } + + /** + * Consumes and returns the body as a JavaScript Object. + * + * @see https://fetch.spec.whatwg.org/#dom-body-json + * @returns {Promise} + */ + json () { + return consume(this, 'json') + } + + /** + * Consumes and returns the body as a Blob + * + * @see https://fetch.spec.whatwg.org/#dom-body-blob + * @returns {Promise} + */ + blob () { + return consume(this, 'blob') + } + + /** + * Consumes and returns the body as an Uint8Array. + * + * @see https://fetch.spec.whatwg.org/#dom-body-bytes + * @returns {Promise} + */ + bytes () { + return consume(this, 'bytes') + } + + /** + * Consumes and returns the body as an ArrayBuffer. + * + * @see https://fetch.spec.whatwg.org/#dom-body-arraybuffer + * @returns {Promise} + */ + arrayBuffer () { + return consume(this, 'arrayBuffer') + } + + /** + * Not implemented + * + * @see https://fetch.spec.whatwg.org/#dom-body-formdata + * @throws {NotSupportedError} + */ + async formData () { + // TODO: Implement. + throw new NotSupportedError() + } + + /** + * Returns true if the body is not null and the body has been consumed. + * Otherwise, returns false. + * + * @see https://fetch.spec.whatwg.org/#dom-body-bodyused + * @readonly + * @returns {boolean} + */ + get bodyUsed () { + return util.isDisturbed(this) + } + + /** + * @see https://fetch.spec.whatwg.org/#dom-body-body + * @readonly + * @returns {ReadableStream} + */ + get body () { + if (!this[kBody]) { + this[kBody] = ReadableStreamFrom(this) + if (this[kConsume]) { + // TODO: Is this the best way to force a lock? + this[kBody].getReader() // Ensure stream is locked. + assert(this[kBody].locked) + } + } + return this[kBody] + } + + /** + * Dumps the response body by reading `limit` number of bytes. + * @param {object} opts + * @param {number} [opts.limit = 131072] Number of bytes to read. + * @param {AbortSignal} [opts.signal] An AbortSignal to cancel the dump. + * @returns {Promise} + */ + dump (opts) { + const signal = opts?.signal + + if (signal != null && (typeof signal !== 'object' || !('aborted' in signal))) { + return Promise.reject(new InvalidArgumentError('signal must be an AbortSignal')) + } + + const limit = opts?.limit && Number.isFinite(opts.limit) + ? opts.limit + : 128 * 1024 + + if (signal?.aborted) { + return Promise.reject(signal.reason ?? new AbortError()) + } + + if (this._readableState.closeEmitted) { + return Promise.resolve(null) + } + + return new Promise((resolve, reject) => { + if ( + (this[kContentLength] && (this[kContentLength] > limit)) || + this[kBytesRead] > limit + ) { + this.destroy(new AbortError()) + } + + if (signal) { + const onAbort = () => { + this.destroy(signal.reason ?? new AbortError()) + } + signal.addEventListener('abort', onAbort) + this + .on('close', function () { + signal.removeEventListener('abort', onAbort) + if (signal.aborted) { + reject(signal.reason ?? new AbortError()) + } else { + resolve(null) + } + }) + } else { + this.on('close', resolve) + } + + this + .on('error', noop) + .on('data', () => { + if (this[kBytesRead] > limit) { + this.destroy() + } + }) + .resume() + }) + } + + /** + * @param {BufferEncoding} encoding + * @returns {this} + */ + setEncoding (encoding) { + if (Buffer.isEncoding(encoding)) { + this._readableState.encoding = encoding + } + return this + } +} + +/** + * @see https://streams.spec.whatwg.org/#readablestream-locked + * @param {BodyReadable} bodyReadable + * @returns {boolean} + */ +function isLocked (bodyReadable) { + // Consume is an implicit lock. + return bodyReadable[kBody]?.locked === true || bodyReadable[kConsume] !== null +} + +/** + * @see https://fetch.spec.whatwg.org/#body-unusable + * @param {BodyReadable} bodyReadable + * @returns {boolean} + */ +function isUnusable (bodyReadable) { + return util.isDisturbed(bodyReadable) || isLocked(bodyReadable) +} + +/** + * @typedef {'text' | 'json' | 'blob' | 'bytes' | 'arrayBuffer'} ConsumeType + */ + +/** + * @template {ConsumeType} T + * @typedef {T extends 'text' ? string : + * T extends 'json' ? unknown : + * T extends 'blob' ? Blob : + * T extends 'arrayBuffer' ? ArrayBuffer : + * T extends 'bytes' ? Uint8Array : + * never + * } ConsumeReturnType + */ +/** + * @typedef {object} Consume + * @property {ConsumeType} type + * @property {BodyReadable} stream + * @property {((value?: any) => void)} resolve + * @property {((err: Error) => void)} reject + * @property {number} length + * @property {Buffer[]} body + */ + +/** + * @template {ConsumeType} T + * @param {BodyReadable} stream + * @param {T} type + * @returns {Promise>} + */ +function consume (stream, type) { + assert(!stream[kConsume]) + + return new Promise((resolve, reject) => { + if (isUnusable(stream)) { + const rState = stream._readableState + if (rState.destroyed && rState.closeEmitted === false) { + stream + .on('error', reject) + .on('close', () => { + reject(new TypeError('unusable')) + }) + } else { + reject(rState.errored ?? new TypeError('unusable')) + } + } else { + queueMicrotask(() => { + stream[kConsume] = { + type, + stream, + resolve, + reject, + length: 0, + body: [] + } + + stream + .on('error', function (err) { + consumeFinish(this[kConsume], err) + }) + .on('close', function () { + if (this[kConsume].body !== null) { + consumeFinish(this[kConsume], new RequestAbortedError()) + } + }) + + consumeStart(stream[kConsume]) + }) + } + }) +} + +/** + * @param {Consume} consume + * @returns {void} + */ +function consumeStart (consume) { + if (consume.body === null) { + return + } + + const { _readableState: state } = consume.stream + + if (state.bufferIndex) { + const start = state.bufferIndex + const end = state.buffer.length + for (let n = start; n < end; n++) { + consumePush(consume, state.buffer[n]) + } + } else { + for (const chunk of state.buffer) { + consumePush(consume, chunk) + } + } + + if (state.endEmitted) { + consumeEnd(this[kConsume], this._readableState.encoding) + } else { + consume.stream.on('end', function () { + consumeEnd(this[kConsume], this._readableState.encoding) + }) + } + + consume.stream.resume() + + while (consume.stream.read() != null) { + // Loop + } +} + +/** + * @param {Buffer[]} chunks + * @param {number} length + * @param {BufferEncoding} [encoding='utf8'] + * @returns {string} + */ +function chunksDecode (chunks, length, encoding) { + if (chunks.length === 0 || length === 0) { + return '' + } + const buffer = chunks.length === 1 ? chunks[0] : Buffer.concat(chunks, length) + const bufferLength = buffer.length + + // Skip BOM. + const start = + bufferLength > 2 && + buffer[0] === 0xef && + buffer[1] === 0xbb && + buffer[2] === 0xbf + ? 3 + : 0 + if (!encoding || encoding === 'utf8' || encoding === 'utf-8') { + return buffer.utf8Slice(start, bufferLength) + } else { + return buffer.subarray(start, bufferLength).toString(encoding) + } +} + +/** + * @param {Buffer[]} chunks + * @param {number} length + * @returns {Uint8Array} + */ +function chunksConcat (chunks, length) { + if (chunks.length === 0 || length === 0) { + return new Uint8Array(0) + } + if (chunks.length === 1) { + // fast-path + return new Uint8Array(chunks[0]) + } + const buffer = new Uint8Array(Buffer.allocUnsafeSlow(length).buffer) + + let offset = 0 + for (let i = 0; i < chunks.length; ++i) { + const chunk = chunks[i] + buffer.set(chunk, offset) + offset += chunk.length + } + + return buffer +} + +/** + * @param {Consume} consume + * @param {BufferEncoding} encoding + * @returns {void} + */ +function consumeEnd (consume, encoding) { + const { type, body, resolve, stream, length } = consume + + try { + if (type === 'text') { + resolve(chunksDecode(body, length, encoding)) + } else if (type === 'json') { + resolve(JSON.parse(chunksDecode(body, length, encoding))) + } else if (type === 'arrayBuffer') { + resolve(chunksConcat(body, length).buffer) + } else if (type === 'blob') { + resolve(new Blob(body, { type: stream[kContentType] })) + } else if (type === 'bytes') { + resolve(chunksConcat(body, length)) + } + + consumeFinish(consume) + } catch (err) { + stream.destroy(err) + } +} + +/** + * @param {Consume} consume + * @param {Buffer} chunk + * @returns {void} + */ +function consumePush (consume, chunk) { + consume.length += chunk.length + consume.body.push(chunk) +} + +/** + * @param {Consume} consume + * @param {Error} [err] + * @returns {void} + */ +function consumeFinish (consume, err) { + if (consume.body === null) { + return + } + + if (err) { + consume.reject(err) + } else { + consume.resolve() + } + + // Reset the consume object to allow for garbage collection. + consume.type = null + consume.stream = null + consume.resolve = null + consume.reject = null + consume.length = 0 + consume.body = null +} + +module.exports = { + Readable: BodyReadable, + chunksDecode +} diff --git a/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/cache/memory-cache-store.js b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/cache/memory-cache-store.js new file mode 100644 index 000000000..17a2b8490 --- /dev/null +++ b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/cache/memory-cache-store.js @@ -0,0 +1,279 @@ +'use strict' + +const { Writable } = require('node:stream') +const { EventEmitter } = require('node:events') +const { assertCacheKey, assertCacheValue } = require('../util/cache.js') + +/** + * @typedef {import('../../types/cache-interceptor.d.ts').default.CacheKey} CacheKey + * @typedef {import('../../types/cache-interceptor.d.ts').default.CacheValue} CacheValue + * @typedef {import('../../types/cache-interceptor.d.ts').default.CacheStore} CacheStore + * @typedef {import('../../types/cache-interceptor.d.ts').default.GetResult} GetResult + */ + +/** + * @implements {CacheStore} + * @extends {EventEmitter} + */ +class MemoryCacheStore extends EventEmitter { + #maxCount = 1024 + #maxSize = 104857600 // 100MB + #maxEntrySize = 5242880 // 5MB + + #size = 0 + #count = 0 + #entries = new Map() + #hasEmittedMaxSizeEvent = false + + /** + * @param {import('../../types/cache-interceptor.d.ts').default.MemoryCacheStoreOpts | undefined} [opts] + */ + constructor (opts) { + super() + if (opts) { + if (typeof opts !== 'object') { + throw new TypeError('MemoryCacheStore options must be an object') + } + + if (opts.maxCount !== undefined) { + if ( + typeof opts.maxCount !== 'number' || + !Number.isInteger(opts.maxCount) || + opts.maxCount < 0 + ) { + throw new TypeError('MemoryCacheStore options.maxCount must be a non-negative integer') + } + this.#maxCount = opts.maxCount + } + + if (opts.maxSize !== undefined) { + if ( + typeof opts.maxSize !== 'number' || + !Number.isInteger(opts.maxSize) || + opts.maxSize < 0 + ) { + throw new TypeError('MemoryCacheStore options.maxSize must be a non-negative integer') + } + this.#maxSize = opts.maxSize + } + + if (opts.maxEntrySize !== undefined) { + if ( + typeof opts.maxEntrySize !== 'number' || + !Number.isInteger(opts.maxEntrySize) || + opts.maxEntrySize < 0 + ) { + throw new TypeError('MemoryCacheStore options.maxEntrySize must be a non-negative integer') + } + this.#maxEntrySize = opts.maxEntrySize + } + } + } + + /** + * Get the current size of the cache in bytes + * @returns {number} The current size of the cache in bytes + */ + get size () { + return this.#size + } + + /** + * Check if the cache is full (either max size or max count reached) + * @returns {boolean} True if the cache is full, false otherwise + */ + isFull () { + return this.#size >= this.#maxSize || this.#count >= this.#maxCount + } + + /** + * @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} req + * @returns {import('../../types/cache-interceptor.d.ts').default.GetResult | undefined} + */ + get (key) { + assertCacheKey(key) + + const topLevelKey = `${key.origin}:${key.path}` + + const now = Date.now() + const entries = this.#entries.get(topLevelKey) + + const entry = entries ? findEntry(key, entries, now) : null + + return entry == null + ? undefined + : { + statusMessage: entry.statusMessage, + statusCode: entry.statusCode, + headers: entry.headers, + body: entry.body, + vary: entry.vary ? entry.vary : undefined, + etag: entry.etag, + cacheControlDirectives: entry.cacheControlDirectives, + cachedAt: entry.cachedAt, + staleAt: entry.staleAt, + deleteAt: entry.deleteAt + } + } + + /** + * @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} key + * @param {import('../../types/cache-interceptor.d.ts').default.CacheValue} val + * @returns {Writable | undefined} + */ + createWriteStream (key, val) { + assertCacheKey(key) + assertCacheValue(val) + + const topLevelKey = `${key.origin}:${key.path}` + + const store = this + const entry = { ...key, ...val, body: [], size: 0 } + + return new Writable({ + write (chunk, encoding, callback) { + if (typeof chunk === 'string') { + chunk = Buffer.from(chunk, encoding) + } + + entry.size += chunk.byteLength + + if (entry.size >= store.#maxEntrySize) { + this.destroy() + } else { + entry.body.push(chunk) + } + + callback(null) + }, + final (callback) { + let entries = store.#entries.get(topLevelKey) + if (!entries) { + entries = [] + store.#entries.set(topLevelKey, entries) + } + const previousEntry = findEntry(key, entries, Date.now()) + if (previousEntry) { + const index = entries.indexOf(previousEntry) + entries.splice(index, 1, entry) + store.#size -= previousEntry.size + } else { + entries.push(entry) + store.#count += 1 + } + + store.#size += entry.size + + // Check if cache is full and emit event if needed + if (store.#size > store.#maxSize || store.#count > store.#maxCount) { + // Emit maxSizeExceeded event if we haven't already + if (!store.#hasEmittedMaxSizeEvent) { + store.emit('maxSizeExceeded', { + size: store.#size, + maxSize: store.#maxSize, + count: store.#count, + maxCount: store.#maxCount + }) + store.#hasEmittedMaxSizeEvent = true + } + + // Perform eviction + for (const [key, entries] of store.#entries) { + for (const entry of entries.splice(0, entries.length / 2)) { + store.#size -= entry.size + store.#count -= 1 + } + if (entries.length === 0) { + store.#entries.delete(key) + } + } + + // Reset the event flag after eviction + if (store.#size < store.#maxSize && store.#count < store.#maxCount) { + store.#hasEmittedMaxSizeEvent = false + } + } + + callback(null) + } + }) + } + + /** + * @param {CacheKey} key + */ + delete (key) { + if (typeof key !== 'object') { + throw new TypeError(`expected key to be object, got ${typeof key}`) + } + + const topLevelKey = `${key.origin}:${key.path}` + + for (const entry of this.#entries.get(topLevelKey) ?? []) { + this.#size -= entry.size + this.#count -= 1 + } + this.#entries.delete(topLevelKey) + } +} + +function findEntry (key, entries, now) { + for (let i = 0; i < entries.length; i++) { + const entry = entries[i] + if ( + entry.deleteAt > now && + entry.method === key.method && + varyMatches(key, entry) + ) { + return entry + } + } +} + +function varyMatches (key, entry) { + if (entry.vary == null) { + return true + } + + for (const headerName in entry.vary) { + if (Object.hasOwn(entry.vary, headerName) && !headerValueEquals(key.headers?.[headerName], entry.vary[headerName])) { + return false + } + } + + return true +} + +/** + * @param {string|string[]|null|undefined} lhs + * @param {string|string[]|null|undefined} rhs + * @returns {boolean} + */ +function headerValueEquals (lhs, rhs) { + if (lhs == null && rhs == null) { + return true + } + + if ((lhs == null && rhs != null) || + (lhs != null && rhs == null)) { + return false + } + + if (Array.isArray(lhs) && Array.isArray(rhs)) { + if (lhs.length !== rhs.length) { + return false + } + + for (let i = 0; i < lhs.length; i++) { + if (lhs[i] !== rhs[i]) { + return false + } + } + + return true + } + + return lhs === rhs +} + +module.exports = MemoryCacheStore diff --git a/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/cache/sqlite-cache-store.js b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/cache/sqlite-cache-store.js new file mode 100644 index 000000000..4da06c2dc --- /dev/null +++ b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/cache/sqlite-cache-store.js @@ -0,0 +1,467 @@ +'use strict' + +const { Writable } = require('node:stream') +const { assertCacheKey, assertCacheValue } = require('../util/cache.js') + +let DatabaseSync + +const VERSION = 3 + +// 2gb +const MAX_ENTRY_SIZE = 2 * 1000 * 1000 * 1000 + +/** + * @typedef {import('../../types/cache-interceptor.d.ts').default.CacheStore} CacheStore + * @implements {CacheStore} + * + * @typedef {{ + * id: Readonly, + * body?: Uint8Array + * statusCode: number + * statusMessage: string + * headers?: string + * vary?: string + * etag?: string + * cacheControlDirectives?: string + * cachedAt: number + * staleAt: number + * deleteAt: number + * }} SqliteStoreValue + */ +module.exports = class SqliteCacheStore { + #maxEntrySize = MAX_ENTRY_SIZE + #maxCount = Infinity + + /** + * @type {import('node:sqlite').DatabaseSync} + */ + #db + + /** + * @type {import('node:sqlite').StatementSync} + */ + #getValuesQuery + + /** + * @type {import('node:sqlite').StatementSync} + */ + #updateValueQuery + + /** + * @type {import('node:sqlite').StatementSync} + */ + #insertValueQuery + + /** + * @type {import('node:sqlite').StatementSync} + */ + #deleteExpiredValuesQuery + + /** + * @type {import('node:sqlite').StatementSync} + */ + #deleteByUrlQuery + + /** + * @type {import('node:sqlite').StatementSync} + */ + #countEntriesQuery + + /** + * @type {import('node:sqlite').StatementSync | null} + */ + #deleteOldValuesQuery + + /** + * @param {import('../../types/cache-interceptor.d.ts').default.SqliteCacheStoreOpts | undefined} opts + */ + constructor (opts) { + if (opts) { + if (typeof opts !== 'object') { + throw new TypeError('SqliteCacheStore options must be an object') + } + + if (opts.maxEntrySize !== undefined) { + if ( + typeof opts.maxEntrySize !== 'number' || + !Number.isInteger(opts.maxEntrySize) || + opts.maxEntrySize < 0 + ) { + throw new TypeError('SqliteCacheStore options.maxEntrySize must be a non-negative integer') + } + + if (opts.maxEntrySize > MAX_ENTRY_SIZE) { + throw new TypeError('SqliteCacheStore options.maxEntrySize must be less than 2gb') + } + + this.#maxEntrySize = opts.maxEntrySize + } + + if (opts.maxCount !== undefined) { + if ( + typeof opts.maxCount !== 'number' || + !Number.isInteger(opts.maxCount) || + opts.maxCount < 0 + ) { + throw new TypeError('SqliteCacheStore options.maxCount must be a non-negative integer') + } + this.#maxCount = opts.maxCount + } + } + + if (!DatabaseSync) { + DatabaseSync = require('node:sqlite').DatabaseSync + } + this.#db = new DatabaseSync(opts?.location ?? ':memory:') + + this.#db.exec(` + PRAGMA journal_mode = WAL; + PRAGMA synchronous = NORMAL; + PRAGMA temp_store = memory; + PRAGMA optimize; + + CREATE TABLE IF NOT EXISTS cacheInterceptorV${VERSION} ( + -- Data specific to us + id INTEGER PRIMARY KEY AUTOINCREMENT, + url TEXT NOT NULL, + method TEXT NOT NULL, + + -- Data returned to the interceptor + body BUF NULL, + deleteAt INTEGER NOT NULL, + statusCode INTEGER NOT NULL, + statusMessage TEXT NOT NULL, + headers TEXT NULL, + cacheControlDirectives TEXT NULL, + etag TEXT NULL, + vary TEXT NULL, + cachedAt INTEGER NOT NULL, + staleAt INTEGER NOT NULL + ); + + CREATE INDEX IF NOT EXISTS idx_cacheInterceptorV${VERSION}_getValuesQuery ON cacheInterceptorV${VERSION}(url, method, deleteAt); + CREATE INDEX IF NOT EXISTS idx_cacheInterceptorV${VERSION}_deleteByUrlQuery ON cacheInterceptorV${VERSION}(deleteAt); + `) + + this.#getValuesQuery = this.#db.prepare(` + SELECT + id, + body, + deleteAt, + statusCode, + statusMessage, + headers, + etag, + cacheControlDirectives, + vary, + cachedAt, + staleAt + FROM cacheInterceptorV${VERSION} + WHERE + url = ? + AND method = ? + ORDER BY + deleteAt ASC + `) + + this.#updateValueQuery = this.#db.prepare(` + UPDATE cacheInterceptorV${VERSION} SET + body = ?, + deleteAt = ?, + statusCode = ?, + statusMessage = ?, + headers = ?, + etag = ?, + cacheControlDirectives = ?, + cachedAt = ?, + staleAt = ? + WHERE + id = ? + `) + + this.#insertValueQuery = this.#db.prepare(` + INSERT INTO cacheInterceptorV${VERSION} ( + url, + method, + body, + deleteAt, + statusCode, + statusMessage, + headers, + etag, + cacheControlDirectives, + vary, + cachedAt, + staleAt + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `) + + this.#deleteByUrlQuery = this.#db.prepare( + `DELETE FROM cacheInterceptorV${VERSION} WHERE url = ?` + ) + + this.#countEntriesQuery = this.#db.prepare( + `SELECT COUNT(*) AS total FROM cacheInterceptorV${VERSION}` + ) + + this.#deleteExpiredValuesQuery = this.#db.prepare( + `DELETE FROM cacheInterceptorV${VERSION} WHERE deleteAt <= ?` + ) + + this.#deleteOldValuesQuery = this.#maxCount === Infinity + ? null + : this.#db.prepare(` + DELETE FROM cacheInterceptorV${VERSION} + WHERE id IN ( + SELECT + id + FROM cacheInterceptorV${VERSION} + ORDER BY cachedAt ASC + LIMIT ? + ) + `) + } + + close () { + this.#db.close() + } + + /** + * @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} key + * @returns {(import('../../types/cache-interceptor.d.ts').default.GetResult & { body?: Buffer }) | undefined} + */ + get (key) { + assertCacheKey(key) + + const value = this.#findValue(key) + return value + ? { + body: value.body ? Buffer.from(value.body.buffer, value.body.byteOffset, value.body.byteLength) : undefined, + statusCode: value.statusCode, + statusMessage: value.statusMessage, + headers: value.headers ? JSON.parse(value.headers) : undefined, + etag: value.etag ? value.etag : undefined, + vary: value.vary ? JSON.parse(value.vary) : undefined, + cacheControlDirectives: value.cacheControlDirectives + ? JSON.parse(value.cacheControlDirectives) + : undefined, + cachedAt: value.cachedAt, + staleAt: value.staleAt, + deleteAt: value.deleteAt + } + : undefined + } + + /** + * @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} key + * @param {import('../../types/cache-interceptor.d.ts').default.CacheValue & { body: null | Buffer | Array}} value + */ + set (key, value) { + assertCacheKey(key) + + const url = this.#makeValueUrl(key) + const body = Array.isArray(value.body) ? Buffer.concat(value.body) : value.body + const size = body?.byteLength + + if (size && size > this.#maxEntrySize) { + return + } + + const existingValue = this.#findValue(key, true) + if (existingValue) { + // Updating an existing response, let's overwrite it + this.#updateValueQuery.run( + body, + value.deleteAt, + value.statusCode, + value.statusMessage, + value.headers ? JSON.stringify(value.headers) : null, + value.etag ? value.etag : null, + value.cacheControlDirectives ? JSON.stringify(value.cacheControlDirectives) : null, + value.cachedAt, + value.staleAt, + existingValue.id + ) + } else { + // New response, let's insert it + this.#insertValueQuery.run( + url, + key.method, + body, + value.deleteAt, + value.statusCode, + value.statusMessage, + value.headers ? JSON.stringify(value.headers) : null, + value.etag ? value.etag : null, + value.cacheControlDirectives ? JSON.stringify(value.cacheControlDirectives) : null, + value.vary ? JSON.stringify(value.vary) : null, + value.cachedAt, + value.staleAt + ) + this.#prune() + } + } + + /** + * @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} key + * @param {import('../../types/cache-interceptor.d.ts').default.CacheValue} value + * @returns {Writable | undefined} + */ + createWriteStream (key, value) { + assertCacheKey(key) + assertCacheValue(value) + + let size = 0 + /** + * @type {Buffer[] | null} + */ + const body = [] + const store = this + + return new Writable({ + decodeStrings: true, + write (chunk, encoding, callback) { + size += chunk.byteLength + + if (size < store.#maxEntrySize) { + body.push(chunk) + } else { + this.destroy() + } + + callback() + }, + final (callback) { + store.set(key, { ...value, body }) + callback() + } + }) + } + + /** + * @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} key + */ + delete (key) { + if (typeof key !== 'object') { + throw new TypeError(`expected key to be object, got ${typeof key}`) + } + + this.#deleteByUrlQuery.run(this.#makeValueUrl(key)) + } + + #prune () { + if (Number.isFinite(this.#maxCount) && this.size <= this.#maxCount) { + return 0 + } + + { + const removed = this.#deleteExpiredValuesQuery.run(Date.now()).changes + if (removed) { + return removed + } + } + + { + const removed = this.#deleteOldValuesQuery?.run(Math.max(Math.floor(this.#maxCount * 0.1), 1)).changes + if (removed) { + return removed + } + } + + return 0 + } + + /** + * Counts the number of rows in the cache + * @returns {Number} + */ + get size () { + const { total } = this.#countEntriesQuery.get() + return total + } + + /** + * @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} key + * @returns {string} + */ + #makeValueUrl (key) { + return `${key.origin}/${key.path}` + } + + /** + * @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} key + * @param {boolean} [canBeExpired=false] + * @returns {SqliteStoreValue | undefined} + */ + #findValue (key, canBeExpired = false) { + const url = this.#makeValueUrl(key) + const { headers, method } = key + + /** + * @type {SqliteStoreValue[]} + */ + const values = this.#getValuesQuery.all(url, method) + + if (values.length === 0) { + return undefined + } + + const now = Date.now() + for (const value of values) { + if (now >= value.deleteAt && !canBeExpired) { + continue + } + + let matches = true + + if (value.vary) { + const vary = JSON.parse(value.vary) + + for (const header in vary) { + if (!headerValueEquals(headers[header], vary[header])) { + matches = false + break + } + } + } + + if (matches) { + return value + } + } + + return undefined + } +} + +/** + * @param {string|string[]|null|undefined} lhs + * @param {string|string[]|null|undefined} rhs + * @returns {boolean} + */ +function headerValueEquals (lhs, rhs) { + if (lhs == null && rhs == null) { + return true + } + + if ((lhs == null && rhs != null) || + (lhs != null && rhs == null)) { + return false + } + + if (Array.isArray(lhs) && Array.isArray(rhs)) { + if (lhs.length !== rhs.length) { + return false + } + + for (let i = 0; i < lhs.length; i++) { + if (lhs[i] !== rhs[i]) { + return false + } + } + + return true + } + + return lhs === rhs +} diff --git a/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/core/connect.js b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/core/connect.js new file mode 100644 index 000000000..89a20c740 --- /dev/null +++ b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/core/connect.js @@ -0,0 +1,153 @@ +'use strict' + +const net = require('node:net') +const assert = require('node:assert') +const util = require('./util') +const { InvalidArgumentError } = require('./errors') + +let tls // include tls conditionally since it is not always available + +// TODO: session re-use does not wait for the first +// connection to resolve the session and might therefore +// resolve the same servername multiple times even when +// re-use is enabled. + +const SessionCache = class WeakSessionCache { + constructor (maxCachedSessions) { + this._maxCachedSessions = maxCachedSessions + this._sessionCache = new Map() + this._sessionRegistry = new FinalizationRegistry((key) => { + if (this._sessionCache.size < this._maxCachedSessions) { + return + } + + const ref = this._sessionCache.get(key) + if (ref !== undefined && ref.deref() === undefined) { + this._sessionCache.delete(key) + } + }) + } + + get (sessionKey) { + const ref = this._sessionCache.get(sessionKey) + return ref ? ref.deref() : null + } + + set (sessionKey, session) { + if (this._maxCachedSessions === 0) { + return + } + + if (this._sessionCache.has(sessionKey)) { + this._sessionCache.delete(sessionKey) + } else if (this._sessionCache.size >= this._maxCachedSessions) { + for (const [key, ref] of this._sessionCache) { + if (ref.deref() === undefined) { + this._sessionCache.delete(key) + return + } + } + + const oldest = this._sessionCache.keys().next() + if (!oldest.done) { + this._sessionCache.delete(oldest.value) + } + } + + this._sessionCache.set(sessionKey, new WeakRef(session)) + this._sessionRegistry.register(session, sessionKey) + } +} + +function buildConnector ({ allowH2, useH2c, maxCachedSessions, socketPath, timeout, session: customSession, ...opts }) { + if (maxCachedSessions != null && (!Number.isInteger(maxCachedSessions) || maxCachedSessions < 0)) { + throw new InvalidArgumentError('maxCachedSessions must be a positive integer or zero') + } + + const options = { path: socketPath, ...opts } + const sessionCache = new SessionCache(maxCachedSessions == null ? 100 : maxCachedSessions) + timeout = timeout == null ? 10e3 : timeout + allowH2 = allowH2 != null ? allowH2 : false + return function connect ({ hostname, host, protocol, port, servername, localAddress, httpSocket }, callback) { + let socket + if (protocol === 'https:') { + if (!tls) { + tls = require('node:tls') + } + servername = servername || options.servername || util.getServerName(host) || null + + const sessionKey = servername || hostname + assert(sessionKey) + + const session = customSession || sessionCache.get(sessionKey) || null + + port = port || 443 + + socket = tls.connect({ + highWaterMark: 16384, // TLS in node can't have bigger HWM anyway... + ...options, + servername, + session, + localAddress, + ALPNProtocols: allowH2 ? ['http/1.1', 'h2'] : ['http/1.1'], + socket: httpSocket, // upgrade socket connection + port, + host: hostname + }) + + socket + .on('session', function (session) { + // TODO (fix): Can a session become invalid once established? Don't think so? + sessionCache.set(sessionKey, session) + }) + } else { + assert(!httpSocket, 'httpSocket can only be sent on TLS update') + + port = port || 80 + + socket = net.connect({ + highWaterMark: 64 * 1024, // Same as nodejs fs streams. + ...options, + localAddress, + port, + host: hostname + }) + if (useH2c === true) { + socket.alpnProtocol = 'h2' + } + } + + // Set TCP keep alive options on the socket here instead of in connect() for the case of assigning the socket + if (options.keepAlive == null || options.keepAlive) { + const keepAliveInitialDelay = options.keepAliveInitialDelay === undefined ? 60e3 : options.keepAliveInitialDelay + socket.setKeepAlive(true, keepAliveInitialDelay) + } + + const clearConnectTimeout = util.setupConnectTimeout(new WeakRef(socket), { timeout, hostname, port }) + + socket + .setNoDelay(true) + .once(protocol === 'https:' ? 'secureConnect' : 'connect', function () { + queueMicrotask(clearConnectTimeout) + + if (callback) { + const cb = callback + callback = null + cb(null, this) + } + }) + .on('error', function (err) { + queueMicrotask(clearConnectTimeout) + + if (callback) { + const cb = callback + callback = null + cb(err) + } + }) + + return socket + } +} + +module.exports = buildConnector diff --git a/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/core/constants.js b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/core/constants.js new file mode 100644 index 000000000..088cf47d8 --- /dev/null +++ b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/core/constants.js @@ -0,0 +1,143 @@ +'use strict' + +/** + * @see https://developer.mozilla.org/docs/Web/HTTP/Headers + */ +const wellknownHeaderNames = /** @type {const} */ ([ + 'Accept', + 'Accept-Encoding', + 'Accept-Language', + 'Accept-Ranges', + 'Access-Control-Allow-Credentials', + 'Access-Control-Allow-Headers', + 'Access-Control-Allow-Methods', + 'Access-Control-Allow-Origin', + 'Access-Control-Expose-Headers', + 'Access-Control-Max-Age', + 'Access-Control-Request-Headers', + 'Access-Control-Request-Method', + 'Age', + 'Allow', + 'Alt-Svc', + 'Alt-Used', + 'Authorization', + 'Cache-Control', + 'Clear-Site-Data', + 'Connection', + 'Content-Disposition', + 'Content-Encoding', + 'Content-Language', + 'Content-Length', + 'Content-Location', + 'Content-Range', + 'Content-Security-Policy', + 'Content-Security-Policy-Report-Only', + 'Content-Type', + 'Cookie', + 'Cross-Origin-Embedder-Policy', + 'Cross-Origin-Opener-Policy', + 'Cross-Origin-Resource-Policy', + 'Date', + 'Device-Memory', + 'Downlink', + 'ECT', + 'ETag', + 'Expect', + 'Expect-CT', + 'Expires', + 'Forwarded', + 'From', + 'Host', + 'If-Match', + 'If-Modified-Since', + 'If-None-Match', + 'If-Range', + 'If-Unmodified-Since', + 'Keep-Alive', + 'Last-Modified', + 'Link', + 'Location', + 'Max-Forwards', + 'Origin', + 'Permissions-Policy', + 'Pragma', + 'Proxy-Authenticate', + 'Proxy-Authorization', + 'RTT', + 'Range', + 'Referer', + 'Referrer-Policy', + 'Refresh', + 'Retry-After', + 'Sec-WebSocket-Accept', + 'Sec-WebSocket-Extensions', + 'Sec-WebSocket-Key', + 'Sec-WebSocket-Protocol', + 'Sec-WebSocket-Version', + 'Server', + 'Server-Timing', + 'Service-Worker-Allowed', + 'Service-Worker-Navigation-Preload', + 'Set-Cookie', + 'SourceMap', + 'Strict-Transport-Security', + 'Supports-Loading-Mode', + 'TE', + 'Timing-Allow-Origin', + 'Trailer', + 'Transfer-Encoding', + 'Upgrade', + 'Upgrade-Insecure-Requests', + 'User-Agent', + 'Vary', + 'Via', + 'WWW-Authenticate', + 'X-Content-Type-Options', + 'X-DNS-Prefetch-Control', + 'X-Frame-Options', + 'X-Permitted-Cross-Domain-Policies', + 'X-Powered-By', + 'X-Requested-With', + 'X-XSS-Protection' +]) + +/** @type {Record, string>} */ +const headerNameLowerCasedRecord = {} + +// Note: object prototypes should not be able to be referenced. e.g. `Object#hasOwnProperty`. +Object.setPrototypeOf(headerNameLowerCasedRecord, null) + +/** + * @type {Record, Buffer>} + */ +const wellknownHeaderNameBuffers = {} + +// Note: object prototypes should not be able to be referenced. e.g. `Object#hasOwnProperty`. +Object.setPrototypeOf(wellknownHeaderNameBuffers, null) + +/** + * @param {string} header Lowercased header + * @returns {Buffer} + */ +function getHeaderNameAsBuffer (header) { + let buffer = wellknownHeaderNameBuffers[header] + + if (buffer === undefined) { + buffer = Buffer.from(header) + } + + return buffer +} + +for (let i = 0; i < wellknownHeaderNames.length; ++i) { + const key = wellknownHeaderNames[i] + const lowerCasedKey = key.toLowerCase() + headerNameLowerCasedRecord[key] = headerNameLowerCasedRecord[lowerCasedKey] = + lowerCasedKey +} + +module.exports = { + wellknownHeaderNames, + headerNameLowerCasedRecord, + getHeaderNameAsBuffer +} diff --git a/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/core/diagnostics.js b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/core/diagnostics.js new file mode 100644 index 000000000..454ab379b --- /dev/null +++ b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/core/diagnostics.js @@ -0,0 +1,227 @@ +'use strict' + +const diagnosticsChannel = require('node:diagnostics_channel') +const util = require('node:util') + +const undiciDebugLog = util.debuglog('undici') +const fetchDebuglog = util.debuglog('fetch') +const websocketDebuglog = util.debuglog('websocket') + +const channels = { + // Client + beforeConnect: diagnosticsChannel.channel('undici:client:beforeConnect'), + connected: diagnosticsChannel.channel('undici:client:connected'), + connectError: diagnosticsChannel.channel('undici:client:connectError'), + sendHeaders: diagnosticsChannel.channel('undici:client:sendHeaders'), + // Request + create: diagnosticsChannel.channel('undici:request:create'), + bodySent: diagnosticsChannel.channel('undici:request:bodySent'), + bodyChunkSent: diagnosticsChannel.channel('undici:request:bodyChunkSent'), + bodyChunkReceived: diagnosticsChannel.channel('undici:request:bodyChunkReceived'), + headers: diagnosticsChannel.channel('undici:request:headers'), + trailers: diagnosticsChannel.channel('undici:request:trailers'), + error: diagnosticsChannel.channel('undici:request:error'), + // WebSocket + open: diagnosticsChannel.channel('undici:websocket:open'), + close: diagnosticsChannel.channel('undici:websocket:close'), + socketError: diagnosticsChannel.channel('undici:websocket:socket_error'), + ping: diagnosticsChannel.channel('undici:websocket:ping'), + pong: diagnosticsChannel.channel('undici:websocket:pong'), + // ProxyAgent + proxyConnected: diagnosticsChannel.channel('undici:proxy:connected') +} + +let isTrackingClientEvents = false + +function trackClientEvents (debugLog = undiciDebugLog) { + if (isTrackingClientEvents) { + return + } + + // Check if any of the channels already have subscribers to prevent duplicate subscriptions + // This can happen when both Node.js built-in undici and undici as a dependency are present + if (channels.beforeConnect.hasSubscribers || channels.connected.hasSubscribers || + channels.connectError.hasSubscribers || channels.sendHeaders.hasSubscribers) { + isTrackingClientEvents = true + return + } + + isTrackingClientEvents = true + + diagnosticsChannel.subscribe('undici:client:beforeConnect', + evt => { + const { + connectParams: { version, protocol, port, host } + } = evt + debugLog( + 'connecting to %s%s using %s%s', + host, + port ? `:${port}` : '', + protocol, + version + ) + }) + + diagnosticsChannel.subscribe('undici:client:connected', + evt => { + const { + connectParams: { version, protocol, port, host } + } = evt + debugLog( + 'connected to %s%s using %s%s', + host, + port ? `:${port}` : '', + protocol, + version + ) + }) + + diagnosticsChannel.subscribe('undici:client:connectError', + evt => { + const { + connectParams: { version, protocol, port, host }, + error + } = evt + debugLog( + 'connection to %s%s using %s%s errored - %s', + host, + port ? `:${port}` : '', + protocol, + version, + error.message + ) + }) + + diagnosticsChannel.subscribe('undici:client:sendHeaders', + evt => { + const { + request: { method, path, origin } + } = evt + debugLog('sending request to %s %s%s', method, origin, path) + }) +} + +let isTrackingRequestEvents = false + +function trackRequestEvents (debugLog = undiciDebugLog) { + if (isTrackingRequestEvents) { + return + } + + // Check if any of the channels already have subscribers to prevent duplicate subscriptions + // This can happen when both Node.js built-in undici and undici as a dependency are present + if (channels.headers.hasSubscribers || channels.trailers.hasSubscribers || + channels.error.hasSubscribers) { + isTrackingRequestEvents = true + return + } + + isTrackingRequestEvents = true + + diagnosticsChannel.subscribe('undici:request:headers', + evt => { + const { + request: { method, path, origin }, + response: { statusCode } + } = evt + debugLog( + 'received response to %s %s%s - HTTP %d', + method, + origin, + path, + statusCode + ) + }) + + diagnosticsChannel.subscribe('undici:request:trailers', + evt => { + const { + request: { method, path, origin } + } = evt + debugLog('trailers received from %s %s%s', method, origin, path) + }) + + diagnosticsChannel.subscribe('undici:request:error', + evt => { + const { + request: { method, path, origin }, + error + } = evt + debugLog( + 'request to %s %s%s errored - %s', + method, + origin, + path, + error.message + ) + }) +} + +let isTrackingWebSocketEvents = false + +function trackWebSocketEvents (debugLog = websocketDebuglog) { + if (isTrackingWebSocketEvents) { + return + } + + // Check if any of the channels already have subscribers to prevent duplicate subscriptions + // This can happen when both Node.js built-in undici and undici as a dependency are present + if (channels.open.hasSubscribers || channels.close.hasSubscribers || + channels.socketError.hasSubscribers || channels.ping.hasSubscribers || + channels.pong.hasSubscribers) { + isTrackingWebSocketEvents = true + return + } + + isTrackingWebSocketEvents = true + + diagnosticsChannel.subscribe('undici:websocket:open', + evt => { + if (evt.address != null) { + const { address, port } = evt.address + debugLog('connection opened %s%s', address, port ? `:${port}` : '') + } else { + debugLog('connection opened') + } + }) + + diagnosticsChannel.subscribe('undici:websocket:close', + evt => { + const { websocket, code, reason } = evt + debugLog( + 'closed connection to %s - %s %s', + websocket.url, + code, + reason + ) + }) + + diagnosticsChannel.subscribe('undici:websocket:socket_error', + err => { + debugLog('connection errored - %s', err.message) + }) + + diagnosticsChannel.subscribe('undici:websocket:ping', + evt => { + debugLog('ping received') + }) + + diagnosticsChannel.subscribe('undici:websocket:pong', + evt => { + debugLog('pong received') + }) +} + +if (undiciDebugLog.enabled || fetchDebuglog.enabled) { + trackClientEvents(fetchDebuglog.enabled ? fetchDebuglog : undiciDebugLog) + trackRequestEvents(fetchDebuglog.enabled ? fetchDebuglog : undiciDebugLog) +} + +if (websocketDebuglog.enabled) { + trackClientEvents(undiciDebugLog.enabled ? undiciDebugLog : websocketDebuglog) + trackWebSocketEvents(websocketDebuglog) +} + +module.exports = { + channels +} diff --git a/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/core/errors.js b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/core/errors.js new file mode 100644 index 000000000..5a7fcbff3 --- /dev/null +++ b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/core/errors.js @@ -0,0 +1,477 @@ +'use strict' + +const kUndiciError = Symbol.for('undici.error.UND_ERR') +class UndiciError extends Error { + constructor (message, options) { + super(message, options) + this.name = 'UndiciError' + this.code = 'UND_ERR' + } + + static [Symbol.hasInstance] (instance) { + return instance && instance[kUndiciError] === true + } + + get [kUndiciError] () { + return true + } +} + +const kConnectTimeoutError = Symbol.for('undici.error.UND_ERR_CONNECT_TIMEOUT') +class ConnectTimeoutError extends UndiciError { + constructor (message) { + super(message) + this.name = 'ConnectTimeoutError' + this.message = message || 'Connect Timeout Error' + this.code = 'UND_ERR_CONNECT_TIMEOUT' + } + + static [Symbol.hasInstance] (instance) { + return instance && instance[kConnectTimeoutError] === true + } + + get [kConnectTimeoutError] () { + return true + } +} + +const kHeadersTimeoutError = Symbol.for('undici.error.UND_ERR_HEADERS_TIMEOUT') +class HeadersTimeoutError extends UndiciError { + constructor (message) { + super(message) + this.name = 'HeadersTimeoutError' + this.message = message || 'Headers Timeout Error' + this.code = 'UND_ERR_HEADERS_TIMEOUT' + } + + static [Symbol.hasInstance] (instance) { + return instance && instance[kHeadersTimeoutError] === true + } + + get [kHeadersTimeoutError] () { + return true + } +} + +const kHeadersOverflowError = Symbol.for('undici.error.UND_ERR_HEADERS_OVERFLOW') +class HeadersOverflowError extends UndiciError { + constructor (message) { + super(message) + this.name = 'HeadersOverflowError' + this.message = message || 'Headers Overflow Error' + this.code = 'UND_ERR_HEADERS_OVERFLOW' + } + + static [Symbol.hasInstance] (instance) { + return instance && instance[kHeadersOverflowError] === true + } + + get [kHeadersOverflowError] () { + return true + } +} + +const kBodyTimeoutError = Symbol.for('undici.error.UND_ERR_BODY_TIMEOUT') +class BodyTimeoutError extends UndiciError { + constructor (message) { + super(message) + this.name = 'BodyTimeoutError' + this.message = message || 'Body Timeout Error' + this.code = 'UND_ERR_BODY_TIMEOUT' + } + + static [Symbol.hasInstance] (instance) { + return instance && instance[kBodyTimeoutError] === true + } + + get [kBodyTimeoutError] () { + return true + } +} + +const kInvalidArgumentError = Symbol.for('undici.error.UND_ERR_INVALID_ARG') +class InvalidArgumentError extends UndiciError { + constructor (message) { + super(message) + this.name = 'InvalidArgumentError' + this.message = message || 'Invalid Argument Error' + this.code = 'UND_ERR_INVALID_ARG' + } + + static [Symbol.hasInstance] (instance) { + return instance && instance[kInvalidArgumentError] === true + } + + get [kInvalidArgumentError] () { + return true + } +} + +const kInvalidReturnValueError = Symbol.for('undici.error.UND_ERR_INVALID_RETURN_VALUE') +class InvalidReturnValueError extends UndiciError { + constructor (message) { + super(message) + this.name = 'InvalidReturnValueError' + this.message = message || 'Invalid Return Value Error' + this.code = 'UND_ERR_INVALID_RETURN_VALUE' + } + + static [Symbol.hasInstance] (instance) { + return instance && instance[kInvalidReturnValueError] === true + } + + get [kInvalidReturnValueError] () { + return true + } +} + +const kAbortError = Symbol.for('undici.error.UND_ERR_ABORT') +class AbortError extends UndiciError { + constructor (message) { + super(message) + this.name = 'AbortError' + this.message = message || 'The operation was aborted' + this.code = 'UND_ERR_ABORT' + } + + static [Symbol.hasInstance] (instance) { + return instance && instance[kAbortError] === true + } + + get [kAbortError] () { + return true + } +} + +const kRequestAbortedError = Symbol.for('undici.error.UND_ERR_ABORTED') +class RequestAbortedError extends AbortError { + constructor (message) { + super(message) + this.name = 'AbortError' + this.message = message || 'Request aborted' + this.code = 'UND_ERR_ABORTED' + } + + static [Symbol.hasInstance] (instance) { + return instance && instance[kRequestAbortedError] === true + } + + get [kRequestAbortedError] () { + return true + } +} + +const kInformationalError = Symbol.for('undici.error.UND_ERR_INFO') +class InformationalError extends UndiciError { + constructor (message) { + super(message) + this.name = 'InformationalError' + this.message = message || 'Request information' + this.code = 'UND_ERR_INFO' + } + + static [Symbol.hasInstance] (instance) { + return instance && instance[kInformationalError] === true + } + + get [kInformationalError] () { + return true + } +} + +const kRequestContentLengthMismatchError = Symbol.for('undici.error.UND_ERR_REQ_CONTENT_LENGTH_MISMATCH') +class RequestContentLengthMismatchError extends UndiciError { + constructor (message) { + super(message) + this.name = 'RequestContentLengthMismatchError' + this.message = message || 'Request body length does not match content-length header' + this.code = 'UND_ERR_REQ_CONTENT_LENGTH_MISMATCH' + } + + static [Symbol.hasInstance] (instance) { + return instance && instance[kRequestContentLengthMismatchError] === true + } + + get [kRequestContentLengthMismatchError] () { + return true + } +} + +const kResponseContentLengthMismatchError = Symbol.for('undici.error.UND_ERR_RES_CONTENT_LENGTH_MISMATCH') +class ResponseContentLengthMismatchError extends UndiciError { + constructor (message) { + super(message) + this.name = 'ResponseContentLengthMismatchError' + this.message = message || 'Response body length does not match content-length header' + this.code = 'UND_ERR_RES_CONTENT_LENGTH_MISMATCH' + } + + static [Symbol.hasInstance] (instance) { + return instance && instance[kResponseContentLengthMismatchError] === true + } + + get [kResponseContentLengthMismatchError] () { + return true + } +} + +const kClientDestroyedError = Symbol.for('undici.error.UND_ERR_DESTROYED') +class ClientDestroyedError extends UndiciError { + constructor (message) { + super(message) + this.name = 'ClientDestroyedError' + this.message = message || 'The client is destroyed' + this.code = 'UND_ERR_DESTROYED' + } + + static [Symbol.hasInstance] (instance) { + return instance && instance[kClientDestroyedError] === true + } + + get [kClientDestroyedError] () { + return true + } +} + +const kClientClosedError = Symbol.for('undici.error.UND_ERR_CLOSED') +class ClientClosedError extends UndiciError { + constructor (message) { + super(message) + this.name = 'ClientClosedError' + this.message = message || 'The client is closed' + this.code = 'UND_ERR_CLOSED' + } + + static [Symbol.hasInstance] (instance) { + return instance && instance[kClientClosedError] === true + } + + get [kClientClosedError] () { + return true + } +} + +const kSocketError = Symbol.for('undici.error.UND_ERR_SOCKET') +class SocketError extends UndiciError { + constructor (message, socket) { + super(message) + this.name = 'SocketError' + this.message = message || 'Socket error' + this.code = 'UND_ERR_SOCKET' + this.socket = socket + } + + static [Symbol.hasInstance] (instance) { + return instance && instance[kSocketError] === true + } + + get [kSocketError] () { + return true + } +} + +const kNotSupportedError = Symbol.for('undici.error.UND_ERR_NOT_SUPPORTED') +class NotSupportedError extends UndiciError { + constructor (message) { + super(message) + this.name = 'NotSupportedError' + this.message = message || 'Not supported error' + this.code = 'UND_ERR_NOT_SUPPORTED' + } + + static [Symbol.hasInstance] (instance) { + return instance && instance[kNotSupportedError] === true + } + + get [kNotSupportedError] () { + return true + } +} + +const kBalancedPoolMissingUpstreamError = Symbol.for('undici.error.UND_ERR_BPL_MISSING_UPSTREAM') +class BalancedPoolMissingUpstreamError extends UndiciError { + constructor (message) { + super(message) + this.name = 'MissingUpstreamError' + this.message = message || 'No upstream has been added to the BalancedPool' + this.code = 'UND_ERR_BPL_MISSING_UPSTREAM' + } + + static [Symbol.hasInstance] (instance) { + return instance && instance[kBalancedPoolMissingUpstreamError] === true + } + + get [kBalancedPoolMissingUpstreamError] () { + return true + } +} + +const kHTTPParserError = Symbol.for('undici.error.UND_ERR_HTTP_PARSER') +class HTTPParserError extends Error { + constructor (message, code, data) { + super(message) + this.name = 'HTTPParserError' + this.code = code ? `HPE_${code}` : undefined + this.data = data ? data.toString() : undefined + } + + static [Symbol.hasInstance] (instance) { + return instance && instance[kHTTPParserError] === true + } + + get [kHTTPParserError] () { + return true + } +} + +const kResponseExceededMaxSizeError = Symbol.for('undici.error.UND_ERR_RES_EXCEEDED_MAX_SIZE') +class ResponseExceededMaxSizeError extends UndiciError { + constructor (message) { + super(message) + this.name = 'ResponseExceededMaxSizeError' + this.message = message || 'Response content exceeded max size' + this.code = 'UND_ERR_RES_EXCEEDED_MAX_SIZE' + } + + static [Symbol.hasInstance] (instance) { + return instance && instance[kResponseExceededMaxSizeError] === true + } + + get [kResponseExceededMaxSizeError] () { + return true + } +} + +const kRequestRetryError = Symbol.for('undici.error.UND_ERR_REQ_RETRY') +class RequestRetryError extends UndiciError { + constructor (message, code, { headers, data }) { + super(message) + this.name = 'RequestRetryError' + this.message = message || 'Request retry error' + this.code = 'UND_ERR_REQ_RETRY' + this.statusCode = code + this.data = data + this.headers = headers + } + + static [Symbol.hasInstance] (instance) { + return instance && instance[kRequestRetryError] === true + } + + get [kRequestRetryError] () { + return true + } +} + +const kResponseError = Symbol.for('undici.error.UND_ERR_RESPONSE') +class ResponseError extends UndiciError { + constructor (message, code, { headers, body }) { + super(message) + this.name = 'ResponseError' + this.message = message || 'Response error' + this.code = 'UND_ERR_RESPONSE' + this.statusCode = code + this.body = body + this.headers = headers + } + + static [Symbol.hasInstance] (instance) { + return instance && instance[kResponseError] === true + } + + get [kResponseError] () { + return true + } +} + +const kSecureProxyConnectionError = Symbol.for('undici.error.UND_ERR_PRX_TLS') +class SecureProxyConnectionError extends UndiciError { + constructor (cause, message, options = {}) { + super(message, { cause, ...options }) + this.name = 'SecureProxyConnectionError' + this.message = message || 'Secure Proxy Connection failed' + this.code = 'UND_ERR_PRX_TLS' + this.cause = cause + } + + static [Symbol.hasInstance] (instance) { + return instance && instance[kSecureProxyConnectionError] === true + } + + get [kSecureProxyConnectionError] () { + return true + } +} + +const kMaxOriginsReachedError = Symbol.for('undici.error.UND_ERR_MAX_ORIGINS_REACHED') +class MaxOriginsReachedError extends UndiciError { + constructor (message) { + super(message) + this.name = 'MaxOriginsReachedError' + this.message = message || 'Maximum allowed origins reached' + this.code = 'UND_ERR_MAX_ORIGINS_REACHED' + } + + static [Symbol.hasInstance] (instance) { + return instance && instance[kMaxOriginsReachedError] === true + } + + get [kMaxOriginsReachedError] () { + return true + } +} + +class Socks5ProxyError extends UndiciError { + constructor (message, code) { + super(message) + this.name = 'Socks5ProxyError' + this.message = message || 'SOCKS5 proxy error' + this.code = code || 'UND_ERR_SOCKS5' + } +} + +const kMessageSizeExceededError = Symbol.for('undici.error.UND_ERR_WS_MESSAGE_SIZE_EXCEEDED') +class MessageSizeExceededError extends UndiciError { + constructor (message) { + super(message) + this.name = 'MessageSizeExceededError' + this.message = message || 'Max decompressed message size exceeded' + this.code = 'UND_ERR_WS_MESSAGE_SIZE_EXCEEDED' + } + + static [Symbol.hasInstance] (instance) { + return instance && instance[kMessageSizeExceededError] === true + } + + get [kMessageSizeExceededError] () { + return true + } +} + +module.exports = { + AbortError, + HTTPParserError, + UndiciError, + HeadersTimeoutError, + HeadersOverflowError, + BodyTimeoutError, + RequestContentLengthMismatchError, + ConnectTimeoutError, + InvalidArgumentError, + InvalidReturnValueError, + RequestAbortedError, + ClientDestroyedError, + ClientClosedError, + InformationalError, + SocketError, + NotSupportedError, + ResponseContentLengthMismatchError, + BalancedPoolMissingUpstreamError, + ResponseExceededMaxSizeError, + RequestRetryError, + ResponseError, + SecureProxyConnectionError, + MaxOriginsReachedError, + Socks5ProxyError, + MessageSizeExceededError +} diff --git a/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/core/request.js b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/core/request.js new file mode 100644 index 000000000..4b5887d74 --- /dev/null +++ b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/core/request.js @@ -0,0 +1,485 @@ +'use strict' + +const { + InvalidArgumentError, + NotSupportedError +} = require('./errors') +const assert = require('node:assert') +const { + isValidHTTPToken, + isValidHeaderValue, + isStream, + destroy, + isBuffer, + isFormDataLike, + isIterable, + hasSafeIterator, + isBlobLike, + serializePathWithQuery, + assertRequestHandler, + getServerName, + normalizedMethodRecords, + getProtocolFromUrlString +} = require('./util') +const { channels } = require('./diagnostics.js') +const { headerNameLowerCasedRecord } = require('./constants') + +// Verifies that a given path is valid does not contain control chars \x00 to \x20 +const invalidPathRegex = /[^\u0021-\u00ff]/ + +function isValidContentLengthHeaderValue (val) { + if (typeof val !== 'string' || val.length === 0) { + return false + } + + for (let i = 0; i < val.length; i++) { + const charCode = val.charCodeAt(i) + if (charCode < 48 || charCode > 57) { + return false + } + } + + return true +} + +const kHandler = Symbol('handler') + +class Request { + constructor (origin, { + path, + method, + body, + headers, + query, + idempotent, + blocking, + upgrade, + headersTimeout, + bodyTimeout, + reset, + expectContinue, + servername, + throwOnError, + maxRedirections, + typeOfService + }, handler) { + if (typeof path !== 'string') { + throw new InvalidArgumentError('path must be a string') + } else if ( + path[0] !== '/' && + !(path.startsWith('http://') || path.startsWith('https://')) && + method !== 'CONNECT' + ) { + throw new InvalidArgumentError('path must be an absolute URL or start with a slash') + } else if (invalidPathRegex.test(path)) { + throw new InvalidArgumentError('invalid request path') + } + + if (typeof method !== 'string') { + throw new InvalidArgumentError('method must be a string') + } else if (normalizedMethodRecords[method] === undefined && !isValidHTTPToken(method)) { + throw new InvalidArgumentError('invalid request method') + } + + if (upgrade && typeof upgrade !== 'string') { + throw new InvalidArgumentError('upgrade must be a string') + } + + if (upgrade && !isValidHeaderValue(upgrade)) { + throw new InvalidArgumentError('invalid upgrade header') + } + + if (headersTimeout != null && (!Number.isFinite(headersTimeout) || headersTimeout < 0)) { + throw new InvalidArgumentError('invalid headersTimeout') + } + + if (bodyTimeout != null && (!Number.isFinite(bodyTimeout) || bodyTimeout < 0)) { + throw new InvalidArgumentError('invalid bodyTimeout') + } + + if (reset != null && typeof reset !== 'boolean') { + throw new InvalidArgumentError('invalid reset') + } + + if (expectContinue != null && typeof expectContinue !== 'boolean') { + throw new InvalidArgumentError('invalid expectContinue') + } + + if (throwOnError != null) { + throw new InvalidArgumentError('invalid throwOnError') + } + + if (maxRedirections != null && maxRedirections !== 0) { + throw new InvalidArgumentError('maxRedirections is not supported, use the redirect interceptor') + } + + if (typeOfService != null && (!Number.isInteger(typeOfService) || typeOfService < 0 || typeOfService > 255)) { + throw new InvalidArgumentError('typeOfService must be an integer between 0 and 255') + } + + this.headersTimeout = headersTimeout + + this.bodyTimeout = bodyTimeout + + this.method = method + + this.typeOfService = typeOfService ?? 0 + + this.abort = null + + if (body == null) { + this.body = null + } else if (isStream(body)) { + this.body = body + + const rState = this.body._readableState + if (!rState || !rState.autoDestroy) { + this.endHandler = function autoDestroy () { + destroy(this) + } + this.body.on('end', this.endHandler) + } + + this.errorHandler = err => { + if (this.abort) { + this.abort(err) + } else { + this.error = err + } + } + this.body.on('error', this.errorHandler) + } else if (isBuffer(body)) { + this.body = body.byteLength ? body : null + } else if (ArrayBuffer.isView(body)) { + this.body = body.buffer.byteLength ? Buffer.from(body.buffer, body.byteOffset, body.byteLength) : null + } else if (body instanceof ArrayBuffer) { + this.body = body.byteLength ? Buffer.from(body) : null + } else if (typeof body === 'string') { + this.body = body.length ? Buffer.from(body) : null + } else if (isFormDataLike(body) || isIterable(body) || isBlobLike(body)) { + this.body = body + } else { + throw new InvalidArgumentError('body must be a string, a Buffer, a Readable stream, an iterable, or an async iterable') + } + + this.completed = false + this.aborted = false + + this.upgrade = upgrade || null + + this.path = query ? serializePathWithQuery(path, query) : path + + // TODO: shall we maybe standardize it to an URL object? + this.origin = origin + + this.protocol = getProtocolFromUrlString(origin) + + this.idempotent = idempotent == null + ? method === 'HEAD' || method === 'GET' + : idempotent + + this.blocking = blocking ?? this.method !== 'HEAD' + + this.reset = reset == null ? null : reset + + this.host = null + + this.contentLength = null + + this.contentType = null + + this.headers = [] + + // Only for H2 + this.expectContinue = expectContinue != null ? expectContinue : false + + if (Array.isArray(headers)) { + if (headers.length % 2 !== 0) { + throw new InvalidArgumentError('headers array must be even') + } + for (let i = 0; i < headers.length; i += 2) { + processHeader(this, headers[i], headers[i + 1]) + } + } else if (headers && typeof headers === 'object') { + if (hasSafeIterator(headers)) { + for (const header of headers) { + if (!Array.isArray(header) || header.length !== 2) { + throw new InvalidArgumentError('headers must be in key-value pair format') + } + processHeader(this, header[0], header[1]) + } + } else { + const keys = Object.keys(headers) + for (let i = 0; i < keys.length; ++i) { + processHeader(this, keys[i], headers[keys[i]]) + } + } + } else if (headers != null) { + throw new InvalidArgumentError('headers must be an object or an array') + } + + assertRequestHandler(handler, method, upgrade) + + this.servername = servername || getServerName(this.host) || null + + this[kHandler] = handler + + if (channels.create.hasSubscribers) { + channels.create.publish({ request: this }) + } + } + + onBodySent (chunk) { + if (channels.bodyChunkSent.hasSubscribers) { + channels.bodyChunkSent.publish({ request: this, chunk }) + } + if (this[kHandler].onBodySent) { + try { + return this[kHandler].onBodySent(chunk) + } catch (err) { + this.abort(err) + } + } + } + + onRequestSent () { + if (channels.bodySent.hasSubscribers) { + channels.bodySent.publish({ request: this }) + } + + if (this[kHandler].onRequestSent) { + try { + return this[kHandler].onRequestSent() + } catch (err) { + this.abort(err) + } + } + } + + onConnect (abort) { + assert(!this.aborted) + assert(!this.completed) + + if (this.error) { + abort(this.error) + } else { + this.abort = abort + return this[kHandler].onConnect(abort) + } + } + + onResponseStarted () { + return this[kHandler].onResponseStarted?.() + } + + onHeaders (statusCode, headers, resume, statusText) { + assert(!this.aborted) + assert(!this.completed) + + if (channels.headers.hasSubscribers) { + channels.headers.publish({ request: this, response: { statusCode, headers, statusText } }) + } + + try { + return this[kHandler].onHeaders(statusCode, headers, resume, statusText) + } catch (err) { + this.abort(err) + } + } + + onData (chunk) { + assert(!this.aborted) + assert(!this.completed) + + if (channels.bodyChunkReceived.hasSubscribers) { + channels.bodyChunkReceived.publish({ request: this, chunk }) + } + try { + return this[kHandler].onData(chunk) + } catch (err) { + this.abort(err) + return false + } + } + + /** + * @param {number} statusCode + * @param {Buffer[]|string[]} headers + * @param {import('node:stream').Duplex} socket + * @param {string} [statusText] + */ + onUpgrade (statusCode, headers, socket, statusText = '') { + this.onFinally() + + assert(!this.aborted) + assert(!this.completed) + + if (channels.headers.hasSubscribers) { + channels.headers.publish({ request: this, response: { statusCode, headers, statusText } }) + } + + const result = this[kHandler].onUpgrade(statusCode, headers, socket) + + if (!this.aborted) { + this.completed = true + if (channels.trailers.hasSubscribers) { + channels.trailers.publish({ request: this, trailers: [] }) + } + } + + return result + } + + onComplete (trailers) { + this.onFinally() + + assert(!this.aborted) + assert(!this.completed) + + this.completed = true + if (channels.trailers.hasSubscribers) { + channels.trailers.publish({ request: this, trailers }) + } + + try { + return this[kHandler].onComplete(trailers) + } catch (err) { + // TODO (fix): This might be a bad idea? + this.onError(err) + } + } + + onError (error) { + this.onFinally() + + if (channels.error.hasSubscribers) { + channels.error.publish({ request: this, error }) + } + + if (this.aborted) { + return + } + this.aborted = true + + return this[kHandler].onError(error) + } + + onFinally () { + if (this.errorHandler) { + this.body.off('error', this.errorHandler) + this.errorHandler = null + } + + if (this.endHandler) { + this.body.off('end', this.endHandler) + this.endHandler = null + } + } + + addHeader (key, value) { + processHeader(this, key, value) + return this + } +} + +function processHeader (request, key, val) { + if (val && (typeof val === 'object' && !Array.isArray(val))) { + throw new InvalidArgumentError(`invalid ${key} header`) + } else if (val === undefined) { + return + } + + let headerName = headerNameLowerCasedRecord[key] + + if (headerName === undefined) { + headerName = key.toLowerCase() + if (headerNameLowerCasedRecord[headerName] === undefined && !isValidHTTPToken(headerName)) { + throw new InvalidArgumentError('invalid header key') + } + } + + if (Array.isArray(val)) { + const arr = [] + for (let i = 0; i < val.length; i++) { + if (typeof val[i] === 'string') { + if (!isValidHeaderValue(val[i])) { + throw new InvalidArgumentError(`invalid ${key} header`) + } + arr.push(val[i]) + } else if (val[i] === null) { + arr.push('') + } else if (typeof val[i] === 'object') { + throw new InvalidArgumentError(`invalid ${key} header`) + } else { + // Coerce primitives (and reject unsafe coercions such as functions + // with a crafted toString/Symbol.toPrimitive). + const str = `${val[i]}` + if (!isValidHeaderValue(str)) { + throw new InvalidArgumentError(`invalid ${key} header`) + } + arr.push(str) + } + } + val = arr + } else if (typeof val === 'string') { + if (!isValidHeaderValue(val)) { + throw new InvalidArgumentError(`invalid ${key} header`) + } + } else if (val === null) { + val = '' + } else { + // Coerce primitives (and reject unsafe coercions such as functions + // with a crafted toString/Symbol.toPrimitive). + val = `${val}` + if (!isValidHeaderValue(val)) { + throw new InvalidArgumentError(`invalid ${key} header`) + } + } + + if (headerName === 'host') { + if (request.host !== null) { + throw new InvalidArgumentError('duplicate host header') + } + if (typeof val !== 'string') { + throw new InvalidArgumentError('invalid host header') + } + // Consumed by Client + request.host = val + } else if (headerName === 'content-length') { + if (request.contentLength !== null) { + throw new InvalidArgumentError('duplicate content-length header') + } + if (!isValidContentLengthHeaderValue(val)) { + throw new InvalidArgumentError('invalid content-length header') + } + request.contentLength = parseInt(val, 10) + } else if (request.contentType === null && headerName === 'content-type') { + request.contentType = val + request.headers.push(key, val) + } else if (headerName === 'transfer-encoding' || headerName === 'keep-alive' || headerName === 'upgrade') { + throw new InvalidArgumentError(`invalid ${headerName} header`) + } else if (headerName === 'connection') { + // Per RFC 7230 Section 6.1, Connection header can contain + // a comma-separated list of connection option tokens (header names) + const value = typeof val === 'string' ? val : null + if (value === null) { + throw new InvalidArgumentError('invalid connection header') + } + + for (const token of value.toLowerCase().split(',')) { + const trimmed = token.trim() + if (!isValidHTTPToken(trimmed)) { + throw new InvalidArgumentError('invalid connection header') + } + if (trimmed === 'close') { + request.reset = true + } + } + } else if (headerName === 'expect') { + throw new NotSupportedError('expect header not supported') + } else { + request.headers.push(key, val) + } +} + +module.exports = Request diff --git a/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/core/socks5-client.js b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/core/socks5-client.js new file mode 100644 index 000000000..90f6d0a68 --- /dev/null +++ b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/core/socks5-client.js @@ -0,0 +1,422 @@ +'use strict' + +const { EventEmitter } = require('node:events') +const { Buffer } = require('node:buffer') +const { InvalidArgumentError, Socks5ProxyError } = require('./errors') +const { debuglog } = require('node:util') +const { parseAddress } = require('./socks5-utils') + +const debug = debuglog('undici:socks5') +const EMPTY_BUFFER = Buffer.alloc(0) + +// SOCKS5 constants +const SOCKS_VERSION = 0x05 + +// Authentication methods +const AUTH_METHODS = { + NO_AUTH: 0x00, + GSSAPI: 0x01, + USERNAME_PASSWORD: 0x02, + NO_ACCEPTABLE: 0xFF +} + +// SOCKS5 commands +const COMMANDS = { + CONNECT: 0x01, + BIND: 0x02, + UDP_ASSOCIATE: 0x03 +} + +// Address types +const ADDRESS_TYPES = { + IPV4: 0x01, + DOMAIN: 0x03, + IPV6: 0x04 +} + +// Reply codes +const REPLY_CODES = { + SUCCEEDED: 0x00, + GENERAL_FAILURE: 0x01, + CONNECTION_NOT_ALLOWED: 0x02, + NETWORK_UNREACHABLE: 0x03, + HOST_UNREACHABLE: 0x04, + CONNECTION_REFUSED: 0x05, + TTL_EXPIRED: 0x06, + COMMAND_NOT_SUPPORTED: 0x07, + ADDRESS_TYPE_NOT_SUPPORTED: 0x08 +} + +// State machine states +const STATES = { + INITIAL: 'initial', + HANDSHAKING: 'handshaking', + AUTHENTICATING: 'authenticating', + AUTHENTICATED: 'authenticated', + CONNECTING: 'connecting', + CONNECTED: 'connected', + ERROR: 'error', + CLOSED: 'closed' +} + +/** + * SOCKS5 client implementation + * Handles SOCKS5 protocol negotiation and connection establishment + */ +class Socks5Client extends EventEmitter { + constructor (socket, options = {}) { + super() + + if (!socket) { + throw new InvalidArgumentError('socket is required') + } + + this.socket = socket + this.options = options + this.state = STATES.INITIAL + this.buffer = EMPTY_BUFFER + this.onSocketData = this.onData.bind(this) + this.onSocketError = this.onError.bind(this) + this.onSocketClose = this.onClose.bind(this) + + // Authentication settings + this.authMethods = [] + if (options.username && options.password) { + this.authMethods.push(AUTH_METHODS.USERNAME_PASSWORD) + } + this.authMethods.push(AUTH_METHODS.NO_AUTH) + + // Socket event handlers + this.socket.on('data', this.onSocketData) + this.socket.on('error', this.onSocketError) + this.socket.on('close', this.onSocketClose) + } + + /** + * Handle incoming data from the socket + */ + onData (data) { + debug('received data', data.length, 'bytes in state', this.state) + this.buffer = Buffer.concat([this.buffer, data]) + + try { + switch (this.state) { + case STATES.HANDSHAKING: + this.handleHandshakeResponse() + break + case STATES.AUTHENTICATING: + this.handleAuthResponse() + break + case STATES.CONNECTING: + this.handleConnectResponse() + break + } + } catch (err) { + this.onError(err) + } + } + + /** + * Handle socket errors + */ + onError (err) { + debug('socket error', err) + this.state = STATES.ERROR + this.emit('error', err) + this.destroy() + } + + /** + * Handle socket close + */ + onClose () { + debug('socket closed') + this.state = STATES.CLOSED + this.emit('close') + } + + /** + * Destroy the client and underlying socket + */ + destroy () { + if (this.socket && !this.socket.destroyed) { + this.socket.destroy() + } + } + + markAuthenticated () { + this.state = STATES.AUTHENTICATED + this.emit('authenticated') + } + + /** + * Start the SOCKS5 handshake + */ + handshake () { + if (this.state !== STATES.INITIAL) { + throw new InvalidArgumentError('Handshake already started') + } + + debug('starting handshake with', this.authMethods.length, 'auth methods') + this.state = STATES.HANDSHAKING + + // Build handshake request + // +----+----------+----------+ + // |VER | NMETHODS | METHODS | + // +----+----------+----------+ + // | 1 | 1 | 1 to 255 | + // +----+----------+----------+ + const request = Buffer.alloc(2 + this.authMethods.length) + request[0] = SOCKS_VERSION + request[1] = this.authMethods.length + this.authMethods.forEach((method, i) => { + request[2 + i] = method + }) + + this.socket.write(request) + } + + /** + * Handle handshake response from server + */ + handleHandshakeResponse () { + if (this.buffer.length < 2) { + return // Not enough data yet + } + + const version = this.buffer[0] + const method = this.buffer[1] + + if (version !== SOCKS_VERSION) { + throw new Socks5ProxyError(`Invalid SOCKS version: ${version}`, 'UND_ERR_SOCKS5_VERSION') + } + + if (method === AUTH_METHODS.NO_ACCEPTABLE) { + throw new Socks5ProxyError('No acceptable authentication method', 'UND_ERR_SOCKS5_AUTH_REJECTED') + } + + this.buffer = this.buffer.subarray(2) + debug('server selected auth method', method) + + if (method === AUTH_METHODS.NO_AUTH) { + this.markAuthenticated() + } else if (method === AUTH_METHODS.USERNAME_PASSWORD) { + this.state = STATES.AUTHENTICATING + this.sendAuthRequest() + } else { + throw new Socks5ProxyError(`Unsupported authentication method: ${method}`, 'UND_ERR_SOCKS5_AUTH_METHOD') + } + } + + /** + * Send username/password authentication request + */ + sendAuthRequest () { + const { username, password } = this.options + + if (!username || !password) { + throw new InvalidArgumentError('Username and password required for authentication') + } + + debug('sending username/password auth') + + // Username/Password authentication request (RFC 1929) + // +----+------+----------+------+----------+ + // |VER | ULEN | UNAME | PLEN | PASSWD | + // +----+------+----------+------+----------+ + // | 1 | 1 | 1 to 255 | 1 | 1 to 255 | + // +----+------+----------+------+----------+ + const usernameBuffer = Buffer.from(username) + const passwordBuffer = Buffer.from(password) + + if (usernameBuffer.length > 255 || passwordBuffer.length > 255) { + throw new InvalidArgumentError('Username or password too long') + } + + const request = Buffer.alloc(3 + usernameBuffer.length + passwordBuffer.length) + request[0] = 0x01 // Sub-negotiation version + request[1] = usernameBuffer.length + usernameBuffer.copy(request, 2) + request[2 + usernameBuffer.length] = passwordBuffer.length + passwordBuffer.copy(request, 3 + usernameBuffer.length) + + this.socket.write(request) + } + + /** + * Handle authentication response + */ + handleAuthResponse () { + if (this.buffer.length < 2) { + return // Not enough data yet + } + + const version = this.buffer[0] + const status = this.buffer[1] + + if (version !== 0x01) { + throw new Socks5ProxyError(`Invalid auth sub-negotiation version: ${version}`, 'UND_ERR_SOCKS5_AUTH_VERSION') + } + + if (status !== 0x00) { + throw new Socks5ProxyError('Authentication failed', 'UND_ERR_SOCKS5_AUTH_FAILED') + } + + this.buffer = this.buffer.subarray(2) + debug('authentication successful') + this.markAuthenticated() + } + + /** + * Send CONNECT command + * @param {string} address - Target address (IP or domain) + * @param {number} port - Target port + */ + connect (address, port) { + if (this.state === STATES.CONNECTING || this.state === STATES.CONNECTED) { + throw new InvalidArgumentError('Connection already in progress') + } + + if (this.state !== STATES.AUTHENTICATED) { + throw new InvalidArgumentError('Client must be authenticated before CONNECT') + } + + debug('connecting to', address, port) + this.state = STATES.CONNECTING + + const request = this.buildConnectRequest(COMMANDS.CONNECT, address, port) + this.socket.write(request) + } + + /** + * Build a SOCKS5 request + */ + buildConnectRequest (command, address, port) { + // Parse address to determine type and buffer + const { type: addressType, buffer: addressBuffer } = parseAddress(address) + + // Build request + // +----+-----+-------+------+----------+----------+ + // |VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT | + // +----+-----+-------+------+----------+----------+ + // | 1 | 1 | X'00' | 1 | Variable | 2 | + // +----+-----+-------+------+----------+----------+ + const request = Buffer.alloc(4 + addressBuffer.length + 2) + request[0] = SOCKS_VERSION + request[1] = command + request[2] = 0x00 // Reserved + request[3] = addressType + addressBuffer.copy(request, 4) + request.writeUInt16BE(port, 4 + addressBuffer.length) + + return request + } + + /** + * Handle CONNECT response + */ + handleConnectResponse () { + if (this.buffer.length < 4) { + return // Not enough data for header + } + + const version = this.buffer[0] + const reply = this.buffer[1] + const addressType = this.buffer[3] + + if (version !== SOCKS_VERSION) { + throw new Socks5ProxyError(`Invalid SOCKS version in reply: ${version}`, 'UND_ERR_SOCKS5_REPLY_VERSION') + } + + // Calculate the expected response length + let responseLength = 4 // VER + REP + RSV + ATYP + if (addressType === ADDRESS_TYPES.IPV4) { + responseLength += 4 + 2 // IPv4 + port + } else if (addressType === ADDRESS_TYPES.DOMAIN) { + if (this.buffer.length < 5) { + return // Need domain length byte + } + responseLength += 1 + this.buffer[4] + 2 // length byte + domain + port + } else if (addressType === ADDRESS_TYPES.IPV6) { + responseLength += 16 + 2 // IPv6 + port + } else { + throw new Socks5ProxyError(`Invalid address type in reply: ${addressType}`, 'UND_ERR_SOCKS5_ADDR_TYPE') + } + + if (this.buffer.length < responseLength) { + return // Not enough data for full response + } + + if (reply !== REPLY_CODES.SUCCEEDED) { + const errorMessage = this.getReplyErrorMessage(reply) + throw new Socks5ProxyError(`SOCKS5 connection failed: ${errorMessage}`, `UND_ERR_SOCKS5_REPLY_${reply}`) + } + + // Parse bound address and port + let boundAddress + let offset = 4 + + if (addressType === ADDRESS_TYPES.IPV4) { + boundAddress = Array.from(this.buffer.subarray(offset, offset + 4)).join('.') + offset += 4 + } else if (addressType === ADDRESS_TYPES.DOMAIN) { + const domainLength = this.buffer[offset] + offset += 1 + boundAddress = this.buffer.subarray(offset, offset + domainLength).toString() + offset += domainLength + } else if (addressType === ADDRESS_TYPES.IPV6) { + // Parse IPv6 address from 16-byte buffer + const parts = [] + for (let i = 0; i < 8; i++) { + const value = this.buffer.readUInt16BE(offset + i * 2) + parts.push(value.toString(16)) + } + boundAddress = parts.join(':') + offset += 16 + } + + const boundPort = this.buffer.readUInt16BE(offset) + + this.buffer = EMPTY_BUFFER + this.state = STATES.CONNECTED + this.socket.removeListener('data', this.onSocketData) + + debug('connected, bound address:', boundAddress, 'port:', boundPort) + this.emit('connected', { address: boundAddress, port: boundPort }) + } + + /** + * Get human-readable error message for reply code + */ + getReplyErrorMessage (reply) { + switch (reply) { + case REPLY_CODES.GENERAL_FAILURE: + return 'General SOCKS server failure' + case REPLY_CODES.CONNECTION_NOT_ALLOWED: + return 'Connection not allowed by ruleset' + case REPLY_CODES.NETWORK_UNREACHABLE: + return 'Network unreachable' + case REPLY_CODES.HOST_UNREACHABLE: + return 'Host unreachable' + case REPLY_CODES.CONNECTION_REFUSED: + return 'Connection refused' + case REPLY_CODES.TTL_EXPIRED: + return 'TTL expired' + case REPLY_CODES.COMMAND_NOT_SUPPORTED: + return 'Command not supported' + case REPLY_CODES.ADDRESS_TYPE_NOT_SUPPORTED: + return 'Address type not supported' + default: + return `Unknown error code: ${reply}` + } + } +} + +module.exports = { + Socks5Client, + AUTH_METHODS, + COMMANDS, + ADDRESS_TYPES, + REPLY_CODES, + STATES +} diff --git a/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/core/socks5-utils.js b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/core/socks5-utils.js new file mode 100644 index 000000000..e6098bef0 --- /dev/null +++ b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/core/socks5-utils.js @@ -0,0 +1,212 @@ +'use strict' + +const { Buffer } = require('node:buffer') +const net = require('node:net') +const { InvalidArgumentError } = require('./errors') + +/** + * Parse an address and determine its type + * @param {string} address - The address to parse + * @returns {{type: number, buffer: Buffer}} Address type and buffer + */ +function parseAddress (address) { + // Check if it's an IPv4 address + if (net.isIPv4(address)) { + const parts = address.split('.').map(Number) + return { + type: 0x01, // IPv4 + buffer: Buffer.from(parts) + } + } + + // Check if it's an IPv6 address + if (net.isIPv6(address)) { + return { + type: 0x04, // IPv6 + buffer: parseIPv6(address) + } + } + + // Otherwise, treat as domain name + const domainBuffer = Buffer.from(address, 'utf8') + if (domainBuffer.length > 255) { + throw new InvalidArgumentError('Domain name too long (max 255 bytes)') + } + + return { + type: 0x03, // Domain + buffer: Buffer.concat([Buffer.from([domainBuffer.length]), domainBuffer]) + } +} + +/** + * Parse IPv6 address to buffer + * @param {string} address - IPv6 address string + * @returns {Buffer} 16-byte buffer + */ +function parseIPv6 (address) { + const buffer = Buffer.alloc(16) + let normalizedAddress = address + + // Expand an embedded IPv4 tail into the last two IPv6 groups. + if (address.includes('.')) { + const lastColonIndex = address.lastIndexOf(':') + const ipv4Part = address.slice(lastColonIndex + 1) + + if (net.isIPv4(ipv4Part)) { + const octets = ipv4Part.split('.').map(Number) + const high = ((octets[0] << 8) | octets[1]).toString(16) + const low = ((octets[2] << 8) | octets[3]).toString(16) + normalizedAddress = `${address.slice(0, lastColonIndex)}:${high}:${low}` + } + } + + // Handle compressed notation (::) + const doubleColonIndex = normalizedAddress.indexOf('::') + if (doubleColonIndex !== -1) { + const before = normalizedAddress.slice(0, doubleColonIndex) + const after = normalizedAddress.slice(doubleColonIndex + 2) + const beforeParts = before === '' ? [] : before.split(':') + const afterParts = after === '' ? [] : after.split(':') + + let bufferIndex = 0 + for (const part of beforeParts) { + buffer.writeUInt16BE(parseInt(part, 16), bufferIndex) + bufferIndex += 2 + } + bufferIndex = 16 - afterParts.length * 2 + for (const part of afterParts) { + buffer.writeUInt16BE(parseInt(part, 16), bufferIndex) + bufferIndex += 2 + } + } else { + const parts = normalizedAddress.split(':') + for (let i = 0; i < parts.length; i++) { + buffer.writeUInt16BE(parseInt(parts[i], 16), i * 2) + } + } + + return buffer +} + +/** + * Build a SOCKS5 address buffer + * @param {number} type - Address type (1=IPv4, 3=Domain, 4=IPv6) + * @param {Buffer} addressBuffer - The address data + * @param {number} port - Port number + * @returns {Buffer} Complete address buffer including type, address, and port + */ +function buildAddressBuffer (type, addressBuffer, port) { + const portBuffer = Buffer.allocUnsafe(2) + portBuffer.writeUInt16BE(port, 0) + + return Buffer.concat([ + Buffer.from([type]), + addressBuffer, + portBuffer + ]) +} + +/** + * Parse address from SOCKS5 response + * @param {Buffer} buffer - Buffer containing the address + * @param {number} offset - Starting offset in buffer + * @returns {{address: string, port: number, bytesRead: number}} + */ +function parseResponseAddress (buffer, offset = 0) { + if (buffer.length < offset + 1) { + throw new InvalidArgumentError('Buffer too small to contain address type') + } + + const addressType = buffer[offset] + let address + let currentOffset = offset + 1 + + switch (addressType) { + case 0x01: { // IPv4 + if (buffer.length < currentOffset + 6) { + throw new InvalidArgumentError('Buffer too small for IPv4 address') + } + address = Array.from(buffer.subarray(currentOffset, currentOffset + 4)).join('.') + currentOffset += 4 + break + } + + case 0x03: { // Domain + if (buffer.length < currentOffset + 1) { + throw new InvalidArgumentError('Buffer too small for domain length') + } + const domainLength = buffer[currentOffset] + currentOffset += 1 + + if (buffer.length < currentOffset + domainLength + 2) { + throw new InvalidArgumentError('Buffer too small for domain address') + } + address = buffer.subarray(currentOffset, currentOffset + domainLength).toString('utf8') + currentOffset += domainLength + break + } + + case 0x04: { // IPv6 + if (buffer.length < currentOffset + 18) { + throw new InvalidArgumentError('Buffer too small for IPv6 address') + } + // Convert buffer to IPv6 string + const parts = [] + for (let i = 0; i < 8; i++) { + const value = buffer.readUInt16BE(currentOffset + i * 2) + parts.push(value.toString(16)) + } + address = parts.join(':') + currentOffset += 16 + break + } + + default: + throw new InvalidArgumentError(`Invalid address type: ${addressType}`) + } + + // Parse port + if (buffer.length < currentOffset + 2) { + throw new InvalidArgumentError('Buffer too small for port') + } + const port = buffer.readUInt16BE(currentOffset) + currentOffset += 2 + + return { + address, + port, + bytesRead: currentOffset - offset + } +} + +/** + * Create error for SOCKS5 reply code + * @param {number} replyCode - SOCKS5 reply code + * @returns {Error} Appropriate error object + */ +function createReplyError (replyCode) { + const messages = { + 0x01: 'General SOCKS server failure', + 0x02: 'Connection not allowed by ruleset', + 0x03: 'Network unreachable', + 0x04: 'Host unreachable', + 0x05: 'Connection refused', + 0x06: 'TTL expired', + 0x07: 'Command not supported', + 0x08: 'Address type not supported' + } + + const message = messages[replyCode] || `Unknown SOCKS5 error code: ${replyCode}` + const error = new Error(message) + error.code = `SOCKS5_${replyCode}` + return error +} + +module.exports = { + parseAddress, + parseIPv6, + buildAddressBuffer, + parseResponseAddress, + createReplyError +} diff --git a/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/core/symbols.js b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/core/symbols.js new file mode 100644 index 000000000..ff37adc04 --- /dev/null +++ b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/core/symbols.js @@ -0,0 +1,75 @@ +'use strict' + +module.exports = { + kClose: Symbol('close'), + kDestroy: Symbol('destroy'), + kDispatch: Symbol('dispatch'), + kUrl: Symbol('url'), + kWriting: Symbol('writing'), + kResuming: Symbol('resuming'), + kQueue: Symbol('queue'), + kConnect: Symbol('connect'), + kConnecting: Symbol('connecting'), + kKeepAliveDefaultTimeout: Symbol('default keep alive timeout'), + kKeepAliveMaxTimeout: Symbol('max keep alive timeout'), + kKeepAliveTimeoutThreshold: Symbol('keep alive timeout threshold'), + kKeepAliveTimeoutValue: Symbol('keep alive timeout'), + kKeepAlive: Symbol('keep alive'), + kHeadersTimeout: Symbol('headers timeout'), + kBodyTimeout: Symbol('body timeout'), + kServerName: Symbol('server name'), + kLocalAddress: Symbol('local address'), + kHost: Symbol('host'), + kNoRef: Symbol('no ref'), + kBodyUsed: Symbol('used'), + kBody: Symbol('abstracted request body'), + kRunning: Symbol('running'), + kBlocking: Symbol('blocking'), + kPending: Symbol('pending'), + kSize: Symbol('size'), + kBusy: Symbol('busy'), + kQueued: Symbol('queued'), + kFree: Symbol('free'), + kConnected: Symbol('connected'), + kClosed: Symbol('closed'), + kNeedDrain: Symbol('need drain'), + kReset: Symbol('reset'), + kDestroyed: Symbol.for('nodejs.stream.destroyed'), + kResume: Symbol('resume'), + kOnError: Symbol('on error'), + kMaxHeadersSize: Symbol('max headers size'), + kRunningIdx: Symbol('running index'), + kPendingIdx: Symbol('pending index'), + kError: Symbol('error'), + kClients: Symbol('clients'), + kClient: Symbol('client'), + kParser: Symbol('parser'), + kOnDestroyed: Symbol('destroy callbacks'), + kPipelining: Symbol('pipelining'), + kSocket: Symbol('socket'), + kHostHeader: Symbol('host header'), + kConnector: Symbol('connector'), + kStrictContentLength: Symbol('strict content length'), + kMaxRedirections: Symbol('maxRedirections'), + kMaxRequests: Symbol('maxRequestsPerClient'), + kProxy: Symbol('proxy agent options'), + kCounter: Symbol('socket request counter'), + kMaxResponseSize: Symbol('max response size'), + kHTTP2Session: Symbol('http2Session'), + kHTTP2SessionState: Symbol('http2Session state'), + kRetryHandlerDefaultRetry: Symbol('retry agent default retry'), + kConstruct: Symbol('constructable'), + kListeners: Symbol('listeners'), + kHTTPContext: Symbol('http context'), + kMaxConcurrentStreams: Symbol('max concurrent streams'), + kHTTP2InitialWindowSize: Symbol('http2 initial window size'), + kHTTP2ConnectionWindowSize: Symbol('http2 connection window size'), + kEnableConnectProtocol: Symbol('http2session connect protocol'), + kRemoteSettings: Symbol('http2session remote settings'), + kHTTP2Stream: Symbol('http2session client stream'), + kPingInterval: Symbol('ping interval'), + kNoProxyAgent: Symbol('no proxy agent'), + kHttpProxyAgent: Symbol('http proxy agent'), + kHttpsProxyAgent: Symbol('https proxy agent'), + kSocks5ProxyAgent: Symbol('socks5 proxy agent') +} diff --git a/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/core/tree.js b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/core/tree.js new file mode 100644 index 000000000..6eed58aad --- /dev/null +++ b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/core/tree.js @@ -0,0 +1,160 @@ +'use strict' + +const { + wellknownHeaderNames, + headerNameLowerCasedRecord +} = require('./constants') + +class TstNode { + /** @type {any} */ + value = null + /** @type {null | TstNode} */ + left = null + /** @type {null | TstNode} */ + middle = null + /** @type {null | TstNode} */ + right = null + /** @type {number} */ + code + /** + * @param {string} key + * @param {any} value + * @param {number} index + */ + constructor (key, value, index) { + if (index === undefined || index >= key.length) { + throw new TypeError('Unreachable') + } + const code = this.code = key.charCodeAt(index) + // check code is ascii string + if (code > 0x7F) { + throw new TypeError('key must be ascii string') + } + if (key.length !== ++index) { + this.middle = new TstNode(key, value, index) + } else { + this.value = value + } + } + + /** + * @param {string} key + * @param {any} value + * @returns {void} + */ + add (key, value) { + const length = key.length + if (length === 0) { + throw new TypeError('Unreachable') + } + let index = 0 + /** + * @type {TstNode} + */ + let node = this + while (true) { + const code = key.charCodeAt(index) + // check code is ascii string + if (code > 0x7F) { + throw new TypeError('key must be ascii string') + } + if (node.code === code) { + if (length === ++index) { + node.value = value + break + } else if (node.middle !== null) { + node = node.middle + } else { + node.middle = new TstNode(key, value, index) + break + } + } else if (node.code < code) { + if (node.left !== null) { + node = node.left + } else { + node.left = new TstNode(key, value, index) + break + } + } else if (node.right !== null) { + node = node.right + } else { + node.right = new TstNode(key, value, index) + break + } + } + } + + /** + * @param {Uint8Array} key + * @returns {TstNode | null} + */ + search (key) { + const keylength = key.length + let index = 0 + /** + * @type {TstNode|null} + */ + let node = this + while (node !== null && index < keylength) { + let code = key[index] + // A-Z + // First check if it is bigger than 0x5a. + // Lowercase letters have higher char codes than uppercase ones. + // Also we assume that headers will mostly contain lowercase characters. + if (code <= 0x5a && code >= 0x41) { + // Lowercase for uppercase. + code |= 32 + } + while (node !== null) { + if (code === node.code) { + if (keylength === ++index) { + // Returns Node since it is the last key. + return node + } + node = node.middle + break + } + node = node.code < code ? node.left : node.right + } + } + return null + } +} + +class TernarySearchTree { + /** @type {TstNode | null} */ + node = null + + /** + * @param {string} key + * @param {any} value + * @returns {void} + * */ + insert (key, value) { + if (this.node === null) { + this.node = new TstNode(key, value, 0) + } else { + this.node.add(key, value) + } + } + + /** + * @param {Uint8Array} key + * @returns {any} + */ + lookup (key) { + return this.node?.search(key)?.value ?? null + } +} + +const tree = new TernarySearchTree() + +for (let i = 0; i < wellknownHeaderNames.length; ++i) { + const key = headerNameLowerCasedRecord[wellknownHeaderNames[i]] + tree.insert(key, key) +} + +module.exports = { + TernarySearchTree, + tree +} diff --git a/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/core/util.js b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/core/util.js new file mode 100644 index 000000000..767d586b9 --- /dev/null +++ b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/core/util.js @@ -0,0 +1,992 @@ +'use strict' + +const assert = require('node:assert') +const { kDestroyed, kBodyUsed, kListeners, kBody } = require('./symbols') +const { IncomingMessage } = require('node:http') +const stream = require('node:stream') +const net = require('node:net') +const { stringify } = require('node:querystring') +const { EventEmitter: EE } = require('node:events') +const timers = require('../util/timers') +const { InvalidArgumentError, ConnectTimeoutError } = require('./errors') +const { headerNameLowerCasedRecord } = require('./constants') +const { tree } = require('./tree') + +const [nodeMajor, nodeMinor] = process.versions.node.split('.', 2).map(v => Number(v)) + +class BodyAsyncIterable { + constructor (body) { + this[kBody] = body + this[kBodyUsed] = false + } + + async * [Symbol.asyncIterator] () { + assert(!this[kBodyUsed], 'disturbed') + this[kBodyUsed] = true + yield * this[kBody] + } +} + +function noop () {} + +/** + * @param {*} body + * @returns {*} + */ +function wrapRequestBody (body) { + if (isStream(body)) { + // TODO (fix): Provide some way for the user to cache the file to e.g. /tmp + // so that it can be dispatched again? + // TODO (fix): Do we need 100-expect support to provide a way to do this properly? + if (bodyLength(body) === 0) { + body + .on('data', function () { + assert(false) + }) + } + + if (typeof body.readableDidRead !== 'boolean') { + body[kBodyUsed] = false + EE.prototype.on.call(body, 'data', function () { + this[kBodyUsed] = true + }) + } + + return body + } else if (body && typeof body.pipeTo === 'function') { + // TODO (fix): We can't access ReadableStream internal state + // to determine whether or not it has been disturbed. This is just + // a workaround. + return new BodyAsyncIterable(body) + } else if (body && isFormDataLike(body)) { + return body + } else if ( + body && + typeof body !== 'string' && + !ArrayBuffer.isView(body) && + isIterable(body) + ) { + // TODO: Should we allow re-using iterable if !this.opts.idempotent + // or through some other flag? + return new BodyAsyncIterable(body) + } else { + return body + } +} + +/** + * @param {*} obj + * @returns {obj is import('node:stream').Stream} + */ +function isStream (obj) { + return obj && typeof obj === 'object' && typeof obj.pipe === 'function' && typeof obj.on === 'function' +} + +/** + * @param {*} object + * @returns {object is Blob} + * based on https://github.com/node-fetch/fetch-blob/blob/8ab587d34080de94140b54f07168451e7d0b655e/index.js#L229-L241 (MIT License) + */ +function isBlobLike (object) { + if (object === null) { + return false + } else if (object instanceof Blob) { + return true + } else if (typeof object !== 'object') { + return false + } else { + const sTag = object[Symbol.toStringTag] + + return (sTag === 'Blob' || sTag === 'File') && ( + ('stream' in object && typeof object.stream === 'function') || + ('arrayBuffer' in object && typeof object.arrayBuffer === 'function') + ) + } +} + +/** + * @param {string} url The path to check for query strings or fragments. + * @returns {boolean} Returns true if the path contains a query string or fragment. + */ +function pathHasQueryOrFragment (url) { + return ( + url.includes('?') || + url.includes('#') + ) +} + +/** + * @param {string} url The URL to add the query params to + * @param {import('node:querystring').ParsedUrlQueryInput} queryParams The object to serialize into a URL query string + * @returns {string} The URL with the query params added + */ +function serializePathWithQuery (url, queryParams) { + if (pathHasQueryOrFragment(url)) { + throw new Error('Query params cannot be passed when url already contains "?" or "#".') + } + + const stringified = stringify(queryParams) + + if (stringified) { + url += '?' + stringified + } + + return url +} + +/** + * @param {number|string|undefined} port + * @returns {boolean} + */ +function isValidPort (port) { + const value = parseInt(port, 10) + return ( + value === Number(port) && + value >= 0 && + value <= 65535 + ) +} + +/** + * Check if the value is a valid http or https prefixed string. + * + * @param {string} value + * @returns {boolean} + */ +function isHttpOrHttpsPrefixed (value) { + return ( + value != null && + value[0] === 'h' && + value[1] === 't' && + value[2] === 't' && + value[3] === 'p' && + ( + value[4] === ':' || + ( + value[4] === 's' && + value[5] === ':' + ) + ) + ) +} + +/** + * @param {string|URL|Record} url + * @returns {URL} + */ +function parseURL (url) { + if (typeof url === 'string') { + /** + * @type {URL} + */ + url = new URL(url) + + if (!isHttpOrHttpsPrefixed(url.origin || url.protocol)) { + throw new InvalidArgumentError('Invalid URL protocol: the URL must start with `http:` or `https:`.') + } + + return url + } + + if (!url || typeof url !== 'object') { + throw new InvalidArgumentError('Invalid URL: The URL argument must be a non-null object.') + } + + if (!(url instanceof URL)) { + if (url.port != null && url.port !== '' && isValidPort(url.port) === false) { + throw new InvalidArgumentError('Invalid URL: port must be a valid integer or a string representation of an integer.') + } + + if (url.path != null && typeof url.path !== 'string') { + throw new InvalidArgumentError('Invalid URL path: the path must be a string or null/undefined.') + } + + if (url.pathname != null && typeof url.pathname !== 'string') { + throw new InvalidArgumentError('Invalid URL pathname: the pathname must be a string or null/undefined.') + } + + if (url.hostname != null && typeof url.hostname !== 'string') { + throw new InvalidArgumentError('Invalid URL hostname: the hostname must be a string or null/undefined.') + } + + if (url.origin != null && typeof url.origin !== 'string') { + throw new InvalidArgumentError('Invalid URL origin: the origin must be a string or null/undefined.') + } + + if (!isHttpOrHttpsPrefixed(url.origin || url.protocol)) { + throw new InvalidArgumentError('Invalid URL protocol: the URL must start with `http:` or `https:`.') + } + + const port = url.port != null + ? url.port + : (url.protocol === 'https:' ? 443 : 80) + let origin = url.origin != null + ? url.origin + : `${url.protocol || ''}//${url.hostname || ''}:${port}` + let path = url.path != null + ? url.path + : `${url.pathname || ''}${url.search || ''}` + + if (origin[origin.length - 1] === '/') { + origin = origin.slice(0, origin.length - 1) + } + + if (path && path[0] !== '/') { + path = `/${path}` + } + // new URL(path, origin) is unsafe when `path` contains an absolute URL + // From https://developer.mozilla.org/en-US/docs/Web/API/URL/URL: + // If first parameter is a relative URL, second param is required, and will be used as the base URL. + // If first parameter is an absolute URL, a given second param will be ignored. + return new URL(`${origin}${path}`) + } + + if (!isHttpOrHttpsPrefixed(url.origin || url.protocol)) { + throw new InvalidArgumentError('Invalid URL protocol: the URL must start with `http:` or `https:`.') + } + + return url +} + +/** + * @param {string|URL|Record} url + * @returns {URL} + */ +function parseOrigin (url) { + url = parseURL(url) + + if (url.pathname !== '/' || url.search || url.hash) { + throw new InvalidArgumentError('invalid url') + } + + return url +} + +/** + * @param {string} host + * @returns {string} + */ +function getHostname (host) { + if (host[0] === '[') { + const idx = host.indexOf(']') + + assert(idx !== -1) + return host.substring(1, idx) + } + + const idx = host.indexOf(':') + if (idx === -1) return host + + return host.substring(0, idx) +} + +/** + * IP addresses are not valid server names per RFC6066 + * Currently, the only server names supported are DNS hostnames + * @param {string|null} host + * @returns {string|null} + */ +function getServerName (host) { + if (!host) { + return null + } + + assert(typeof host === 'string') + + const servername = getHostname(host) + if (net.isIP(servername)) { + return '' + } + + return servername +} + +/** + * @function + * @template T + * @param {T} obj + * @returns {T} + */ +function deepClone (obj) { + return JSON.parse(JSON.stringify(obj)) +} + +/** + * @param {*} obj + * @returns {obj is AsyncIterable} + */ +function isAsyncIterable (obj) { + return !!(obj != null && typeof obj[Symbol.asyncIterator] === 'function') +} + +/** + * @param {*} obj + * @returns {obj is Iterable} + */ +function isIterable (obj) { + return !!(obj != null && (typeof obj[Symbol.iterator] === 'function' || typeof obj[Symbol.asyncIterator] === 'function')) +} + +/** + * Checks whether an object has a safe Symbol.iterator — i.e. one that is + * either own or inherited from a non-Object.prototype chain. This prevents + * prototype-pollution attacks from injecting a fake iterator on + * Object.prototype. + * @param {object} obj + * @returns {boolean} + */ +function hasSafeIterator (obj) { + const prototype = Object.getPrototypeOf(obj) + const ownIterator = Object.prototype.hasOwnProperty.call(obj, Symbol.iterator) + return ownIterator || (prototype != null && prototype !== Object.prototype && typeof obj[Symbol.iterator] === 'function') +} + +/** + * @param {Blob|Buffer|import ('stream').Stream} body + * @returns {number|null} + */ +function bodyLength (body) { + if (body == null) { + return 0 + } else if (isStream(body)) { + const state = body._readableState + return state && state.objectMode === false && state.ended === true && Number.isFinite(state.length) + ? state.length + : null + } else if (isBlobLike(body)) { + return body.size != null ? body.size : null + } else if (isBuffer(body)) { + return body.byteLength + } + + return null +} + +/** + * @param {import ('stream').Stream} body + * @returns {boolean} + */ +function isDestroyed (body) { + return body && !!(body.destroyed || body[kDestroyed] || (stream.isDestroyed?.(body))) +} + +/** + * @param {import ('stream').Stream} stream + * @param {Error} [err] + * @returns {void} + */ +function destroy (stream, err) { + if (stream == null || !isStream(stream) || isDestroyed(stream)) { + return + } + + if (typeof stream.destroy === 'function') { + if (Object.getPrototypeOf(stream).constructor === IncomingMessage) { + // See: https://github.com/nodejs/node/pull/38505/files + stream.socket = null + } + + stream.destroy(err) + } else if (err) { + queueMicrotask(() => { + stream.emit('error', err) + }) + } + + if (stream.destroyed !== true) { + stream[kDestroyed] = true + } +} + +const KEEPALIVE_TIMEOUT_EXPR = /timeout=(\d+)/ +/** + * @param {string} val + * @returns {number | null} + */ +function parseKeepAliveTimeout (val) { + const m = val.match(KEEPALIVE_TIMEOUT_EXPR) + return m ? parseInt(m[1], 10) * 1000 : null +} + +/** + * Retrieves a header name and returns its lowercase value. + * @param {string | Buffer} value Header name + * @returns {string} + */ +function headerNameToString (value) { + return typeof value === 'string' + ? headerNameLowerCasedRecord[value] ?? value.toLowerCase() + : tree.lookup(value) ?? value.toString('latin1').toLowerCase() +} + +/** + * Receive the buffer as a string and return its lowercase value. + * @param {Buffer} value Header name + * @returns {string} + */ +function bufferToLowerCasedHeaderName (value) { + return tree.lookup(value) ?? value.toString('latin1').toLowerCase() +} + +/** + * @param {(Buffer | string)[]} headers + * @param {Record} [obj] + * @returns {Record} + */ +function parseHeaders (headers, obj) { + if (obj === undefined) obj = {} + + for (let i = 0; i < headers.length; i += 2) { + const key = headerNameToString(headers[i]) + let val = obj[key] + + if (val !== undefined) { + if (!Object.hasOwn(obj, key)) { + const headersValue = typeof headers[i + 1] === 'string' + ? headers[i + 1] + : Array.isArray(headers[i + 1]) + ? headers[i + 1].map(x => x.toString('latin1')) + : headers[i + 1].toString('latin1') + + if (key === '__proto__') { + Object.defineProperty(obj, key, { + value: headersValue, + enumerable: true, + configurable: true, + writable: true + }) + } else { + obj[key] = headersValue + } + } else { + if (typeof val === 'string') { + val = [val] + obj[key] = val + } + val.push(headers[i + 1].toString('latin1')) + } + } else { + const headersValue = typeof headers[i + 1] === 'string' + ? headers[i + 1] + : Array.isArray(headers[i + 1]) + ? headers[i + 1].map(x => x.toString('latin1')) + : headers[i + 1].toString('latin1') + + obj[key] = headersValue + } + } + + return obj +} + +/** + * @param {Buffer[]} headers + * @returns {string[]} + */ +function parseRawHeaders (headers) { + const headersLength = headers.length + /** + * @type {string[]} + */ + const ret = new Array(headersLength) + + let key + let val + + for (let n = 0; n < headersLength; n += 2) { + key = headers[n] + val = headers[n + 1] + + typeof key !== 'string' && (key = key.toString()) + typeof val !== 'string' && (val = val.toString('latin1')) + + ret[n] = key + ret[n + 1] = val + } + + return ret +} + +/** + * @param {string[]} headers + * @param {Buffer[]} headers + */ +function encodeRawHeaders (headers) { + if (!Array.isArray(headers)) { + throw new TypeError('expected headers to be an array') + } + return headers.map(x => Buffer.from(x)) +} + +/** + * @param {*} buffer + * @returns {buffer is Buffer} + */ +function isBuffer (buffer) { + // See, https://github.com/mcollina/undici/pull/319 + return buffer instanceof Uint8Array || Buffer.isBuffer(buffer) +} + +/** + * Asserts that the handler object is a request handler. + * + * @param {object} handler + * @param {string} method + * @param {string} [upgrade] + * @returns {asserts handler is import('../api/api-request').RequestHandler} + */ +function assertRequestHandler (handler, method, upgrade) { + if (!handler || typeof handler !== 'object') { + throw new InvalidArgumentError('handler must be an object') + } + + if (typeof handler.onRequestStart === 'function') { + // TODO (fix): More checks... + return + } + + if (typeof handler.onConnect !== 'function') { + throw new InvalidArgumentError('invalid onConnect method') + } + + if (typeof handler.onError !== 'function') { + throw new InvalidArgumentError('invalid onError method') + } + + if (typeof handler.onBodySent !== 'function' && handler.onBodySent !== undefined) { + throw new InvalidArgumentError('invalid onBodySent method') + } + + if (upgrade || method === 'CONNECT') { + if (typeof handler.onUpgrade !== 'function') { + throw new InvalidArgumentError('invalid onUpgrade method') + } + } else { + if (typeof handler.onHeaders !== 'function') { + throw new InvalidArgumentError('invalid onHeaders method') + } + + if (typeof handler.onData !== 'function') { + throw new InvalidArgumentError('invalid onData method') + } + + if (typeof handler.onComplete !== 'function') { + throw new InvalidArgumentError('invalid onComplete method') + } + } +} + +/** + * A body is disturbed if it has been read from and it cannot be re-used without + * losing state or data. + * @param {import('node:stream').Readable} body + * @returns {boolean} + */ +function isDisturbed (body) { + // TODO (fix): Why is body[kBodyUsed] needed? + return !!(body && (stream.isDisturbed(body) || body[kBodyUsed])) +} + +/** + * @typedef {object} SocketInfo + * @property {string} [localAddress] + * @property {number} [localPort] + * @property {string} [remoteAddress] + * @property {number} [remotePort] + * @property {string} [remoteFamily] + * @property {number} [timeout] + * @property {number} bytesWritten + * @property {number} bytesRead + */ + +/** + * @param {import('net').Socket} socket + * @returns {SocketInfo} + */ +function getSocketInfo (socket) { + return { + localAddress: socket.localAddress, + localPort: socket.localPort, + remoteAddress: socket.remoteAddress, + remotePort: socket.remotePort, + remoteFamily: socket.remoteFamily, + timeout: socket.timeout, + bytesWritten: socket.bytesWritten, + bytesRead: socket.bytesRead + } +} + +/** + * @param {Iterable} iterable + * @returns {ReadableStream} + */ +function ReadableStreamFrom (iterable) { + // We cannot use ReadableStream.from here because it does not return a byte stream. + + let iterator + return new ReadableStream( + { + start () { + iterator = iterable[Symbol.asyncIterator]() + }, + pull (controller) { + return iterator.next().then(({ done, value }) => { + if (done) { + return queueMicrotask(() => { + controller.close() + controller.byobRequest?.respond(0) + }) + } else { + const buf = Buffer.isBuffer(value) ? value : Buffer.from(value) + if (buf.byteLength) { + return controller.enqueue(new Uint8Array(buf)) + } else { + return this.pull(controller) + } + } + }) + }, + cancel () { + return iterator.return() + }, + type: 'bytes' + } + ) +} + +/** + * The object should be a FormData instance and contains all the required + * methods. + * @param {*} object + * @returns {object is FormData} + */ +function isFormDataLike (object) { + return ( + object && + typeof object === 'object' && + typeof object.append === 'function' && + typeof object.delete === 'function' && + typeof object.get === 'function' && + typeof object.getAll === 'function' && + typeof object.has === 'function' && + typeof object.set === 'function' && + object[Symbol.toStringTag] === 'FormData' + ) +} + +function addAbortListener (signal, listener) { + if ('addEventListener' in signal) { + signal.addEventListener('abort', listener, { once: true }) + return () => signal.removeEventListener('abort', listener) + } + signal.once('abort', listener) + return () => signal.removeListener('abort', listener) +} + +const validTokenChars = new Uint8Array([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0-15 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 16-31 + 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, // 32-47 (!"#$%&'()*+,-./) + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, // 48-63 (0-9:;<=>?) + 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 64-79 (@A-O) + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, // 80-95 (P-Z[\]^_) + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 96-111 (`a-o) + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, // 112-127 (p-z{|}~) + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 128-143 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 144-159 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 160-175 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 176-191 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 192-207 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 208-223 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 224-239 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 // 240-255 +]) + +/** + * @see https://tools.ietf.org/html/rfc7230#section-3.2.6 + * @param {number} c + * @returns {boolean} + */ +function isTokenCharCode (c) { + return (validTokenChars[c] === 1) +} + +const tokenRegExp = /^[\^_`a-zA-Z\-0-9!#$%&'*+.|~]+$/ + +/** + * @param {string} characters + * @returns {boolean} + */ +function isValidHTTPToken (characters) { + if (characters.length >= 12) return tokenRegExp.test(characters) + if (characters.length === 0) return false + + for (let i = 0; i < characters.length; i++) { + if (validTokenChars[characters.charCodeAt(i)] !== 1) { + return false + } + } + return true +} + +// headerCharRegex have been lifted from +// https://github.com/nodejs/node/blob/main/lib/_http_common.js + +/** + * Matches if val contains an invalid field-vchar + * field-value = *( field-content / obs-fold ) + * field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ] + * field-vchar = VCHAR / obs-text + */ +const headerCharRegex = /[^\t\x20-\x7e\x80-\xff]/ + +/** + * @param {string} characters + * @returns {boolean} + */ +function isValidHeaderValue (characters) { + return !headerCharRegex.test(characters) +} + +const rangeHeaderRegex = /^bytes (\d+)-(\d+)\/(\d+)?$/ + +/** + * @typedef {object} RangeHeader + * @property {number} start + * @property {number | null} end + * @property {number | null} size + */ + +/** + * Parse accordingly to RFC 9110 + * @see https://www.rfc-editor.org/rfc/rfc9110#field.content-range + * @param {string} [range] + * @returns {RangeHeader|null} + */ +function parseRangeHeader (range) { + if (range == null || range === '') return { start: 0, end: null, size: null } + + const m = range ? range.match(rangeHeaderRegex) : null + return m + ? { + start: parseInt(m[1]), + end: m[2] ? parseInt(m[2]) : null, + size: m[3] ? parseInt(m[3]) : null + } + : null +} + +/** + * @template {import("events").EventEmitter} T + * @param {T} obj + * @param {string} name + * @param {(...args: any[]) => void} listener + * @returns {T} + */ +function addListener (obj, name, listener) { + const listeners = (obj[kListeners] ??= []) + listeners.push([name, listener]) + obj.on(name, listener) + return obj +} + +/** + * @template {import("events").EventEmitter} T + * @param {T} obj + * @returns {T} + */ +function removeAllListeners (obj) { + if (obj[kListeners] != null) { + for (const [name, listener] of obj[kListeners]) { + obj.removeListener(name, listener) + } + obj[kListeners] = null + } + return obj +} + +/** + * @param {import ('../dispatcher/client')} client + * @param {import ('../core/request')} request + * @param {Error} err + */ +function errorRequest (client, request, err) { + try { + request.onError(err) + assert(request.aborted) + } catch (err) { + client.emit('error', err) + } +} + +/** + * @param {WeakRef} socketWeakRef + * @param {object} opts + * @param {number} opts.timeout + * @param {string} opts.hostname + * @param {number} opts.port + * @returns {() => void} + */ +const setupConnectTimeout = process.platform === 'win32' + ? (socketWeakRef, opts) => { + if (!opts.timeout) { + return noop + } + + let s1 = null + let s2 = null + const fastTimer = timers.setFastTimeout(() => { + // setImmediate is added to make sure that we prioritize socket error events over timeouts + s1 = setImmediate(() => { + // Windows needs an extra setImmediate probably due to implementation differences in the socket logic + s2 = setImmediate(() => onConnectTimeout(socketWeakRef.deref(), opts)) + }) + }, opts.timeout) + return () => { + timers.clearFastTimeout(fastTimer) + clearImmediate(s1) + clearImmediate(s2) + } + } + : (socketWeakRef, opts) => { + if (!opts.timeout) { + return noop + } + + let s1 = null + const fastTimer = timers.setFastTimeout(() => { + // setImmediate is added to make sure that we prioritize socket error events over timeouts + s1 = setImmediate(() => { + onConnectTimeout(socketWeakRef.deref(), opts) + }) + }, opts.timeout) + return () => { + timers.clearFastTimeout(fastTimer) + clearImmediate(s1) + } + } + +/** + * @param {net.Socket} socket + * @param {object} opts + * @param {number} opts.timeout + * @param {string} opts.hostname + * @param {number} opts.port + */ +function onConnectTimeout (socket, opts) { + // The socket could be already garbage collected + if (socket == null) { + return + } + + let message = 'Connect Timeout Error' + if (Array.isArray(socket.autoSelectFamilyAttemptedAddresses)) { + message += ` (attempted addresses: ${socket.autoSelectFamilyAttemptedAddresses.join(', ')},` + } else { + message += ` (attempted address: ${opts.hostname}:${opts.port},` + } + + message += ` timeout: ${opts.timeout}ms)` + + destroy(socket, new ConnectTimeoutError(message)) +} + +/** + * @param {string} urlString + * @returns {string} + */ +function getProtocolFromUrlString (urlString) { + if ( + urlString[0] === 'h' && + urlString[1] === 't' && + urlString[2] === 't' && + urlString[3] === 'p' + ) { + switch (urlString[4]) { + case ':': + return 'http:' + case 's': + if (urlString[5] === ':') { + return 'https:' + } + } + } + // fallback if none of the usual suspects + return urlString.slice(0, urlString.indexOf(':') + 1) +} + +const kEnumerableProperty = Object.create(null) +kEnumerableProperty.enumerable = true + +const normalizedMethodRecordsBase = { + delete: 'DELETE', + DELETE: 'DELETE', + get: 'GET', + GET: 'GET', + head: 'HEAD', + HEAD: 'HEAD', + options: 'OPTIONS', + OPTIONS: 'OPTIONS', + post: 'POST', + POST: 'POST', + put: 'PUT', + PUT: 'PUT' +} + +const normalizedMethodRecords = { + ...normalizedMethodRecordsBase, + patch: 'patch', + PATCH: 'PATCH' +} + +// Note: object prototypes should not be able to be referenced. e.g. `Object#hasOwnProperty`. +Object.setPrototypeOf(normalizedMethodRecordsBase, null) +Object.setPrototypeOf(normalizedMethodRecords, null) + +module.exports = { + kEnumerableProperty, + isDisturbed, + isBlobLike, + parseOrigin, + parseURL, + getServerName, + isStream, + isIterable, + hasSafeIterator, + isAsyncIterable, + isDestroyed, + headerNameToString, + bufferToLowerCasedHeaderName, + addListener, + removeAllListeners, + errorRequest, + parseRawHeaders, + encodeRawHeaders, + parseHeaders, + parseKeepAliveTimeout, + destroy, + bodyLength, + deepClone, + ReadableStreamFrom, + isBuffer, + assertRequestHandler, + getSocketInfo, + isFormDataLike, + pathHasQueryOrFragment, + serializePathWithQuery, + addAbortListener, + isValidHTTPToken, + isValidHeaderValue, + isTokenCharCode, + parseRangeHeader, + normalizedMethodRecordsBase, + normalizedMethodRecords, + isValidPort, + isHttpOrHttpsPrefixed, + nodeMajor, + nodeMinor, + safeHTTPMethods: Object.freeze(['GET', 'HEAD', 'OPTIONS', 'TRACE']), + wrapRequestBody, + setupConnectTimeout, + getProtocolFromUrlString +} diff --git a/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/dispatcher/agent.js b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/dispatcher/agent.js new file mode 100644 index 000000000..09fdd4be3 --- /dev/null +++ b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/dispatcher/agent.js @@ -0,0 +1,158 @@ +'use strict' + +const { InvalidArgumentError, MaxOriginsReachedError } = require('../core/errors') +const { kClients, kRunning, kClose, kDestroy, kDispatch, kUrl } = require('../core/symbols') +const DispatcherBase = require('./dispatcher-base') +const Pool = require('./pool') +const Client = require('./client') +const util = require('../core/util') + +const kOnConnect = Symbol('onConnect') +const kOnDisconnect = Symbol('onDisconnect') +const kOnConnectionError = Symbol('onConnectionError') +const kOnDrain = Symbol('onDrain') +const kFactory = Symbol('factory') +const kOptions = Symbol('options') +const kOrigins = Symbol('origins') + +function defaultFactory (origin, opts) { + return opts && opts.connections === 1 + ? new Client(origin, opts) + : new Pool(origin, opts) +} + +class Agent extends DispatcherBase { + constructor ({ factory = defaultFactory, maxOrigins = Infinity, connect, ...options } = {}) { + if (typeof factory !== 'function') { + throw new InvalidArgumentError('factory must be a function.') + } + + if (connect != null && typeof connect !== 'function' && typeof connect !== 'object') { + throw new InvalidArgumentError('connect must be a function or an object') + } + + if (typeof maxOrigins !== 'number' || Number.isNaN(maxOrigins) || maxOrigins <= 0) { + throw new InvalidArgumentError('maxOrigins must be a number greater than 0') + } + + super(options) + + if (connect && typeof connect !== 'function') { + connect = { ...connect } + } + + this[kOptions] = { ...util.deepClone(options), maxOrigins, connect } + this[kFactory] = factory + this[kClients] = new Map() + this[kOrigins] = new Set() + + this[kOnDrain] = (origin, targets) => { + this.emit('drain', origin, [this, ...targets]) + } + + this[kOnConnect] = (origin, targets) => { + this.emit('connect', origin, [this, ...targets]) + } + + this[kOnDisconnect] = (origin, targets, err) => { + this.emit('disconnect', origin, [this, ...targets], err) + } + + this[kOnConnectionError] = (origin, targets, err) => { + this.emit('connectionError', origin, [this, ...targets], err) + } + } + + get [kRunning] () { + let ret = 0 + for (const { dispatcher } of this[kClients].values()) { + ret += dispatcher[kRunning] + } + return ret + } + + [kDispatch] (opts, handler) { + let key + if (opts.origin && (typeof opts.origin === 'string' || opts.origin instanceof URL)) { + key = String(opts.origin) + } else { + throw new InvalidArgumentError('opts.origin must be a non-empty string or URL.') + } + + if (this[kOrigins].size >= this[kOptions].maxOrigins && !this[kOrigins].has(key)) { + throw new MaxOriginsReachedError() + } + + const result = this[kClients].get(key) + let dispatcher = result && result.dispatcher + if (!dispatcher) { + const closeClientIfUnused = (connected) => { + const result = this[kClients].get(key) + if (result) { + if (connected) result.count -= 1 + if (result.count <= 0) { + this[kClients].delete(key) + if (!result.dispatcher.destroyed) { + result.dispatcher.close() + } + } + this[kOrigins].delete(key) + } + } + dispatcher = this[kFactory](opts.origin, this[kOptions]) + .on('drain', this[kOnDrain]) + .on('connect', (origin, targets) => { + const result = this[kClients].get(key) + if (result) { + result.count += 1 + } + this[kOnConnect](origin, targets) + }) + .on('disconnect', (origin, targets, err) => { + closeClientIfUnused(true) + this[kOnDisconnect](origin, targets, err) + }) + .on('connectionError', (origin, targets, err) => { + closeClientIfUnused(false) + this[kOnConnectionError](origin, targets, err) + }) + + this[kClients].set(key, { count: 0, dispatcher }) + this[kOrigins].add(key) + } + + return dispatcher.dispatch(opts, handler) + } + + [kClose] () { + const closePromises = [] + for (const { dispatcher } of this[kClients].values()) { + closePromises.push(dispatcher.close()) + } + this[kClients].clear() + + return Promise.all(closePromises) + } + + [kDestroy] (err) { + const destroyPromises = [] + for (const { dispatcher } of this[kClients].values()) { + destroyPromises.push(dispatcher.destroy(err)) + } + this[kClients].clear() + + return Promise.all(destroyPromises) + } + + get stats () { + const allClientStats = {} + for (const { dispatcher } of this[kClients].values()) { + if (dispatcher.stats) { + allClientStats[dispatcher[kUrl].origin] = dispatcher.stats + } + } + return allClientStats + } +} + +module.exports = Agent diff --git a/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/dispatcher/balanced-pool.js b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/dispatcher/balanced-pool.js new file mode 100644 index 000000000..386023d03 --- /dev/null +++ b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/dispatcher/balanced-pool.js @@ -0,0 +1,221 @@ +'use strict' + +const { + BalancedPoolMissingUpstreamError, + InvalidArgumentError +} = require('../core/errors') +const { + PoolBase, + kClients, + kNeedDrain, + kAddClient, + kRemoveClient, + kGetDispatcher +} = require('./pool-base') +const Pool = require('./pool') +const { kUrl } = require('../core/symbols') +const util = require('../core/util') +const kFactory = Symbol('factory') + +const kOptions = Symbol('options') +const kGreatestCommonDivisor = Symbol('kGreatestCommonDivisor') +const kCurrentWeight = Symbol('kCurrentWeight') +const kIndex = Symbol('kIndex') +const kWeight = Symbol('kWeight') +const kMaxWeightPerServer = Symbol('kMaxWeightPerServer') +const kErrorPenalty = Symbol('kErrorPenalty') + +/** + * Calculate the greatest common divisor of two numbers by + * using the Euclidean algorithm. + * + * @param {number} a + * @param {number} b + * @returns {number} + */ +function getGreatestCommonDivisor (a, b) { + if (a === 0) return b + + while (b !== 0) { + const t = b + b = a % b + a = t + } + return a +} + +function defaultFactory (origin, opts) { + return new Pool(origin, opts) +} + +class BalancedPool extends PoolBase { + constructor (upstreams = [], { factory = defaultFactory, connect, tls, ...opts } = {}) { + if (typeof factory !== 'function') { + throw new InvalidArgumentError('factory must be a function.') + } + + super(opts) + + if (connect && typeof connect !== 'function') connect = { ...connect } + if (tls && typeof tls !== 'function') tls = { ...tls } + this[kOptions] = { ...util.deepClone(opts), connect, tls } + this[kOptions].interceptors = opts.interceptors + ? { ...opts.interceptors } + : undefined + this[kIndex] = -1 + this[kCurrentWeight] = 0 + + this[kMaxWeightPerServer] = this[kOptions].maxWeightPerServer || 100 + this[kErrorPenalty] = this[kOptions].errorPenalty || 15 + + if (!Array.isArray(upstreams)) { + upstreams = [upstreams] + } + + this[kFactory] = factory + + for (const upstream of upstreams) { + this.addUpstream(upstream) + } + this._updateBalancedPoolStats() + } + + addUpstream (upstream) { + const upstreamOrigin = util.parseOrigin(upstream).origin + + if (this[kClients].find((pool) => ( + pool[kUrl].origin === upstreamOrigin && + pool.closed !== true && + pool.destroyed !== true + ))) { + return this + } + const pool = this[kFactory](upstreamOrigin, this[kOptions]) + + this[kAddClient](pool) + pool.on('connect', () => { + pool[kWeight] = Math.min(this[kMaxWeightPerServer], pool[kWeight] + this[kErrorPenalty]) + }) + + pool.on('connectionError', () => { + pool[kWeight] = Math.max(1, pool[kWeight] - this[kErrorPenalty]) + this._updateBalancedPoolStats() + }) + + pool.on('disconnect', (...args) => { + const err = args[2] + if (err && err.code === 'UND_ERR_SOCKET') { + // decrease the weight of the pool. + pool[kWeight] = Math.max(1, pool[kWeight] - this[kErrorPenalty]) + this._updateBalancedPoolStats() + } + }) + + for (const client of this[kClients]) { + client[kWeight] = this[kMaxWeightPerServer] + } + + this._updateBalancedPoolStats() + + return this + } + + _updateBalancedPoolStats () { + let result = 0 + for (let i = 0; i < this[kClients].length; i++) { + result = getGreatestCommonDivisor(this[kClients][i][kWeight], result) + } + + this[kGreatestCommonDivisor] = result + } + + removeUpstream (upstream) { + const upstreamOrigin = util.parseOrigin(upstream).origin + + const pool = this[kClients].find((pool) => ( + pool[kUrl].origin === upstreamOrigin && + pool.closed !== true && + pool.destroyed !== true + )) + + if (pool) { + this[kRemoveClient](pool) + } + + return this + } + + getUpstream (upstream) { + const upstreamOrigin = util.parseOrigin(upstream).origin + + return this[kClients].find((pool) => ( + pool[kUrl].origin === upstreamOrigin && + pool.closed !== true && + pool.destroyed !== true + )) + } + + get upstreams () { + return this[kClients] + .filter(dispatcher => dispatcher.closed !== true && dispatcher.destroyed !== true) + .map((p) => p[kUrl].origin) + } + + [kGetDispatcher] () { + // We validate that pools is greater than 0, + // otherwise we would have to wait until an upstream + // is added, which might never happen. + if (this[kClients].length === 0) { + throw new BalancedPoolMissingUpstreamError() + } + + const dispatcher = this[kClients].find(dispatcher => ( + !dispatcher[kNeedDrain] && + dispatcher.closed !== true && + dispatcher.destroyed !== true + )) + + if (!dispatcher) { + return + } + + const allClientsBusy = this[kClients].map(pool => pool[kNeedDrain]).reduce((a, b) => a && b, true) + + if (allClientsBusy) { + return + } + + let counter = 0 + + let maxWeightIndex = this[kClients].findIndex(pool => !pool[kNeedDrain]) + + while (counter++ < this[kClients].length) { + this[kIndex] = (this[kIndex] + 1) % this[kClients].length + const pool = this[kClients][this[kIndex]] + + // find pool index with the largest weight + if (pool[kWeight] > this[kClients][maxWeightIndex][kWeight] && !pool[kNeedDrain]) { + maxWeightIndex = this[kIndex] + } + + // decrease the current weight every `this[kClients].length`. + if (this[kIndex] === 0) { + // Set the current weight to the next lower weight. + this[kCurrentWeight] = this[kCurrentWeight] - this[kGreatestCommonDivisor] + + if (this[kCurrentWeight] <= 0) { + this[kCurrentWeight] = this[kMaxWeightPerServer] + } + } + if (pool[kWeight] >= this[kCurrentWeight] && (!pool[kNeedDrain])) { + return pool + } + } + + this[kCurrentWeight] = this[kClients][maxWeightIndex][kWeight] + this[kIndex] = maxWeightIndex + return this[kClients][maxWeightIndex] + } +} + +module.exports = BalancedPool diff --git a/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/dispatcher/client-h1.js b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/dispatcher/client-h1.js new file mode 100644 index 000000000..2be549948 --- /dev/null +++ b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/dispatcher/client-h1.js @@ -0,0 +1,1746 @@ +'use strict' + +/* global WebAssembly */ + +const assert = require('node:assert') +const util = require('../core/util.js') +const { channels } = require('../core/diagnostics.js') +const timers = require('../util/timers.js') +const { + RequestContentLengthMismatchError, + ResponseContentLengthMismatchError, + RequestAbortedError, + InvalidArgumentError, + HeadersTimeoutError, + HeadersOverflowError, + SocketError, + InformationalError, + BodyTimeoutError, + HTTPParserError, + ResponseExceededMaxSizeError +} = require('../core/errors.js') +const { + kUrl, + kReset, + kClient, + kParser, + kBlocking, + kRunning, + kPending, + kSize, + kWriting, + kQueue, + kNoRef, + kKeepAliveDefaultTimeout, + kHostHeader, + kPendingIdx, + kRunningIdx, + kError, + kPipelining, + kSocket, + kKeepAliveTimeoutValue, + kMaxHeadersSize, + kKeepAliveMaxTimeout, + kKeepAliveTimeoutThreshold, + kHeadersTimeout, + kBodyTimeout, + kStrictContentLength, + kMaxRequests, + kCounter, + kMaxResponseSize, + kOnError, + kResume, + kHTTPContext, + kClosed +} = require('../core/symbols.js') + +const constants = require('../llhttp/constants.js') +const EMPTY_BUF = Buffer.alloc(0) +const FastBuffer = Buffer[Symbol.species] +const removeAllListeners = util.removeAllListeners +const kIdleSocketValidation = Symbol('kIdleSocketValidation') +const kIdleSocketValidationTimeout = Symbol('kIdleSocketValidationTimeout') +const kSocketUsed = Symbol('kSocketUsed') + +let extractBody + +function lazyllhttp () { + const llhttpWasmData = process.env.JEST_WORKER_ID ? require('../llhttp/llhttp-wasm.js') : undefined + + let mod + + // We disable wasm SIMD on older versions of Node.js on ppc64 that are broken on Power >=9 architectures. + let useWasmSIMD = true + if (process.arch === 'ppc64') { + const [major, minor] = process.versions.node.split('.').map(n => parseInt(n, 10)) + if (major < 24 || (major === 24 && minor < 12)) { + useWasmSIMD = false + } + } + // The Env Variable UNDICI_NO_WASM_SIMD allows explicitly overriding the default behavior + if (process.env.UNDICI_NO_WASM_SIMD === '1') { + useWasmSIMD = false + } else if (process.env.UNDICI_NO_WASM_SIMD === '0') { + useWasmSIMD = true + } + + if (useWasmSIMD) { + try { + mod = new WebAssembly.Module(require('../llhttp/llhttp_simd-wasm.js')) + } catch { + } + } + + if (!mod) { + // We could check if the error was caused by the simd option not + // being enabled, but the occurring of this other error + // * https://github.com/emscripten-core/emscripten/issues/11495 + // got me to remove that check to avoid breaking Node 12. + mod = new WebAssembly.Module(llhttpWasmData || require('../llhttp/llhttp-wasm.js')) + } + + return new WebAssembly.Instance(mod, { + env: { + /** + * @param {number} p + * @param {number} at + * @param {number} len + * @returns {number} + */ + wasm_on_url: (p, at, len) => { + return 0 + }, + /** + * @param {number} p + * @param {number} at + * @param {number} len + * @returns {number} + */ + wasm_on_status: (p, at, len) => { + assert(currentParser.ptr === p) + const start = at - currentBufferPtr + currentBufferRef.byteOffset + return currentParser.onStatus(new FastBuffer(currentBufferRef.buffer, start, len)) + }, + /** + * @param {number} p + * @returns {number} + */ + wasm_on_message_begin: (p) => { + assert(currentParser.ptr === p) + return currentParser.onMessageBegin() + }, + /** + * @param {number} p + * @param {number} at + * @param {number} len + * @returns {number} + */ + wasm_on_header_field: (p, at, len) => { + assert(currentParser.ptr === p) + const start = at - currentBufferPtr + currentBufferRef.byteOffset + return currentParser.onHeaderField(new FastBuffer(currentBufferRef.buffer, start, len)) + }, + /** + * @param {number} p + * @param {number} at + * @param {number} len + * @returns {number} + */ + wasm_on_header_value: (p, at, len) => { + assert(currentParser.ptr === p) + const start = at - currentBufferPtr + currentBufferRef.byteOffset + return currentParser.onHeaderValue(new FastBuffer(currentBufferRef.buffer, start, len)) + }, + /** + * @param {number} p + * @param {number} statusCode + * @param {0|1} upgrade + * @param {0|1} shouldKeepAlive + * @returns {number} + */ + wasm_on_headers_complete: (p, statusCode, upgrade, shouldKeepAlive) => { + assert(currentParser.ptr === p) + return currentParser.onHeadersComplete(statusCode, upgrade === 1, shouldKeepAlive === 1) + }, + /** + * @param {number} p + * @param {number} at + * @param {number} len + * @returns {number} + */ + wasm_on_body: (p, at, len) => { + assert(currentParser.ptr === p) + const start = at - currentBufferPtr + currentBufferRef.byteOffset + return currentParser.onBody(new FastBuffer(currentBufferRef.buffer, start, len)) + }, + /** + * @param {number} p + * @returns {number} + */ + wasm_on_message_complete: (p) => { + assert(currentParser.ptr === p) + return currentParser.onMessageComplete() + } + + } + }) +} + +let llhttpInstance = null + +/** + * @type {Parser|null} + */ +let currentParser = null +let currentBufferRef = null +/** + * @type {number} + */ +let currentBufferSize = 0 +let currentBufferPtr = null + +const USE_NATIVE_TIMER = 0 +const USE_FAST_TIMER = 1 + +// Use fast timers for headers and body to take eventual event loop +// latency into account. +const TIMEOUT_HEADERS = 2 | USE_FAST_TIMER +const TIMEOUT_BODY = 4 | USE_FAST_TIMER + +// Use native timers to ignore event loop latency for keep-alive +// handling. +const TIMEOUT_KEEP_ALIVE = 8 | USE_NATIVE_TIMER + +class Parser { + /** + * @param {import('./client.js')} client + * @param {import('net').Socket} socket + * @param {*} llhttp + */ + constructor (client, socket, { exports }) { + this.llhttp = exports + this.ptr = this.llhttp.llhttp_alloc(constants.TYPE.RESPONSE) + this.client = client + /** + * @type {import('net').Socket} + */ + this.socket = socket + this.timeout = null + this.timeoutWeakRef = new WeakRef(this) + this.timeoutValue = null + this.timeoutType = null + this.statusCode = 0 + this.statusText = '' + this.upgrade = false + this.headers = [] + this.headersSize = 0 + this.headersMaxSize = client[kMaxHeadersSize] + this.shouldKeepAlive = false + this.paused = false + this.resume = this.resume.bind(this) + + this.bytesRead = 0 + + this.keepAlive = '' + this.contentLength = '' + this.connection = '' + this.maxResponseSize = client[kMaxResponseSize] + } + + setTimeout (delay, type) { + // If the existing timer and the new timer are of different timer type + // (fast or native) or have different delay, we need to clear the existing + // timer and set a new one. + if ( + delay !== this.timeoutValue || + (type & USE_FAST_TIMER) ^ (this.timeoutType & USE_FAST_TIMER) + ) { + // If a timeout is already set, clear it with clearTimeout of the fast + // timer implementation, as it can clear fast and native timers. + if (this.timeout) { + timers.clearTimeout(this.timeout) + this.timeout = null + } + + if (delay) { + if (type & USE_FAST_TIMER) { + this.timeout = timers.setFastTimeout(onParserTimeout, delay, this.timeoutWeakRef) + } else { + this.timeout = setTimeout(onParserTimeout, delay, this.timeoutWeakRef) + this.timeout?.unref() + } + } + + this.timeoutValue = delay + } else if (this.timeout) { + if (this.timeout.refresh) { + this.timeout.refresh() + } + } + + this.timeoutType = type + } + + resume () { + if (this.socket.destroyed || !this.paused) { + return + } + + assert(this.ptr != null) + assert(currentParser === null) + + this.llhttp.llhttp_resume(this.ptr) + + assert(this.timeoutType === TIMEOUT_BODY) + if (this.timeout) { + if (this.timeout.refresh) { + this.timeout.refresh() + } + } + + this.paused = false + this.execute(this.socket.read() || EMPTY_BUF) // Flush parser. + this.readMore() + } + + readMore () { + while (!this.paused && this.ptr) { + const chunk = this.socket.read() + if (chunk === null) { + break + } + this.execute(chunk) + } + } + + /** + * @param {Buffer} chunk + */ + execute (chunk) { + assert(currentParser === null) + assert(this.ptr != null) + assert(!this.paused) + + const { socket, llhttp } = this + + // Allocate a new buffer if the current buffer is too small. + if (chunk.length > currentBufferSize) { + if (currentBufferPtr) { + llhttp.free(currentBufferPtr) + } + // Allocate a buffer that is a multiple of 4096 bytes. + currentBufferSize = Math.ceil(chunk.length / 4096) * 4096 + currentBufferPtr = llhttp.malloc(currentBufferSize) + } + + new Uint8Array(llhttp.memory.buffer, currentBufferPtr, currentBufferSize).set(chunk) + + // Call `execute` on the wasm parser. + // We pass the `llhttp_parser` pointer address, the pointer address of buffer view data, + // and finally the length of bytes to parse. + // The return value is an error code or `constants.ERROR.OK`. + try { + let ret + + try { + currentBufferRef = chunk + currentParser = this + ret = llhttp.llhttp_execute(this.ptr, currentBufferPtr, chunk.length) + } finally { + currentParser = null + currentBufferRef = null + } + + if (ret !== constants.ERROR.OK) { + const data = chunk.subarray(llhttp.llhttp_get_error_pos(this.ptr) - currentBufferPtr) + + if (ret === constants.ERROR.PAUSED_UPGRADE) { + this.onUpgrade(data) + } else if (ret === constants.ERROR.PAUSED) { + this.paused = true + socket.unshift(data) + } else { + throw this.createError(ret, data) + } + } + } catch (err) { + util.destroy(socket, err) + } + } + + finish () { + assert(currentParser === null) + assert(this.ptr != null) + assert(!this.paused) + + const { llhttp } = this + + let ret + + try { + currentParser = this + ret = llhttp.llhttp_finish(this.ptr) + } finally { + currentParser = null + } + + if (ret === constants.ERROR.OK) { + return null + } + + if (ret === constants.ERROR.PAUSED || ret === constants.ERROR.PAUSED_UPGRADE) { + this.paused = true + return null + } + + return this.createError(ret, EMPTY_BUF) + } + + createError (ret, data) { + const { llhttp, contentLength, bytesRead } = this + + if (contentLength && bytesRead !== parseInt(contentLength, 10)) { + return new ResponseContentLengthMismatchError() + } + + const ptr = llhttp.llhttp_get_error_reason(this.ptr) + let message = '' + if (ptr) { + const len = new Uint8Array(llhttp.memory.buffer, ptr).indexOf(0) + message = + 'Response does not match the HTTP/1.1 protocol (' + + Buffer.from(llhttp.memory.buffer, ptr, len).toString() + + ')' + } + + return new HTTPParserError(message, constants.ERROR[ret], data) + } + + destroy () { + assert(currentParser === null) + assert(this.ptr != null) + + this.llhttp.llhttp_free(this.ptr) + this.ptr = null + + this.timeout && timers.clearTimeout(this.timeout) + this.timeout = null + this.timeoutValue = null + this.timeoutType = null + + this.paused = false + } + + /** + * @param {Buffer} buf + * @returns {0} + */ + onStatus (buf) { + this.statusText = buf.toString() + return 0 + } + + /** + * @returns {0|-1} + */ + onMessageBegin () { + const { socket, client } = this + + if (socket.destroyed) { + return -1 + } + + if (client[kRunning] === 0) { + util.destroy(socket, new SocketError('bad response', util.getSocketInfo(socket))) + return -1 + } + + const request = client[kQueue][client[kRunningIdx]] + if (!request) { + return -1 + } + request.onResponseStarted() + + return 0 + } + + /** + * @param {Buffer} buf + * @returns {number} + */ + onHeaderField (buf) { + const len = this.headers.length + + if ((len & 1) === 0) { + this.headers.push(buf) + } else { + this.headers[len - 1] = Buffer.concat([this.headers[len - 1], buf]) + } + + this.trackHeader(buf.length) + + return 0 + } + + /** + * @param {Buffer} buf + * @returns {number} + */ + onHeaderValue (buf) { + let len = this.headers.length + + if ((len & 1) === 1) { + this.headers.push(buf) + len += 1 + } else { + this.headers[len - 1] = Buffer.concat([this.headers[len - 1], buf]) + } + + const key = this.headers[len - 2] + if (key.length === 10) { + const headerName = util.bufferToLowerCasedHeaderName(key) + if (headerName === 'keep-alive') { + this.keepAlive += buf.toString() + } else if (headerName === 'connection') { + this.connection += buf.toString() + } + } else if (key.length === 14 && util.bufferToLowerCasedHeaderName(key) === 'content-length') { + this.contentLength += buf.toString() + } + + this.trackHeader(buf.length) + + return 0 + } + + /** + * @param {number} len + */ + trackHeader (len) { + this.headersSize += len + if (this.headersSize >= this.headersMaxSize) { + util.destroy(this.socket, new HeadersOverflowError()) + } + } + + /** + * @param {Buffer} head + */ + onUpgrade (head) { + const { upgrade, client, socket, headers, statusCode, statusText } = this + + assert(upgrade) + assert(client[kSocket] === socket) + assert(!socket.destroyed) + assert(!this.paused) + assert((headers.length & 1) === 0) + + const request = client[kQueue][client[kRunningIdx]] + assert(request) + assert(request.upgrade || request.method === 'CONNECT') + + this.statusCode = 0 + this.statusText = '' + this.shouldKeepAlive = false + + this.headers = [] + this.headersSize = 0 + + socket.unshift(head) + + socket[kParser].destroy() + socket[kParser] = null + + socket[kClient] = null + socket[kError] = null + + removeAllListeners(socket) + + client[kSocket] = null + client[kHTTPContext] = null // TODO (fix): This is hacky... + client[kQueue][client[kRunningIdx]++] = null + client.emit('disconnect', client[kUrl], [client], new InformationalError('upgrade')) + + try { + request.onUpgrade(statusCode, headers, socket, statusText) + } catch (err) { + util.errorRequest(client, request, err) + util.destroy(socket, err) + } + + client[kResume]() + } + + /** + * @param {number} statusCode + * @param {boolean} upgrade + * @param {boolean} shouldKeepAlive + * @returns {number} + */ + onHeadersComplete (statusCode, upgrade, shouldKeepAlive) { + const { client, socket, headers, statusText } = this + + if (socket.destroyed) { + return -1 + } + + if (client[kRunning] === 0) { + util.destroy(socket, new SocketError('bad response', util.getSocketInfo(socket))) + return -1 + } + + const request = client[kQueue][client[kRunningIdx]] + + if (!request) { + return -1 + } + + assert(!this.upgrade) + assert(this.statusCode < 200) + + if (statusCode === 100) { + util.destroy(socket, new SocketError('bad response', util.getSocketInfo(socket))) + return -1 + } + + /* this can only happen if server is misbehaving */ + if (upgrade && !request.upgrade) { + util.destroy(socket, new SocketError('bad upgrade', util.getSocketInfo(socket))) + return -1 + } + + assert(this.timeoutType === TIMEOUT_HEADERS) + + this.statusCode = statusCode + this.shouldKeepAlive = ( + shouldKeepAlive || + // Override llhttp value which does not allow keepAlive for HEAD. + (request.method === 'HEAD' && !socket[kReset] && this.connection.toLowerCase() === 'keep-alive') + ) + + if (this.statusCode >= 200) { + const bodyTimeout = request.bodyTimeout != null + ? request.bodyTimeout + : client[kBodyTimeout] + this.setTimeout(bodyTimeout, TIMEOUT_BODY) + } else if (this.timeout) { + if (this.timeout.refresh) { + this.timeout.refresh() + } + } + + if (request.method === 'CONNECT') { + assert(client[kRunning] === 1) + this.upgrade = true + return 2 + } + + if (upgrade) { + assert(client[kRunning] === 1) + this.upgrade = true + return 2 + } + + assert((this.headers.length & 1) === 0) + this.headers = [] + this.headersSize = 0 + + if (this.shouldKeepAlive && client[kPipelining]) { + const keepAliveTimeout = this.keepAlive ? util.parseKeepAliveTimeout(this.keepAlive) : null + + if (keepAliveTimeout != null) { + const timeout = Math.min( + keepAliveTimeout - client[kKeepAliveTimeoutThreshold], + client[kKeepAliveMaxTimeout] + ) + if (timeout <= 0) { + socket[kReset] = true + } else { + client[kKeepAliveTimeoutValue] = timeout + } + } else { + client[kKeepAliveTimeoutValue] = client[kKeepAliveDefaultTimeout] + } + } else { + // Stop more requests from being dispatched. + socket[kReset] = true + } + + const pause = request.onHeaders(statusCode, headers, this.resume, statusText) === false + + if (request.aborted) { + return -1 + } + + if (request.method === 'HEAD') { + return 1 + } + + if (statusCode < 200) { + return 1 + } + + if (socket[kBlocking]) { + socket[kBlocking] = false + client[kResume]() + } + + return pause ? constants.ERROR.PAUSED : 0 + } + + /** + * @param {Buffer} buf + * @returns {number} + */ + onBody (buf) { + const { client, socket, statusCode, maxResponseSize } = this + + if (socket.destroyed) { + return -1 + } + + const request = client[kQueue][client[kRunningIdx]] + assert(request) + + assert(this.timeoutType === TIMEOUT_BODY) + if (this.timeout) { + if (this.timeout.refresh) { + this.timeout.refresh() + } + } + + assert(statusCode >= 200) + + if (maxResponseSize > -1 && this.bytesRead + buf.length > maxResponseSize) { + util.destroy(socket, new ResponseExceededMaxSizeError()) + return -1 + } + + this.bytesRead += buf.length + + if (request.onData(buf) === false) { + return constants.ERROR.PAUSED + } + + return 0 + } + + /** + * @returns {number} + */ + onMessageComplete () { + const { client, socket, statusCode, upgrade, headers, contentLength, bytesRead, shouldKeepAlive } = this + + if (socket.destroyed && (!statusCode || shouldKeepAlive)) { + return -1 + } + + if (upgrade) { + return 0 + } + + assert(statusCode >= 100) + assert((this.headers.length & 1) === 0) + + const request = client[kQueue][client[kRunningIdx]] + assert(request) + + this.statusCode = 0 + this.statusText = '' + this.bytesRead = 0 + this.contentLength = '' + this.keepAlive = '' + this.connection = '' + + this.headers = [] + this.headersSize = 0 + + if (statusCode < 200) { + return 0 + } + + if (request.method !== 'HEAD' && contentLength && bytesRead !== parseInt(contentLength, 10)) { + util.destroy(socket, new ResponseContentLengthMismatchError()) + return -1 + } + + request.onComplete(headers) + + client[kQueue][client[kRunningIdx]++] = null + socket[kSocketUsed] = client[kPending] === 0 + + if (socket[kWriting]) { + assert(client[kRunning] === 0) + // Response completed before request. + util.destroy(socket, new InformationalError('reset')) + return constants.ERROR.PAUSED + } else if (!shouldKeepAlive) { + util.destroy(socket, new InformationalError('reset')) + return constants.ERROR.PAUSED + } else if (socket[kReset] && client[kRunning] === 0) { + // Destroy socket once all requests have completed. + // The request at the tail of the pipeline is the one + // that requested reset and no further requests should + // have been queued since then. + util.destroy(socket, new InformationalError('reset')) + return constants.ERROR.PAUSED + } else if (client[kPipelining] == null || client[kPipelining] === 1) { + // We must wait a full event loop cycle to reuse this socket to make sure + // that non-spec compliant servers are not closing the connection even if they + // said they won't. + setImmediate(client[kResume]) + } else { + client[kResume]() + } + + return 0 + } +} + +function onParserTimeout (parserWeakRef) { + const parser = parserWeakRef.deref() + if (!parser) { + return + } + + const { socket, timeoutType, client, paused } = parser + + if (timeoutType === TIMEOUT_HEADERS) { + if (!socket[kWriting] || socket.writableNeedDrain || client[kRunning] > 1) { + assert(!paused, 'cannot be paused while waiting for headers') + util.destroy(socket, new HeadersTimeoutError()) + } + } else if (timeoutType === TIMEOUT_BODY) { + if (!paused) { + util.destroy(socket, new BodyTimeoutError()) + } + } else if (timeoutType === TIMEOUT_KEEP_ALIVE) { + assert(client[kRunning] === 0 && client[kKeepAliveTimeoutValue]) + util.destroy(socket, new InformationalError('socket idle timeout')) + } +} + +/** + * @param {import ('./client.js')} client + * @param {import('net').Socket} socket + * @returns + */ +function connectH1 (client, socket) { + client[kSocket] = socket + + if (!llhttpInstance) { + llhttpInstance = lazyllhttp() + } + + if (socket.errored) { + throw socket.errored + } + + if (socket.destroyed) { + throw new SocketError('destroyed') + } + + socket[kNoRef] = false + socket[kWriting] = false + socket[kReset] = false + socket[kBlocking] = false + socket[kIdleSocketValidation] = 0 + socket[kIdleSocketValidationTimeout] = null + socket[kSocketUsed] = false + socket[kParser] = new Parser(client, socket, llhttpInstance) + + util.addListener(socket, 'error', onHttpSocketError) + util.addListener(socket, 'readable', onHttpSocketReadable) + util.addListener(socket, 'end', onHttpSocketEnd) + util.addListener(socket, 'close', onHttpSocketClose) + + socket[kClosed] = false + socket.on('close', onSocketClose) + + return { + version: 'h1', + defaultPipelining: 1, + write (request) { + return writeH1(client, request) + }, + resume () { + resumeH1(client) + }, + /** + * @param {Error|undefined} err + * @param {() => void} callback + */ + destroy (err, callback) { + if (socket[kClosed]) { + queueMicrotask(callback) + } else { + socket.on('close', callback) + socket.destroy(err) + } + }, + /** + * @returns {boolean} + */ + get destroyed () { + return socket.destroyed + }, + /** + * @param {import('../core/request.js')} request + * @returns {boolean} + */ + busy (request) { + if (socket[kWriting] || socket[kReset] || socket[kBlocking] || socket[kIdleSocketValidation] === 1) { + return true + } + + if (request) { + if (client[kRunning] > 0 && !request.idempotent) { + // Non-idempotent request cannot be retried. + // Ensure that no other requests are inflight and + // could cause failure. + return true + } + + if (client[kRunning] > 0 && (request.upgrade || request.method === 'CONNECT')) { + // Don't dispatch an upgrade until all preceding requests have completed. + // A misbehaving server might upgrade the connection before all pipelined + // request has completed. + return true + } + + if (client[kRunning] > 0 && util.bodyLength(request.body) !== 0 && + (util.isStream(request.body) || util.isAsyncIterable(request.body) || util.isFormDataLike(request.body))) { + // Request with stream or iterator body can error while other requests + // are inflight and indirectly error those as well. + // Ensure this doesn't happen by waiting for inflight + // to complete before dispatching. + + // Request with stream or iterator body cannot be retried. + // Ensure that no other requests are inflight and + // could cause failure. + return true + } + } + + return false + } + } +} + +function onHttpSocketError (err) { + assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID') + + const parser = this[kParser] + + // On Mac OS, we get an ECONNRESET even if there is a full body to be forwarded + // to the user. + if (err.code === 'ECONNRESET' && parser.statusCode && !parser.shouldKeepAlive) { + const parserErr = parser.finish() + if (parserErr) { + this[kError] = parserErr + this[kClient][kOnError](parserErr) + } + return + } + + this[kError] = err + + this[kClient][kOnError](err) +} + +function onHttpSocketReadable () { + this[kParser]?.readMore() +} + +function onHttpSocketEnd () { + const parser = this[kParser] + + if (parser.statusCode && !parser.shouldKeepAlive) { + const parserErr = parser.finish() + if (parserErr) { + util.destroy(this, parserErr) + } + return + } + + util.destroy(this, new SocketError('other side closed', util.getSocketInfo(this))) +} + +function onHttpSocketClose () { + const parser = this[kParser] + + clearIdleSocketValidation(this) + + if (parser) { + if (!this[kError] && parser.statusCode && !parser.shouldKeepAlive) { + this[kError] = parser.finish() || this[kError] + } + + this[kParser].destroy() + this[kParser] = null + } + + const err = this[kError] || new SocketError('closed', util.getSocketInfo(this)) + + const client = this[kClient] + + client[kSocket] = null + client[kHTTPContext] = null // TODO (fix): This is hacky... + + if (client.destroyed) { + assert(client[kPending] === 0) + + // Fail entire queue. + const requests = client[kQueue].splice(client[kRunningIdx]) + for (let i = 0; i < requests.length; i++) { + const request = requests[i] + util.errorRequest(client, request, err) + } + } else if (client[kRunning] > 0 && err.code !== 'UND_ERR_INFO') { + // Fail head of pipeline. + const request = client[kQueue][client[kRunningIdx]] + client[kQueue][client[kRunningIdx]++] = null + + util.errorRequest(client, request, err) + } + + client[kPendingIdx] = client[kRunningIdx] + + assert(client[kRunning] === 0) + + client.emit('disconnect', client[kUrl], [client], err) + + client[kResume]() +} + +function onSocketClose () { + this[kClosed] = true +} + +function clearIdleSocketValidation (socket) { + if (socket[kIdleSocketValidationTimeout]) { + clearImmediate(socket[kIdleSocketValidationTimeout]) + socket[kIdleSocketValidationTimeout] = null + } + + socket[kIdleSocketValidation] = 0 +} + +function scheduleIdleSocketValidation (client, socket) { + socket[kIdleSocketValidation] = 1 + // Yield to the check phase (after poll) so unsolicited bytes / FIN / RST + // already pending on this idle keep-alive socket are processed before the + // next request is written (GHSA-35p6-xmwp-9g52). + // + // setTimeout(0) pays Node's ~1ms timer floor on every sequential reuse + // (#5493). setImmediate avoids that, but an *unref'd* Immediate lets poll + // block for ~500ms when the event loop is otherwise idle (#5600 / #5606). + // A ref'd Immediate both keeps the pending request alive and makes poll + // return immediately — the hybrid those issues asked for. + socket[kIdleSocketValidationTimeout] = setImmediate(() => { + socket[kIdleSocketValidationTimeout] = null + socket[kIdleSocketValidation] = 2 + + if (client[kSocket] === socket && !socket.destroyed) { + client[kResume]() + } + }) +} + +/** + * @param {import('./client.js')} client + */ +function resumeH1 (client) { + const socket = client[kSocket] + + if (socket && !socket.destroyed) { + if (client[kSize] === 0) { + if (!socket[kNoRef] && socket.unref) { + socket.unref() + socket[kNoRef] = true + } + } else if (socket[kNoRef] && socket.ref) { + socket.ref() + socket[kNoRef] = false + } + + if (client[kRunning] === 0 && client[kPending] > 0 && socket[kSocketUsed]) { + if (socket[kIdleSocketValidation] === 0) { + scheduleIdleSocketValidation(client, socket) + socket[kParser].readMore() + if (socket.destroyed) { + return + } + return + } + + if (socket[kIdleSocketValidation] === 1) { + socket[kParser].readMore() + if (socket.destroyed) { + return + } + return + } + } + + if (client[kRunning] === 0) { + socket[kParser].readMore() + if (socket.destroyed) { + return + } + } + + if (client[kSize] === 0) { + if (socket[kParser].timeoutType !== TIMEOUT_KEEP_ALIVE) { + socket[kParser].setTimeout(client[kKeepAliveTimeoutValue], TIMEOUT_KEEP_ALIVE) + } + } else if (client[kRunning] > 0 && socket[kParser].statusCode < 200) { + if (socket[kParser].timeoutType !== TIMEOUT_HEADERS) { + const request = client[kQueue][client[kRunningIdx]] + const headersTimeout = request.headersTimeout != null + ? request.headersTimeout + : client[kHeadersTimeout] + socket[kParser].setTimeout(headersTimeout, TIMEOUT_HEADERS) + } + } + } +} + +// https://www.rfc-editor.org/rfc/rfc7230#section-3.3.2 +function shouldSendContentLength (method) { + return method !== 'GET' && method !== 'HEAD' && method !== 'OPTIONS' && method !== 'TRACE' && method !== 'CONNECT' +} + +/** + * @param {import('./client.js')} client + * @param {import('../core/request.js')} request + * @returns + */ +function writeH1 (client, request) { + const { method, path, host, upgrade, blocking, reset } = request + + let { body, headers, contentLength } = request + + // https://tools.ietf.org/html/rfc7231#section-4.3.1 + // https://tools.ietf.org/html/rfc7231#section-4.3.2 + // https://tools.ietf.org/html/rfc7231#section-4.3.5 + + // Sending a payload body on a request that does not + // expect it can cause undefined behavior on some + // servers and corrupt connection state. Do not + // re-use the connection for further requests. + + const expectsPayload = ( + method === 'PUT' || + method === 'POST' || + method === 'PATCH' || + method === 'QUERY' || + method === 'PROPFIND' || + method === 'PROPPATCH' + ) + + if (util.isFormDataLike(body)) { + if (!extractBody) { + extractBody = require('../web/fetch/body.js').extractBody + } + + const [bodyStream, contentType] = extractBody(body) + if (request.contentType == null) { + headers.push('content-type', contentType) + } + body = bodyStream.stream + contentLength = bodyStream.length + } else if (util.isBlobLike(body) && request.contentType == null) { + const contentType = body.type + if (contentType) { + const contentTypeValue = `${contentType}` + if (!util.isValidHeaderValue(contentTypeValue)) { + util.errorRequest(client, request, new InvalidArgumentError('invalid content-type header')) + return false + } + headers.push('content-type', contentTypeValue) + } + } + + if (body && typeof body.read === 'function') { + // Try to read EOF in order to get length. + body.read(0) + } + + const bodyLength = util.bodyLength(body) + + contentLength = bodyLength ?? contentLength + + if (contentLength === null) { + contentLength = request.contentLength + } + + if (contentLength === 0 && !expectsPayload) { + // https://tools.ietf.org/html/rfc7230#section-3.3.2 + // A user agent SHOULD NOT send a Content-Length header field when + // the request message does not contain a payload body and the method + // semantics do not anticipate such a body. + + contentLength = null + } + + // https://github.com/nodejs/undici/issues/2046 + // A user agent may send a Content-Length header with 0 value, this should be allowed. + if (shouldSendContentLength(method) && contentLength > 0 && request.contentLength !== null && request.contentLength !== contentLength) { + if (client[kStrictContentLength]) { + util.errorRequest(client, request, new RequestContentLengthMismatchError()) + return false + } + + process.emitWarning(new RequestContentLengthMismatchError()) + } + + const socket = client[kSocket] + clearIdleSocketValidation(socket) + + /** + * @param {Error} [err] + * @returns {void} + */ + const abort = (err) => { + if (request.aborted || request.completed) { + return + } + + util.errorRequest(client, request, err || new RequestAbortedError()) + + util.destroy(body) + util.destroy(socket, new InformationalError('aborted')) + } + + try { + request.onConnect(abort) + } catch (err) { + util.errorRequest(client, request, err) + } + + if (request.aborted) { + return false + } + + if (method === 'HEAD') { + // https://github.com/mcollina/undici/issues/258 + // Close after a HEAD request to interop with misbehaving servers + // that may send a body in the response. + + socket[kReset] = true + } + + if (upgrade || method === 'CONNECT') { + // On CONNECT or upgrade, block pipeline from dispatching further + // requests on this connection. + + socket[kReset] = true + } + + if (reset != null) { + socket[kReset] = reset + } + + if (client[kMaxRequests] && socket[kCounter]++ >= client[kMaxRequests]) { + socket[kReset] = true + } + + if (blocking) { + socket[kBlocking] = true + } + + if (socket.setTypeOfService) { + socket.setTypeOfService(request.typeOfService) + } + + let header = `${method} ${path} HTTP/1.1\r\n` + + if (typeof host === 'string') { + header += `host: ${host}\r\n` + } else { + header += client[kHostHeader] + } + + if (upgrade) { + header += `connection: upgrade\r\nupgrade: ${upgrade}\r\n` + } else if (client[kPipelining] && !socket[kReset]) { + header += 'connection: keep-alive\r\n' + } else { + header += 'connection: close\r\n' + } + + if (Array.isArray(headers)) { + for (let n = 0; n < headers.length; n += 2) { + const key = headers[n + 0] + const val = headers[n + 1] + + if (Array.isArray(val)) { + for (let i = 0; i < val.length; i++) { + header += `${key}: ${val[i]}\r\n` + } + } else { + header += `${key}: ${val}\r\n` + } + } + } + + if (channels.sendHeaders.hasSubscribers) { + channels.sendHeaders.publish({ request, headers: header, socket }) + } + + if (!body || bodyLength === 0) { + writeBuffer(abort, null, client, request, socket, contentLength, header, expectsPayload) + } else if (util.isBuffer(body)) { + writeBuffer(abort, body, client, request, socket, contentLength, header, expectsPayload) + } else if (util.isBlobLike(body)) { + if (typeof body.stream === 'function') { + writeIterable(abort, body.stream(), client, request, socket, contentLength, header, expectsPayload) + } else { + writeBlob(abort, body, client, request, socket, contentLength, header, expectsPayload) + } + } else if (util.isStream(body)) { + writeStream(abort, body, client, request, socket, contentLength, header, expectsPayload) + } else if (util.isIterable(body)) { + writeIterable(abort, body, client, request, socket, contentLength, header, expectsPayload) + } else { + assert(false) + } + + return true +} + +/** + * @param {AbortCallback} abort + * @param {import('stream').Stream} body + * @param {import('./client.js')} client + * @param {import('../core/request.js')} request + * @param {import('net').Socket} socket + * @param {number} contentLength + * @param {string} header + * @param {boolean} expectsPayload + */ +function writeStream (abort, body, client, request, socket, contentLength, header, expectsPayload) { + assert(contentLength !== 0 || client[kRunning] === 0, 'stream body cannot be pipelined') + + let finished = false + + const writer = new AsyncWriter({ abort, socket, request, contentLength, client, expectsPayload, header }) + + /** + * @param {Buffer} chunk + * @returns {void} + */ + const onData = function (chunk) { + if (finished) { + return + } + + try { + if (!writer.write(chunk) && this.pause) { + this.pause() + } + } catch (err) { + util.destroy(this, err) + } + } + + /** + * @returns {void} + */ + const onDrain = function () { + if (finished) { + return + } + + if (body.resume) { + body.resume() + } + } + + /** + * @returns {void} + */ + const onClose = function () { + // 'close' might be emitted *before* 'error' for + // broken streams. Wait a tick to avoid this case. + queueMicrotask(() => { + // It's only safe to remove 'error' listener after + // 'close'. + body.removeListener('error', onFinished) + }) + + if (!finished) { + const err = new RequestAbortedError() + queueMicrotask(() => onFinished(err)) + } + } + + /** + * @param {Error} [err] + * @returns + */ + const onFinished = function (err) { + if (finished) { + return + } + + finished = true + + assert(socket.destroyed || (socket[kWriting] && client[kRunning] <= 1)) + + socket + .off('drain', onDrain) + .off('error', onFinished) + + body + .removeListener('data', onData) + .removeListener('end', onFinished) + .removeListener('close', onClose) + + if (!err) { + try { + writer.end() + } catch (er) { + err = er + } + } + + writer.destroy(err) + + if (err && (err.code !== 'UND_ERR_INFO' || err.message !== 'reset')) { + util.destroy(body, err) + } else { + util.destroy(body) + } + } + + body + .on('data', onData) + .on('end', onFinished) + .on('error', onFinished) + .on('close', onClose) + + if (body.resume) { + body.resume() + } + + socket + .on('drain', onDrain) + .on('error', onFinished) + + if (body.errorEmitted ?? body.errored) { + setImmediate(onFinished, body.errored) + } else if (body.endEmitted ?? body.readableEnded) { + setImmediate(onFinished, null) + } + + if (body.closeEmitted ?? body.closed) { + setImmediate(onClose) + } +} + +/** + * @typedef AbortCallback + * @type {Function} + * @param {Error} [err] + * @returns {void} + */ + +/** + * @param {AbortCallback} abort + * @param {Uint8Array|null} body + * @param {import('./client.js')} client + * @param {import('../core/request.js')} request + * @param {import('net').Socket} socket + * @param {number} contentLength + * @param {string} header + * @param {boolean} expectsPayload + * @returns {void} + */ +function writeBuffer (abort, body, client, request, socket, contentLength, header, expectsPayload) { + try { + if (!body) { + if (contentLength === 0) { + socket.write(`${header}content-length: 0\r\n\r\n`, 'latin1') + } else { + assert(contentLength === null, 'no body must not have content length') + socket.write(`${header}\r\n`, 'latin1') + } + } else if (util.isBuffer(body)) { + assert(contentLength === body.byteLength, 'buffer body must have content length') + + socket.cork() + socket.write(`${header}content-length: ${contentLength}\r\n\r\n`, 'latin1') + socket.write(body) + socket.uncork() + request.onBodySent(body) + + if (!expectsPayload && request.reset !== false) { + socket[kReset] = true + } + } + request.onRequestSent() + + client[kResume]() + } catch (err) { + abort(err) + } +} + +/** + * @param {AbortCallback} abort + * @param {Blob} body + * @param {import('./client.js')} client + * @param {import('../core/request.js')} request + * @param {import('net').Socket} socket + * @param {number} contentLength + * @param {string} header + * @param {boolean} expectsPayload + * @returns {Promise} + */ +async function writeBlob (abort, body, client, request, socket, contentLength, header, expectsPayload) { + assert(contentLength === body.size, 'blob body must have content length') + + try { + if (contentLength != null && contentLength !== body.size) { + throw new RequestContentLengthMismatchError() + } + + const buffer = Buffer.from(await body.arrayBuffer()) + + socket.cork() + socket.write(`${header}content-length: ${contentLength}\r\n\r\n`, 'latin1') + socket.write(buffer) + socket.uncork() + + request.onBodySent(buffer) + request.onRequestSent() + + if (!expectsPayload && request.reset !== false) { + socket[kReset] = true + } + + client[kResume]() + } catch (err) { + abort(err) + } +} + +/** + * @param {AbortCallback} abort + * @param {Iterable} body + * @param {import('./client.js')} client + * @param {import('../core/request.js')} request + * @param {import('net').Socket} socket + * @param {number} contentLength + * @param {string} header + * @param {boolean} expectsPayload + * @returns {Promise} + */ +async function writeIterable (abort, body, client, request, socket, contentLength, header, expectsPayload) { + assert(contentLength !== 0 || client[kRunning] === 0, 'iterator body cannot be pipelined') + + let callback = null + function onDrain () { + if (callback) { + const cb = callback + callback = null + cb() + } + } + + const waitForDrain = () => new Promise((resolve, reject) => { + assert(callback === null) + + if (socket[kError]) { + reject(socket[kError]) + } else { + callback = resolve + } + }) + + socket + .on('close', onDrain) + .on('drain', onDrain) + + const writer = new AsyncWriter({ abort, socket, request, contentLength, client, expectsPayload, header }) + try { + // It's up to the user to somehow abort the async iterable. + for await (const chunk of body) { + if (socket[kError]) { + throw socket[kError] + } + + if (!writer.write(chunk)) { + await waitForDrain() + } + } + + writer.end() + } catch (err) { + writer.destroy(err) + } finally { + socket + .off('close', onDrain) + .off('drain', onDrain) + } +} + +class AsyncWriter { + /** + * + * @param {object} arg + * @param {AbortCallback} arg.abort + * @param {import('net').Socket} arg.socket + * @param {import('../core/request.js')} arg.request + * @param {number} arg.contentLength + * @param {import('./client.js')} arg.client + * @param {boolean} arg.expectsPayload + * @param {string} arg.header + */ + constructor ({ abort, socket, request, contentLength, client, expectsPayload, header }) { + this.socket = socket + this.request = request + this.contentLength = contentLength + this.client = client + this.bytesWritten = 0 + this.expectsPayload = expectsPayload + this.header = header + this.abort = abort + + socket[kWriting] = true + } + + /** + * @param {Buffer} chunk + * @returns + */ + write (chunk) { + const { socket, request, contentLength, client, bytesWritten, expectsPayload, header } = this + + if (socket[kError]) { + throw socket[kError] + } + + if (socket.destroyed) { + return false + } + + const len = Buffer.byteLength(chunk) + if (!len) { + return true + } + + // We should defer writing chunks. + if (contentLength !== null && bytesWritten + len > contentLength) { + if (client[kStrictContentLength]) { + throw new RequestContentLengthMismatchError() + } + + process.emitWarning(new RequestContentLengthMismatchError()) + } + + socket.cork() + + if (bytesWritten === 0) { + if (!expectsPayload && request.reset !== false) { + socket[kReset] = true + } + + if (contentLength === null) { + socket.write(`${header}transfer-encoding: chunked\r\n`, 'latin1') + } else { + socket.write(`${header}content-length: ${contentLength}\r\n\r\n`, 'latin1') + } + } + + if (contentLength === null) { + socket.write(`\r\n${len.toString(16)}\r\n`, 'latin1') + } + + this.bytesWritten += len + + const ret = socket.write(chunk) + + socket.uncork() + + request.onBodySent(chunk) + + if (!ret) { + if (socket[kParser].timeout && socket[kParser].timeoutType === TIMEOUT_HEADERS) { + if (socket[kParser].timeout.refresh) { + socket[kParser].timeout.refresh() + } + } + } + + return ret + } + + /** + * @returns {void} + */ + end () { + const { socket, contentLength, client, bytesWritten, expectsPayload, header, request } = this + request.onRequestSent() + + socket[kWriting] = false + + if (socket[kError]) { + throw socket[kError] + } + + if (socket.destroyed) { + return + } + + if (bytesWritten === 0) { + if (expectsPayload) { + // https://tools.ietf.org/html/rfc7230#section-3.3.2 + // A user agent SHOULD send a Content-Length in a request message when + // no Transfer-Encoding is sent and the request method defines a meaning + // for an enclosed payload body. + + socket.write(`${header}content-length: 0\r\n\r\n`, 'latin1') + } else { + socket.write(`${header}\r\n`, 'latin1') + } + } else if (contentLength === null) { + socket.write('\r\n0\r\n\r\n', 'latin1') + } + + if (contentLength !== null && bytesWritten !== contentLength) { + if (client[kStrictContentLength]) { + throw new RequestContentLengthMismatchError() + } else { + process.emitWarning(new RequestContentLengthMismatchError()) + } + } + + if (socket[kParser].timeout && socket[kParser].timeoutType === TIMEOUT_HEADERS) { + if (socket[kParser].timeout.refresh) { + socket[kParser].timeout.refresh() + } + } + + client[kResume]() + } + + /** + * @param {Error} [err] + * @returns {void} + */ + destroy (err) { + const { socket, client, abort } = this + + socket[kWriting] = false + + if (err) { + assert(client[kRunning] <= 1, 'pipeline should only contain this request') + abort(err) + } + } +} + +module.exports = connectH1 diff --git a/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/dispatcher/client-h2.js b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/dispatcher/client-h2.js new file mode 100644 index 000000000..92666f5d4 --- /dev/null +++ b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/dispatcher/client-h2.js @@ -0,0 +1,1060 @@ +'use strict' + +const assert = require('node:assert') +const { pipeline } = require('node:stream') +const util = require('../core/util.js') +const { + RequestContentLengthMismatchError, + RequestAbortedError, + SocketError, + InformationalError, + InvalidArgumentError, + HeadersTimeoutError, + BodyTimeoutError +} = require('../core/errors.js') +const { + kUrl, + kReset, + kClient, + kRunning, + kPending, + kQueue, + kPendingIdx, + kRunningIdx, + kError, + kSocket, + kStrictContentLength, + kOnError, + kMaxConcurrentStreams, + kPingInterval, + kHTTP2Session, + kHTTP2InitialWindowSize, + kHTTP2ConnectionWindowSize, + kResume, + kSize, + kHTTPContext, + kClosed, + kBodyTimeout, + kHeadersTimeout, + kEnableConnectProtocol, + kRemoteSettings, + kHTTP2Stream, + kHTTP2SessionState +} = require('../core/symbols.js') +const { channels } = require('../core/diagnostics.js') + +const kOpenStreams = Symbol('open streams') + +let extractBody + +/** @type {import('http2')} */ +let http2 +try { + http2 = require('node:http2') +} catch { + // @ts-ignore + http2 = { constants: {} } +} + +const { + constants: { + HTTP2_HEADER_AUTHORITY, + HTTP2_HEADER_METHOD, + HTTP2_HEADER_PATH, + HTTP2_HEADER_SCHEME, + HTTP2_HEADER_CONTENT_LENGTH, + HTTP2_HEADER_EXPECT, + HTTP2_HEADER_STATUS, + HTTP2_HEADER_PROTOCOL, + NGHTTP2_REFUSED_STREAM, + NGHTTP2_CANCEL + } +} = http2 + +function parseH2Headers (headers) { + const result = [] + + for (const [name, value] of Object.entries(headers)) { + // h2 may concat the header value by array + // e.g. Set-Cookie + if (Array.isArray(value)) { + for (const subvalue of value) { + // we need to provide each header value of header name + // because the headers handler expect name-value pair + result.push(Buffer.from(name), Buffer.from(subvalue)) + } + } else { + result.push(Buffer.from(name), Buffer.from(value)) + } + } + + return result +} + +function connectH2 (client, socket) { + client[kSocket] = socket + + const http2InitialWindowSize = client[kHTTP2InitialWindowSize] + const http2ConnectionWindowSize = client[kHTTP2ConnectionWindowSize] + + const session = http2.connect(client[kUrl], { + createConnection: () => socket, + peerMaxConcurrentStreams: client[kMaxConcurrentStreams], + settings: { + // TODO(metcoder95): add support for PUSH + enablePush: false, + ...(http2InitialWindowSize != null ? { initialWindowSize: http2InitialWindowSize } : null) + } + }) + + client[kSocket] = socket + session[kOpenStreams] = 0 + session[kClient] = client + session[kSocket] = socket + session[kHTTP2SessionState] = { + ping: { + interval: client[kPingInterval] === 0 ? null : setInterval(onHttp2SendPing, client[kPingInterval], session).unref() + } + } + // We set it to true by default in a best-effort; however once connected to an H2 server + // we will check if extended CONNECT protocol is supported or not + // and set this value accordingly. + session[kEnableConnectProtocol] = false + // States whether or not we have received the remote settings from the server + session[kRemoteSettings] = false + + // Apply connection-level flow control once connected (if supported). + if (http2ConnectionWindowSize) { + util.addListener(session, 'connect', applyConnectionWindowSize.bind(session, http2ConnectionWindowSize)) + } + + util.addListener(session, 'error', onHttp2SessionError) + util.addListener(session, 'frameError', onHttp2FrameError) + util.addListener(session, 'end', onHttp2SessionEnd) + util.addListener(session, 'goaway', onHttp2SessionGoAway) + util.addListener(session, 'close', onHttp2SessionClose) + util.addListener(session, 'remoteSettings', onHttp2RemoteSettings) + // TODO (@metcoder95): implement SETTINGS support + // util.addListener(session, 'localSettings', onHttp2RemoteSettings) + + session.unref() + + client[kHTTP2Session] = session + socket[kHTTP2Session] = session + + util.addListener(socket, 'error', onHttp2SocketError) + util.addListener(socket, 'end', onHttp2SocketEnd) + util.addListener(socket, 'close', onHttp2SocketClose) + + socket[kClosed] = false + socket.on('close', onSocketClose) + + return { + version: 'h2', + defaultPipelining: Infinity, + /** + * @param {import('../core/request.js')} request + * @returns {boolean} + */ + write (request) { + return writeH2(client, request) + }, + /** + * @returns {void} + */ + resume () { + resumeH2(client) + }, + /** + * @param {Error | null} err + * @param {() => void} callback + */ + destroy (err, callback) { + if (socket[kClosed]) { + queueMicrotask(callback) + } else { + socket.destroy(err).on('close', callback) + } + }, + /** + * @type {boolean} + */ + get destroyed () { + return socket.destroyed + }, + /** + * @param {import('../core/request.js')} request + * @returns {boolean} + */ + busy (request) { + if (request != null) { + if (client[kRunning] > 0) { + // We are already processing requests + + // Non-idempotent request cannot be retried. + // Ensure that no other requests are inflight and + // could cause failure. + if (request.idempotent === false) return true + // Don't dispatch an upgrade until all preceding requests have completed. + // Possibly, we do not have remote settings confirmed yet. + if ((request.upgrade === 'websocket' || request.method === 'CONNECT') && session[kRemoteSettings] === false) return true + // Request with stream or iterator body can error while other requests + // are inflight and indirectly error those as well. + // Ensure this doesn't happen by waiting for inflight + // to complete before dispatching. + + // Request with stream or iterator body cannot be retried. + // Ensure that no other requests are inflight and + // could cause failure. + if (util.bodyLength(request.body) !== 0 && + (util.isStream(request.body) || util.isAsyncIterable(request.body) || util.isFormDataLike(request.body))) return true + } else { + return (request.upgrade === 'websocket' || request.method === 'CONNECT') && session[kRemoteSettings] === false + } + } + + return false + } + } +} + +function resumeH2 (client) { + const socket = client[kSocket] + + if (socket?.destroyed === false) { + // Only let the process exit when there is genuinely nothing outstanding. + // Unreffing because the peer advertised MAX_CONCURRENT_STREAMS = 0 left + // queued requests with nothing holding the event loop open, so the process + // could exit with status 0 while an awaited request never settled. + if (client[kSize] === 0) { + socket.unref() + client[kHTTP2Session].unref() + } else { + socket.ref() + client[kHTTP2Session].ref() + } + } +} + +function applyConnectionWindowSize (connectionWindowSize) { + try { + if (typeof this.setLocalWindowSize === 'function') { + this.setLocalWindowSize(connectionWindowSize) + } + } catch { + // Best-effort only. + } +} + +function onHttp2RemoteSettings (settings) { + // Fallbacks are a safe bet, remote setting will always override + this[kClient][kMaxConcurrentStreams] = settings.maxConcurrentStreams ?? this[kClient][kMaxConcurrentStreams] + /** + * From RFC-8441 + * A sender MUST NOT send a SETTINGS_ENABLE_CONNECT_PROTOCOL parameter + * with the value of 0 after previously sending a value of 1. + */ + // Note: Cannot be tested in Node, it does not supports disabling the extended CONNECT protocol once enabled + if (this[kRemoteSettings] === true && this[kEnableConnectProtocol] === true && settings.enableConnectProtocol === false) { + const err = new InformationalError('HTTP/2: Server disabled extended CONNECT protocol against RFC-8441') + this[kSocket][kError] = err + this[kClient][kOnError](err) + return + } + + this[kEnableConnectProtocol] = settings.enableConnectProtocol ?? this[kEnableConnectProtocol] + this[kRemoteSettings] = true + this[kClient][kResume]() +} + +function onHttp2SendPing (session) { + const state = session[kHTTP2SessionState] + if ((session.closed || session.destroyed) && state.ping.interval != null) { + clearInterval(state.ping.interval) + state.ping.interval = null + return + } + + // If no ping sent, do nothing + session.ping(onPing.bind(session)) + + function onPing (err, duration) { + const client = this[kClient] + const socket = this[kClient] + + if (err != null) { + const error = new InformationalError(`HTTP/2: "PING" errored - type ${err.message}`) + socket[kError] = error + client[kOnError](error) + } else { + client.emit('ping', duration) + } + } +} + +function onHttp2SessionError (err) { + assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID') + + this[kSocket][kError] = err + this[kClient][kOnError](err) +} + +function onHttp2FrameError (type, code, id) { + if (id === 0) { + const err = new InformationalError(`HTTP/2: "frameError" received - type ${type}, code ${code}`) + this[kSocket][kError] = err + this[kClient][kOnError](err) + } +} + +function onHttp2SessionEnd () { + const err = new SocketError('other side closed', util.getSocketInfo(this[kSocket])) + this.destroy(err) + util.destroy(this[kSocket], err) +} + +/** + * This is the root cause of #3011 + * We need to handle GOAWAY frames properly, and trigger the session close + * along with the socket right away + * + * @this {import('http2').ClientHttp2Session} + * @param {number} errorCode + */ +// Backport of #5410 and #5569. HTTP/2 multiplexes, so requests complete out of +// order; advancing kRunningIdx blindly retired whichever request happened to +// sit at the head instead of the one that actually finished, which both lost +// requests and left phantom running slots behind. +function completeRequest (client, request, resetPendingIdx = false) { + const queue = client[kQueue] + const runningIdx = client[kRunningIdx] + + // In-order completion: clear the request and advance without splicing. + // The client's resume loop compacts cleared slots once the index grows. + if (runningIdx < client[kPendingIdx] && queue[runningIdx] === request) { + queue[runningIdx] = null + client[kRunningIdx] = runningIdx + 1 + return + } + + const index = queue.indexOf(request, runningIdx) + + if (index === -1 || index >= client[kPendingIdx]) { + return + } + + queue.splice(index, 1) + client[kPendingIdx]-- + + if (resetPendingIdx && client[kPendingIdx] < client[kRunningIdx]) { + client[kPendingIdx] = client[kRunningIdx] + } +} + +function onHttp2SessionGoAway (errorCode) { + // TODO(mcollina): Verify if GOAWAY implements the spec correctly: + // https://datatracker.ietf.org/doc/html/rfc7540#section-6.8 + // Specifically, we do not verify the "valid" stream id. + + const err = this[kError] || new SocketError(`HTTP/2: "GOAWAY" frame received with code ${errorCode}`, util.getSocketInfo(this[kSocket])) + const client = this[kClient] + + client[kSocket] = null + client[kHTTPContext] = null + + // this is an HTTP2 session + this.close() + this[kHTTP2Session] = null + + util.destroy(this[kSocket], err) + + // Fail head of pipeline. + if (client[kRunningIdx] < client[kQueue].length) { + const request = client[kQueue][client[kRunningIdx]] + client[kQueue][client[kRunningIdx]++] = null + if (request != null) { + util.errorRequest(client, request, err) + } + client[kPendingIdx] = client[kRunningIdx] + } + + assert(client[kRunning] === 0) + + client.emit('disconnect', client[kUrl], [client], err) + client.emit('connectionError', client[kUrl], [client], err) + + client[kResume]() +} + +function onHttp2SessionClose () { + const { [kClient]: client, [kHTTP2SessionState]: state } = this + const { [kSocket]: socket } = client + + const err = this[kSocket][kError] || this[kError] || new SocketError('closed', util.getSocketInfo(socket)) + + client[kSocket] = null + client[kHTTPContext] = null + + if (state.ping.interval != null) { + clearInterval(state.ping.interval) + state.ping.interval = null + } + + if (client.destroyed) { + assert(client[kPending] === 0) + + // Fail entire queue. + const requests = client[kQueue].splice(client[kRunningIdx]) + for (let i = 0; i < requests.length; i++) { + const request = requests[i] + if (request != null) { + util.errorRequest(client, request, err) + } + } + } +} + +function onHttp2SocketClose () { + const err = this[kError] || new SocketError('closed', util.getSocketInfo(this)) + + const client = this[kHTTP2Session][kClient] + + client[kSocket] = null + client[kHTTPContext] = null + + if (this[kHTTP2Session] !== null) { + this[kHTTP2Session].destroy(err) + } + + client[kPendingIdx] = client[kRunningIdx] + + assert(client[kRunning] === 0) + + client.emit('disconnect', client[kUrl], [client], err) + + client[kResume]() +} + +function onHttp2SocketError (err) { + assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID') + + this[kError] = err + + this[kClient][kOnError](err) +} + +function onHttp2SocketEnd () { + util.destroy(this, new SocketError('other side closed', util.getSocketInfo(this))) +} + +function onSocketClose () { + this[kClosed] = true +} + +// https://www.rfc-editor.org/rfc/rfc7230#section-3.3.2 +function shouldSendContentLength (method) { + return method !== 'GET' && method !== 'HEAD' && method !== 'OPTIONS' && method !== 'TRACE' && method !== 'CONNECT' +} + +function writeH2 (client, request) { + // Time to the response headers, then time between body chunks. Using + // bodyTimeout for both made headersTimeout a no-op over HTTP/2. + const headersTimeout = request.headersTimeout ?? client[kHeadersTimeout] + const bodyTimeout = request.bodyTimeout ?? client[kBodyTimeout] + const session = client[kHTTP2Session] + const { method, path, host, upgrade, expectContinue, signal, protocol, headers: reqHeaders } = request + let { body } = request + + if (upgrade != null && upgrade !== 'websocket') { + util.errorRequest(client, request, new InvalidArgumentError(`Custom upgrade "${upgrade}" not supported over HTTP/2`)) + return false + } + + const headers = {} + for (let n = 0; n < reqHeaders.length; n += 2) { + const key = reqHeaders[n + 0] + const val = reqHeaders[n + 1] + + if (key === 'cookie') { + if (headers[key] != null) { + headers[key] = Array.isArray(headers[key]) ? (headers[key].push(val), headers[key]) : [headers[key], val] + } else { + headers[key] = val + } + + continue + } + + if (Array.isArray(val)) { + for (let i = 0; i < val.length; i++) { + if (headers[key]) { + headers[key] += `, ${val[i]}` + } else { + headers[key] = val[i] + } + } + } else if (headers[key]) { + headers[key] += `, ${val}` + } else { + headers[key] = val + } + } + + /** @type {import('node:http2').ClientHttp2Stream} */ + let stream = null + + const { hostname, port } = client[kUrl] + + headers[HTTP2_HEADER_AUTHORITY] = host || `${hostname}${port ? `:${port}` : ''}` + headers[HTTP2_HEADER_METHOD] = method + + const abort = (err) => { + if (request.aborted || request.completed) { + return + } + + err = err || new RequestAbortedError() + + util.errorRequest(client, request, err) + + if (stream != null) { + // Some chunks might still come after abort, + // let's ignore them + stream.removeAllListeners('data') + + // On Abort, we close the stream to send RST_STREAM frame + stream.close() + + // We move the running index to the next request + client[kOnError](err) + completeRequest(client, request) + client[kResume]() + } + + // We do not destroy the socket as we can continue using the session + // the stream gets destroyed and the session remains to create new streams + util.destroy(body, err) + } + + try { + // We are already connected, streams are pending. + // We can call on connect, and wait for abort + request.onConnect(abort) + } catch (err) { + util.errorRequest(client, request, err) + } + + if (request.aborted) { + return false + } + + if (upgrade || method === 'CONNECT') { + session.ref() + + if (upgrade === 'websocket') { + // We cannot upgrade to websocket if extended CONNECT protocol is not supported + if (session[kEnableConnectProtocol] === false) { + util.errorRequest(client, request, new InformationalError('HTTP/2: Extended CONNECT protocol not supported by server')) + session.unref() + return false + } + + // We force the method to CONNECT + // as per RFC-8441 + // https://datatracker.ietf.org/doc/html/rfc8441#section-4 + headers[HTTP2_HEADER_METHOD] = 'CONNECT' + headers[HTTP2_HEADER_PROTOCOL] = 'websocket' + // :path and :scheme headers must be omitted when sending CONNECT but set if extended-CONNECT + headers[HTTP2_HEADER_PATH] = path + + if (protocol === 'ws:' || protocol === 'wss:') { + headers[HTTP2_HEADER_SCHEME] = protocol === 'ws:' ? 'http' : 'https' + } else { + headers[HTTP2_HEADER_SCHEME] = protocol === 'http:' ? 'http' : 'https' + } + + stream = session.request(headers, { endStream: false, signal }) + stream[kHTTP2Stream] = true + ++session[kOpenStreams] + + stream.once('response', (headers, _flags) => { + const { [HTTP2_HEADER_STATUS]: statusCode, ...realHeaders } = headers + + try { + request.onUpgrade(statusCode, parseH2Headers(realHeaders), stream) + } catch (err) { + abort(err) + return + } + completeRequest(client, request) + }) + + stream.on('error', () => { + if (stream.rstCode === NGHTTP2_REFUSED_STREAM || stream.rstCode === NGHTTP2_CANCEL) { + // NGHTTP2_REFUSED_STREAM (7) or NGHTTP2_CANCEL (8) + // We do not treat those as errors as the server might + // not support websockets and refuse the stream + abort(new InformationalError(`HTTP/2: "stream error" received - code ${stream.rstCode}`)) + } + }) + + stream.once('close', () => { + session[kOpenStreams] -= 1 + if (session[kOpenStreams] === 0) session.unref() + }) + + stream.setTimeout(headersTimeout) + return true + } + + // TODO: consolidate once we support CONNECT properly + // NOTE: We are already connected, streams are pending, first request + // will create a new stream. We trigger a request to create the stream and wait until + // `ready` event is triggered + // We disabled endStream to allow the user to write to the stream + stream = session.request(headers, { endStream: false, signal }) + stream[kHTTP2Stream] = true + ++session[kOpenStreams] + stream.on('response', headers => { + const { [HTTP2_HEADER_STATUS]: statusCode, ...realHeaders } = headers + + try { + request.onUpgrade(statusCode, parseH2Headers(realHeaders), stream) + } catch (err) { + abort(err) + return + } + completeRequest(client, request) + }) + stream.on('error', abort) + stream.once('close', () => { + session[kOpenStreams] -= 1 + if (session[kOpenStreams] === 0) session.unref() + }) + stream.setTimeout(headersTimeout) + + return true + } + + // https://tools.ietf.org/html/rfc7540#section-8.3 + // :path and :scheme headers must be omitted when sending CONNECT + headers[HTTP2_HEADER_PATH] = path + headers[HTTP2_HEADER_SCHEME] = protocol === 'http:' ? 'http' : 'https' + + // https://tools.ietf.org/html/rfc7231#section-4.3.1 + // https://tools.ietf.org/html/rfc7231#section-4.3.2 + // https://tools.ietf.org/html/rfc7231#section-4.3.5 + + // Sending a payload body on a request that does not + // expect it can cause undefined behavior on some + // servers and corrupt connection state. Do not + // re-use the connection for further requests. + + const expectsPayload = ( + method === 'PUT' || + method === 'POST' || + method === 'PATCH' + ) + + if (body && typeof body.read === 'function') { + // Try to read EOF in order to get length. + body.read(0) + } + + let contentLength = util.bodyLength(body) + + if (util.isFormDataLike(body)) { + extractBody ??= require('../web/fetch/body.js').extractBody + + const [bodyStream, contentType] = extractBody(body) + headers['content-type'] = contentType + + body = bodyStream.stream + contentLength = bodyStream.length + } + + if (contentLength == null) { + contentLength = request.contentLength + } + + if (!expectsPayload) { + // https://tools.ietf.org/html/rfc7230#section-3.3.2 + // A user agent SHOULD NOT send a Content-Length header field when + // the request message does not contain a payload body and the method + // semantics do not anticipate such a body. + // And for methods that don't expect a payload, omit Content-Length. + contentLength = null + } + + // https://github.com/nodejs/undici/issues/2046 + // A user agent may send a Content-Length header with 0 value, this should be allowed. + if (shouldSendContentLength(method) && contentLength > 0 && request.contentLength != null && request.contentLength !== contentLength) { + if (client[kStrictContentLength]) { + util.errorRequest(client, request, new RequestContentLengthMismatchError()) + return false + } + + process.emitWarning(new RequestContentLengthMismatchError()) + } + + if (contentLength != null) { + assert(body || contentLength === 0, 'no body must not have content length') + headers[HTTP2_HEADER_CONTENT_LENGTH] = `${contentLength}` + } + + session.ref() + + if (channels.sendHeaders.hasSubscribers) { + let header = '' + for (const key in headers) { + header += `${key}: ${headers[key]}\r\n` + } + channels.sendHeaders.publish({ request, headers: header, socket: session[kSocket] }) + } + + // TODO(metcoder95): add support for sending trailers + const shouldEndStream = method === 'GET' || method === 'HEAD' || body === null + if (expectContinue) { + headers[HTTP2_HEADER_EXPECT] = '100-continue' + stream = session.request(headers, { endStream: shouldEndStream, signal }) + stream[kHTTP2Stream] = true + + stream.once('continue', writeBodyH2) + } else { + stream = session.request(headers, { + endStream: shouldEndStream, + signal + }) + stream[kHTTP2Stream] = true + + writeBodyH2() + } + + // Increment counter as we have new streams open + ++session[kOpenStreams] + stream.setTimeout(headersTimeout) + + // Track whether we received a response (headers) + let responseReceived = false + + stream.once('response', headers => { + const { [HTTP2_HEADER_STATUS]: statusCode, ...realHeaders } = headers + request.onResponseStarted() + responseReceived = true + stream.setTimeout(bodyTimeout) + + // Due to the stream nature, it is possible we face a race condition + // where the stream has been assigned, but the request has been aborted + // the request remains in-flight and headers hasn't been received yet + // for those scenarios, best effort is to destroy the stream immediately + // as there's no value to keep it open. + if (request.aborted) { + stream.removeAllListeners('data') + return + } + + if (request.onHeaders(Number(statusCode), parseH2Headers(realHeaders), stream.resume.bind(stream), '') === false) { + stream.pause() + } + + stream.on('data', (chunk) => { + if (request.aborted || request.completed) { + return + } + + if (request.onData(chunk) === false) { + stream.pause() + } + }) + }) + + stream.once('end', () => { + stream.removeAllListeners('data') + // If we received a response, this is a normal completion + if (responseReceived) { + if (!request.aborted && !request.completed) { + request.onComplete({}) + } + + completeRequest(client, request) + client[kResume]() + } else { + // Stream ended without receiving a response - this is an error + // (e.g., server destroyed the stream before sending headers) + abort(new InformationalError('HTTP/2: stream half-closed (remote)')) + completeRequest(client, request, true) + client[kResume]() + } + }) + + stream.once('close', () => { + stream.removeAllListeners('data') + session[kOpenStreams] -= 1 + if (session[kOpenStreams] === 0) { + session.unref() + } + + // A stream can close without ever emitting 'end' or 'error': a peer's + // RST_STREAM(CANCEL) received before the response is reported by Node as a + // bare 'close', and destroying the stream unenrolls its timeout, so no + // 'timeout' follows either. Nothing else would ever settle this request. + if (!request.aborted && !request.completed) { + abort(new InformationalError('HTTP/2: stream closed before the response was complete')) + } + }) + + stream.once('error', function (err) { + stream.removeAllListeners('data') + abort(err) + }) + + stream.once('frameError', (type, code) => { + stream.removeAllListeners('data') + abort(new InformationalError(`HTTP/2: "frameError" received - type ${type}, code ${code}`)) + }) + + stream.on('aborted', () => { + stream.removeAllListeners('data') + }) + + stream.on('timeout', () => { + const err = responseReceived + ? new BodyTimeoutError(`HTTP/2: "body timeout after ${bodyTimeout}"`) + : new HeadersTimeoutError(`HTTP/2: "headers timeout after ${headersTimeout}"`) + stream.removeAllListeners('data') + session[kOpenStreams] -= 1 + + if (session[kOpenStreams] === 0) { + session.unref() + } + + abort(err) + }) + + stream.once('trailers', trailers => { + if (request.aborted || request.completed) { + return + } + + stream.removeAllListeners('data') + request.onComplete(trailers) + }) + + return true + + function writeBodyH2 () { + if (!body || contentLength === 0) { + writeBuffer( + abort, + stream, + null, + client, + request, + client[kSocket], + contentLength, + expectsPayload + ) + } else if (util.isBuffer(body)) { + writeBuffer( + abort, + stream, + body, + client, + request, + client[kSocket], + contentLength, + expectsPayload + ) + } else if (util.isBlobLike(body)) { + if (typeof body.stream === 'function') { + writeIterable( + abort, + stream, + body.stream(), + client, + request, + client[kSocket], + contentLength, + expectsPayload + ) + } else { + writeBlob( + abort, + stream, + body, + client, + request, + client[kSocket], + contentLength, + expectsPayload + ) + } + } else if (util.isStream(body)) { + writeStream( + abort, + client[kSocket], + expectsPayload, + stream, + body, + client, + request, + contentLength + ) + } else if (util.isIterable(body)) { + writeIterable( + abort, + stream, + body, + client, + request, + client[kSocket], + contentLength, + expectsPayload + ) + } else { + assert(false) + } + } +} + +function writeBuffer (abort, h2stream, body, client, request, socket, contentLength, expectsPayload) { + try { + if (body != null && util.isBuffer(body)) { + assert(contentLength === body.byteLength, 'buffer body must have content length') + h2stream.cork() + h2stream.write(body) + h2stream.uncork() + h2stream.end() + + request.onBodySent(body) + } + + if (!expectsPayload) { + socket[kReset] = true + } + + request.onRequestSent() + client[kResume]() + } catch (error) { + abort(error) + } +} + +function writeStream (abort, socket, expectsPayload, h2stream, body, client, request, contentLength) { + assert(contentLength !== 0 || client[kRunning] === 0, 'stream body cannot be pipelined') + + // For HTTP/2, is enough to pipe the stream + const pipe = pipeline( + body, + h2stream, + (err) => { + if (err) { + util.destroy(pipe, err) + abort(err) + } else { + util.removeAllListeners(pipe) + request.onRequestSent() + + if (!expectsPayload) { + socket[kReset] = true + } + + client[kResume]() + } + } + ) + + util.addListener(pipe, 'data', onPipeData) + + function onPipeData (chunk) { + request.onBodySent(chunk) + } +} + +async function writeBlob (abort, h2stream, body, client, request, socket, contentLength, expectsPayload) { + assert(contentLength === body.size, 'blob body must have content length') + + try { + if (contentLength != null && contentLength !== body.size) { + throw new RequestContentLengthMismatchError() + } + + const buffer = Buffer.from(await body.arrayBuffer()) + + h2stream.cork() + h2stream.write(buffer) + h2stream.uncork() + h2stream.end() + + request.onBodySent(buffer) + request.onRequestSent() + + if (!expectsPayload) { + socket[kReset] = true + } + + client[kResume]() + } catch (err) { + abort(err) + } +} + +async function writeIterable (abort, h2stream, body, client, request, socket, contentLength, expectsPayload) { + assert(contentLength !== 0 || client[kRunning] === 0, 'iterator body cannot be pipelined') + + let callback = null + function onDrain () { + if (callback) { + const cb = callback + callback = null + cb() + } + } + + const waitForDrain = () => new Promise((resolve, reject) => { + assert(callback === null) + + if (socket[kError]) { + reject(socket[kError]) + } else { + callback = resolve + } + }) + + h2stream + .on('close', onDrain) + .on('drain', onDrain) + + try { + // It's up to the user to somehow abort the async iterable. + for await (const chunk of body) { + if (socket[kError]) { + throw socket[kError] + } + + const res = h2stream.write(chunk) + request.onBodySent(chunk) + if (!res) { + await waitForDrain() + } + } + + h2stream.end() + + request.onRequestSent() + + if (!expectsPayload) { + socket[kReset] = true + } + + client[kResume]() + } catch (err) { + abort(err) + } finally { + h2stream + .off('close', onDrain) + .off('drain', onDrain) + } +} + +module.exports = connectH2 diff --git a/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/dispatcher/client.js b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/dispatcher/client.js new file mode 100644 index 000000000..ee67336c9 --- /dev/null +++ b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/dispatcher/client.js @@ -0,0 +1,668 @@ +'use strict' + +const assert = require('node:assert') +const net = require('node:net') +const http = require('node:http') +const util = require('../core/util.js') +const { ClientStats } = require('../util/stats.js') +const { channels } = require('../core/diagnostics.js') +const Request = require('../core/request.js') +const DispatcherBase = require('./dispatcher-base') +const { + InvalidArgumentError, + InformationalError, + ClientDestroyedError +} = require('../core/errors.js') +const buildConnector = require('../core/connect.js') +const { + kUrl, + kServerName, + kClient, + kBusy, + kConnect, + kResuming, + kRunning, + kPending, + kSize, + kQueue, + kConnected, + kConnecting, + kNeedDrain, + kKeepAliveDefaultTimeout, + kHostHeader, + kPendingIdx, + kRunningIdx, + kError, + kPipelining, + kKeepAliveTimeoutValue, + kMaxHeadersSize, + kKeepAliveMaxTimeout, + kKeepAliveTimeoutThreshold, + kHeadersTimeout, + kBodyTimeout, + kStrictContentLength, + kConnector, + kMaxRequests, + kCounter, + kClose, + kDestroy, + kDispatch, + kLocalAddress, + kMaxResponseSize, + kOnError, + kHTTPContext, + kMaxConcurrentStreams, + kHTTP2InitialWindowSize, + kHTTP2ConnectionWindowSize, + kResume, + kPingInterval +} = require('../core/symbols.js') +const connectH1 = require('./client-h1.js') +const connectH2 = require('./client-h2.js') + +const kClosedResolve = Symbol('kClosedResolve') + +const getDefaultNodeMaxHeaderSize = http && + http.maxHeaderSize && + Number.isInteger(http.maxHeaderSize) && + http.maxHeaderSize > 0 + ? () => http.maxHeaderSize + : () => { throw new InvalidArgumentError('http module not available or http.maxHeaderSize invalid') } + +const noop = () => { } + +function getPipelining (client) { + return client[kPipelining] ?? client[kHTTPContext]?.defaultPipelining ?? 1 +} + +/** + * @type {import('../../types/client.js').default} + */ +class Client extends DispatcherBase { + /** + * + * @param {string|URL} url + * @param {import('../../types/client.js').Client.Options} options + */ + constructor (url, { + maxHeaderSize, + headersTimeout, + socketTimeout, + requestTimeout, + connectTimeout, + bodyTimeout, + idleTimeout, + keepAlive, + keepAliveTimeout, + maxKeepAliveTimeout, + keepAliveMaxTimeout, + keepAliveTimeoutThreshold, + socketPath, + pipelining, + tls, + strictContentLength, + maxCachedSessions, + connect, + maxRequestsPerClient, + localAddress, + maxResponseSize, + autoSelectFamily, + autoSelectFamilyAttemptTimeout, + // h2 + maxConcurrentStreams, + allowH2, + useH2c, + initialWindowSize, + connectionWindowSize, + pingInterval, + webSocket + } = {}) { + if (keepAlive !== undefined) { + throw new InvalidArgumentError('unsupported keepAlive, use pipelining=0 instead') + } + + if (socketTimeout !== undefined) { + throw new InvalidArgumentError('unsupported socketTimeout, use headersTimeout & bodyTimeout instead') + } + + if (requestTimeout !== undefined) { + throw new InvalidArgumentError('unsupported requestTimeout, use headersTimeout & bodyTimeout instead') + } + + if (idleTimeout !== undefined) { + throw new InvalidArgumentError('unsupported idleTimeout, use keepAliveTimeout instead') + } + + if (maxKeepAliveTimeout !== undefined) { + throw new InvalidArgumentError('unsupported maxKeepAliveTimeout, use keepAliveMaxTimeout instead') + } + + if (maxHeaderSize != null) { + if (!Number.isInteger(maxHeaderSize) || maxHeaderSize < 1) { + throw new InvalidArgumentError('invalid maxHeaderSize') + } + } else { + // If maxHeaderSize is not provided, use the default value from the http module + // or if that is not available, throw an error. + maxHeaderSize = getDefaultNodeMaxHeaderSize() + } + + if (socketPath != null && typeof socketPath !== 'string') { + throw new InvalidArgumentError('invalid socketPath') + } + + if (connectTimeout != null && (!Number.isFinite(connectTimeout) || connectTimeout < 0)) { + throw new InvalidArgumentError('invalid connectTimeout') + } + + if (keepAliveTimeout != null && (!Number.isFinite(keepAliveTimeout) || keepAliveTimeout <= 0)) { + throw new InvalidArgumentError('invalid keepAliveTimeout') + } + + if (keepAliveMaxTimeout != null && (!Number.isFinite(keepAliveMaxTimeout) || keepAliveMaxTimeout <= 0)) { + throw new InvalidArgumentError('invalid keepAliveMaxTimeout') + } + + if (keepAliveTimeoutThreshold != null && !Number.isFinite(keepAliveTimeoutThreshold)) { + throw new InvalidArgumentError('invalid keepAliveTimeoutThreshold') + } + + if (headersTimeout != null && (!Number.isInteger(headersTimeout) || headersTimeout < 0)) { + throw new InvalidArgumentError('headersTimeout must be a positive integer or zero') + } + + if (bodyTimeout != null && (!Number.isInteger(bodyTimeout) || bodyTimeout < 0)) { + throw new InvalidArgumentError('bodyTimeout must be a positive integer or zero') + } + + if (connect != null && typeof connect !== 'function' && typeof connect !== 'object') { + throw new InvalidArgumentError('connect must be a function or an object') + } + + if (maxRequestsPerClient != null && (!Number.isInteger(maxRequestsPerClient) || maxRequestsPerClient < 0)) { + throw new InvalidArgumentError('maxRequestsPerClient must be a positive number') + } + + if (localAddress != null && (typeof localAddress !== 'string' || net.isIP(localAddress) === 0)) { + throw new InvalidArgumentError('localAddress must be valid string IP address') + } + + if (maxResponseSize != null && (!Number.isInteger(maxResponseSize) || maxResponseSize < -1)) { + throw new InvalidArgumentError('maxResponseSize must be a positive number') + } + + if ( + autoSelectFamilyAttemptTimeout != null && + (!Number.isInteger(autoSelectFamilyAttemptTimeout) || autoSelectFamilyAttemptTimeout < -1) + ) { + throw new InvalidArgumentError('autoSelectFamilyAttemptTimeout must be a positive number') + } + + // h2 + if (allowH2 != null && typeof allowH2 !== 'boolean') { + throw new InvalidArgumentError('allowH2 must be a valid boolean value') + } + + if (maxConcurrentStreams != null && (typeof maxConcurrentStreams !== 'number' || maxConcurrentStreams < 1)) { + throw new InvalidArgumentError('maxConcurrentStreams must be a positive integer, greater than 0') + } + + if (useH2c != null && typeof useH2c !== 'boolean') { + throw new InvalidArgumentError('useH2c must be a valid boolean value') + } + + if (initialWindowSize != null && (!Number.isInteger(initialWindowSize) || initialWindowSize < 1)) { + throw new InvalidArgumentError('initialWindowSize must be a positive integer, greater than 0') + } + + if (connectionWindowSize != null && (!Number.isInteger(connectionWindowSize) || connectionWindowSize < 1)) { + throw new InvalidArgumentError('connectionWindowSize must be a positive integer, greater than 0') + } + + if (pingInterval != null && (typeof pingInterval !== 'number' || !Number.isInteger(pingInterval) || pingInterval < 0)) { + throw new InvalidArgumentError('pingInterval must be a positive integer, greater or equal to 0') + } + + super({ webSocket }) + + if (typeof connect !== 'function') { + connect = buildConnector({ + ...tls, + maxCachedSessions, + allowH2, + useH2c, + socketPath, + timeout: connectTimeout, + ...(typeof autoSelectFamily === 'boolean' ? { autoSelectFamily, autoSelectFamilyAttemptTimeout } : undefined), + ...connect + }) + } else { + const customConnect = connect + connect = (opts, callback) => customConnect({ + ...opts, + ...(socketPath != null ? { socketPath } : null), + ...(allowH2 != null ? { allowH2 } : null) + }, callback) + } + + this[kUrl] = util.parseOrigin(url) + this[kConnector] = connect + this[kPipelining] = pipelining != null ? pipelining : 1 + this[kMaxHeadersSize] = maxHeaderSize + this[kKeepAliveDefaultTimeout] = keepAliveTimeout == null ? 4e3 : keepAliveTimeout + this[kKeepAliveMaxTimeout] = keepAliveMaxTimeout == null ? 600e3 : keepAliveMaxTimeout + this[kKeepAliveTimeoutThreshold] = keepAliveTimeoutThreshold == null ? 2e3 : keepAliveTimeoutThreshold + this[kKeepAliveTimeoutValue] = this[kKeepAliveDefaultTimeout] + this[kServerName] = null + this[kLocalAddress] = localAddress != null ? localAddress : null + this[kResuming] = 0 // 0, idle, 1, scheduled, 2 resuming + this[kNeedDrain] = 0 // 0, idle, 1, scheduled, 2 resuming + this[kHostHeader] = `host: ${this[kUrl].hostname}${this[kUrl].port ? `:${this[kUrl].port}` : ''}\r\n` + this[kBodyTimeout] = bodyTimeout != null ? bodyTimeout : 300e3 + this[kHeadersTimeout] = headersTimeout != null ? headersTimeout : 300e3 + this[kStrictContentLength] = strictContentLength == null ? true : strictContentLength + this[kMaxRequests] = maxRequestsPerClient + this[kClosedResolve] = null + this[kMaxResponseSize] = maxResponseSize > -1 ? maxResponseSize : -1 + this[kHTTPContext] = null + // h2 + this[kMaxConcurrentStreams] = maxConcurrentStreams != null ? maxConcurrentStreams : 100 // Max peerConcurrentStreams for a Node h2 server + // HTTP/2 window sizes are set to higher defaults than Node.js core for better performance: + // - initialWindowSize: 262144 (256KB) vs Node.js default 65535 (64KB - 1) + // Allows more data to be sent before requiring acknowledgment, improving throughput + // especially on high-latency networks. This matches common production HTTP/2 servers. + // - connectionWindowSize: 524288 (512KB) vs Node.js default (none set) + // Provides better flow control for the entire connection across multiple streams. + this[kHTTP2InitialWindowSize] = initialWindowSize != null ? initialWindowSize : 262144 + this[kHTTP2ConnectionWindowSize] = connectionWindowSize != null ? connectionWindowSize : 524288 + this[kPingInterval] = pingInterval != null ? pingInterval : 60e3 // Default ping interval for h2 - 1 minute + + // kQueue is built up of 3 sections separated by + // the kRunningIdx and kPendingIdx indices. + // | complete | running | pending | + // ^ kRunningIdx ^ kPendingIdx ^ kQueue.length + // kRunningIdx points to the first running element. + // kPendingIdx points to the first pending element. + // This implements a fast queue with an amortized + // time of O(1). + + this[kQueue] = [] + this[kRunningIdx] = 0 + this[kPendingIdx] = 0 + + this[kResume] = (sync) => resume(this, sync) + this[kOnError] = (err) => onError(this, err) + } + + get pipelining () { + return this[kPipelining] + } + + set pipelining (value) { + this[kPipelining] = value + this[kResume](true) + } + + get stats () { + return new ClientStats(this) + } + + get [kPending] () { + return this[kQueue].length - this[kPendingIdx] + } + + get [kRunning] () { + return this[kPendingIdx] - this[kRunningIdx] + } + + get [kSize] () { + return this[kQueue].length - this[kRunningIdx] + } + + get [kConnected] () { + return !!this[kHTTPContext] && !this[kConnecting] && !this[kHTTPContext].destroyed + } + + get [kBusy] () { + return Boolean( + this[kHTTPContext]?.busy(null) || + (this[kSize] >= (getPipelining(this) || 1)) || + this[kPending] > 0 + ) + } + + [kConnect] (cb) { + connect(this) + this.once('connect', cb) + } + + [kDispatch] (opts, handler) { + const request = new Request(this[kUrl].origin, opts, handler) + + this[kQueue].push(request) + if (this[kResuming]) { + // Do nothing. + } else if (util.bodyLength(request.body) == null && util.isIterable(request.body)) { + // Wait a tick in case stream/iterator is ended in the same tick. + this[kResuming] = 1 + queueMicrotask(() => resume(this)) + } else { + this[kResume](true) + } + + if (this[kResuming] && this[kNeedDrain] !== 2 && this[kBusy]) { + this[kNeedDrain] = 2 + } + + return this[kNeedDrain] < 2 + } + + [kClose] () { + // TODO: for H2 we need to gracefully flush the remaining enqueued + // request and close each stream. + return new Promise((resolve) => { + if (this[kSize]) { + this[kClosedResolve] = resolve + } else { + resolve(null) + } + }) + } + + [kDestroy] (err) { + return new Promise((resolve) => { + const requests = this[kQueue].splice(this[kPendingIdx]) + for (let i = 0; i < requests.length; i++) { + const request = requests[i] + if (request != null) { + util.errorRequest(this, request, err) + } + } + + const callback = () => { + if (this[kClosedResolve]) { + // TODO (fix): Should we error here with ClientDestroyedError? + this[kClosedResolve]() + this[kClosedResolve] = null + } + resolve(null) + } + + if (this[kHTTPContext]) { + this[kHTTPContext].destroy(err, callback) + this[kHTTPContext] = null + } else { + queueMicrotask(callback) + } + + this[kResume]() + }) + } +} + +function onError (client, err) { + if ( + client[kRunning] === 0 && + err.code !== 'UND_ERR_INFO' && + err.code !== 'UND_ERR_SOCKET' + ) { + // Error is not caused by running request and not a recoverable + // socket error. + + assert(client[kPendingIdx] === client[kRunningIdx]) + + const requests = client[kQueue].splice(client[kRunningIdx]) + + for (let i = 0; i < requests.length; i++) { + const request = requests[i] + if (request != null) { + util.errorRequest(client, request, err) + } + } + assert(client[kSize] === 0) + } +} + +/** + * @param {Client} client + * @returns {void} + */ +function connect (client) { + assert(!client[kConnecting]) + assert(!client[kHTTPContext]) + + let { host, hostname, protocol, port } = client[kUrl] + + // Resolve ipv6 + if (hostname[0] === '[') { + const idx = hostname.indexOf(']') + + assert(idx !== -1) + const ip = hostname.substring(1, idx) + + assert(net.isIPv6(ip)) + hostname = ip + } + + client[kConnecting] = true + + if (channels.beforeConnect.hasSubscribers) { + channels.beforeConnect.publish({ + connectParams: { + host, + hostname, + protocol, + port, + version: client[kHTTPContext]?.version, + servername: client[kServerName], + localAddress: client[kLocalAddress] + }, + connector: client[kConnector] + }) + } + + try { + client[kConnector]({ + host, + hostname, + protocol, + port, + servername: client[kServerName], + localAddress: client[kLocalAddress] + }, (err, socket) => { + if (err) { + handleConnectError(client, err, { host, hostname, protocol, port }) + client[kResume]() + return + } + + if (client.destroyed) { + util.destroy(socket.on('error', noop), new ClientDestroyedError()) + client[kResume]() + return + } + + assert(socket) + + try { + client[kHTTPContext] = socket.alpnProtocol === 'h2' + ? connectH2(client, socket) + : connectH1(client, socket) + } catch (err) { + socket.destroy().on('error', noop) + handleConnectError(client, err, { host, hostname, protocol, port }) + client[kResume]() + return + } + + client[kConnecting] = false + + socket[kCounter] = 0 + socket[kMaxRequests] = client[kMaxRequests] + socket[kClient] = client + socket[kError] = null + + if (channels.connected.hasSubscribers) { + channels.connected.publish({ + connectParams: { + host, + hostname, + protocol, + port, + version: client[kHTTPContext]?.version, + servername: client[kServerName], + localAddress: client[kLocalAddress] + }, + connector: client[kConnector], + socket + }) + } + + client.emit('connect', client[kUrl], [client]) + client[kResume]() + }) + } catch (err) { + handleConnectError(client, err, { host, hostname, protocol, port }) + client[kResume]() + } +} + +function handleConnectError (client, err, { host, hostname, protocol, port }) { + if (client.destroyed) { + return + } + + client[kConnecting] = false + + if (channels.connectError.hasSubscribers) { + channels.connectError.publish({ + connectParams: { + host, + hostname, + protocol, + port, + version: client[kHTTPContext]?.version, + servername: client[kServerName], + localAddress: client[kLocalAddress] + }, + connector: client[kConnector], + error: err + }) + } + + if (err.code === 'ERR_TLS_CERT_ALTNAME_INVALID') { + assert(client[kRunning] === 0) + while (client[kPending] > 0 && client[kQueue][client[kPendingIdx]].servername === client[kServerName]) { + const request = client[kQueue][client[kPendingIdx]++] + util.errorRequest(client, request, err) + } + } else { + onError(client, err) + } + + client.emit('connectionError', client[kUrl], [client], err) +} + +function emitDrain (client) { + client[kNeedDrain] = 0 + client.emit('drain', client[kUrl], [client]) +} + +function resume (client, sync) { + if (client[kResuming] === 2) { + return + } + + client[kResuming] = 2 + + _resume(client, sync) + client[kResuming] = 0 + + if (client[kRunningIdx] > 256) { + client[kQueue].splice(0, client[kRunningIdx]) + client[kPendingIdx] -= client[kRunningIdx] + client[kRunningIdx] = 0 + } +} + +function _resume (client, sync) { + while (true) { + if (client.destroyed) { + assert(client[kPending] === 0) + return + } + + if (client[kClosedResolve] && !client[kSize]) { + client[kClosedResolve]() + client[kClosedResolve] = null + return + } + + if (client[kHTTPContext]) { + client[kHTTPContext].resume() + } + + if (client[kBusy]) { + client[kNeedDrain] = 2 + } else if (client[kNeedDrain] === 2) { + if (sync) { + client[kNeedDrain] = 1 + queueMicrotask(() => emitDrain(client)) + } else { + emitDrain(client) + } + continue + } + + if (client[kPending] === 0) { + return + } + + if (client[kRunning] >= (getPipelining(client) || 1)) { + return + } + + const request = client[kQueue][client[kPendingIdx]] + + if (request === null) { + return + } + + if (client[kUrl].protocol === 'https:' && client[kServerName] !== request.servername) { + if (client[kRunning] > 0) { + return + } + + client[kServerName] = request.servername + client[kHTTPContext]?.destroy(new InformationalError('servername changed'), () => { + client[kHTTPContext] = null + resume(client) + }) + } + + if (client[kConnecting]) { + return + } + + if (!client[kHTTPContext]) { + connect(client) + return + } + + if (client[kHTTPContext].destroyed) { + return + } + + if (client[kHTTPContext].busy(request)) { + return + } + + if (!request.aborted && client[kHTTPContext].write(request)) { + client[kPendingIdx]++ + } else { + client[kQueue].splice(client[kPendingIdx], 1) + } + } +} + +module.exports = Client diff --git a/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/dispatcher/dispatcher-base.js b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/dispatcher/dispatcher-base.js new file mode 100644 index 000000000..feb2571ab --- /dev/null +++ b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/dispatcher/dispatcher-base.js @@ -0,0 +1,184 @@ +'use strict' + +const Dispatcher = require('./dispatcher') +const UnwrapHandler = require('../handler/unwrap-handler') +const { + ClientDestroyedError, + ClientClosedError, + InvalidArgumentError +} = require('../core/errors') +const { kDestroy, kClose, kClosed, kDestroyed, kDispatch } = require('../core/symbols') + +const kOnDestroyed = Symbol('onDestroyed') +const kOnClosed = Symbol('onClosed') +const kWebSocketOptions = Symbol('webSocketOptions') + +class DispatcherBase extends Dispatcher { + /** @type {boolean} */ + [kDestroyed] = false; + + /** @type {Array|null} */ + [kOnClosed] = null + + /** + * @param {import('../../types/dispatcher').DispatcherOptions} [opts] + */ + constructor (opts) { + super() + this[kWebSocketOptions] = opts?.webSocket ?? {} + } + + /** + * @returns {import('../../types/dispatcher').WebSocketOptions} + */ + get webSocketOptions () { + return { + maxFragments: this[kWebSocketOptions].maxFragments ?? 131072, + maxPayloadSize: this[kWebSocketOptions].maxPayloadSize ?? 128 * 1024 * 1024 // 128 MB default + } + } + + /** @returns {boolean} */ + get destroyed () { + return this[kDestroyed] + } + + /** @returns {boolean} */ + get closed () { + return this[kClosed] + } + + close (callback) { + if (callback === undefined) { + return new Promise((resolve, reject) => { + this.close((err, data) => { + return err ? reject(err) : resolve(data) + }) + }) + } + + if (typeof callback !== 'function') { + throw new InvalidArgumentError('invalid callback') + } + + if (this[kDestroyed]) { + const err = new ClientDestroyedError() + queueMicrotask(() => callback(err, null)) + return + } + + if (this[kClosed]) { + if (this[kOnClosed]) { + this[kOnClosed].push(callback) + } else { + queueMicrotask(() => callback(null, null)) + } + return + } + + this[kClosed] = true + this[kOnClosed] ??= [] + this[kOnClosed].push(callback) + + const onClosed = () => { + const callbacks = this[kOnClosed] + this[kOnClosed] = null + for (let i = 0; i < callbacks.length; i++) { + callbacks[i](null, null) + } + } + + // Should not error. + this[kClose]() + .then(() => this.destroy()) + .then(() => queueMicrotask(onClosed)) + } + + destroy (err, callback) { + if (typeof err === 'function') { + callback = err + err = null + } + + if (callback === undefined) { + return new Promise((resolve, reject) => { + this.destroy(err, (err, data) => { + return err ? reject(err) : resolve(data) + }) + }) + } + + if (typeof callback !== 'function') { + throw new InvalidArgumentError('invalid callback') + } + + if (this[kDestroyed]) { + if (this[kOnDestroyed]) { + this[kOnDestroyed].push(callback) + } else { + queueMicrotask(() => callback(null, null)) + } + return + } + + if (!err) { + err = new ClientDestroyedError() + } + + this[kDestroyed] = true + this[kOnDestroyed] ??= [] + this[kOnDestroyed].push(callback) + + const onDestroyed = () => { + const callbacks = this[kOnDestroyed] + this[kOnDestroyed] = null + for (let i = 0; i < callbacks.length; i++) { + callbacks[i](null, null) + } + } + + // Should not error. + this[kDestroy](err) + .then(() => queueMicrotask(onDestroyed)) + } + + dispatch (opts, handler) { + if (!handler || typeof handler !== 'object') { + throw new InvalidArgumentError('handler must be an object') + } + + handler = UnwrapHandler.unwrap(handler) + + try { + if (!opts || typeof opts !== 'object') { + throw new InvalidArgumentError('opts must be an object.') + } + + if (this[kDestroyed] || this[kOnDestroyed]) { + throw new ClientDestroyedError() + } + + if (this[kClosed]) { + throw new ClientClosedError() + } + + return this[kDispatch](opts, handler) + } catch (err) { + if (typeof handler.onError !== 'function') { + throw err + } + + handler.onError(err) + + return false + } + } +} + +module.exports = DispatcherBase diff --git a/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/dispatcher/dispatcher.js b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/dispatcher/dispatcher.js new file mode 100644 index 000000000..824dfb6d8 --- /dev/null +++ b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/dispatcher/dispatcher.js @@ -0,0 +1,48 @@ +'use strict' +const EventEmitter = require('node:events') +const WrapHandler = require('../handler/wrap-handler') + +const wrapInterceptor = (dispatch) => (opts, handler) => dispatch(opts, WrapHandler.wrap(handler)) + +class Dispatcher extends EventEmitter { + dispatch () { + throw new Error('not implemented') + } + + close () { + throw new Error('not implemented') + } + + destroy () { + throw new Error('not implemented') + } + + compose (...args) { + // So we handle [interceptor1, interceptor2] or interceptor1, interceptor2, ... + const interceptors = Array.isArray(args[0]) ? args[0] : args + let dispatch = this.dispatch.bind(this) + + for (const interceptor of interceptors) { + if (interceptor == null) { + continue + } + + if (typeof interceptor !== 'function') { + throw new TypeError(`invalid interceptor, expected function received ${typeof interceptor}`) + } + + dispatch = interceptor(dispatch) + dispatch = wrapInterceptor(dispatch) + + if (dispatch == null || typeof dispatch !== 'function' || dispatch.length !== 2) { + throw new TypeError('invalid interceptor') + } + } + + return new Proxy(this, { + get: (target, key) => key === 'dispatch' ? dispatch : target[key] + }) + } +} + +module.exports = Dispatcher diff --git a/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/dispatcher/env-http-proxy-agent.js b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/dispatcher/env-http-proxy-agent.js new file mode 100644 index 000000000..f88437f19 --- /dev/null +++ b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/dispatcher/env-http-proxy-agent.js @@ -0,0 +1,146 @@ +'use strict' + +const DispatcherBase = require('./dispatcher-base') +const { kClose, kDestroy, kClosed, kDestroyed, kDispatch, kNoProxyAgent, kHttpProxyAgent, kHttpsProxyAgent } = require('../core/symbols') +const ProxyAgent = require('./proxy-agent') +const Agent = require('./agent') + +const DEFAULT_PORTS = { + 'http:': 80, + 'https:': 443 +} + +class EnvHttpProxyAgent extends DispatcherBase { + #noProxyValue = null + #noProxyEntries = null + #opts = null + + constructor (opts = {}) { + super() + this.#opts = opts + + const { httpProxy, httpsProxy, noProxy, ...agentOpts } = opts + + this[kNoProxyAgent] = new Agent(agentOpts) + + const HTTP_PROXY = httpProxy ?? process.env.http_proxy ?? process.env.HTTP_PROXY + if (HTTP_PROXY) { + this[kHttpProxyAgent] = new ProxyAgent({ ...agentOpts, uri: HTTP_PROXY }) + } else { + this[kHttpProxyAgent] = this[kNoProxyAgent] + } + + const HTTPS_PROXY = httpsProxy ?? process.env.https_proxy ?? process.env.HTTPS_PROXY + if (HTTPS_PROXY) { + this[kHttpsProxyAgent] = new ProxyAgent({ ...agentOpts, uri: HTTPS_PROXY }) + } else { + this[kHttpsProxyAgent] = this[kHttpProxyAgent] + } + + this.#parseNoProxy() + } + + [kDispatch] (opts, handler) { + const url = new URL(opts.origin) + const agent = this.#getProxyAgentForUrl(url) + return agent.dispatch(opts, handler) + } + + [kClose] () { + return Promise.all([ + this[kNoProxyAgent].close(), + !this[kHttpProxyAgent][kClosed] && this[kHttpProxyAgent].close(), + !this[kHttpsProxyAgent][kClosed] && this[kHttpsProxyAgent].close() + ]) + } + + [kDestroy] (err) { + return Promise.all([ + this[kNoProxyAgent].destroy(err), + !this[kHttpProxyAgent][kDestroyed] && this[kHttpProxyAgent].destroy(err), + !this[kHttpsProxyAgent][kDestroyed] && this[kHttpsProxyAgent].destroy(err) + ]) + } + + #getProxyAgentForUrl (url) { + let { protocol, host: hostname, port } = url + + // Stripping ports in this way instead of using parsedUrl.hostname to make + // sure that the brackets around IPv6 addresses are kept. + hostname = hostname.replace(/:\d*$/, '').toLowerCase() + port = Number.parseInt(port, 10) || DEFAULT_PORTS[protocol] || 0 + if (!this.#shouldProxy(hostname, port)) { + return this[kNoProxyAgent] + } + if (protocol === 'https:') { + return this[kHttpsProxyAgent] + } + return this[kHttpProxyAgent] + } + + #shouldProxy (hostname, port) { + if (this.#noProxyChanged) { + this.#parseNoProxy() + } + + if (this.#noProxyEntries.length === 0) { + return true // Always proxy if NO_PROXY is not set or empty. + } + if (this.#noProxyValue === '*') { + return false // Never proxy if wildcard is set. + } + + for (let i = 0; i < this.#noProxyEntries.length; i++) { + const entry = this.#noProxyEntries[i] + if (entry.port && entry.port !== port) { + continue // Skip if ports don't match. + } + // Don't proxy if the hostname is equal with the no_proxy host. + if (hostname === entry.hostname) { + return false + } + // Don't proxy if the hostname is the subdomain of the no_proxy host. + // Reference - https://github.com/denoland/deno/blob/6fbce91e40cc07fc6da74068e5cc56fdd40f7b4c/ext/fetch/proxy.rs#L485 + if (hostname.slice(-(entry.hostname.length + 1)) === `.${entry.hostname}`) { + return false + } + } + + return true + } + + #parseNoProxy () { + const noProxyValue = this.#opts.noProxy ?? this.#noProxyEnv + const noProxySplit = noProxyValue.split(/[,\s]/) + const noProxyEntries = [] + + for (let i = 0; i < noProxySplit.length; i++) { + const entry = noProxySplit[i] + if (!entry) { + continue + } + const parsed = entry.match(/^(.+):(\d+)$/) + noProxyEntries.push({ + // strip leading dot or asterisk with dot + hostname: (parsed ? parsed[1] : entry).replace(/^\*?\./, '').toLowerCase(), + port: parsed ? Number.parseInt(parsed[2], 10) : 0 + }) + } + + this.#noProxyValue = noProxyValue + this.#noProxyEntries = noProxyEntries + } + + get #noProxyChanged () { + if (this.#opts.noProxy !== undefined) { + return false + } + return this.#noProxyValue !== this.#noProxyEnv + } + + get #noProxyEnv () { + return process.env.no_proxy ?? process.env.NO_PROXY ?? '' + } +} + +module.exports = EnvHttpProxyAgent diff --git a/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/dispatcher/fixed-queue.js b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/dispatcher/fixed-queue.js new file mode 100644 index 000000000..f918849c4 --- /dev/null +++ b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/dispatcher/fixed-queue.js @@ -0,0 +1,135 @@ +'use strict' + +// Extracted from node/lib/internal/fixed_queue.js + +// Currently optimal queue size, tested on V8 6.0 - 6.6. Must be power of two. +const kSize = 2048 +const kMask = kSize - 1 + +// The FixedQueue is implemented as a singly-linked list of fixed-size +// circular buffers. It looks something like this: +// +// head tail +// | | +// v v +// +-----------+ <-----\ +-----------+ <------\ +-----------+ +// | [null] | \----- | next | \------- | next | +// +-----------+ +-----------+ +-----------+ +// | item | <-- bottom | item | <-- bottom | undefined | +// | item | | item | | undefined | +// | item | | item | | undefined | +// | item | | item | | undefined | +// | item | | item | bottom --> | item | +// | item | | item | | item | +// | ... | | ... | | ... | +// | item | | item | | item | +// | item | | item | | item | +// | undefined | <-- top | item | | item | +// | undefined | | item | | item | +// | undefined | | undefined | <-- top top --> | undefined | +// +-----------+ +-----------+ +-----------+ +// +// Or, if there is only one circular buffer, it looks something +// like either of these: +// +// head tail head tail +// | | | | +// v v v v +// +-----------+ +-----------+ +// | [null] | | [null] | +// +-----------+ +-----------+ +// | undefined | | item | +// | undefined | | item | +// | item | <-- bottom top --> | undefined | +// | item | | undefined | +// | undefined | <-- top bottom --> | item | +// | undefined | | item | +// +-----------+ +-----------+ +// +// Adding a value means moving `top` forward by one, removing means +// moving `bottom` forward by one. After reaching the end, the queue +// wraps around. +// +// When `top === bottom` the current queue is empty and when +// `top + 1 === bottom` it's full. This wastes a single space of storage +// but allows much quicker checks. + +/** + * @type {FixedCircularBuffer} + * @template T + */ +class FixedCircularBuffer { + /** @type {number} */ + bottom = 0 + /** @type {number} */ + top = 0 + /** @type {Array} */ + list = new Array(kSize).fill(undefined) + /** @type {T|null} */ + next = null + + /** @returns {boolean} */ + isEmpty () { + return this.top === this.bottom + } + + /** @returns {boolean} */ + isFull () { + return ((this.top + 1) & kMask) === this.bottom + } + + /** + * @param {T} data + * @returns {void} + */ + push (data) { + this.list[this.top] = data + this.top = (this.top + 1) & kMask + } + + /** @returns {T|null} */ + shift () { + const nextItem = this.list[this.bottom] + if (nextItem === undefined) { return null } + this.list[this.bottom] = undefined + this.bottom = (this.bottom + 1) & kMask + return nextItem + } +} + +/** + * @template T + */ +module.exports = class FixedQueue { + constructor () { + /** @type {FixedCircularBuffer} */ + this.head = this.tail = new FixedCircularBuffer() + } + + /** @returns {boolean} */ + isEmpty () { + return this.head.isEmpty() + } + + /** @param {T} data */ + push (data) { + if (this.head.isFull()) { + // Head is full: Creates a new queue, sets the old queue's `.next` to it, + // and sets it as the new main queue. + this.head = this.head.next = new FixedCircularBuffer() + } + this.head.push(data) + } + + /** @returns {T|null} */ + shift () { + const tail = this.tail + const next = tail.shift() + if (tail.isEmpty() && tail.next !== null) { + // If there is another queue, it forms the new tail. + this.tail = tail.next + tail.next = null + } + return next + } +} diff --git a/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/dispatcher/h2c-client.js b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/dispatcher/h2c-client.js new file mode 100644 index 000000000..5fe7e778b --- /dev/null +++ b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/dispatcher/h2c-client.js @@ -0,0 +1,51 @@ +'use strict' + +const { InvalidArgumentError } = require('../core/errors') +const Client = require('./client') + +class H2CClient extends Client { + constructor (origin, clientOpts) { + if (typeof origin === 'string') { + origin = new URL(origin) + } + + if (origin.protocol !== 'http:') { + throw new InvalidArgumentError( + 'h2c-client: Only h2c protocol is supported' + ) + } + + const { maxConcurrentStreams, pipelining, ...opts } = + clientOpts ?? {} + let defaultMaxConcurrentStreams = 100 + let defaultPipelining = 100 + + if ( + maxConcurrentStreams != null && + Number.isInteger(maxConcurrentStreams) && + maxConcurrentStreams > 0 + ) { + defaultMaxConcurrentStreams = maxConcurrentStreams + } + + if (pipelining != null && Number.isInteger(pipelining) && pipelining > 0) { + defaultPipelining = pipelining + } + + if (defaultPipelining > defaultMaxConcurrentStreams) { + throw new InvalidArgumentError( + 'h2c-client: pipelining cannot be greater than maxConcurrentStreams' + ) + } + + super(origin, { + ...opts, + maxConcurrentStreams: defaultMaxConcurrentStreams, + pipelining: defaultPipelining, + allowH2: true, + useH2c: true + }) + } +} + +module.exports = H2CClient diff --git a/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/dispatcher/pool-base.js b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/dispatcher/pool-base.js new file mode 100644 index 000000000..6c1f23887 --- /dev/null +++ b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/dispatcher/pool-base.js @@ -0,0 +1,214 @@ +'use strict' + +const { PoolStats } = require('../util/stats.js') +const DispatcherBase = require('./dispatcher-base') +const FixedQueue = require('./fixed-queue') +const { kConnected, kSize, kRunning, kPending, kQueued, kBusy, kFree, kUrl, kClose, kDestroy, kDispatch } = require('../core/symbols') + +const kClients = Symbol('clients') +const kNeedDrain = Symbol('needDrain') +const kQueue = Symbol('queue') +const kClosedResolve = Symbol('closed resolve') +const kOnDrain = Symbol('onDrain') +const kOnConnect = Symbol('onConnect') +const kOnDisconnect = Symbol('onDisconnect') +const kOnConnectionError = Symbol('onConnectionError') +const kGetDispatcher = Symbol('get dispatcher') +const kAddClient = Symbol('add client') +const kRemoveClient = Symbol('remove client') + +class PoolBase extends DispatcherBase { + [kQueue] = new FixedQueue(); + + [kQueued] = 0; + + [kClients] = []; + + [kNeedDrain] = false; + + [kOnDrain] (client, origin, targets) { + const queue = this[kQueue] + + let needDrain = false + + while (!needDrain) { + const item = queue.shift() + if (!item) { + break + } + this[kQueued]-- + needDrain = !client.dispatch(item.opts, item.handler) + } + + client[kNeedDrain] = needDrain + + if (!needDrain && this[kNeedDrain]) { + this[kNeedDrain] = false + this.emit('drain', origin, [this, ...targets]) + } + + if (this[kClosedResolve] && queue.isEmpty()) { + const closeAll = [] + for (let i = 0; i < this[kClients].length; i++) { + const client = this[kClients][i] + if (!client.destroyed) { + closeAll.push(client.close()) + } + } + return Promise.all(closeAll) + .then(this[kClosedResolve]) + } + } + + [kOnConnect] = (origin, targets) => { + this.emit('connect', origin, [this, ...targets]) + }; + + [kOnDisconnect] = (origin, targets, err) => { + this.emit('disconnect', origin, [this, ...targets], err) + }; + + [kOnConnectionError] = (origin, targets, err) => { + this.emit('connectionError', origin, [this, ...targets], err) + } + + get [kBusy] () { + return this[kNeedDrain] + } + + get [kConnected] () { + let ret = 0 + for (const { [kConnected]: connected } of this[kClients]) { + ret += connected + } + return ret + } + + get [kFree] () { + let ret = 0 + for (const { [kConnected]: connected, [kNeedDrain]: needDrain } of this[kClients]) { + ret += connected && !needDrain + } + return ret + } + + get [kPending] () { + let ret = this[kQueued] + for (const { [kPending]: pending } of this[kClients]) { + ret += pending + } + return ret + } + + get [kRunning] () { + let ret = 0 + for (const { [kRunning]: running } of this[kClients]) { + ret += running + } + return ret + } + + get [kSize] () { + let ret = this[kQueued] + for (const { [kSize]: size } of this[kClients]) { + ret += size + } + return ret + } + + get stats () { + return new PoolStats(this) + } + + [kClose] () { + if (this[kQueue].isEmpty()) { + const closeAll = [] + for (let i = 0; i < this[kClients].length; i++) { + const client = this[kClients][i] + if (!client.destroyed) { + closeAll.push(client.close()) + } + } + return Promise.all(closeAll) + } else { + return new Promise((resolve) => { + this[kClosedResolve] = resolve + }) + } + } + + [kDestroy] (err) { + while (true) { + const item = this[kQueue].shift() + if (!item) { + break + } + item.handler.onError(err) + } + + const destroyAll = new Array(this[kClients].length) + for (let i = 0; i < this[kClients].length; i++) { + destroyAll[i] = this[kClients][i].destroy(err) + } + return Promise.all(destroyAll) + } + + [kDispatch] (opts, handler) { + const dispatcher = this[kGetDispatcher]() + + if (!dispatcher) { + this[kNeedDrain] = true + this[kQueue].push({ opts, handler }) + this[kQueued]++ + } else if (!dispatcher.dispatch(opts, handler)) { + dispatcher[kNeedDrain] = true + this[kNeedDrain] = !this[kGetDispatcher]() + } + + return !this[kNeedDrain] + } + + [kAddClient] (client) { + client + .on('drain', this[kOnDrain].bind(this, client)) + .on('connect', this[kOnConnect]) + .on('disconnect', this[kOnDisconnect]) + .on('connectionError', this[kOnConnectionError]) + + this[kClients].push(client) + + if (this[kNeedDrain]) { + queueMicrotask(() => { + if (this[kNeedDrain]) { + this[kOnDrain](client, client[kUrl], [client, this]) + } + }) + } + + return this + } + + [kRemoveClient] (client) { + client.close(() => { + const idx = this[kClients].indexOf(client) + if (idx !== -1) { + this[kClients].splice(idx, 1) + } + }) + + this[kNeedDrain] = this[kClients].some(dispatcher => ( + !dispatcher[kNeedDrain] && + dispatcher.closed !== true && + dispatcher.destroyed !== true + )) + } +} + +module.exports = { + PoolBase, + kClients, + kNeedDrain, + kAddClient, + kRemoveClient, + kGetDispatcher +} diff --git a/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/dispatcher/pool.js b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/dispatcher/pool.js new file mode 100644 index 000000000..0c5dbe44d --- /dev/null +++ b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/dispatcher/pool.js @@ -0,0 +1,118 @@ +'use strict' + +const { + PoolBase, + kClients, + kNeedDrain, + kAddClient, + kGetDispatcher, + kRemoveClient +} = require('./pool-base') +const Client = require('./client') +const { + InvalidArgumentError +} = require('../core/errors') +const util = require('../core/util') +const { kUrl } = require('../core/symbols') +const buildConnector = require('../core/connect') + +const kOptions = Symbol('options') +const kConnections = Symbol('connections') +const kFactory = Symbol('factory') + +function defaultFactory (origin, opts) { + return new Client(origin, opts) +} + +class Pool extends PoolBase { + constructor (origin, { + connections, + factory = defaultFactory, + connect, + connectTimeout, + tls, + maxCachedSessions, + socketPath, + autoSelectFamily, + autoSelectFamilyAttemptTimeout, + allowH2, + clientTtl, + ...options + } = {}) { + if (connections != null && (!Number.isFinite(connections) || connections < 0)) { + throw new InvalidArgumentError('invalid connections') + } + + if (typeof factory !== 'function') { + throw new InvalidArgumentError('factory must be a function.') + } + + if (connect != null && typeof connect !== 'function' && typeof connect !== 'object') { + throw new InvalidArgumentError('connect must be a function or an object') + } + + if (typeof connect !== 'function') { + connect = buildConnector({ + ...tls, + maxCachedSessions, + allowH2, + socketPath, + timeout: connectTimeout, + ...(typeof autoSelectFamily === 'boolean' ? { autoSelectFamily, autoSelectFamilyAttemptTimeout } : undefined), + ...connect + }) + } + + super(options) + + this[kConnections] = connections || null + this[kUrl] = util.parseOrigin(origin) + this[kOptions] = { ...util.deepClone(options), connect, allowH2, clientTtl, socketPath } + this[kOptions].interceptors = options.interceptors + ? { ...options.interceptors } + : undefined + this[kFactory] = factory + + this.on('connect', (origin, targets) => { + if (clientTtl != null && clientTtl > 0) { + for (const target of targets) { + Object.assign(target, { ttl: Date.now() }) + } + } + }) + + this.on('connectionError', (origin, targets, error) => { + // If a connection error occurs, we remove the client from the pool, + // and emit a connectionError event. They will not be re-used. + // Fixes https://github.com/nodejs/undici/issues/3895 + for (const target of targets) { + // Do not use kRemoveClient here, as it will close the client, + // but the client cannot be closed in this state. + const idx = this[kClients].indexOf(target) + if (idx !== -1) { + this[kClients].splice(idx, 1) + } + } + }) + } + + [kGetDispatcher] () { + const clientTtlOption = this[kOptions].clientTtl + for (const client of this[kClients]) { + // check ttl of client and if it's stale, remove it from the pool + if (clientTtlOption != null && clientTtlOption > 0 && client.ttl && ((Date.now() - client.ttl) > clientTtlOption)) { + this[kRemoveClient](client) + } else if (!client[kNeedDrain]) { + return client + } + } + + if (!this[kConnections] || this[kClients].length < this[kConnections]) { + const dispatcher = this[kFactory](this[kUrl], this[kOptions]) + this[kAddClient](dispatcher) + return dispatcher + } + } +} + +module.exports = Pool diff --git a/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/dispatcher/proxy-agent.js b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/dispatcher/proxy-agent.js new file mode 100644 index 000000000..d2a2f23cb --- /dev/null +++ b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/dispatcher/proxy-agent.js @@ -0,0 +1,319 @@ +'use strict' + +const { kProxy, kClose, kDestroy, kDispatch } = require('../core/symbols') +const Agent = require('./agent') +const Pool = require('./pool') +const DispatcherBase = require('./dispatcher-base') +const { InvalidArgumentError, RequestAbortedError, SecureProxyConnectionError } = require('../core/errors') +const buildConnector = require('../core/connect') +const Client = require('./client') +const { channels } = require('../core/diagnostics') +const Socks5ProxyAgent = require('./socks5-proxy-agent') + +const kAgent = Symbol('proxy agent') +const kClient = Symbol('proxy client') +const kProxyHeaders = Symbol('proxy headers') +const kRequestTls = Symbol('request tls settings') +const kProxyTls = Symbol('proxy tls settings') +const kConnectEndpoint = Symbol('connect endpoint function') +const kTunnelProxy = Symbol('tunnel proxy') + +function defaultProtocolPort (protocol) { + return protocol === 'https:' ? 443 : 80 +} + +function defaultFactory (origin, opts) { + return new Pool(origin, opts) +} + +const noop = () => {} + +function defaultAgentFactory (origin, opts) { + if (opts.connections === 1) { + return new Client(origin, opts) + } + return new Pool(origin, opts) +} + +class Http1ProxyWrapper extends DispatcherBase { + #client + + constructor (proxyUrl, { headers = {}, connect, factory }) { + if (!proxyUrl) { + throw new InvalidArgumentError('Proxy URL is mandatory') + } + + super() + + this[kProxyHeaders] = headers + if (factory) { + this.#client = factory(proxyUrl, { connect }) + } else { + this.#client = new Client(proxyUrl, { connect }) + } + } + + [kDispatch] (opts, handler) { + const onHeaders = handler.onHeaders + handler.onHeaders = function (statusCode, data, resume) { + if (statusCode === 407) { + if (typeof handler.onError === 'function') { + handler.onError(new InvalidArgumentError('Proxy Authentication Required (407)')) + } + return + } + if (onHeaders) onHeaders.call(this, statusCode, data, resume) + } + + // Rewrite request as an HTTP1 Proxy request, without tunneling. + const { + origin, + path = '/', + headers = {} + } = opts + + opts.path = origin + path + + if (!('host' in headers) && !('Host' in headers)) { + const { host } = new URL(origin) + headers.host = host + } + opts.headers = { ...this[kProxyHeaders], ...headers } + + return this.#client[kDispatch](opts, handler) + } + + [kClose] () { + return this.#client.close() + } + + [kDestroy] (err) { + return this.#client.destroy(err) + } +} + +class ProxyAgent extends DispatcherBase { + constructor (opts) { + if (!opts || (typeof opts === 'object' && !(opts instanceof URL) && !opts.uri)) { + throw new InvalidArgumentError('Proxy uri is mandatory') + } + + const { clientFactory = defaultFactory } = opts + if (typeof clientFactory !== 'function') { + throw new InvalidArgumentError('Proxy opts.clientFactory must be a function.') + } + + const { proxyTunnel = true } = opts + + super() + + const url = this.#getUrl(opts) + const { href, origin, port, protocol, username, password, hostname: proxyHostname } = url + + this[kProxy] = { uri: href, protocol } + this[kRequestTls] = opts.requestTls + this[kProxyTls] = opts.proxyTls + this[kProxyHeaders] = opts.headers || {} + this[kTunnelProxy] = proxyTunnel + + if (opts.auth && opts.token) { + throw new InvalidArgumentError('opts.auth cannot be used in combination with opts.token') + } else if (opts.auth) { + /* @deprecated in favour of opts.token */ + this[kProxyHeaders]['proxy-authorization'] = `Basic ${opts.auth}` + } else if (opts.token) { + this[kProxyHeaders]['proxy-authorization'] = opts.token + } else if (username && password) { + this[kProxyHeaders]['proxy-authorization'] = `Basic ${Buffer.from(`${decodeURIComponent(username)}:${decodeURIComponent(password)}`).toString('base64')}` + } + + const connect = buildConnector({ ...opts.proxyTls }) + this[kConnectEndpoint] = buildConnector({ ...opts.requestTls }) + + const agentFactory = opts.factory || defaultAgentFactory + const factory = (origin, options) => { + const { protocol } = new URL(origin) + + // Handle SOCKS5 proxy + if (this[kProxy].protocol === 'socks5:' || this[kProxy].protocol === 'socks:') { + return new Socks5ProxyAgent(this[kProxy].uri, { + headers: this[kProxyHeaders], + connect, + factory: agentFactory, + username: opts.username || username, + password: opts.password || password, + proxyTls: opts.proxyTls, + requestTls: opts.requestTls + }) + } + + if (!this[kTunnelProxy] && protocol === 'http:' && this[kProxy].protocol === 'http:') { + return new Http1ProxyWrapper(this[kProxy].uri, { + headers: this[kProxyHeaders], + connect, + factory: agentFactory + }) + } + return agentFactory(origin, options) + } + + // For SOCKS5 proxies, we don't need a client to the proxy itself + // The SOCKS5 connection is handled within Socks5ProxyAgent + if (protocol === 'socks5:' || protocol === 'socks:') { + this[kClient] = null + } else { + this[kClient] = clientFactory(url, { connect }) + } + + this[kAgent] = new Agent({ + ...opts, + factory, + connect: async (opts, callback) => { + // SOCKS5 proxies handle their own connections via Socks5ProxyAgent, + // so this connect function should never be called for them. + if (!this[kClient]) { + callback(new InvalidArgumentError('Cannot establish tunnel connection without a proxy client')) + return + } + + let requestedPath = opts.host + if (!opts.port) { + requestedPath += `:${defaultProtocolPort(opts.protocol)}` + } + try { + const connectParams = { + origin, + port, + path: requestedPath, + signal: opts.signal, + headers: { + ...this[kProxyHeaders], + host: opts.host, + ...(opts.connections == null || opts.connections > 0 ? { 'proxy-connection': 'keep-alive' } : {}) + }, + servername: this[kProxyTls]?.servername || proxyHostname + } + const { socket, statusCode } = await this[kClient].connect(connectParams) + if (statusCode !== 200) { + socket.on('error', noop).destroy() + callback(new RequestAbortedError(`Proxy response (${statusCode}) !== 200 when HTTP Tunneling`)) + return + } + + if (channels.proxyConnected.hasSubscribers) { + channels.proxyConnected.publish({ + socket, + connectParams + }) + } + + if (opts.protocol !== 'https:') { + callback(null, socket) + return + } + let servername + if (this[kRequestTls]) { + servername = this[kRequestTls].servername + } else { + servername = opts.servername + } + this[kConnectEndpoint]({ ...opts, servername, httpSocket: socket }, callback) + } catch (err) { + if (err.code === 'ERR_TLS_CERT_ALTNAME_INVALID') { + // Throw a custom error to avoid loop in client.js#connect + callback(new SecureProxyConnectionError(err)) + } else { + callback(err) + } + } + } + }) + } + + dispatch (opts, handler) { + const headers = buildHeaders(opts.headers) + throwIfProxyAuthIsSent(headers) + + if (headers && !('host' in headers) && !('Host' in headers)) { + const { host } = new URL(opts.origin) + headers.host = host + } + + return this[kAgent].dispatch( + { + ...opts, + headers + }, + handler + ) + } + + /** + * @param {import('../../types/proxy-agent').ProxyAgent.Options | string | URL} opts + * @returns {URL} + */ + #getUrl (opts) { + if (typeof opts === 'string') { + return new URL(opts) + } else if (opts instanceof URL) { + return opts + } else { + return new URL(opts.uri) + } + } + + [kClose] () { + const promises = [this[kAgent].close()] + if (this[kClient]) { + promises.push(this[kClient].close()) + } + return Promise.all(promises) + } + + [kDestroy] () { + const promises = [this[kAgent].destroy()] + if (this[kClient]) { + promises.push(this[kClient].destroy()) + } + return Promise.all(promises) + } +} + +/** + * @param {string[] | Record} headers + * @returns {Record} + */ +function buildHeaders (headers) { + // When using undici.fetch, the headers list is stored + // as an array. + if (Array.isArray(headers)) { + /** @type {Record} */ + const headersPair = {} + + for (let i = 0; i < headers.length; i += 2) { + headersPair[headers[i]] = headers[i + 1] + } + + return headersPair + } + + return headers +} + +/** + * @param {Record} headers + * + * Previous versions of ProxyAgent suggests the Proxy-Authorization in request headers + * Nevertheless, it was changed and to avoid a security vulnerability by end users + * this check was created. + * It should be removed in the next major version for performance reasons + */ +function throwIfProxyAuthIsSent (headers) { + const existProxyAuth = headers && Object.keys(headers) + .find((key) => key.toLowerCase() === 'proxy-authorization') + if (existProxyAuth) { + throw new InvalidArgumentError('Proxy-Authorization should be sent in ProxyAgent constructor') + } +} + +module.exports = ProxyAgent diff --git a/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/dispatcher/retry-agent.js b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/dispatcher/retry-agent.js new file mode 100644 index 000000000..0c2120d6f --- /dev/null +++ b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/dispatcher/retry-agent.js @@ -0,0 +1,35 @@ +'use strict' + +const Dispatcher = require('./dispatcher') +const RetryHandler = require('../handler/retry-handler') + +class RetryAgent extends Dispatcher { + #agent = null + #options = null + constructor (agent, options = {}) { + super(options) + this.#agent = agent + this.#options = options + } + + dispatch (opts, handler) { + const retry = new RetryHandler({ + ...opts, + retryOptions: this.#options + }, { + dispatch: this.#agent.dispatch.bind(this.#agent), + handler + }) + return this.#agent.dispatch(opts, retry) + } + + close () { + return this.#agent.close() + } + + destroy () { + return this.#agent.destroy() + } +} + +module.exports = RetryAgent diff --git a/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/dispatcher/round-robin-pool.js b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/dispatcher/round-robin-pool.js new file mode 100644 index 000000000..dd0653510 --- /dev/null +++ b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/dispatcher/round-robin-pool.js @@ -0,0 +1,137 @@ +'use strict' + +const { + PoolBase, + kClients, + kNeedDrain, + kAddClient, + kGetDispatcher, + kRemoveClient +} = require('./pool-base') +const Client = require('./client') +const { + InvalidArgumentError +} = require('../core/errors') +const util = require('../core/util') +const { kUrl } = require('../core/symbols') +const buildConnector = require('../core/connect') + +const kOptions = Symbol('options') +const kConnections = Symbol('connections') +const kFactory = Symbol('factory') +const kIndex = Symbol('index') + +function defaultFactory (origin, opts) { + return new Client(origin, opts) +} + +class RoundRobinPool extends PoolBase { + constructor (origin, { + connections, + factory = defaultFactory, + connect, + connectTimeout, + tls, + maxCachedSessions, + socketPath, + autoSelectFamily, + autoSelectFamilyAttemptTimeout, + allowH2, + clientTtl, + ...options + } = {}) { + if (connections != null && (!Number.isFinite(connections) || connections < 0)) { + throw new InvalidArgumentError('invalid connections') + } + + if (typeof factory !== 'function') { + throw new InvalidArgumentError('factory must be a function.') + } + + if (connect != null && typeof connect !== 'function' && typeof connect !== 'object') { + throw new InvalidArgumentError('connect must be a function or an object') + } + + if (typeof connect !== 'function') { + connect = buildConnector({ + ...tls, + maxCachedSessions, + allowH2, + socketPath, + timeout: connectTimeout, + ...(typeof autoSelectFamily === 'boolean' ? { autoSelectFamily, autoSelectFamilyAttemptTimeout } : undefined), + ...connect + }) + } + + super() + + this[kConnections] = connections || null + this[kUrl] = util.parseOrigin(origin) + this[kOptions] = { ...util.deepClone(options), connect, allowH2, clientTtl, socketPath } + this[kOptions].interceptors = options.interceptors + ? { ...options.interceptors } + : undefined + this[kFactory] = factory + this[kIndex] = -1 + + this.on('connect', (origin, targets) => { + if (clientTtl != null && clientTtl > 0) { + for (const target of targets) { + Object.assign(target, { ttl: Date.now() }) + } + } + }) + + this.on('connectionError', (origin, targets, error) => { + for (const target of targets) { + const idx = this[kClients].indexOf(target) + if (idx !== -1) { + this[kClients].splice(idx, 1) + } + } + }) + } + + [kGetDispatcher] () { + const clientTtlOption = this[kOptions].clientTtl + const clientsLength = this[kClients].length + + // If we have no clients yet, create one + if (clientsLength === 0) { + const dispatcher = this[kFactory](this[kUrl], this[kOptions]) + this[kAddClient](dispatcher) + return dispatcher + } + + // Round-robin through existing clients + let checked = 0 + while (checked < clientsLength) { + this[kIndex] = (this[kIndex] + 1) % clientsLength + const client = this[kClients][this[kIndex]] + + // Check if client is stale (TTL expired) + if (clientTtlOption != null && clientTtlOption > 0 && client.ttl && ((Date.now() - client.ttl) > clientTtlOption)) { + this[kRemoveClient](client) + checked++ + continue + } + + // Return client if it's not draining + if (!client[kNeedDrain]) { + return client + } + + checked++ + } + + // All clients are busy, create a new one if we haven't reached the limit + if (!this[kConnections] || clientsLength < this[kConnections]) { + const dispatcher = this[kFactory](this[kUrl], this[kOptions]) + this[kAddClient](dispatcher) + return dispatcher + } + } +} + +module.exports = RoundRobinPool diff --git a/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/dispatcher/socks5-proxy-agent.js b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/dispatcher/socks5-proxy-agent.js new file mode 100644 index 000000000..544d3025e --- /dev/null +++ b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/dispatcher/socks5-proxy-agent.js @@ -0,0 +1,260 @@ +'use strict' + +const { URL } = require('node:url') + +let tls // include tls conditionally since it is not always available +const DispatcherBase = require('./dispatcher-base') +const { InvalidArgumentError } = require('../core/errors') +const { Socks5Client, STATES } = require('../core/socks5-client') +const { kDispatch, kClose, kDestroy } = require('../core/symbols') +const Pool = require('./pool') +const buildConnector = require('../core/connect') +const { debuglog } = require('node:util') + +const debug = debuglog('undici:socks5-proxy') + +const kProxyUrl = Symbol('proxy url') +const kProxyHeaders = Symbol('proxy headers') +const kProxyAuth = Symbol('proxy auth') +const kProxyProtocol = Symbol('proxy protocol') +const kPools = Symbol('pools') +const kConnector = Symbol('connector') +const kRequestTls = Symbol('request tls settings') + +// Static flag to ensure warning is only emitted once per process +let experimentalWarningEmitted = false + +/** + * SOCKS5 proxy agent for dispatching requests through a SOCKS5 proxy + */ +class Socks5ProxyAgent extends DispatcherBase { + constructor (proxyUrl, options = {}) { + super() + + // Emit experimental warning only once + if (!experimentalWarningEmitted) { + process.emitWarning( + 'SOCKS5 proxy support is experimental and subject to change', + 'ExperimentalWarning' + ) + experimentalWarningEmitted = true + } + + if (!proxyUrl) { + throw new InvalidArgumentError('Proxy URL is mandatory') + } + + // Parse proxy URL + const url = typeof proxyUrl === 'string' ? new URL(proxyUrl) : proxyUrl + + if (url.protocol !== 'socks5:' && url.protocol !== 'socks:') { + throw new InvalidArgumentError('Proxy URL must use socks5:// or socks:// protocol') + } + + this[kProxyUrl] = url + this[kProxyHeaders] = options.headers || {} + this[kProxyProtocol] = options.proxyTls ? 'https:' : 'http:' + this[kRequestTls] = options.requestTls + + // Extract auth from URL or options + this[kProxyAuth] = { + username: options.username || (url.username ? decodeURIComponent(url.username) : null), + password: options.password || (url.password ? decodeURIComponent(url.password) : null) + } + + // Create connector for proxy connection + this[kConnector] = options.connect || buildConnector({ + ...options.proxyTls, + servername: options.proxyTls?.servername || url.hostname + }) + + // Pools for the actual HTTP connections (with SOCKS5 tunnel connect function), keyed by origin + this[kPools] = new Map() + } + + /** + * Create a SOCKS5 connection to the proxy + */ + async createSocks5Connection (targetHost, targetPort) { + const proxyHost = this[kProxyUrl].hostname + const proxyPort = parseInt(this[kProxyUrl].port) || 1080 + + debug('creating SOCKS5 connection to', proxyHost, proxyPort) + + // Connect to the SOCKS5 proxy + const socket = await new Promise((resolve, reject) => { + this[kConnector]({ + hostname: proxyHost, + host: proxyHost, + port: proxyPort, + protocol: this[kProxyProtocol] + }, (err, socket) => { + if (err) { + reject(err) + } else { + resolve(socket) + } + }) + }) + + // Create SOCKS5 client + const socks5Client = new Socks5Client(socket, this[kProxyAuth]) + + // Handle SOCKS5 errors + socks5Client.on('error', (err) => { + debug('SOCKS5 error:', err) + socket.destroy() + }) + + // Perform SOCKS5 handshake + await socks5Client.handshake() + + // Wait for authentication (if required) + await new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + reject(new Error('SOCKS5 authentication timeout')) + }, 5000) + + const onAuthenticated = () => { + clearTimeout(timeout) + socks5Client.removeListener('error', onError) + resolve() + } + + const onError = (err) => { + clearTimeout(timeout) + socks5Client.removeListener('authenticated', onAuthenticated) + reject(err) + } + + // Check if already authenticated (for NO_AUTH method) + if (socks5Client.state === STATES.AUTHENTICATED) { + clearTimeout(timeout) + resolve() + } else { + socks5Client.once('authenticated', onAuthenticated) + socks5Client.once('error', onError) + } + }) + + // Send CONNECT command + await socks5Client.connect(targetHost, targetPort) + + // Wait for connection + await new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + reject(new Error('SOCKS5 connection timeout')) + }, 5000) + + const onConnected = (info) => { + debug('SOCKS5 tunnel established to', targetHost, targetPort, 'via', info) + clearTimeout(timeout) + socks5Client.removeListener('error', onError) + resolve() + } + + const onError = (err) => { + clearTimeout(timeout) + socks5Client.removeListener('connected', onConnected) + reject(err) + } + + socks5Client.once('connected', onConnected) + socks5Client.once('error', onError) + }) + + return socket + } + + /** + * Dispatch a request through the SOCKS5 proxy + */ + [kDispatch] (opts, handler) { + const { origin } = opts + + debug('dispatching request to', origin, 'via SOCKS5') + + try { + const originKey = String(origin) + let pool = this[kPools].get(originKey) + // Create a Pool per origin so requests are not routed to the wrong host + if (!pool || pool.destroyed || pool.closed) { + pool = new Pool(origin, { + pipelining: opts.pipelining, + connections: opts.connections, + connect: async (connectOpts, callback) => { + try { + const url = new URL(origin) + const targetHost = url.hostname + const targetPort = parseInt(url.port) || (url.protocol === 'https:' ? 443 : 80) + + debug('establishing SOCKS5 connection to', targetHost, targetPort) + + // Create SOCKS5 tunnel + const socket = await this.createSocks5Connection(targetHost, targetPort) + + // Handle TLS if needed + let finalSocket = socket + if (url.protocol === 'https:') { + if (!tls) { + tls = require('node:tls') + } + debug('upgrading to TLS') + finalSocket = tls.connect({ + ...this[kRequestTls], + socket, + servername: this[kRequestTls]?.servername || targetHost + }) + + await new Promise((resolve, reject) => { + finalSocket.once('secureConnect', resolve) + finalSocket.once('error', reject) + }) + } + + callback(null, finalSocket) + } catch (err) { + debug('SOCKS5 connection error:', err) + callback(err) + } + } + }) + this[kPools].set(originKey, pool) + } + + // Dispatch the request through the per-origin pool + return pool[kDispatch](opts, handler) + } catch (err) { + debug('dispatch error:', err) + if (typeof handler.onResponseError === 'function') { + handler.onResponseError(null, err) + return false + } else if (typeof handler.onError === 'function') { + handler.onError(err) + return false + } else { + throw err + } + } + } + + async [kClose] () { + const closePromises = [] + for (const pool of this[kPools].values()) { + closePromises.push(pool.close()) + } + this[kPools].clear() + await Promise.all(closePromises) + } + + async [kDestroy] (err) { + const destroyPromises = [] + for (const pool of this[kPools].values()) { + destroyPromises.push(pool.destroy(err)) + } + this[kPools].clear() + await Promise.all(destroyPromises) + } +} + +module.exports = Socks5ProxyAgent diff --git a/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/encoding/index.js b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/encoding/index.js new file mode 100644 index 000000000..41f7794d1 --- /dev/null +++ b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/encoding/index.js @@ -0,0 +1,33 @@ +'use strict' + +const textDecoder = new TextDecoder() + +/** + * @see https://encoding.spec.whatwg.org/#utf-8-decode + * @param {Uint8Array} buffer + */ +function utf8DecodeBytes (buffer) { + if (buffer.length === 0) { + return '' + } + + // 1. Let buffer be the result of peeking three bytes from + // ioQueue, converted to a byte sequence. + + // 2. If buffer is 0xEF 0xBB 0xBF, then read three + // bytes from ioQueue. (Do nothing with those bytes.) + if (buffer[0] === 0xEF && buffer[1] === 0xBB && buffer[2] === 0xBF) { + buffer = buffer.subarray(3) + } + + // 3. Process a queue with an instance of UTF-8’s + // decoder, ioQueue, output, and "replacement". + const output = textDecoder.decode(buffer) + + // 4. Return output. + return output +} + +module.exports = { + utf8DecodeBytes +} diff --git a/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/global.js b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/global.js new file mode 100644 index 000000000..565aceb4a --- /dev/null +++ b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/global.js @@ -0,0 +1,59 @@ +'use strict' + +// We include a version number for the Dispatcher API. In case of breaking changes, +// this version number must be increased to avoid conflicts. +const globalDispatcher = Symbol.for('undici.globalDispatcher.2') +const legacyGlobalDispatcher = Symbol.for('undici.globalDispatcher.1') +const { InvalidArgumentError } = require('./core/errors') +const Agent = require('./dispatcher/agent') + +if (getGlobalDispatcher() === undefined) { + setGlobalDispatcher(new Agent()) +} + +function setGlobalDispatcher (agent) { + if (!agent || typeof agent.dispatch !== 'function') { + throw new InvalidArgumentError('Argument agent must implement Agent') + } + + Object.defineProperty(globalThis, globalDispatcher, { + value: agent, + writable: true, + enumerable: false, + configurable: false + }) + + Object.defineProperty(globalThis, legacyGlobalDispatcher, { + value: agent, + writable: true, + enumerable: false, + configurable: false + }) +} + +function getGlobalDispatcher () { + return globalThis[legacyGlobalDispatcher] +} + +// These are the globals that can be installed by undici.install(). +// Not exported by index.js to avoid use outside of this module. +const installedExports = /** @type {const} */ ( + [ + 'fetch', + 'Headers', + 'Response', + 'Request', + 'FormData', + 'WebSocket', + 'CloseEvent', + 'ErrorEvent', + 'MessageEvent', + 'EventSource' + ] +) + +module.exports = { + setGlobalDispatcher, + getGlobalDispatcher, + installedExports +} diff --git a/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/handler/cache-handler.js b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/handler/cache-handler.js new file mode 100644 index 000000000..9b162f65a --- /dev/null +++ b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/handler/cache-handler.js @@ -0,0 +1,776 @@ +'use strict' + +const util = require('../core/util') +const { + parseCacheControlHeader, + hasInvalidCacheControlDirective, + parseVaryHeader, + hasVaryStar, + isInvalidOrWildcardVaryHeader, + isEtagUsable +} = require('../util/cache') +const { parseHttpDate } = require('../util/date.js') + +function noop () {} + +// Status codes that we can use some heuristics on to cache +const HEURISTICALLY_CACHEABLE_STATUS_CODES = [ + 200, 203, 204, 206, 300, 301, 308, 404, 405, 410, 414, 501 +] + +// Status codes which semantic is not handled by the cache +// https://datatracker.ietf.org/doc/html/rfc9111#section-3 +// This list should not grow beyond 206 unless the RFC is updated +// by a newer one including more. Please introduce another list if +// implementing caching of responses with the 'must-understand' directive. +const NOT_UNDERSTOOD_STATUS_CODES = [ + 206 +] + +const MAX_RESPONSE_AGE = 2147483647000 + +function trimOWS (value) { + return value.replace(/^[\t ]+|[\t ]+$/g, '') +} + +function arrayIncludes (array, value) { + for (let i = 0; i < array.length; i++) { + if (array[i] === value) { + return true + } + } + + return false +} + +function appendConnectionHeaderTokens (headersToRemove, connectionHeader) { + const values = Array.isArray(connectionHeader) ? connectionHeader : [connectionHeader] + + for (let i = 0; i < values.length; i++) { + const tokens = values[i].split(',') + for (let j = 0; j < tokens.length; j++) { + headersToRemove.push(trimOWS(tokens[j]).toLowerCase()) + } + } +} + +function getSameOriginPath (cacheKey, location) { + if (typeof location !== 'string') { + return undefined + } + + let originUrl + let requestUrl + let locationUrl + try { + originUrl = new URL(cacheKey.origin) + requestUrl = new URL(cacheKey.path, originUrl) + locationUrl = new URL(location, requestUrl) + } catch { + return undefined + } + + if (locationUrl.origin !== originUrl.origin) { + return undefined + } + + return locationUrl.pathname + locationUrl.search +} + +function deleteCachedUri (store, cacheKey, path) { + deleteCachedValue(store, { + ...cacheKey, + path + }) + + for (let i = 0; i < util.safeHTTPMethods.length; i++) { + const method = util.safeHTTPMethods[i] + if (method !== cacheKey.method) { + deleteCachedValue(store, { + ...cacheKey, + method, + path + }) + } + } +} + +function deleteLocationTargets (store, cacheKey, headerValue) { + if (headerValue === undefined) { + return + } + + const values = Array.isArray(headerValue) ? headerValue : [headerValue] + for (let i = 0; i < values.length; i++) { + const path = getSameOriginPath(cacheKey, values[i]) + if (path !== undefined) { + deleteCachedUri(store, cacheKey, path) + } + } +} + +function invalidateUnsafeRequest (store, cacheKey, resHeaders) { + deleteCachedUri(store, cacheKey, cacheKey.path) + deleteLocationTargets(store, cacheKey, resHeaders.location) + deleteLocationTargets(store, cacheKey, resHeaders['content-location']) +} + +/** + * @typedef {import('../../types/dispatcher.d.ts').default.DispatchHandler} DispatchHandler + * + * @implements {DispatchHandler} + */ +class CacheHandler { + /** + * @type {import('../../types/cache-interceptor.d.ts').default.CacheKey} + */ + #cacheKey + + /** + * @type {import('../../types/cache-interceptor.d.ts').default.CacheHandlerOptions['type']} + */ + #cacheType + + /** + * @type {number | undefined} + */ + #cacheByDefault + + /** + * @type {import('../../types/cache-interceptor.d.ts').default.CacheStore} + */ + #store + + /** + * @type {import('../../types/dispatcher.d.ts').default.DispatchHandler} + */ + #handler + + /** + * @type {import('node:stream').Writable | undefined} + */ + #writeStream + + /** + * @param {import('../../types/cache-interceptor.d.ts').default.CacheHandlerOptions} opts + * @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} cacheKey + * @param {import('../../types/dispatcher.d.ts').default.DispatchHandler} handler + */ + constructor ({ store, type, cacheByDefault }, cacheKey, handler) { + this.#store = store + this.#cacheType = type + this.#cacheByDefault = cacheByDefault + this.#cacheKey = cacheKey + this.#handler = handler + } + + onRequestStart (controller, context) { + this.#writeStream?.destroy() + this.#writeStream = undefined + this.#handler.onRequestStart?.(controller, context) + } + + onRequestUpgrade (controller, statusCode, headers, socket) { + this.#handler.onRequestUpgrade?.(controller, statusCode, headers, socket) + } + + /** + * @param {import('../../types/dispatcher.d.ts').default.DispatchController} controller + * @param {number} statusCode + * @param {import('../../types/header.d.ts').IncomingHttpHeaders} resHeaders + * @param {string} statusMessage + */ + onResponseStart ( + controller, + statusCode, + resHeaders, + statusMessage + ) { + const downstreamOnHeaders = () => + this.#handler.onResponseStart?.( + controller, + statusCode, + resHeaders, + statusMessage + ) + const handler = this + + if ( + !arrayIncludes(util.safeHTTPMethods, this.#cacheKey.method) && + statusCode >= 200 && + statusCode <= 399 + ) { + // Successful response to an unsafe method, delete it from cache + // https://www.rfc-editor.org/rfc/rfc9111.html#name-invalidating-stored-response + invalidateUnsafeRequest(this.#store, this.#cacheKey, resHeaders) + return downstreamOnHeaders() + } + + const cacheControlHeader = resHeaders['cache-control'] + const cacheControlDirectives = cacheControlHeader ? parseCacheControlHeader(cacheControlHeader) : {} + + if (revalidationResponseDisallowsCachedReuse(this.#cacheType, resHeaders, cacheControlDirectives)) { + deleteCachedValue(this.#store, this.#cacheKey) + return downstreamOnHeaders() + } + + const heuristicallyCacheable = resHeaders['last-modified'] && arrayIncludes(HEURISTICALLY_CACHEABLE_STATUS_CODES, statusCode) + if ( + !cacheControlHeader && + !resHeaders['expires'] && + !heuristicallyCacheable && + !this.#cacheByDefault + ) { + if (statusCode === 304 && resHeaders.vary && isInvalidOrWildcardVaryHeader(resHeaders.vary)) { + deleteCachedValue(this.#store, this.#cacheKey) + } + + // Don't have anything to tell us this response is cachable and we're not + // caching by default + return downstreamOnHeaders() + } + + if (!canCacheResponse(this.#cacheType, this.#cacheKey.method, statusCode, resHeaders, cacheControlDirectives, this.#cacheKey.headers)) { + if (statusCode === 304 && (cacheControlHeader || revalidationResponseDisallowsCachedReuse(this.#cacheType, resHeaders, cacheControlDirectives))) { + deleteCachedValue(this.#store, this.#cacheKey) + } + + return downstreamOnHeaders() + } + + const now = Date.now() + const resAge = Object.hasOwn(resHeaders, 'age') ? getAge(resHeaders.age) : undefined + if (resAge !== undefined && resAge >= MAX_RESPONSE_AGE) { + // Response considered stale + deleteCachedValueIfNotModified(statusCode, this.#store, this.#cacheKey) + return downstreamOnHeaders() + } + + const resDate = Object.hasOwn(resHeaders, 'date') ? getDate(resHeaders.date) : undefined + if (resDate === null) { + deleteCachedValueIfNotModified(statusCode, this.#store, this.#cacheKey) + return downstreamOnHeaders() + } + + const apparentAge = resDate ? Math.max(0, now - resDate.getTime()) : 0 + const currentAge = Math.max(apparentAge, resAge ?? 0) + + const staleAt = + determineStaleAt(this.#cacheType, now, resAge, resHeaders, resDate, cacheControlDirectives) ?? + this.#cacheByDefault + if (staleAt === undefined || currentAge >= staleAt) { + if (cacheControlHeader || staleAt !== undefined) { + deleteCachedValueIfNotModified(statusCode, this.#store, this.#cacheKey) + } + + return downstreamOnHeaders() + } + + const baseTime = now - currentAge + const absoluteStaleAt = staleAt + baseTime + if (now >= absoluteStaleAt) { + // Response is already stale + deleteCachedValueIfNotModified(statusCode, this.#store, this.#cacheKey) + return downstreamOnHeaders() + } + + let varyDirectives + if (this.#cacheKey.headers && resHeaders.vary) { + varyDirectives = parseVaryHeader(resHeaders.vary, this.#cacheKey.headers) + if (!varyDirectives) { + // Parse error + return downstreamOnHeaders() + } + } + + const cachedAt = baseTime + const deleteAt = determineDeleteAt(baseTime, now, cacheControlDirectives, absoluteStaleAt) + const strippedHeaders = stripNecessaryHeaders(resHeaders, cacheControlDirectives) + + /** + * @type {import('../../types/cache-interceptor.d.ts').default.CacheValue} + */ + const value = { + statusCode, + statusMessage, + headers: strippedHeaders, + vary: varyDirectives, + cacheControlDirectives, + cachedAt, + staleAt: absoluteStaleAt, + deleteAt + } + + // Not modified, re-use the cached value + // https://www.rfc-editor.org/rfc/rfc9111.html#name-handling-304-not-modified + if (statusCode === 304) { + const handle304 = (cachedValue) => { + if (!cachedValue) { + // Do not create a new cache entry, as a 304 won't have a body - so cannot be cached. + return downstreamOnHeaders() + } + + // Re-use the cached value: statuscode, statusmessage, headers and body + value.statusCode = cachedValue.statusCode + value.statusMessage = cachedValue.statusMessage + value.etag = cachedValue.etag + value.vary = varyDirectives ?? cachedValue.vary + value.headers = { ...cachedValue.headers, ...strippedHeaders } + + downstreamOnHeaders() + + this.#writeStream = this.#store.createWriteStream(this.#cacheKey, value) + + if (!this.#writeStream || !cachedValue?.body) { + return + } + + if (typeof cachedValue.body.values === 'function') { + const bodyIterator = cachedValue.body.values() + + const streamCachedBody = () => { + for (const chunk of bodyIterator) { + const full = this.#writeStream.write(chunk) === false + this.#handler.onResponseData?.(controller, chunk) + // when stream is full stop writing until we get a 'drain' event + if (full) { + break + } + } + } + + this.#writeStream + .on('error', function () { + handler.#writeStream = undefined + handler.#store.delete(handler.#cacheKey) + }) + .on('drain', () => { + streamCachedBody() + }) + .on('close', function () { + if (handler.#writeStream === this) { + handler.#writeStream = undefined + } + }) + + streamCachedBody() + } else if (typeof cachedValue.body.on === 'function') { + // Readable stream body (e.g. from async/remote cache stores) + cachedValue.body + .on('data', (chunk) => { + this.#writeStream.write(chunk) + this.#handler.onResponseData?.(controller, chunk) + }) + .on('end', () => { + this.#writeStream.end() + }) + .on('error', () => { + this.#writeStream = undefined + this.#store.delete(this.#cacheKey) + }) + + this.#writeStream + .on('error', function () { + handler.#writeStream = undefined + handler.#store.delete(handler.#cacheKey) + }) + .on('close', function () { + if (handler.#writeStream === this) { + handler.#writeStream = undefined + } + }) + } + } + + /** + * @type {import('../../types/cache-interceptor.d.ts').default.CacheValue} + */ + const result = this.#store.get(this.#cacheKey) + if (result && typeof result.then === 'function') { + result.then(handle304) + } else { + handle304(result) + } + } else { + if (typeof resHeaders.etag === 'string' && isEtagUsable(resHeaders.etag)) { + value.etag = resHeaders.etag + } + + this.#writeStream = this.#store.createWriteStream(this.#cacheKey, value) + + if (!this.#writeStream) { + return downstreamOnHeaders() + } + + this.#writeStream + .on('drain', () => controller.resume()) + .on('error', function () { + // TODO (fix): Make error somehow observable? + handler.#writeStream = undefined + + // Delete the value in case the cache store is holding onto state from + // the call to createWriteStream + handler.#store.delete(handler.#cacheKey) + }) + .on('close', function () { + if (handler.#writeStream === this) { + handler.#writeStream = undefined + } + + // TODO (fix): Should we resume even if was paused downstream? + controller.resume() + }) + + downstreamOnHeaders() + } + } + + onResponseData (controller, chunk) { + if (this.#writeStream?.write(chunk) === false) { + controller.pause() + } + + this.#handler.onResponseData?.(controller, chunk) + } + + onResponseEnd (controller, trailers) { + this.#writeStream?.end() + this.#handler.onResponseEnd?.(controller, trailers) + } + + onResponseError (controller, err) { + this.#writeStream?.destroy(err) + this.#writeStream = undefined + this.#handler.onResponseError?.(controller, err) + } +} + +/** + * @param {import('../../types/cache-interceptor.d.ts').default.CacheStore} store + * @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} cacheKey + */ +function deleteCachedValue (store, cacheKey) { + try { + store.delete(cacheKey)?.catch?.(noop) + } catch { + // Fail silently + } +} + +function deleteCachedValueIfNotModified (statusCode, store, cacheKey) { + if (statusCode === 304) { + deleteCachedValue(store, cacheKey) + } +} + +/** + * @param {import('../../types/cache-interceptor.d.ts').default.CacheOptions['type']} cacheType + * @param {import('../../types/header.d.ts').IncomingHttpHeaders} resHeaders + * @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives} cacheControlDirectives + * @returns {boolean} + */ +function revalidationResponseDisallowsCachedReuse (cacheType, resHeaders, cacheControlDirectives) { + return cacheControlDirectives['no-store'] === true || + (cacheType === 'shared' && ( + cacheControlDirectives.private === true || + Object.hasOwn(resHeaders, 'set-cookie') + )) || + (resHeaders.vary ? isInvalidOrWildcardVaryHeader(resHeaders.vary) : false) +} + +/** + * @see https://www.rfc-editor.org/rfc/rfc9111.html#name-storing-responses-to-authen + * + * @param {import('../../types/cache-interceptor.d.ts').default.CacheOptions['type']} cacheType + * @param {string} method + * @param {number} statusCode + * @param {import('../../types/header.d.ts').IncomingHttpHeaders} resHeaders + * @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives} cacheControlDirectives + * @param {import('../../types/header.d.ts').IncomingHttpHeaders} [reqHeaders] + */ +function canCacheResponse (cacheType, method, statusCode, resHeaders, cacheControlDirectives, reqHeaders) { + if (!arrayIncludes(util.safeHTTPMethods, method)) { + return false + } + // Status code must be final and understood. + if (statusCode < 200 || arrayIncludes(NOT_UNDERSTOOD_STATUS_CODES, statusCode)) { + return false + } + // Responses with neither status codes that are heuristically cacheable, nor "explicit enough" caching + // directives, are not cacheable. "Explicit enough": see https://www.rfc-editor.org/rfc/rfc9111.html#section-3 + if (!arrayIncludes(HEURISTICALLY_CACHEABLE_STATUS_CODES, statusCode) && !resHeaders['expires'] && + !cacheControlDirectives.public && + cacheControlDirectives['max-age'] === undefined && + // RFC 9111: a private response directive, if the cache is not shared + !(cacheControlDirectives.private && cacheType === 'private') && + !(cacheControlDirectives['s-maxage'] !== undefined && cacheType === 'shared') + ) { + return false + } + + if (cacheControlDirectives['no-store']) { + return false + } + + if (cacheType === 'shared' && ( + cacheControlDirectives.private === true || + Object.hasOwn(resHeaders, 'set-cookie') + )) { + return false + } + + // https://www.rfc-editor.org/rfc/rfc9111.html#section-4.1-5 + if (resHeaders.vary && hasVaryStar(resHeaders.vary)) { + return false + } + + // https://www.rfc-editor.org/rfc/rfc9111.html#name-storing-responses-to-authen + if (reqHeaders != null && Object.hasOwn(reqHeaders, 'authorization')) { + if ( + !cacheControlDirectives.public && + !cacheControlDirectives['s-maxage'] && + !cacheControlDirectives['must-revalidate'] + ) { + return false + } + + if (typeof reqHeaders.authorization !== 'string') { + return false + } + + if ( + Array.isArray(cacheControlDirectives['no-cache']) && + arrayIncludes(cacheControlDirectives['no-cache'], 'authorization') + ) { + return false + } + + if ( + Array.isArray(cacheControlDirectives['private']) && + arrayIncludes(cacheControlDirectives['private'], 'authorization') + ) { + return false + } + } + + return true +} + +/** + * @param {string | string[]} dateHeader + * @returns {Date | null | undefined} + */ +function getDate (dateHeader) { + let dateValue = dateHeader + if (Array.isArray(dateValue)) { + if (dateValue.length !== 1) { + return null + } + + dateValue = dateValue[0] + } + + if (typeof dateValue !== 'string') { + return null + } + + return parseHttpDate(dateValue) +} + +/** + * @param {string | string[]} ageHeader + * @returns {number | undefined} + */ +function getAge (ageHeader) { + let ageValue = ageHeader + if (Array.isArray(ageValue)) { + if (ageValue.length !== 1) { + return MAX_RESPONSE_AGE + } + + ageValue = ageValue[0] + } + + if (typeof ageValue !== 'string' || !/^[\t ]*[0-9]+[\t ]*$/.test(ageValue)) { + return MAX_RESPONSE_AGE + } + + const age = BigInt(ageValue.replace(/^[\t ]+|[\t ]+$/g, '')) + if (age >= BigInt(MAX_RESPONSE_AGE / 1000)) { + return MAX_RESPONSE_AGE + } + + return Number(age) * 1000 +} + +/** + * @param {import('../../types/cache-interceptor.d.ts').default.CacheOptions['type']} cacheType + * @param {number} now + * @param {number | undefined} age + * @param {import('../../types/header.d.ts').IncomingHttpHeaders} resHeaders + * @param {Date | undefined} responseDate + * @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives} cacheControlDirectives + * + * @returns {number | undefined} time that the value is stale at in seconds or undefined if it shouldn't be cached + */ +function determineStaleAt (cacheType, now, age, resHeaders, responseDate, cacheControlDirectives) { + if (cacheType === 'shared') { + // Prioritize s-maxage since we're a shared cache + // s-maxage > max-age > Expire + // https://www.rfc-editor.org/rfc/rfc9111.html#section-5.2.2.10-3 + if (hasInvalidCacheControlDirective(cacheControlDirectives, 's-maxage')) { + return 0 + } + + const sMaxAge = cacheControlDirectives['s-maxage'] + if (sMaxAge !== undefined) { + return sMaxAge * 1000 + } + } + + if (hasInvalidCacheControlDirective(cacheControlDirectives, 'max-age')) { + return 0 + } + + const maxAge = cacheControlDirectives['max-age'] + if (maxAge !== undefined) { + return maxAge * 1000 + } + + if (Object.hasOwn(resHeaders, 'expires')) { + // https://www.rfc-editor.org/rfc/rfc9111.html#section-5.3 + if (typeof resHeaders.expires !== 'string') { + return 0 + } + + const expiresDate = parseHttpDate(resHeaders.expires) + if (!expiresDate) { + return 0 + } + + if (now >= expiresDate.getTime()) { + return 0 + } + + if (responseDate) { + if (responseDate >= expiresDate) { + return 0 + } + + const freshnessLifetime = expiresDate.getTime() - responseDate.getTime() + if (age !== undefined && age >= freshnessLifetime) { + return 0 + } + + return freshnessLifetime + } + + return expiresDate.getTime() - now + } + + if (typeof resHeaders['last-modified'] === 'string') { + // https://www.rfc-editor.org/rfc/rfc9111.html#name-calculating-heuristic-fresh + const lastModified = parseHttpDate(resHeaders['last-modified']) + if (lastModified) { + if (lastModified.getTime() >= now) { + return undefined + } + + const responseAge = now - lastModified.getTime() + + return responseAge * 0.1 + } + } + + if (cacheControlDirectives.immutable) { + // https://www.rfc-editor.org/rfc/rfc8246.html#section-2.2 + return 31536000000 + } + + return undefined +} + +/** + * @param {number} baseTime + * @param {number} cachedAt + * @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives} cacheControlDirectives + * @param {number} staleAt + */ +function determineDeleteAt (baseTime, cachedAt, cacheControlDirectives, staleAt) { + let staleWhileRevalidate = -Infinity + let staleIfError = -Infinity + let immutable = -Infinity + + if (cacheControlDirectives['stale-while-revalidate']) { + staleWhileRevalidate = staleAt + (cacheControlDirectives['stale-while-revalidate'] * 1000) + } + + if (cacheControlDirectives['stale-if-error']) { + staleIfError = staleAt + (cacheControlDirectives['stale-if-error'] * 1000) + } + + if (cacheControlDirectives.immutable && staleWhileRevalidate === -Infinity && staleIfError === -Infinity) { + immutable = cachedAt + 31536000000 + } + + // When no stale directives or immutable flag, add a revalidation buffer + // equal to the freshness lifetime so the entry survives past staleAt long + // enough to be revalidated instead of silently disappearing. + // + // Response Date headers only have second precision, so baseTime can trail the + // actual cache insertion time by up to ~1s. Pad the buffer by that bounded + // skew so short-lived entries do not disappear exactly when they should be + // revalidated. + if (staleWhileRevalidate === -Infinity && staleIfError === -Infinity && immutable === -Infinity) { + const freshnessLifetime = staleAt - baseTime + const datePrecisionPadding = Math.min(Math.max(cachedAt - baseTime, 0), 1000) + return staleAt + freshnessLifetime + datePrecisionPadding + } + + return Math.max(staleAt, staleWhileRevalidate, staleIfError, immutable) +} + +/** + * Strips headers required to be removed in cached responses + * @param {import('../../types/header.d.ts').IncomingHttpHeaders} resHeaders + * @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives} cacheControlDirectives + * @returns {Record} + */ +function stripNecessaryHeaders (resHeaders, cacheControlDirectives) { + const headersToRemove = [ + 'connection', + 'proxy-authenticate', + 'proxy-authentication-info', + 'proxy-authorization', + 'proxy-connection', + 'te', + 'transfer-encoding', + 'upgrade', + // We'll add age back when serving it + 'age' + ] + + if (resHeaders['connection']) { + appendConnectionHeaderTokens(headersToRemove, resHeaders['connection']) + } + + if (Array.isArray(cacheControlDirectives['no-cache'])) { + headersToRemove.push(...cacheControlDirectives['no-cache']) + } + + if (Array.isArray(cacheControlDirectives['private'])) { + headersToRemove.push(...cacheControlDirectives['private']) + } + + let strippedHeaders + for (const headerName of headersToRemove) { + if (Object.hasOwn(resHeaders, headerName)) { + strippedHeaders ??= { ...resHeaders } + delete strippedHeaders[headerName] + } + } + + return strippedHeaders ?? resHeaders +} + +module.exports = CacheHandler diff --git a/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/handler/cache-revalidation-handler.js b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/handler/cache-revalidation-handler.js new file mode 100644 index 000000000..8858d8e2f --- /dev/null +++ b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/handler/cache-revalidation-handler.js @@ -0,0 +1,124 @@ +'use strict' + +const assert = require('node:assert') + +/** + * This takes care of revalidation requests we send to the origin. If we get + * a response indicating that what we have is cached (via a HTTP 304), we can + * continue using the cached value. Otherwise, we'll receive the new response + * here, which we then just pass on to the next handler (most likely a + * CacheHandler). Note that this assumes the proper headers were already + * included in the request to tell the origin that we want to revalidate the + * response (i.e. if-modified-since or if-none-match). + * + * @see https://www.rfc-editor.org/rfc/rfc9111.html#name-validation + * + * @implements {import('../../types/dispatcher.d.ts').default.DispatchHandler} + */ +class CacheRevalidationHandler { + #successful = false + + /** + * @type {((success: boolean, context?: any, statusCode?: number, headers?: import('../../types/header.d.ts').IncomingHttpHeaders) => void) | null} + */ + #callback + + /** + * @type {(import('../../types/dispatcher.d.ts').default.DispatchHandler)} + */ + #handler + + #context + + /** + * @type {boolean} + */ + #allowErrorStatusCodes + + /** + * @param {(success: boolean, context?: any, statusCode?: number, headers?: import('../../types/header.d.ts').IncomingHttpHeaders) => void} callback Function to call if the cached value is valid + * @param {import('../../types/dispatcher.d.ts').default.DispatchHandlers} handler + * @param {boolean} allowErrorStatusCodes + */ + constructor (callback, handler, allowErrorStatusCodes) { + if (typeof callback !== 'function') { + throw new TypeError('callback must be a function') + } + + this.#callback = callback + this.#handler = handler + this.#allowErrorStatusCodes = allowErrorStatusCodes + } + + onRequestStart (_, context) { + this.#successful = false + this.#context = context + } + + onRequestUpgrade (controller, statusCode, headers, socket) { + this.#handler.onRequestUpgrade?.(controller, statusCode, headers, socket) + } + + onResponseStart ( + controller, + statusCode, + headers, + statusMessage + ) { + assert(this.#callback != null) + + // https://www.rfc-editor.org/rfc/rfc9111.html#name-handling-a-validation-respo + // https://datatracker.ietf.org/doc/html/rfc5861#section-4 + this.#successful = statusCode === 304 || + (this.#allowErrorStatusCodes && statusCode >= 500 && statusCode <= 504) + this.#callback(this.#successful, this.#context, statusCode, headers) + this.#callback = null + + if (this.#successful) { + return true + } + + this.#handler.onRequestStart?.(controller, this.#context) + this.#handler.onResponseStart?.( + controller, + statusCode, + headers, + statusMessage + ) + } + + onResponseData (controller, chunk) { + if (this.#successful) { + return + } + + return this.#handler.onResponseData?.(controller, chunk) + } + + onResponseEnd (controller, trailers) { + if (this.#successful) { + return + } + + this.#handler.onResponseEnd?.(controller, trailers) + } + + onResponseError (controller, err) { + if (this.#successful) { + return + } + + if (this.#callback) { + this.#callback(false) + this.#callback = null + } + + if (typeof this.#handler.onResponseError === 'function') { + this.#handler.onResponseError(controller, err) + } else { + throw err + } + } +} + +module.exports = CacheRevalidationHandler diff --git a/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/handler/decorator-handler.js b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/handler/decorator-handler.js new file mode 100644 index 000000000..50fbb0cf8 --- /dev/null +++ b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/handler/decorator-handler.js @@ -0,0 +1,67 @@ +'use strict' + +const assert = require('node:assert') +const WrapHandler = require('./wrap-handler') + +/** + * @deprecated + */ +module.exports = class DecoratorHandler { + #handler + #onCompleteCalled = false + #onErrorCalled = false + #onResponseStartCalled = false + + constructor (handler) { + if (typeof handler !== 'object' || handler === null) { + throw new TypeError('handler must be an object') + } + this.#handler = WrapHandler.wrap(handler) + } + + onRequestStart (...args) { + this.#handler.onRequestStart?.(...args) + } + + onRequestUpgrade (...args) { + assert(!this.#onCompleteCalled) + assert(!this.#onErrorCalled) + + return this.#handler.onRequestUpgrade?.(...args) + } + + onResponseStart (...args) { + assert(!this.#onCompleteCalled) + assert(!this.#onErrorCalled) + assert(!this.#onResponseStartCalled) + + this.#onResponseStartCalled = true + + return this.#handler.onResponseStart?.(...args) + } + + onResponseData (...args) { + assert(!this.#onCompleteCalled) + assert(!this.#onErrorCalled) + + return this.#handler.onResponseData?.(...args) + } + + onResponseEnd (...args) { + assert(!this.#onCompleteCalled) + assert(!this.#onErrorCalled) + + this.#onCompleteCalled = true + return this.#handler.onResponseEnd?.(...args) + } + + onResponseError (...args) { + this.#onErrorCalled = true + return this.#handler.onResponseError?.(...args) + } + + /** + * @deprecated + */ + onBodySent () {} +} diff --git a/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/handler/deduplication-handler.js b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/handler/deduplication-handler.js new file mode 100644 index 000000000..537c4f70e --- /dev/null +++ b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/handler/deduplication-handler.js @@ -0,0 +1,460 @@ +'use strict' + +const { RequestAbortedError } = require('../core/errors') + +/** + * @typedef {import('../../types/dispatcher.d.ts').default.DispatchHandler} DispatchHandler + */ + +const DEFAULT_MAX_BUFFER_SIZE = 5 * 1024 * 1024 + +/** + * @typedef {Object} WaitingHandler + * @property {DispatchHandler} handler + * @property {import('../../types/dispatcher.d.ts').default.DispatchController} controller + * @property {Buffer[]} bufferedChunks + * @property {number} bufferedBytes + * @property {object | null} pendingTrailers + * @property {boolean} done + */ + +/** + * Handler that forwards response events to multiple waiting handlers. + * Used for request deduplication. + * + * @implements {DispatchHandler} + */ +class DeduplicationHandler { + /** + * @type {DispatchHandler} + */ + #primaryHandler + + /** + * @type {WaitingHandler[]} + */ + #waitingHandlers = [] + + /** + * @type {number} + */ + #maxBufferSize = DEFAULT_MAX_BUFFER_SIZE + + /** + * @type {number} + */ + #statusCode = 0 + + /** + * @type {Record} + */ + #headers = {} + + /** + * @type {string} + */ + #statusMessage = '' + + /** + * @type {boolean} + */ + #aborted = false + + /** + * @type {boolean} + */ + #responseStarted = false + + /** + * @type {boolean} + */ + #responseDataStarted = false + + /** + * @type {boolean} + */ + #completed = false + + /** + * @type {import('../../types/dispatcher.d.ts').default.DispatchController | null} + */ + #controller = null + + /** + * @type {(() => void) | null} + */ + #onComplete = null + + /** + * @param {DispatchHandler} primaryHandler The primary handler + * @param {() => void} onComplete Callback when request completes + * @param {number} [maxBufferSize] Maximum paused buffer size per waiting handler + */ + constructor (primaryHandler, onComplete, maxBufferSize = DEFAULT_MAX_BUFFER_SIZE) { + this.#primaryHandler = primaryHandler + this.#onComplete = onComplete + this.#maxBufferSize = maxBufferSize + } + + /** + * Add a waiting handler that will receive response events. + * Returns false if deduplication can no longer safely attach this handler. + * + * @param {DispatchHandler} handler + * @returns {boolean} + */ + addWaitingHandler (handler) { + if (this.#completed || this.#responseDataStarted) { + return false + } + + const waitingHandler = this.#createWaitingHandler(handler) + const waitingController = waitingHandler.controller + + try { + handler.onRequestStart?.(waitingController, null) + + if (waitingController.aborted) { + waitingHandler.done = true + return true + } + + if (this.#responseStarted) { + handler.onResponseStart?.( + waitingController, + this.#statusCode, + this.#headers, + this.#statusMessage + ) + } + } catch { + // Ignore errors from waiting handlers + waitingHandler.done = true + return true + } + + if (!waitingController.aborted) { + this.#waitingHandlers.push(waitingHandler) + } + + return true + } + + /** + * @param {import('../../types/dispatcher.d.ts').default.DispatchController} controller + * @param {any} context + */ + onRequestStart (controller, context) { + this.#controller = controller + this.#primaryHandler.onRequestStart?.(controller, context) + } + + /** + * @param {import('../../types/dispatcher.d.ts').default.DispatchController} controller + * @param {number} statusCode + * @param {import('../../types/header.d.ts').IncomingHttpHeaders} headers + * @param {Socket} socket + */ + onRequestUpgrade (controller, statusCode, headers, socket) { + this.#primaryHandler.onRequestUpgrade?.(controller, statusCode, headers, socket) + } + + /** + * @param {import('../../types/dispatcher.d.ts').default.DispatchController} controller + * @param {number} statusCode + * @param {Record} headers + * @param {string} statusMessage + */ + onResponseStart (controller, statusCode, headers, statusMessage) { + this.#responseStarted = true + this.#statusCode = statusCode + this.#headers = headers + this.#statusMessage = statusMessage + + this.#primaryHandler.onResponseStart?.(controller, statusCode, headers, statusMessage) + + for (const waitingHandler of this.#waitingHandlers) { + const { handler, controller: waitingController } = waitingHandler + + if (waitingHandler.done || waitingController.aborted) { + waitingHandler.done = true + continue + } + + try { + handler.onResponseStart?.( + waitingController, + statusCode, + headers, + statusMessage + ) + } catch { + // Ignore errors from waiting handlers + } + + if (waitingController.aborted) { + waitingHandler.done = true + } + } + + this.#pruneDoneWaitingHandlers() + } + + /** + * @param {import('../../types/dispatcher.d.ts').default.DispatchController} controller + * @param {Buffer} chunk + */ + onResponseData (controller, chunk) { + if (this.#aborted || this.#completed) { + return + } + + this.#responseDataStarted = true + + this.#primaryHandler.onResponseData?.(controller, chunk) + + for (const waitingHandler of this.#waitingHandlers) { + const { handler, controller: waitingController } = waitingHandler + + if (waitingHandler.done || waitingController.aborted) { + waitingHandler.done = true + continue + } + + if (waitingController.paused) { + this.#bufferWaitingChunk(waitingHandler, chunk) + continue + } + + try { + handler.onResponseData?.(waitingController, chunk) + } catch { + // Ignore errors from waiting handlers + } + + if (waitingController.aborted) { + waitingHandler.done = true + waitingHandler.bufferedChunks = [] + waitingHandler.bufferedBytes = 0 + } + } + + this.#pruneDoneWaitingHandlers() + } + + /** + * @param {import('../../types/dispatcher.d.ts').default.DispatchController} controller + * @param {object} trailers + */ + onResponseEnd (controller, trailers) { + if (this.#aborted || this.#completed) { + return + } + + this.#completed = true + this.#primaryHandler.onResponseEnd?.(controller, trailers) + + for (const waitingHandler of this.#waitingHandlers) { + if (waitingHandler.done || waitingHandler.controller.aborted) { + waitingHandler.done = true + continue + } + + this.#flushWaitingHandler(waitingHandler) + + if (waitingHandler.done || waitingHandler.controller.aborted) { + waitingHandler.done = true + continue + } + + if (waitingHandler.controller.paused && waitingHandler.bufferedChunks.length > 0) { + waitingHandler.pendingTrailers = trailers + continue + } + + try { + waitingHandler.handler.onResponseEnd?.(waitingHandler.controller, trailers) + } catch { + // Ignore errors from waiting handlers + } + + waitingHandler.done = true + } + + this.#pruneDoneWaitingHandlers() + this.#onComplete?.() + } + + /** + * @param {import('../../types/dispatcher.d.ts').default.DispatchController} controller + * @param {Error} err + */ + onResponseError (controller, err) { + if (this.#completed) { + return + } + + this.#aborted = true + this.#completed = true + + this.#primaryHandler.onResponseError?.(controller, err) + + for (const waitingHandler of this.#waitingHandlers) { + this.#errorWaitingHandler(waitingHandler, err) + } + + this.#waitingHandlers = [] + this.#onComplete?.() + } + + /** + * @param {DispatchHandler} handler + * @returns {WaitingHandler} + */ + #createWaitingHandler (handler) { + /** @type {WaitingHandler} */ + const waitingHandler = { + handler, + controller: null, + bufferedChunks: [], + bufferedBytes: 0, + pendingTrailers: null, + done: false + } + + const state = { + aborted: false, + paused: false, + reason: null + } + + waitingHandler.controller = { + resume: () => { + if (state.aborted) { + return + } + + state.paused = false + this.#flushWaitingHandler(waitingHandler) + + if ( + this.#completed && + waitingHandler.pendingTrailers && + waitingHandler.bufferedChunks.length === 0 && + !state.paused && + !state.aborted + ) { + try { + waitingHandler.handler.onResponseEnd?.(waitingHandler.controller, waitingHandler.pendingTrailers) + } catch { + // Ignore errors from waiting handlers + } + + waitingHandler.pendingTrailers = null + waitingHandler.done = true + } + + this.#pruneDoneWaitingHandlers() + }, + pause: () => { + if (!state.aborted) { + state.paused = true + } + }, + get paused () { return state.paused }, + get aborted () { return state.aborted }, + get reason () { return state.reason }, + abort: (reason) => { + state.aborted = true + state.reason = reason ?? null + waitingHandler.done = true + waitingHandler.pendingTrailers = null + waitingHandler.bufferedChunks = [] + waitingHandler.bufferedBytes = 0 + } + } + + return waitingHandler + } + + /** + * @param {WaitingHandler} waitingHandler + * @param {Buffer} chunk + */ + #bufferWaitingChunk (waitingHandler, chunk) { + if (waitingHandler.done || waitingHandler.controller.aborted) { + waitingHandler.done = true + waitingHandler.bufferedChunks = [] + waitingHandler.bufferedBytes = 0 + return + } + + const bufferedChunk = Buffer.from(chunk) + waitingHandler.bufferedChunks.push(bufferedChunk) + waitingHandler.bufferedBytes += bufferedChunk.length + + if (waitingHandler.bufferedBytes > this.#maxBufferSize) { + const err = new RequestAbortedError(`Deduplicated waiting handler exceeded maxBufferSize (${this.#maxBufferSize} bytes) while paused`) + this.#errorWaitingHandler(waitingHandler, err) + } + } + + /** + * @param {WaitingHandler} waitingHandler + */ + #flushWaitingHandler (waitingHandler) { + const { handler, controller } = waitingHandler + + while ( + !waitingHandler.done && + !controller.aborted && + !controller.paused && + waitingHandler.bufferedChunks.length > 0 + ) { + const bufferedChunk = waitingHandler.bufferedChunks.shift() + waitingHandler.bufferedBytes -= bufferedChunk.length + + try { + handler.onResponseData?.(controller, bufferedChunk) + } catch { + // Ignore errors from waiting handlers + } + + if (controller.aborted) { + waitingHandler.done = true + waitingHandler.pendingTrailers = null + waitingHandler.bufferedChunks = [] + waitingHandler.bufferedBytes = 0 + break + } + } + } + + /** + * @param {WaitingHandler} waitingHandler + * @param {Error} err + */ + #errorWaitingHandler (waitingHandler, err) { + if (waitingHandler.done) { + return + } + + waitingHandler.done = true + waitingHandler.pendingTrailers = null + waitingHandler.bufferedChunks = [] + waitingHandler.bufferedBytes = 0 + + try { + waitingHandler.controller.abort(err) + waitingHandler.handler.onResponseError?.(waitingHandler.controller, err) + } catch { + // Ignore errors from waiting handlers + } + } + + #pruneDoneWaitingHandlers () { + this.#waitingHandlers = this.#waitingHandlers.filter(waitingHandler => waitingHandler.done === false) + } +} + +module.exports = DeduplicationHandler diff --git a/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/handler/redirect-handler.js b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/handler/redirect-handler.js new file mode 100644 index 000000000..d088d5488 --- /dev/null +++ b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/handler/redirect-handler.js @@ -0,0 +1,238 @@ +'use strict' + +const util = require('../core/util') +const { kBodyUsed } = require('../core/symbols') +const assert = require('node:assert') +const { InvalidArgumentError } = require('../core/errors') +const EE = require('node:events') + +const redirectableStatusCodes = [300, 301, 302, 303, 307, 308] + +const kBody = Symbol('body') + +const noop = () => {} + +class BodyAsyncIterable { + constructor (body) { + this[kBody] = body + this[kBodyUsed] = false + } + + async * [Symbol.asyncIterator] () { + assert(!this[kBodyUsed], 'disturbed') + this[kBodyUsed] = true + yield * this[kBody] + } +} + +class RedirectHandler { + static buildDispatch (dispatcher, maxRedirections) { + if (maxRedirections != null && (!Number.isInteger(maxRedirections) || maxRedirections < 0)) { + throw new InvalidArgumentError('maxRedirections must be a positive number') + } + + const dispatch = dispatcher.dispatch.bind(dispatcher) + return (opts, originalHandler) => dispatch(opts, new RedirectHandler(dispatch, maxRedirections, opts, originalHandler)) + } + + constructor (dispatch, maxRedirections, opts, handler) { + if (maxRedirections != null && (!Number.isInteger(maxRedirections) || maxRedirections < 0)) { + throw new InvalidArgumentError('maxRedirections must be a positive number') + } + + this.dispatch = dispatch + this.location = null + const { maxRedirections: _, ...cleanOpts } = opts + this.opts = cleanOpts // opts must be a copy, exclude maxRedirections + this.maxRedirections = maxRedirections + this.handler = handler + this.history = [] + + if (util.isStream(this.opts.body)) { + // TODO (fix): Provide some way for the user to cache the file to e.g. /tmp + // so that it can be dispatched again? + // TODO (fix): Do we need 100-expect support to provide a way to do this properly? + if (util.bodyLength(this.opts.body) === 0) { + this.opts.body + .on('data', function () { + assert(false) + }) + } + + if (typeof this.opts.body.readableDidRead !== 'boolean') { + this.opts.body[kBodyUsed] = false + EE.prototype.on.call(this.opts.body, 'data', function () { + this[kBodyUsed] = true + }) + } + } else if (this.opts.body && typeof this.opts.body.pipeTo === 'function') { + // TODO (fix): We can't access ReadableStream internal state + // to determine whether or not it has been disturbed. This is just + // a workaround. + this.opts.body = new BodyAsyncIterable(this.opts.body) + } else if ( + this.opts.body && + typeof this.opts.body !== 'string' && + !ArrayBuffer.isView(this.opts.body) && + util.isIterable(this.opts.body) && + !util.isFormDataLike(this.opts.body) + ) { + // TODO: Should we allow re-using iterable if !this.opts.idempotent + // or through some other flag? + this.opts.body = new BodyAsyncIterable(this.opts.body) + } + } + + onRequestStart (controller, context) { + this.handler.onRequestStart?.(controller, { ...context, history: this.history }) + } + + onRequestUpgrade (controller, statusCode, headers, socket) { + this.handler.onRequestUpgrade?.(controller, statusCode, headers, socket) + } + + onResponseStart (controller, statusCode, headers, statusMessage) { + if (this.opts.throwOnMaxRedirect && this.history.length >= this.maxRedirections) { + throw new Error('max redirects') + } + + // https://tools.ietf.org/html/rfc7231#section-6.4.2 + // https://fetch.spec.whatwg.org/#http-redirect-fetch + // In case of HTTP 301 or 302 with POST, change the method to GET + if ((statusCode === 301 || statusCode === 302) && this.opts.method === 'POST') { + this.opts.method = 'GET' + if (util.isStream(this.opts.body)) { + util.destroy(this.opts.body.on('error', noop)) + } + this.opts.body = null + } + + // https://tools.ietf.org/html/rfc7231#section-6.4.4 + // In case of HTTP 303, always replace method to be either HEAD or GET + if (statusCode === 303 && this.opts.method !== 'HEAD') { + this.opts.method = 'GET' + if (util.isStream(this.opts.body)) { + util.destroy(this.opts.body.on('error', noop)) + } + this.opts.body = null + } + + this.location = this.history.length >= this.maxRedirections || util.isDisturbed(this.opts.body) || redirectableStatusCodes.indexOf(statusCode) === -1 + ? null + : headers.location + + if (this.opts.origin) { + this.history.push(new URL(this.opts.path, this.opts.origin)) + } + + if (!this.location) { + this.handler.onResponseStart?.(controller, statusCode, headers, statusMessage) + return + } + + const { origin, pathname, search } = util.parseURL(new URL(this.location, this.opts.origin && new URL(this.opts.path, this.opts.origin))) + const path = search ? `${pathname}${search}` : pathname + + // Check for redirect loops by seeing if we've already visited this URL in our history + // This catches the case where Client/Pool try to handle cross-origin redirects but fail + // and keep redirecting to the same URL in an infinite loop + const redirectUrlString = `${origin}${path}` + for (const historyUrl of this.history) { + if (historyUrl.toString() === redirectUrlString) { + throw new InvalidArgumentError(`Redirect loop detected. Cannot redirect to ${origin}. This typically happens when using a Client or Pool with cross-origin redirects. Use an Agent for cross-origin redirects.`) + } + } + + // Remove headers referring to the original URL. + // By default it is Host only, unless it's a 303 (see below), which removes also all Content-* headers. + // https://tools.ietf.org/html/rfc7231#section-6.4 + this.opts.headers = cleanRequestHeaders(this.opts.headers, statusCode === 303, this.opts.origin !== origin) + this.opts.path = path + this.opts.origin = origin + this.opts.query = null + } + + onResponseData (controller, chunk) { + if (this.location) { + /* + https://tools.ietf.org/html/rfc7231#section-6.4 + + TLDR: undici always ignores 3xx response bodies. + + Redirection is used to serve the requested resource from another URL, so it assumes that + no body is generated (and thus can be ignored). Even though generating a body is not prohibited. + + For status 301, 302, 303, 307 and 308 (the latter from RFC 7238), the specs mention that the body usually + (which means it's optional and not mandated) contain just an hyperlink to the value of + the Location response header, so the body can be ignored safely. + + For status 300, which is "Multiple Choices", the spec mentions both generating a Location + response header AND a response body with the other possible location to follow. + Since the spec explicitly chooses not to specify a format for such body and leave it to + servers and browsers implementors, we ignore the body as there is no specified way to eventually parse it. + */ + } else { + this.handler.onResponseData?.(controller, chunk) + } + } + + onResponseEnd (controller, trailers) { + if (this.location) { + /* + https://tools.ietf.org/html/rfc7231#section-6.4 + + TLDR: undici always ignores 3xx response trailers as they are not expected in case of redirections + and neither are useful if present. + + See comment on onData method above for more detailed information. + */ + this.dispatch(this.opts, this) + } else { + this.handler.onResponseEnd(controller, trailers) + } + } + + onResponseError (controller, error) { + this.handler.onResponseError?.(controller, error) + } +} + +// https://tools.ietf.org/html/rfc7231#section-6.4.4 +function shouldRemoveHeader (header, removeContent, unknownOrigin) { + if (header.length === 4) { + return util.headerNameToString(header) === 'host' + } + if (removeContent && util.headerNameToString(header).startsWith('content-')) { + return true + } + if (unknownOrigin && (header.length === 13 || header.length === 6 || header.length === 19)) { + const name = util.headerNameToString(header) + return name === 'authorization' || name === 'cookie' || name === 'proxy-authorization' + } + return false +} + +// https://tools.ietf.org/html/rfc7231#section-6.4 +function cleanRequestHeaders (headers, removeContent, unknownOrigin) { + const ret = [] + if (Array.isArray(headers)) { + for (let i = 0; i < headers.length; i += 2) { + if (!shouldRemoveHeader(headers[i], removeContent, unknownOrigin)) { + ret.push(headers[i], headers[i + 1]) + } + } + } else if (headers && typeof headers === 'object') { + const entries = util.hasSafeIterator(headers) ? headers : Object.entries(headers) + + for (const [key, value] of entries) { + if (!shouldRemoveHeader(key, removeContent, unknownOrigin)) { + ret.push(key, value) + } + } + } else { + assert(headers == null, 'headers must be an object or an array') + } + return ret +} + +module.exports = RedirectHandler diff --git a/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/handler/retry-handler.js b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/handler/retry-handler.js new file mode 100644 index 000000000..94b27b8e6 --- /dev/null +++ b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/handler/retry-handler.js @@ -0,0 +1,504 @@ +'use strict' +const assert = require('node:assert') + +const { kRetryHandlerDefaultRetry } = require('../core/symbols') +const { RequestRetryError } = require('../core/errors') +const WrapHandler = require('./wrap-handler') +const { + isDisturbed, + parseRangeHeader, + wrapRequestBody +} = require('../core/util') + +function calculateRetryAfterHeader (retryAfter) { + const retryTime = new Date(retryAfter).getTime() + return isNaN(retryTime) ? 0 : retryTime - Date.now() +} + +function validatePartialResponseContentLength (headers, range, statusCode, retryCount) { + const contentLength = headers['content-length'] + if (contentLength == null) { + return + } + + if (!Number.isFinite(range.start) || !Number.isFinite(range.end)) { + return + } + + const length = Number(contentLength) + const expectedLength = range.end - range.start + 1 + if (!Number.isFinite(length) || length !== expectedLength) { + throw new RequestRetryError('Content-Length mismatch', statusCode, { + headers, + data: { count: retryCount } + }) + } +} + +// A stable controller handed to the downstream handler for the lifetime of the +// request. Each transparent retry/resume is a separate dispatch with its own +// connection controller. The proxy always forwards to the active connection +// while preserving a downstream pause across controller replacement. +class RetryController { + #paused = false + #target = null + + set target (target) { + this.#target = target + if (this.#paused) { + target?.pause() + } + } + + get target () { return this.#target } + + pause () { + this.#paused = true + this.#target?.pause() + } + + resume () { + this.#paused = false + this.#target?.resume() + } + + abort (reason) { + this.#target?.abort(reason) + } + + get paused () { return this.#paused || (this.#target?.paused ?? false) } + get aborted () { return this.#target?.aborted ?? false } + get reason () { return this.#target?.reason ?? null } + get rawHeaders () { return this.#target?.rawHeaders ?? null } + set rawHeaders (value) { + if (this.#target) { + this.#target.rawHeaders = value + } + } + + get rawTrailers () { return this.#target?.rawTrailers ?? null } + set rawTrailers (value) { + if (this.#target) { + this.#target.rawTrailers = value + } + } +} + +class RetryHandler { + constructor (opts, { dispatch, handler }) { + const { retryOptions, ...dispatchOpts } = opts + const { + // Retry scoped + retry: retryFn, + maxRetries, + maxTimeout, + minTimeout, + timeoutFactor, + // Response scoped + methods, + errorCodes, + retryAfter, + statusCodes, + throwOnError + } = retryOptions ?? {} + + this.error = null + this.dispatch = dispatch + this.handler = WrapHandler.wrap(handler) + this.opts = { ...dispatchOpts, body: wrapRequestBody(opts.body) } + this.retryOpts = { + throwOnError: throwOnError ?? true, + retry: retryFn ?? RetryHandler[kRetryHandlerDefaultRetry], + retryAfter: retryAfter ?? true, + maxTimeout: maxTimeout ?? 30 * 1000, // 30s, + minTimeout: minTimeout ?? 500, // .5s + timeoutFactor: timeoutFactor ?? 2, + maxRetries: maxRetries ?? 5, + // What errors we should retry + methods: methods ?? ['GET', 'HEAD', 'OPTIONS', 'PUT', 'DELETE', 'TRACE'], + // Indicates which errors to retry + statusCodes: statusCodes ?? [500, 502, 503, 504, 429], + // List of errors to retry + errorCodes: errorCodes ?? [ + 'ECONNRESET', + 'ECONNREFUSED', + 'ENOTFOUND', + 'ENETDOWN', + 'ENETUNREACH', + 'EHOSTDOWN', + 'EHOSTUNREACH', + 'EPIPE', + 'UND_ERR_SOCKET' + ] + } + + this.retryCount = 0 + this.retryCountCheckpoint = 0 + this.headersSent = false + this.start = 0 + this.end = null + this.etag = null + this.controllerProxy = new RetryController() + } + + onResponseStartWithRetry (controller, statusCode, headers, statusMessage, err) { + if (this.retryOpts.throwOnError) { + // Preserve old behavior for status codes that are not eligible for retry + if (this.retryOpts.statusCodes.includes(statusCode) === false) { + if (this.headersSent) { + // The downstream handler already received the response from an + // earlier attempt. Forwarding this response would replace the + // downstream body and leave the original body pending forever. + this.handler.onResponseError?.(this.controllerProxy, err) + } else { + this.headersSent = true + this.checkpointResponseEnd(headers) + this.handler.onResponseStart?.(this.controllerProxy, statusCode, headers, statusMessage) + } + } else { + this.error = err + } + + return + } + + if (isDisturbed(this.opts.body)) { + this.headersSent = true + this.checkpointResponseEnd(headers) + this.handler.onResponseStart?.(this.controllerProxy, statusCode, headers, statusMessage) + return + } + + function shouldRetry (passedErr) { + if (passedErr) { + if (this.headersSent) { + // The downstream handler already received the response from an + // earlier attempt. Forwarding this response would replace the + // downstream body and leave the original body pending forever. + this.handler.onResponseError?.(this.controllerProxy, passedErr) + } else { + this.headersSent = true + this.checkpointResponseEnd(headers) + this.handler.onResponseStart?.(this.controllerProxy, statusCode, headers, statusMessage) + } + controller.resume() + return + } + + this.error = err + controller.resume() + } + + controller.pause() + this.retryOpts.retry( + err, + { + state: { counter: this.retryCount }, + opts: { retryOptions: this.retryOpts, ...this.opts } + }, + shouldRetry.bind(this) + ) + } + + checkpointResponseEnd (headers) { + if (this.end == null && this.opts.method !== 'HEAD') { + const contentLength = headers['content-length'] + this.end = contentLength != null ? Number(contentLength) - 1 : null + + assert( + this.end == null || Number.isFinite(this.end), + 'invalid content-length' + ) + + this.resume = this.end != null + } + } + + onRequestStart (controller, context) { + this.controllerProxy.target = controller + if (!this.headersSent) { + this.handler.onRequestStart?.(this.controllerProxy, context) + } + } + + onRequestUpgrade (_controller, statusCode, headers, socket) { + this.handler.onRequestUpgrade?.(this.controllerProxy, statusCode, headers, socket) + } + + static [kRetryHandlerDefaultRetry] (err, { state, opts }, cb) { + const { statusCode, code, headers } = err + const { method, retryOptions } = opts + const { + maxRetries, + minTimeout, + maxTimeout, + timeoutFactor, + statusCodes, + errorCodes, + methods + } = retryOptions + const { counter } = state + + // Any code that is not a Undici's originated and allowed to retry + if (code && code !== 'UND_ERR_REQ_RETRY' && !errorCodes.includes(code)) { + cb(err) + return + } + + // If a set of method are provided and the current method is not in the list + if (Array.isArray(methods) && !methods.includes(method)) { + cb(err) + return + } + + // If a set of status code are provided and the current status code is not in the list + if ( + statusCode != null && + Array.isArray(statusCodes) && + !statusCodes.includes(statusCode) + ) { + cb(err) + return + } + + // If we reached the max number of retries + if (counter > maxRetries) { + cb(err) + return + } + + let retryAfterHeader = headers?.['retry-after'] + if (retryAfterHeader) { + retryAfterHeader = Number(retryAfterHeader) + retryAfterHeader = Number.isNaN(retryAfterHeader) + ? calculateRetryAfterHeader(headers['retry-after']) + : retryAfterHeader * 1e3 // Retry-After is in seconds + } + + const retryTimeout = + retryAfterHeader > 0 + ? Math.min(retryAfterHeader, maxTimeout) + : Math.min(minTimeout * timeoutFactor ** (counter - 1), maxTimeout) + + setTimeout(() => cb(null), retryTimeout) + } + + onResponseStart (controller, statusCode, headers, statusMessage) { + this.error = null + this.retryCount += 1 + + if (statusCode >= 300) { + const err = new RequestRetryError('Request failed', statusCode, { + headers, + data: { + count: this.retryCount + } + }) + + this.onResponseStartWithRetry(controller, statusCode, headers, statusMessage, err) + return + } + + // Checkpoint for resume from where we left it + if (this.headersSent) { + // Only Partial Content 206 supposed to provide Content-Range, + // any other status code that partially consumed the payload + // should not be retried because it would result in downstream + // wrongly concatenate multiple responses. + if (statusCode !== 206 && (this.start > 0 || statusCode !== 200)) { + throw new RequestRetryError('server does not support the range header and the payload was partially consumed', statusCode, { + headers, + data: { count: this.retryCount } + }) + } + + const contentRange = parseRangeHeader(headers['content-range']) + // If no content range + if (!contentRange) { + // We always throw here as we want to indicate that we entred unexpected path + throw new RequestRetryError('Content-Range mismatch', statusCode, { + headers, + data: { count: this.retryCount } + }) + } + + // Let's start with a weak etag check + if (this.etag != null && this.etag !== headers.etag) { + // We always throw here as we want to indicate that we entred unexpected path + throw new RequestRetryError('ETag mismatch', statusCode, { + headers, + data: { count: this.retryCount } + }) + } + + validatePartialResponseContentLength(headers, contentRange, statusCode, this.retryCount) + + const { start, size, end = size ? size - 1 : null } = contentRange + + if (this.start !== start || (this.end != null && this.end !== end)) { + throw new RequestRetryError('Content-Range mismatch', statusCode, { + headers, + data: { count: this.retryCount } + }) + } + + return + } + + if (this.end == null) { + if (statusCode === 206) { + // First time we receive 206 + const range = parseRangeHeader(headers['content-range']) + + if (range == null) { + this.headersSent = true + this.handler.onResponseStart?.( + this.controllerProxy, + statusCode, + headers, + statusMessage + ) + return + } + + validatePartialResponseContentLength(headers, range, statusCode, this.retryCount) + + const { start, size, end = size ? size - 1 : null } = range + assert( + start != null && Number.isFinite(start), + 'content-range mismatch' + ) + assert(end != null && Number.isFinite(end), 'invalid content-length') + + this.start = start + this.end = end + } + + // We make our best to checkpoint the body for further range headers + if (this.end == null) { + const contentLength = headers['content-length'] + this.end = contentLength != null ? Number(contentLength) - 1 : null + } + + assert(Number.isFinite(this.start)) + assert( + this.end == null || Number.isFinite(this.end), + 'invalid content-length' + ) + + this.resume = true + this.etag = headers.etag != null ? headers.etag : null + + // Weak etags are not useful for comparison nor cache + // for instance not safe to assume if the response is byte-per-byte + // equal + if ( + this.etag != null && + this.etag[0] === 'W' && + this.etag[1] === '/' + ) { + this.etag = null + } + + this.headersSent = true + this.handler.onResponseStart?.( + this.controllerProxy, + statusCode, + headers, + statusMessage + ) + } else { + throw new RequestRetryError('Request failed', statusCode, { + headers, + data: { count: this.retryCount } + }) + } + } + + onResponseData (_controller, chunk) { + if (this.error) { + return + } + + this.start += chunk.length + + this.handler.onResponseData?.(this.controllerProxy, chunk) + } + + onResponseEnd (_controller, trailers) { + if (this.error && this.retryOpts.throwOnError) { + throw this.error + } + + if (!this.error) { + this.retryCount = 0 + return this.handler.onResponseEnd?.(this.controllerProxy, trailers) + } + + this.retry() + } + + retry () { + if (this.start !== 0) { + const headers = { range: `bytes=${this.start}-${this.end ?? ''}` } + + // Weak etag check - weak etags will make comparison algorithms never match + if (this.etag != null) { + headers['if-match'] = this.etag + } + + this.opts = { + ...this.opts, + headers: { + ...this.opts.headers, + ...headers + } + } + } + + try { + this.retryCountCheckpoint = this.retryCount + this.dispatch(this.opts, this) + } catch (err) { + this.handler.onResponseError?.(this.controllerProxy, err) + } + } + + onResponseError (controller, err) { + if (controller?.aborted || isDisturbed(this.opts.body) || (this.headersSent && !this.resume)) { + this.handler.onResponseError?.(this.controllerProxy, err) + return + } + + function shouldRetry (returnedErr) { + if (!returnedErr) { + this.retry() + return + } + + this.handler?.onResponseError?.(this.controllerProxy, returnedErr) + } + + // We reconcile in case of a mix between network errors + // and server error response + if (this.retryCount - this.retryCountCheckpoint > 0) { + // We count the difference between the last checkpoint and the current retry count + this.retryCount = + this.retryCountCheckpoint + + (this.retryCount - this.retryCountCheckpoint) + } else { + this.retryCount += 1 + } + + this.retryOpts.retry( + err, + { + state: { counter: this.retryCount }, + opts: { retryOptions: this.retryOpts, ...this.opts } + }, + shouldRetry.bind(this) + ) + } +} + +module.exports = RetryHandler diff --git a/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/handler/unwrap-handler.js b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/handler/unwrap-handler.js new file mode 100644 index 000000000..2949faea0 --- /dev/null +++ b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/handler/unwrap-handler.js @@ -0,0 +1,106 @@ +'use strict' + +const { parseHeaders } = require('../core/util') +const { InvalidArgumentError } = require('../core/errors') + +const kResume = Symbol('resume') + +class UnwrapController { + #paused = false + #reason = null + #aborted = false + #abort + + [kResume] = null + + rawHeaders = null + rawTrailers = null + + constructor (abort) { + this.#abort = abort + } + + pause () { + this.#paused = true + } + + resume () { + if (this.#paused) { + this.#paused = false + this[kResume]?.() + } + } + + abort (reason) { + if (!this.#aborted) { + this.#aborted = true + this.#reason = reason + this.#abort(reason) + } + } + + get aborted () { + return this.#aborted + } + + get reason () { + return this.#reason + } + + get paused () { + return this.#paused + } +} + +module.exports = class UnwrapHandler { + #handler + #controller + + constructor (handler) { + this.#handler = handler + } + + static unwrap (handler) { + // TODO (fix): More checks... + return !handler.onRequestStart ? handler : new UnwrapHandler(handler) + } + + onConnect (abort, context) { + this.#controller = new UnwrapController(abort) + this.#handler.onRequestStart?.(this.#controller, context) + } + + onResponseStarted () { + return this.#handler.onResponseStarted?.() + } + + onUpgrade (statusCode, rawHeaders, socket) { + this.#controller.rawHeaders = rawHeaders + this.#handler.onRequestUpgrade?.(this.#controller, statusCode, parseHeaders(rawHeaders), socket) + } + + onHeaders (statusCode, rawHeaders, resume, statusMessage) { + this.#controller[kResume] = resume + this.#controller.rawHeaders = rawHeaders + this.#handler.onResponseStart?.(this.#controller, statusCode, parseHeaders(rawHeaders), statusMessage) + return !this.#controller.paused + } + + onData (data) { + this.#handler.onResponseData?.(this.#controller, data) + return !this.#controller.paused + } + + onComplete (rawTrailers) { + this.#controller.rawTrailers = rawTrailers + this.#handler.onResponseEnd?.(this.#controller, parseHeaders(rawTrailers)) + } + + onError (err) { + if (!this.#handler.onResponseError) { + throw new InvalidArgumentError('invalid onError method') + } + + this.#handler.onResponseError?.(this.#controller, err) + } +} diff --git a/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/handler/wrap-handler.js b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/handler/wrap-handler.js new file mode 100644 index 000000000..01440f621 --- /dev/null +++ b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/handler/wrap-handler.js @@ -0,0 +1,105 @@ +'use strict' + +const { InvalidArgumentError } = require('../core/errors') + +module.exports = class WrapHandler { + #handler + + constructor (handler) { + this.#handler = handler + } + + static wrap (handler) { + // TODO (fix): More checks... + return handler.onRequestStart ? handler : new WrapHandler(handler) + } + + // Unwrap Interface + + onConnect (abort, context) { + return this.#handler.onConnect?.(abort, context) + } + + onResponseStarted () { + return this.#handler.onResponseStarted?.() + } + + onHeaders (statusCode, rawHeaders, resume, statusMessage) { + return this.#handler.onHeaders?.(statusCode, rawHeaders, resume, statusMessage) + } + + onUpgrade (statusCode, rawHeaders, socket) { + return this.#handler.onUpgrade?.(statusCode, rawHeaders, socket) + } + + onData (data) { + return this.#handler.onData?.(data) + } + + onComplete (trailers) { + return this.#handler.onComplete?.(trailers) + } + + onError (err) { + if (!this.#handler.onError) { + throw err + } + + return this.#handler.onError?.(err) + } + + // Wrap Interface + + onRequestStart (controller, context) { + this.#handler.onConnect?.((reason) => controller.abort(reason), context) + } + + onRequestUpgrade (controller, statusCode, headers, socket) { + const rawHeaders = [] + for (const [key, val] of Object.entries(headers)) { + rawHeaders.push(Buffer.from(key, 'latin1'), toRawHeaderValue(val)) + } + + this.#handler.onUpgrade?.(statusCode, rawHeaders, socket) + } + + onResponseStart (controller, statusCode, headers, statusMessage) { + const rawHeaders = [] + for (const [key, val] of Object.entries(headers)) { + rawHeaders.push(Buffer.from(key, 'latin1'), toRawHeaderValue(val)) + } + + if (this.#handler.onHeaders?.(statusCode, rawHeaders, () => controller.resume(), statusMessage) === false) { + controller.pause() + } + } + + onResponseData (controller, data) { + if (this.#handler.onData?.(data) === false) { + controller.pause() + } + } + + onResponseEnd (controller, trailers) { + const rawTrailers = [] + for (const [key, val] of Object.entries(trailers)) { + rawTrailers.push(Buffer.from(key, 'latin1'), toRawHeaderValue(val)) + } + + this.#handler.onComplete?.(rawTrailers) + } + + onResponseError (controller, err) { + if (!this.#handler.onError) { + throw new InvalidArgumentError('invalid onError method') + } + + this.#handler.onError?.(err) + } +} + +function toRawHeaderValue (value) { + return Array.isArray(value) + ? value.map((item) => Buffer.from(item, 'latin1')) + : Buffer.from(value, 'latin1') +} diff --git a/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/interceptor/cache.js b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/interceptor/cache.js new file mode 100644 index 000000000..149cf8904 --- /dev/null +++ b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/interceptor/cache.js @@ -0,0 +1,621 @@ +'use strict' + +const assert = require('node:assert') +const { Readable } = require('node:stream') +const util = require('../core/util') +const CacheHandler = require('../handler/cache-handler') +const MemoryCacheStore = require('../cache/memory-cache-store') +const CacheRevalidationHandler = require('../handler/cache-revalidation-handler') +const { assertCacheStore, assertCacheMethods, makeCacheKey, normalizeHeaders, parseCacheControlHeader, isInvalidOrWildcardVaryHeader } = require('../util/cache.js') +const { AbortError } = require('../core/errors.js') +const { parseHttpDate } = require('../util/date.js') + +/** + * @param {(string | RegExp)[] | undefined} origins + * @param {string} name + */ +function assertCacheOrigins (origins, name) { + if (origins === undefined) return + if (!Array.isArray(origins)) { + throw new TypeError(`expected ${name} to be an array or undefined, got ${typeof origins}`) + } + for (let i = 0; i < origins.length; i++) { + const origin = origins[i] + if (typeof origin !== 'string' && !(origin instanceof RegExp)) { + throw new TypeError(`expected ${name}[${i}] to be a string or RegExp, got ${typeof origin}`) + } + } +} + +const nop = () => {} + +function trimOWS (value) { + return value.replace(/^[\t ]+|[\t ]+$/g, '') +} + +function arrayIncludes (array, value) { + for (let i = 0; i < array.length; i++) { + if (array[i] === value) { + return true + } + } + + return false +} + +function hasPragmaNoCache (headers) { + const pragma = headers?.pragma + if (!pragma) { + return false + } + + const values = Array.isArray(pragma) ? pragma : [pragma] + for (let i = 0; i < values.length; i++) { + const value = values[i] + if (typeof value !== 'string') { + continue + } + + const directives = value.split(',') + for (let j = 0; j < directives.length; j++) { + if (trimOWS(directives[j]).toLowerCase() === 'no-cache') { + return true + } + } + } + + return false +} + +/** + * @typedef {(options: import('../../types/dispatcher.d.ts').default.DispatchOptions, handler: import('../../types/dispatcher.d.ts').default.DispatchHandler) => void} DispatchFn + */ + +/** + * @param {import('../../types/cache-interceptor.d.ts').default.GetResult} result + * @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives | undefined} cacheControlDirectives + * @param {import('../../types/dispatcher.d.ts').default.RequestOptions} opts + * @returns {boolean} + */ +function needsRevalidation (result, cacheControlDirectives, { headers = {} }) { + // Always revalidate requests with the no-cache request directive. + if (cacheControlDirectives?.['no-cache']) { + return true + } + + // Always revalidate requests with unqualified no-cache response directive. + if (result.cacheControlDirectives?.['no-cache'] && !Array.isArray(result.cacheControlDirectives['no-cache'])) { + return true + } + + // Always revalidate requests with conditional headers. + if (headers['if-modified-since'] || headers['if-none-match']) { + return true + } + + return false +} + +/** + * @param {import('../../types/cache-interceptor.d.ts').default.GetResult} result + * @param {import('../../types/cache-interceptor.d.ts').default.CacheOptions['type']} cacheType + * @returns {boolean} + */ +function staleResponseRequiresRevalidation (result, cacheType) { + return result.cacheControlDirectives?.['must-revalidate'] === true || + (cacheType === 'shared' && ( + result.cacheControlDirectives?.['proxy-revalidate'] === true || + // https://www.rfc-editor.org/rfc/rfc9111.html#section-5.2.2.10 + // s-maxage implies proxy-revalidate for shared caches. + result.cacheControlDirectives?.['s-maxage'] !== undefined + )) +} + +/** + * @param {import('../../types/cache-interceptor.d.ts').default.CacheOptions['type']} cacheType + * @param {import('../../types/header.d.ts').IncomingHttpHeaders} headers + * @returns {boolean} + */ +function revalidationResponseDisallowsCachedReuse (cacheType, headers) { + if ( + (headers.vary && isInvalidOrWildcardVaryHeader(headers.vary)) || + (cacheType === 'shared' && Object.hasOwn(headers, 'set-cookie')) + ) { + return true + } + + const cacheControl = headers['cache-control'] + if (!cacheControl) { + return false + } + + const cacheControlDirectives = parseCacheControlHeader(cacheControl) + return cacheControlDirectives['no-store'] === true || + (cacheType === 'shared' && cacheControlDirectives.private === true) +} + +function revalidationResponseUpdatesCacheControl (headers) { + return headers['cache-control'] !== undefined +} + +function deleteCachedValue (store, cacheKey) { + try { + store.delete(cacheKey)?.catch?.(nop) + } catch { + // Fail silently + } +} + +function getUsableLastModified (headers) { + const lastModified = headers?.['last-modified'] + if (typeof lastModified === 'string' && parseHttpDate(lastModified)) { + return lastModified + } +} + +function makeRevalidationHeaders (opts, result) { + const headers = { + ...opts.headers, + 'if-modified-since': getUsableLastModified(result.headers) ?? new Date(result.cachedAt).toUTCString() + } + + if (result.etag) { + headers['if-none-match'] = result.etag + } + + if (result.vary) { + for (const key in result.vary) { + if (result.vary[key] != null) { + headers[key] = result.vary[key] + } + } + } + + return headers +} + +/** + * @param {import('../../types/cache-interceptor.d.ts').default.GetResult} result + * @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives | undefined} cacheControlDirectives + * @param {import('../../types/cache-interceptor.d.ts').default.CacheOptions['type']} cacheType + * @returns {boolean} + */ +function isStale (result, cacheControlDirectives, cacheType) { + const now = Date.now() + if (now > result.staleAt) { + // Response is stale + if (!staleResponseRequiresRevalidation(result, cacheType) && cacheControlDirectives?.['max-stale']) { + // There's a threshold where we can serve stale responses, let's see if + // we're in it + // https://www.rfc-editor.org/rfc/rfc9111.html#name-max-stale + const gracePeriod = result.staleAt + (cacheControlDirectives['max-stale'] * 1000) + return now > gracePeriod + } + + return true + } + + if (cacheControlDirectives?.['min-fresh']) { + // https://www.rfc-editor.org/rfc/rfc9111.html#section-5.2.1.3 + + // At this point, staleAt is always > now + const timeLeftTillStale = result.staleAt - now + const threshold = cacheControlDirectives['min-fresh'] * 1000 + + return timeLeftTillStale <= threshold + } + + return false +} + +/** + * Check if we're within the stale-while-revalidate window for a stale response + * @param {import('../../types/cache-interceptor.d.ts').default.GetResult} result + * @param {import('../../types/cache-interceptor.d.ts').default.CacheOptions['type']} cacheType + * @returns {boolean} + */ +function withinStaleWhileRevalidateWindow (result, cacheType) { + const staleWhileRevalidate = result.cacheControlDirectives?.['stale-while-revalidate'] + if (!staleWhileRevalidate || staleResponseRequiresRevalidation(result, cacheType)) { + return false + } + + const now = Date.now() + const staleWhileRevalidateExpiry = result.staleAt + (staleWhileRevalidate * 1000) + return now <= staleWhileRevalidateExpiry +} + +/** + * @param {DispatchFn} dispatch + * @param {import('../../types/cache-interceptor.d.ts').default.CacheHandlerOptions} globalOpts + * @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} cacheKey + * @param {import('../../types/dispatcher.d.ts').default.DispatchHandler} handler + * @param {import('../../types/dispatcher.d.ts').default.RequestOptions} opts + * @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives | undefined} reqCacheControl + */ +function handleUncachedResponse ( + dispatch, + globalOpts, + cacheKey, + handler, + opts, + reqCacheControl +) { + if (reqCacheControl?.['only-if-cached']) { + let aborted = false + try { + if (typeof handler.onConnect === 'function') { + handler.onConnect(() => { + aborted = true + }) + + if (aborted) { + return + } + } + + if (typeof handler.onHeaders === 'function') { + handler.onHeaders(504, [], nop, 'Gateway Timeout') + if (aborted) { + return + } + } + + if (typeof handler.onComplete === 'function') { + handler.onComplete([]) + } + } catch (err) { + if (typeof handler.onError === 'function') { + handler.onError(err) + } + } + + return true + } + + return dispatch(opts, new CacheHandler(globalOpts, cacheKey, handler)) +} + +/** + * @param {import('../../types/dispatcher.d.ts').default.DispatchHandler} handler + * @param {import('../../types/dispatcher.d.ts').default.RequestOptions} opts + * @param {import('../../types/cache-interceptor.d.ts').default.GetResult} result + * @param {number} age + * @param {any} context + * @param {boolean} isStale + */ +function sendCachedValue (handler, opts, result, age, context, isStale) { + // TODO (perf): Readable.from path can be optimized... + const stream = util.isStream(result.body) + ? result.body + : Readable.from(result.body ?? []) + + assert(!stream.destroyed, 'stream should not be destroyed') + assert(!stream.readableDidRead, 'stream should not be readableDidRead') + + const controller = { + resume () { + stream.resume() + }, + pause () { + stream.pause() + }, + get paused () { + return stream.isPaused() + }, + get aborted () { + return stream.destroyed + }, + get reason () { + return stream.errored + }, + abort (reason) { + stream.destroy(reason ?? new AbortError()) + } + } + + stream + .on('error', function (err) { + if (!this.readableEnded) { + if (typeof handler.onResponseError === 'function') { + handler.onResponseError(controller, err) + } else { + throw err + } + } + }) + .on('close', function () { + if (!this.errored) { + handler.onResponseEnd?.(controller, {}) + } + }) + + handler.onRequestStart?.(controller, context) + + if (stream.destroyed) { + return + } + + // Add the age header + // https://www.rfc-editor.org/rfc/rfc9111.html#name-age + const headers = { ...result.headers, age: String(age) } + + if (isStale) { + // Add warning header + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Warning + headers.warning = '110 - "response is stale"' + } + + handler.onResponseStart?.(controller, result.statusCode, headers, result.statusMessage) + + if (opts.method === 'HEAD') { + stream.destroy() + } else { + stream.on('data', function (chunk) { + handler.onResponseData?.(controller, chunk) + }) + } +} + +/** + * @param {DispatchFn} dispatch + * @param {import('../../types/cache-interceptor.d.ts').default.CacheHandlerOptions} globalOpts + * @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} cacheKey + * @param {import('../../types/dispatcher.d.ts').default.DispatchHandler} handler + * @param {import('../../types/dispatcher.d.ts').default.RequestOptions} opts + * @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives | undefined} reqCacheControl + * @param {import('../../types/cache-interceptor.d.ts').default.GetResult | undefined} result + */ +function handleResult ( + dispatch, + globalOpts, + cacheKey, + handler, + opts, + reqCacheControl, + result +) { + if (!result) { + return handleUncachedResponse(dispatch, globalOpts, cacheKey, handler, opts, reqCacheControl) + } + + // Shared stores may outlive the Undici version that wrote them. Do not + // re-serve a Set-Cookie header from an existing shared-cache entry. + if (globalOpts.type === 'shared' && Object.hasOwn(result.headers, 'set-cookie')) { + if (util.isStream(result.body)) { + result.body.on('error', nop).destroy() + } + + deleteCachedValue(globalOpts.store, cacheKey) + return handleUncachedResponse(dispatch, globalOpts, cacheKey, handler, opts, reqCacheControl) + } + + const now = Date.now() + if (now > result.deleteAt) { + // Response is expired, cache store shouldn't have given this to us + return dispatch(opts, new CacheHandler(globalOpts, cacheKey, handler)) + } + + const age = Math.round((now - result.cachedAt) / 1000) + const requestMaxAgeExpired = reqCacheControl?.['max-age'] !== undefined && age >= reqCacheControl['max-age'] + + const stale = requestMaxAgeExpired || isStale(result, reqCacheControl, globalOpts.type) + const revalidate = requestMaxAgeExpired || needsRevalidation(result, reqCacheControl, opts) + + // Check if the response is stale + if (stale || revalidate) { + if (util.isStream(opts.body) && util.bodyLength(opts.body) !== 0) { + // If body is a stream we can't revalidate... + // TODO (fix): This could be less strict... + return dispatch(opts, new CacheHandler(globalOpts, cacheKey, handler)) + } + + // RFC 5861: If we're within stale-while-revalidate window, serve stale immediately + // and revalidate in background, unless immediate revalidation is necessary + if (!revalidate && withinStaleWhileRevalidateWindow(result, globalOpts.type)) { + // Serve stale response immediately + sendCachedValue(handler, opts, result, age, null, true) + + // Start background revalidation (fire-and-forget) + queueMicrotask(() => { + const headers = makeRevalidationHeaders(opts, result) + + // Background revalidation - update cache if we get new data + dispatch( + { + ...opts, + headers + }, + new CacheHandler(globalOpts, cacheKey, { + // Silent handler that just updates the cache + onRequestStart () {}, + onRequestUpgrade () {}, + onResponseStart () {}, + onResponseData () {}, + onResponseEnd () {}, + onResponseError () {} + }) + ) + }) + + return true + } + + let withinStaleIfErrorThreshold = false + if (!staleResponseRequiresRevalidation(result, globalOpts.type)) { + const staleIfErrorExpiry = result.cacheControlDirectives['stale-if-error'] ?? reqCacheControl?.['stale-if-error'] + if (staleIfErrorExpiry) { + withinStaleIfErrorThreshold = now < (result.staleAt + (staleIfErrorExpiry * 1000)) + } + } + + const headers = makeRevalidationHeaders(opts, result) + + // We need to revalidate the response + return dispatch( + { + ...opts, + headers + }, + new CacheRevalidationHandler( + (success, context, statusCode, headers) => { + if (success) { + if (statusCode === 304) { + if (revalidationResponseDisallowsCachedReuse(globalOpts.type, headers)) { + if (util.isStream(result.body)) { + result.body.on('error', nop).destroy() + } + + deleteCachedValue(globalOpts.store, cacheKey) + return dispatch(opts, new CacheHandler(globalOpts, cacheKey, handler)) + } + + if (revalidationResponseUpdatesCacheControl(headers)) { + deleteCachedValue(globalOpts.store, cacheKey) + } + } + + // TODO: successful revalidation should be considered fresh (not give stale warning). + sendCachedValue(handler, opts, result, age, context, stale) + } else if (util.isStream(result.body)) { + result.body.on('error', nop).destroy() + } + }, + new CacheHandler(globalOpts, cacheKey, handler), + withinStaleIfErrorThreshold + ) + ) + } + + // Dump request body. + if (util.isStream(opts.body)) { + opts.body.on('error', nop).destroy() + } + + sendCachedValue(handler, opts, result, age, null, false) +} + +/** + * @param {import('../../types/cache-interceptor.d.ts').default.CacheOptions} [opts] + * @returns {import('../../types/dispatcher.d.ts').default.DispatcherComposeInterceptor} + */ +module.exports = (opts = {}) => { + const { + store = new MemoryCacheStore(), + methods = ['GET'], + cacheByDefault = undefined, + type = 'shared', + origins = undefined + } = opts + + if (typeof opts !== 'object' || opts === null) { + throw new TypeError(`expected type of opts to be an Object, got ${opts === null ? 'null' : typeof opts}`) + } + + assertCacheStore(store, 'opts.store') + assertCacheMethods(methods, 'opts.methods') + assertCacheOrigins(origins, 'opts.origins') + + if (typeof cacheByDefault !== 'undefined' && typeof cacheByDefault !== 'number') { + throw new TypeError(`expected opts.cacheByDefault to be number or undefined, got ${typeof cacheByDefault}`) + } + + if (typeof type !== 'undefined' && type !== 'shared' && type !== 'private') { + throw new TypeError(`expected opts.type to be shared, private, or undefined, got ${typeof type}`) + } + + const globalOpts = { + store, + methods, + cacheByDefault, + type + } + + const safeMethodsToNotCache = [] + for (let i = 0; i < util.safeHTTPMethods.length; i++) { + const method = util.safeHTTPMethods[i] + if (!arrayIncludes(methods, method)) { + safeMethodsToNotCache.push(method) + } + } + + return dispatch => { + return (opts, handler) => { + if (!opts.origin || arrayIncludes(safeMethodsToNotCache, opts.method)) { + // Not a method we want to cache or we don't have the origin, skip + return dispatch(opts, handler) + } + + // Check if origin is in whitelist + if (origins !== undefined) { + const requestOrigin = opts.origin.toString().toLowerCase() + let isAllowed = false + + for (let i = 0; i < origins.length; i++) { + const allowed = origins[i] + if (typeof allowed === 'string') { + if (allowed.toLowerCase() === requestOrigin) { + isAllowed = true + break + } + } else if (allowed.test(requestOrigin)) { + isAllowed = true + break + } + } + + if (!isAllowed) { + return dispatch(opts, handler) + } + } + + opts = { + ...opts, + headers: normalizeHeaders(opts) + } + + const reqCacheControl = opts.headers?.['cache-control'] + ? parseCacheControlHeader(opts.headers['cache-control']) + : hasPragmaNoCache(opts.headers) + ? { 'no-cache': true } + : undefined + + if (reqCacheControl?.['no-store']) { + return dispatch(opts, handler) + } + + /** + * @type {import('../../types/cache-interceptor.d.ts').default.CacheKey} + */ + const cacheKey = makeCacheKey(opts) + + if (!arrayIncludes(util.safeHTTPMethods, opts.method)) { + return dispatch(opts, new CacheHandler(globalOpts, cacheKey, handler)) + } + + const result = store.get(cacheKey) + + if (result && typeof result.then === 'function') { + return result + .then(result => handleResult(dispatch, + globalOpts, + cacheKey, + handler, + opts, + reqCacheControl, + result + )) + } else { + return handleResult( + dispatch, + globalOpts, + cacheKey, + handler, + opts, + reqCacheControl, + result + ) + } + } + } +} diff --git a/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/interceptor/decompress.js b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/interceptor/decompress.js new file mode 100644 index 000000000..02d6ebc06 --- /dev/null +++ b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/interceptor/decompress.js @@ -0,0 +1,551 @@ +'use strict' + +const { createInflate, createGunzip, createBrotliDecompress, createZstdDecompress } = require('node:zlib') +const { pipeline, Transform: TransformStream } = require('node:stream') +const { InvalidArgumentError, ResponseExceededMaxSizeError } = require('../core/errors') +const DecoratorHandler = require('../handler/decorator-handler') +const { runtimeFeatures } = require('../util/runtime-features') + +/** @typedef {import('node:stream').Transform} Transform */ +/** @typedef {import('node:stream').Transform} Controller */ +/** @typedef {Transform&import('node:zlib').Zlib} DecompressorStream */ + +class DecompressController { + #onPause + #onResume + #onAbort + #paused = false + + constructor (onPause, onResume, onAbort) { + this.#onPause = onPause + this.#onResume = onResume + this.#onAbort = onAbort + this.target = null + } + + pause () { + if (this.#paused) { + return + } + + this.#paused = true + this.#onPause() + } + + resume () { + if (!this.#paused) { + return + } + + this.#paused = false + this.#onResume() + } + + abort (reason) { + this.target?.abort(reason) + this.#onAbort(reason) + } + + get paused () { return this.#paused } + get aborted () { return this.target?.aborted ?? false } + get reason () { return this.target?.reason ?? null } + get rawHeaders () { return this.target?.rawHeaders ?? null } + set rawHeaders (value) { + if (this.target) { + this.target.rawHeaders = value + } + } + + get rawTrailers () { return this.target?.rawTrailers ?? null } + set rawTrailers (value) { + if (this.target) { + this.target.rawTrailers = value + } + } +} + +/** @type {Record DecompressorStream>} */ +const supportedEncodings = { + gzip: createGunzip, + 'x-gzip': createGunzip, + br: createBrotliDecompress, + deflate: createInflate, + compress: createInflate, + 'x-compress': createInflate, + ...(runtimeFeatures.has('zstd') ? { zstd: createZstdDecompress } : {}) +} + +const defaultSkipStatusCodes = /** @type {const} */ ([204, 304]) +const defaultMaxSize = 0 + +/** + * Limits the output of one stage in a decompression chain. + * @param {number} maxSize - Maximum output size in bytes + * @returns {Transform} + */ +function createMaxSizeLimiter (maxSize) { + let size = 0 + + return new TransformStream({ + transform (chunk, _encoding, callback) { + const decompressedSize = size + chunk.length + if (decompressedSize > maxSize) { + callback(new ResponseExceededMaxSizeError( + `Decompressed response size (${decompressedSize}) exceeded maxSize (${maxSize})` + )) + return + } + + size = decompressedSize + callback(null, chunk) + } + }) +} + +let warningEmitted = /** @type {boolean} */ (false) + +/** + * @typedef {Object} DecompressHandlerOptions + * @property {number[]|Readonly} [skipStatusCodes=[204, 304]] - List of status codes to skip decompression for + * @property {boolean} [skipErrorResponses] - Whether to skip decompression for error responses (status codes >= 400) + * @property {number} [maxSize=0] - Maximum decompressed response size in bytes. 0 disables the limit + */ + +class DecompressHandler extends DecoratorHandler { + /** @type {Transform[]} */ + #decompressors = [] + /** @type {Record | undefined} */ + #trailers + /** @type {Readonly} */ + #skipStatusCodes + /** @type {boolean} */ + #skipErrorResponses + /** @type {number} */ + #maxSize + /** @type {number} */ + #decompressedSize = 0 + /** @type {boolean} */ + #terminated = false + /** @type {boolean} */ + #inputEnded = false + /** @type {boolean} */ + #inputBackpressured = false + /** @type {boolean} */ + #upstreamPaused = false + /** @type {boolean} */ + #draining = false + /** @type {boolean} */ + #drainRequested = false + /** @type {boolean} */ + #completionPending = false + /** @type {DecompressorStream | undefined} */ + #finalDecompressor + /** @type {DecompressController} */ + #controller + + constructor (handler, { skipStatusCodes = defaultSkipStatusCodes, skipErrorResponses = true, maxSize = defaultMaxSize } = {}) { + if (!Number.isSafeInteger(maxSize) || maxSize < 0) { + throw new InvalidArgumentError('maxSize must be a non-negative integer') + } + + super(handler) + this.#skipStatusCodes = skipStatusCodes + this.#skipErrorResponses = skipErrorResponses + this.#maxSize = maxSize + this.#controller = new DecompressController( + () => this.#onDownstreamPause(), + () => this.#onDownstreamResume(), + reason => { + if (this.#inputEnded && !this.#terminated) { + this.onResponseError(this.#controller, reason) + } + } + ) + } + + #onDownstreamPause () { + this.#pauseUpstream() + } + + #onDownstreamResume () { + const drainWasDeferred = this.#draining + this.#drainOutput() + if (!drainWasDeferred) { + this.#resumeUpstreamIfNeeded() + this.#finishIfReady() + } + } + + #pauseUpstream () { + if (!this.#upstreamPaused && !this.#terminated) { + this.#upstreamPaused = true + this.#controller.target?.pause() + } + } + + #resumeUpstreamIfNeeded () { + if (this.#upstreamPaused && !this.#controller.paused && !this.#inputBackpressured) { + this.#upstreamPaused = false + if (!this.#inputEnded) { + this.#controller.target?.resume() + } + } + } + + #drainOutput () { + if (this.#terminated || this.#controller.paused || !this.#finalDecompressor) { + return + } + + if (this.#draining) { + this.#drainRequested = true + return + } + + this.#draining = true + try { + do { + this.#drainRequested = false + let chunk + while (!this.#terminated && !this.#controller.paused && (chunk = this.#finalDecompressor.read()) !== null) { + if (this.#maxSize > 0) { + const decompressedSize = this.#decompressedSize + chunk.length + if (decompressedSize > this.#maxSize) { + this.#fail(new ResponseExceededMaxSizeError( + `Decompressed response size (${decompressedSize}) exceeded maxSize (${this.#maxSize})` + )) + return + } + + this.#decompressedSize = decompressedSize + } + + const result = super.onResponseData(this.#controller, chunk) + if (result === false && !this.#controller.paused) { + this.#controller.pause() + } + } + } while (this.#drainRequested && !this.#terminated && !this.#controller.paused) + } finally { + this.#draining = false + } + + this.#resumeUpstreamIfNeeded() + this.#finishIfReady() + } + + #finishIfReady () { + if (this.#terminated || !this.#completionPending || this.#controller.paused || this.#draining) { + return + } + + this.#terminated = true + this.#cleanupDecompressors() + super.onResponseEnd(this.#controller, this.#trailers) + } + + #onDecompressionEnd () { + if (this.#terminated) { + return + } + + this.#completionPending = true + this.#drainOutput() + this.#finishIfReady() + } + + /** + * Determines if decompression should be skipped based on encoding and status code + * @param {string} contentEncoding - Content-Encoding header value + * @param {number} statusCode - HTTP status code of the response + * @returns {boolean} - True if decompression should be skipped + */ + #shouldSkipDecompression (contentEncoding, statusCode) { + if (!contentEncoding || statusCode < 200) return true + if (this.#skipStatusCodes.includes(statusCode)) return true + if (this.#skipErrorResponses && statusCode >= 400) return true + return false + } + + /** + * Creates a chain of decompressors for multiple content encodings + * + * @param {string} encodings - Comma-separated list of content encodings + * @returns {Array} - Array of decompressor and limiting streams + * @throws {Error} - If the number of content-encodings exceeds the maximum allowed + */ + #createDecompressionChain (encodings) { + const parts = encodings.split(',') + + // Limit the number of content-encodings to prevent resource exhaustion. + // CVE fix similar to urllib3 (GHSA-gm62-xv2j-4w53) and curl (CVE-2022-32206). + const maxContentEncodings = 5 + if (parts.length > maxContentEncodings) { + throw new Error(`too many content-encodings in response: ${parts.length}, maximum allowed is ${maxContentEncodings}`) + } + + /** @type {DecompressorStream[]} */ + const decompressors = [] + + for (let i = parts.length - 1; i >= 0; i--) { + const encoding = parts[i].trim() + if (!encoding) continue + + if (!supportedEncodings[encoding]) { + decompressors.length = 0 // Clear if unsupported encoding + return decompressors // Unsupported encoding + } + + decompressors.push(supportedEncodings[encoding]()) + } + + if (decompressors.length < 2) { + return decompressors + } + + /** @type {Transform[]} */ + const streams = [] + for (let i = 0; i < decompressors.length; i++) { + streams.push(decompressors[i]) + if (i < decompressors.length - 1 && this.#maxSize > 0) { + streams.push(createMaxSizeLimiter(this.#maxSize)) + } + } + + return streams + } + + /** + * Stops decompression and reports an error. + * @param {Error} error - The decompression error + * @returns {void} + */ + #fail (error) { + if (this.#terminated) { + return + } + + if (this.#inputEnded) { + // The request is already marked complete once the compressed input ends, + // so controller.abort() can no longer propagate decoder flush errors. + this.onResponseError(this.#controller, error) + } else { + this.#controller.abort(error) + } + } + + /** + * Sets up event handlers for the final decompressor stream. + * @param {DecompressorStream} decompressor - The decompressor stream + * @returns {void} + */ + #setupDecompressorEvents (decompressor) { + this.#finalDecompressor = decompressor + decompressor.on('readable', () => this.#drainOutput()) + decompressor.on('error', (error) => this.#fail(error)) + } + + /** + * Sets up event handling for a single decompressor + * @returns {void} + */ + #setupSingleDecompressor () { + const decompressor = this.#decompressors[0] + this.#setupDecompressorEvents(decompressor) + + decompressor.on('end', () => this.#onDecompressionEnd()) + } + + /** + * Sets up event handling for multiple chained decompressors using pipeline + * @returns {void} + */ + #setupMultipleDecompressors () { + const lastDecompressor = this.#decompressors[this.#decompressors.length - 1] + this.#setupDecompressorEvents(lastDecompressor) + + pipeline(this.#decompressors, (err) => { + if (this.#terminated) { + return + } + + if (err) { + this.#fail(err) + return + } + + this.#onDecompressionEnd() + }) + } + + #setupInputBackpressure () { + const decompressor = this.#decompressors[0] + decompressor.on('drain', () => { + if (this.#terminated) { + return + } + + this.#inputBackpressured = false + if (!this.#controller.paused) { + this.#drainOutput() + this.#resumeUpstreamIfNeeded() + } + }) + } + + /** + * Cleans up decompressor references to prevent memory leaks + * @returns {void} + */ + #cleanupDecompressors () { + this.#decompressors.length = 0 + this.#finalDecompressor = undefined + } + + onRequestStart (controller, context) { + this.#controller.target = controller + return super.onRequestStart(this.#controller, context) + } + + onRequestUpgrade (controller, statusCode, headers, socket) { + return super.onRequestUpgrade(this.#controller, statusCode, headers, socket) + } + + /** + * @param {Controller} controller + * @param {number} statusCode + * @param {Record} headers + * @param {string} statusMessage + * @returns {void} + */ + onResponseStart (controller, statusCode, headers, statusMessage) { + const contentEncoding = headers['content-encoding'] + + // If content encoding is not supported or status code is in skip list + if (this.#shouldSkipDecompression(contentEncoding, statusCode)) { + return super.onResponseStart(this.#controller, statusCode, headers, statusMessage) + } + + const decompressors = this.#createDecompressionChain(contentEncoding.toLowerCase()) + + if (decompressors.length === 0) { + this.#cleanupDecompressors() + return super.onResponseStart(this.#controller, statusCode, headers, statusMessage) + } + + this.#decompressors = decompressors + + // Remove compression headers since we're decompressing + const { 'content-encoding': _, 'content-length': __, ...newHeaders } = headers + + if (this.#controller.rawHeaders) { + const rawHeaders = this.#controller.rawHeaders + + if (Array.isArray(rawHeaders)) { + const filteredHeaders = [] + for (let i = 0; i < rawHeaders.length; i += 2) { + const headerName = rawHeaders[i] + const name = Buffer.isBuffer(headerName) ? headerName.toString('latin1') : `${headerName}` + const lowerName = name.toLowerCase() + + if (lowerName === 'content-encoding' || lowerName === 'content-length') { + continue + } + + filteredHeaders.push(rawHeaders[i], rawHeaders[i + 1]) + } + rawHeaders.splice(0, rawHeaders.length, ...filteredHeaders) + } else if (typeof rawHeaders === 'object') { + for (const name of Object.keys(rawHeaders)) { + const lowerName = name.toLowerCase() + if (lowerName === 'content-encoding' || lowerName === 'content-length') { + delete rawHeaders[name] + } + } + } + } + + this.#setupInputBackpressure() + if (this.#decompressors.length === 1) { + this.#setupSingleDecompressor() + } else { + this.#setupMultipleDecompressors() + } + + return super.onResponseStart(this.#controller, statusCode, newHeaders, statusMessage) + } + + /** + * @param {Controller} controller + * @param {Buffer} chunk + * @returns {void} + */ + onResponseData (controller, chunk) { + if (this.#decompressors.length > 0) { + if (!this.#decompressors[0].write(chunk)) { + this.#inputBackpressured = true + this.#pauseUpstream() + } + return + } + return super.onResponseData(this.#controller, chunk) + } + + /** + * @param {Controller} controller + * @param {Record | undefined} trailers + * @returns {void} + */ + onResponseEnd (controller, trailers) { + if (this.#decompressors.length > 0) { + this.#inputEnded = true + this.#trailers = trailers + this.#decompressors[0].end() + return + } + return super.onResponseEnd(this.#controller, trailers) + } + + /** + * @param {Controller} controller + * @param {Error} err + * @returns {void} + */ + onResponseError (controller, err) { + if (this.#terminated) { + return + } + + this.#terminated = true + for (const decompressor of this.#decompressors) { + decompressor.destroy() + } + this.#cleanupDecompressors() + super.onResponseError(this.#controller, err) + } +} + +/** + * Creates a decompression interceptor for HTTP responses + * @param {DecompressHandlerOptions} [options] - Options for the interceptor + * @returns {Function} - Interceptor function + */ +function createDecompressInterceptor (options = {}) { + // Emit experimental warning only once + if (!warningEmitted) { + process.emitWarning( + 'DecompressInterceptor is experimental and subject to change', + 'ExperimentalWarning' + ) + warningEmitted = true + } + + return (dispatch) => { + return (opts, handler) => { + const decompressHandler = new DecompressHandler(handler, options) + return dispatch(opts, decompressHandler) + } + } +} + +module.exports = createDecompressInterceptor diff --git a/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/interceptor/deduplicate.js b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/interceptor/deduplicate.js new file mode 100644 index 000000000..e81525ac5 --- /dev/null +++ b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/interceptor/deduplicate.js @@ -0,0 +1,117 @@ +'use strict' + +const diagnosticsChannel = require('node:diagnostics_channel') +const util = require('../core/util') +const DeduplicationHandler = require('../handler/deduplication-handler') +const { normalizeHeaders, makeCacheKey, makeDeduplicationKey } = require('../util/cache.js') + +const pendingRequestsChannel = diagnosticsChannel.channel('undici:request:pending-requests') + +/** + * @param {import('../../types/interceptors.d.ts').default.DeduplicateInterceptorOpts} [opts] + * @returns {import('../../types/dispatcher.d.ts').default.DispatcherComposeInterceptor} + */ +module.exports = (opts = {}) => { + const { + methods = ['GET'], + skipHeaderNames = [], + excludeHeaderNames = [], + maxBufferSize = 5 * 1024 * 1024 + } = opts + + if (typeof opts !== 'object' || opts === null) { + throw new TypeError(`expected type of opts to be an Object, got ${opts === null ? 'null' : typeof opts}`) + } + + if (!Array.isArray(methods)) { + throw new TypeError(`expected opts.methods to be an array, got ${typeof methods}`) + } + + for (const method of methods) { + if (!util.safeHTTPMethods.includes(method)) { + throw new TypeError(`expected opts.methods to only contain safe HTTP methods, got ${method}`) + } + } + + if (!Array.isArray(skipHeaderNames)) { + throw new TypeError(`expected opts.skipHeaderNames to be an array, got ${typeof skipHeaderNames}`) + } + + if (!Array.isArray(excludeHeaderNames)) { + throw new TypeError(`expected opts.excludeHeaderNames to be an array, got ${typeof excludeHeaderNames}`) + } + + if (!Number.isFinite(maxBufferSize) || maxBufferSize <= 0) { + throw new TypeError(`expected opts.maxBufferSize to be a positive finite number, got ${maxBufferSize}`) + } + + // Convert to lowercase Set for case-insensitive header matching + const skipHeaderNamesSet = new Set(skipHeaderNames.map(name => name.toLowerCase())) + + // Convert to lowercase Set for case-insensitive header exclusion from deduplication key + const excludeHeaderNamesSet = new Set(excludeHeaderNames.map(name => name.toLowerCase())) + + /** + * Map of pending requests for deduplication + * @type {Map} + */ + const pendingRequests = new Map() + + return dispatch => { + return (opts, handler) => { + if (!opts.origin || methods.includes(opts.method) === false) { + return dispatch(opts, handler) + } + + opts = { + ...opts, + headers: normalizeHeaders(opts) + } + + // Skip deduplication if request contains any of the specified headers + if (skipHeaderNamesSet.size > 0) { + for (const headerName of Object.keys(opts.headers)) { + if (skipHeaderNamesSet.has(headerName.toLowerCase())) { + return dispatch(opts, handler) + } + } + } + + const cacheKey = makeCacheKey(opts) + const dedupeKey = makeDeduplicationKey(cacheKey, excludeHeaderNamesSet) + + // Check if there's already a pending request for this key + const pendingHandler = pendingRequests.get(dedupeKey) + if (pendingHandler) { + // Add this handler to the waiting list when safe. + // If body streaming has already started, this request must be sent independently. + if (pendingHandler.addWaitingHandler(handler)) { + return true + } + + return dispatch(opts, handler) + } + + // Create a new deduplication handler + const deduplicationHandler = new DeduplicationHandler( + handler, + () => { + // Clean up when request completes + pendingRequests.delete(dedupeKey) + if (pendingRequestsChannel.hasSubscribers) { + pendingRequestsChannel.publish({ size: pendingRequests.size, key: dedupeKey, type: 'removed' }) + } + }, + maxBufferSize + ) + + // Register the pending request + pendingRequests.set(dedupeKey, deduplicationHandler) + if (pendingRequestsChannel.hasSubscribers) { + pendingRequestsChannel.publish({ size: pendingRequests.size, key: dedupeKey, type: 'added' }) + } + + return dispatch(opts, deduplicationHandler) + } + } +} diff --git a/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/interceptor/dns.js b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/interceptor/dns.js new file mode 100644 index 000000000..38cb2fcbe --- /dev/null +++ b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/interceptor/dns.js @@ -0,0 +1,571 @@ +'use strict' +const { isIP } = require('node:net') +const { lookup } = require('node:dns') +const DecoratorHandler = require('../handler/decorator-handler') +const { InvalidArgumentError, InformationalError } = require('../core/errors') +const maxInt = Math.pow(2, 31) - 1 + +function hasSafeIterator (headers) { + const prototype = Object.getPrototypeOf(headers) + const ownIterator = Object.prototype.hasOwnProperty.call(headers, Symbol.iterator) + return ownIterator || (prototype != null && prototype !== Object.prototype && typeof headers[Symbol.iterator] === 'function') +} + +function isHostHeader (key) { + return typeof key === 'string' && key.toLowerCase() === 'host' +} + +function normalizeHeaders (headers) { + if (headers == null) { + return null + } + + if (Array.isArray(headers)) { + if (headers.length === 0 || !Array.isArray(headers[0])) { + return headers + } + + const normalized = [] + for (const header of headers) { + if (Array.isArray(header) && header.length === 2) { + normalized.push(header[0], header[1]) + } else { + normalized.push(header) + } + } + + return normalized + } + + if (typeof headers === 'object' && hasSafeIterator(headers)) { + const normalized = [] + for (const header of headers) { + if (Array.isArray(header) && header.length === 2) { + normalized.push(header[0], header[1]) + } else { + normalized.push(header) + } + } + + return normalized + } + + return headers +} + +function hasHostHeader (headers) { + if (headers == null) { + return false + } + + if (Array.isArray(headers)) { + if (headers.length === 0) { + return false + } + + for (let i = 0; i < headers.length; i += 2) { + if (isHostHeader(headers[i])) { + return true + } + } + + return false + } + + if (typeof headers === 'object') { + for (const key in headers) { + if (isHostHeader(key)) { + return true + } + } + } + + return false +} + +function withHostHeader (host, headers) { + const normalizedHeaders = normalizeHeaders(headers) + + if (hasHostHeader(normalizedHeaders)) { + return normalizedHeaders + } + + if (Array.isArray(normalizedHeaders)) { + return ['host', host, ...normalizedHeaders] + } + + if (normalizedHeaders && typeof normalizedHeaders === 'object') { + return { + host, + ...normalizedHeaders + } + } + + return { host } +} + +class DNSStorage { + #maxItems = 0 + #records = new Map() + + constructor (opts) { + this.#maxItems = opts.maxItems + } + + get size () { + return this.#records.size + } + + get (hostname) { + return this.#records.get(hostname) ?? null + } + + set (hostname, records) { + this.#records.set(hostname, records) + } + + delete (hostname) { + this.#records.delete(hostname) + } + + // Delegate to storage decide can we do more lookups or not + full () { + return this.size >= this.#maxItems + } +} + +class DNSInstance { + #maxTTL = 0 + #maxItems = 0 + dualStack = true + affinity = null + lookup = null + pick = null + storage = null + + constructor (opts) { + this.#maxTTL = opts.maxTTL + this.#maxItems = opts.maxItems + this.dualStack = opts.dualStack + this.affinity = opts.affinity + this.lookup = opts.lookup ?? this.#defaultLookup + this.pick = opts.pick ?? this.#defaultPick + this.storage = opts.storage ?? new DNSStorage(opts) + } + + runLookup (origin, opts, cb) { + const ips = this.storage.get(origin.hostname) + + // If full, we just return the origin + if (ips == null && this.storage.full()) { + cb(null, origin) + return + } + + const newOpts = { + affinity: this.affinity, + dualStack: this.dualStack, + lookup: this.lookup, + pick: this.pick, + ...opts.dns, + maxTTL: this.#maxTTL, + maxItems: this.#maxItems + } + + // If no IPs we lookup + if (ips == null) { + this.lookup(origin, newOpts, (err, addresses) => { + if (err || addresses == null || addresses.length === 0) { + cb(err ?? new InformationalError('No DNS entries found')) + return + } + + this.setRecords(origin, addresses) + const records = this.storage.get(origin.hostname) + + const ip = this.pick( + origin, + records, + newOpts.affinity + ) + + let port + if (typeof ip.port === 'number') { + port = `:${ip.port}` + } else if (origin.port !== '') { + port = `:${origin.port}` + } else { + port = '' + } + + cb( + null, + new URL(`${origin.protocol}//${ + ip.family === 6 ? `[${ip.address}]` : ip.address + }${port}`) + ) + }) + } else { + // If there's IPs we pick + const ip = this.pick( + origin, + ips, + newOpts.affinity + ) + + // If no IPs we lookup - deleting old records + if (ip == null) { + this.storage.delete(origin.hostname) + this.runLookup(origin, opts, cb) + return + } + + let port + if (typeof ip.port === 'number') { + port = `:${ip.port}` + } else if (origin.port !== '') { + port = `:${origin.port}` + } else { + port = '' + } + + cb( + null, + new URL(`${origin.protocol}//${ + ip.family === 6 ? `[${ip.address}]` : ip.address + }${port}`) + ) + } + } + + #defaultLookup (origin, opts, cb) { + lookup( + origin.hostname, + { + all: true, + family: this.dualStack === false ? this.affinity : 0, + order: 'ipv4first' + }, + (err, addresses) => { + if (err) { + return cb(err) + } + + const results = new Map() + + for (const addr of addresses) { + // On linux we found duplicates, we attempt to remove them with + // the latest record + results.set(`${addr.address}:${addr.family}`, addr) + } + + cb(null, results.values()) + } + ) + } + + #defaultPick (origin, hostnameRecords, affinity) { + let ip = null + const { records, offset } = hostnameRecords + + let family + if (this.dualStack) { + if (affinity == null) { + // Balance between ip families + if (offset == null || offset === maxInt) { + hostnameRecords.offset = 0 + affinity = 4 + } else { + hostnameRecords.offset++ + affinity = (hostnameRecords.offset & 1) === 1 ? 6 : 4 + } + } + + if (records[affinity] != null && records[affinity].ips.length > 0) { + family = records[affinity] + } else { + family = records[affinity === 4 ? 6 : 4] + } + } else { + family = records[affinity] + } + + // If no IPs we return null + if (family == null || family.ips.length === 0) { + return ip + } + + if (family.offset == null || family.offset === maxInt) { + family.offset = 0 + } else { + family.offset++ + } + + const position = family.offset % family.ips.length + ip = family.ips[position] ?? null + + if (ip == null) { + return ip + } + + if (Date.now() - ip.timestamp > ip.ttl) { // record TTL is already in ms + // We delete expired records + // It is possible that they have different TTL, so we manage them individually + family.ips.splice(position, 1) + return this.pick(origin, hostnameRecords, affinity) + } + + return ip + } + + pickFamily (origin, ipFamily) { + const records = this.storage.get(origin.hostname)?.records + if (!records) { + return null + } + + const family = records[ipFamily] + if (!family) { + return null + } + + if (family.offset == null || family.offset === maxInt) { + family.offset = 0 + } else { + family.offset++ + } + + const position = family.offset % family.ips.length + const ip = family.ips[position] ?? null + if (ip == null) { + return ip + } + + if (Date.now() - ip.timestamp > ip.ttl) { // record TTL is already in ms + // We delete expired records + // It is possible that they have different TTL, so we manage them individually + family.ips.splice(position, 1) + } + + return ip + } + + setRecords (origin, addresses) { + const timestamp = Date.now() + const records = { records: { 4: null, 6: null } } + let minTTL = this.#maxTTL + for (const record of addresses) { + record.timestamp = timestamp + if (typeof record.ttl === 'number') { + // The record TTL is expected to be in ms + record.ttl = Math.min(record.ttl, this.#maxTTL) + minTTL = Math.min(minTTL, record.ttl) + } else { + record.ttl = this.#maxTTL + } + + const familyRecords = records.records[record.family] ?? { ips: [] } + + familyRecords.ips.push(record) + records.records[record.family] = familyRecords + } + + // We provide a default TTL if external storage will be used without TTL per record-level support + this.storage.set(origin.hostname, records, { ttl: minTTL }) + } + + deleteRecords (origin) { + this.storage.delete(origin.hostname) + } + + getHandler (meta, opts) { + return new DNSDispatchHandler(this, meta, opts) + } +} + +class DNSDispatchHandler extends DecoratorHandler { + #state = null + #opts = null + #dispatch = null + #origin = null + #controller = null + #newOrigin = null + #firstTry = true + + constructor (state, { origin, handler, dispatch, newOrigin }, opts) { + super(handler) + this.#origin = origin + this.#newOrigin = newOrigin + this.#opts = { ...opts } + this.#state = state + this.#dispatch = dispatch + } + + onResponseError (controller, err) { + switch (err.code) { + case 'ETIMEDOUT': + case 'ECONNREFUSED': { + if (this.#state.dualStack) { + if (!this.#firstTry) { + super.onResponseError(controller, err) + return + } + this.#firstTry = false + + // Pick an ip address from the other family + const otherFamily = this.#newOrigin.hostname[0] === '[' ? 4 : 6 + const ip = this.#state.pickFamily(this.#origin, otherFamily) + if (ip == null) { + super.onResponseError(controller, err) + return + } + + let port + if (typeof ip.port === 'number') { + port = `:${ip.port}` + } else if (this.#origin.port !== '') { + port = `:${this.#origin.port}` + } else { + port = '' + } + + const dispatchOpts = { + ...this.#opts, + origin: `${this.#origin.protocol}//${ + ip.family === 6 ? `[${ip.address}]` : ip.address + }${port}`, + headers: withHostHeader(this.#origin.host, this.#opts.headers) + } + this.#dispatch(dispatchOpts, this) + return + } + + // if dual-stack disabled, we error out + super.onResponseError(controller, err) + break + } + case 'ENOTFOUND': + this.#state.deleteRecords(this.#origin) + super.onResponseError(controller, err) + break + default: + super.onResponseError(controller, err) + break + } + } +} + +module.exports = interceptorOpts => { + if ( + interceptorOpts?.maxTTL != null && + (typeof interceptorOpts?.maxTTL !== 'number' || interceptorOpts?.maxTTL < 0) + ) { + throw new InvalidArgumentError('Invalid maxTTL. Must be a positive number') + } + + if ( + interceptorOpts?.maxItems != null && + (typeof interceptorOpts?.maxItems !== 'number' || + interceptorOpts?.maxItems < 1) + ) { + throw new InvalidArgumentError( + 'Invalid maxItems. Must be a positive number and greater than zero' + ) + } + + if ( + interceptorOpts?.affinity != null && + interceptorOpts?.affinity !== 4 && + interceptorOpts?.affinity !== 6 + ) { + throw new InvalidArgumentError('Invalid affinity. Must be either 4 or 6') + } + + if ( + interceptorOpts?.dualStack != null && + typeof interceptorOpts?.dualStack !== 'boolean' + ) { + throw new InvalidArgumentError('Invalid dualStack. Must be a boolean') + } + + if ( + interceptorOpts?.lookup != null && + typeof interceptorOpts?.lookup !== 'function' + ) { + throw new InvalidArgumentError('Invalid lookup. Must be a function') + } + + if ( + interceptorOpts?.pick != null && + typeof interceptorOpts?.pick !== 'function' + ) { + throw new InvalidArgumentError('Invalid pick. Must be a function') + } + + if ( + interceptorOpts?.storage != null && + (typeof interceptorOpts?.storage?.get !== 'function' || + typeof interceptorOpts?.storage?.set !== 'function' || + typeof interceptorOpts?.storage?.full !== 'function' || + typeof interceptorOpts?.storage?.delete !== 'function' + ) + ) { + throw new InvalidArgumentError('Invalid storage. Must be a object with methods: { get, set, full, delete }') + } + + const dualStack = interceptorOpts?.dualStack ?? true + let affinity + if (dualStack) { + affinity = interceptorOpts?.affinity ?? null + } else { + affinity = interceptorOpts?.affinity ?? 4 + } + + const opts = { + maxTTL: interceptorOpts?.maxTTL ?? 10e3, // Expressed in ms + lookup: interceptorOpts?.lookup ?? null, + pick: interceptorOpts?.pick ?? null, + dualStack, + affinity, + maxItems: interceptorOpts?.maxItems ?? Infinity, + storage: interceptorOpts?.storage + } + + const instance = new DNSInstance(opts) + + return dispatch => { + return function dnsInterceptor (origDispatchOpts, handler) { + const origin = + origDispatchOpts.origin.constructor === URL + ? origDispatchOpts.origin + : new URL(origDispatchOpts.origin) + + if (isIP(origin.hostname) !== 0) { + return dispatch(origDispatchOpts, handler) + } + + instance.runLookup(origin, origDispatchOpts, (err, newOrigin) => { + if (err) { + return handler.onResponseError(null, err) + } + + const dispatchOpts = { + ...origDispatchOpts, + servername: origin.hostname, // For SNI on TLS + origin: newOrigin.origin, + headers: withHostHeader(origin.host, origDispatchOpts.headers) + } + + dispatch( + dispatchOpts, + instance.getHandler( + { origin, dispatch, handler, newOrigin }, + origDispatchOpts + ) + ) + }) + + return true + } + } +} diff --git a/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/interceptor/dump.js b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/interceptor/dump.js new file mode 100644 index 000000000..09b57f216 --- /dev/null +++ b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/interceptor/dump.js @@ -0,0 +1,99 @@ +'use strict' + +const { InvalidArgumentError, RequestAbortedError } = require('../core/errors') +const DecoratorHandler = require('../handler/decorator-handler') + +class DumpHandler extends DecoratorHandler { + #maxSize = 1024 * 1024 + #dumped = false + #size = 0 + aborted = false + reason = false + + constructor ({ maxSize, signal }, handler) { + if (maxSize != null && (!Number.isFinite(maxSize) || maxSize < 1)) { + throw new InvalidArgumentError('maxSize must be a number greater than 0') + } + + super(handler) + + this.#maxSize = maxSize ?? this.#maxSize + // this.#handler = handler + } + + #abort (reason) { + this.aborted = true + this.reason = reason + } + + onRequestStart (controller, context) { + controller.abort = this.#abort.bind(this) + + return super.onRequestStart(controller, context) + } + + onResponseStart (controller, statusCode, headers, statusMessage) { + const contentLength = headers['content-length'] + + if (contentLength != null && contentLength > this.#maxSize) { + throw new RequestAbortedError( + `Response size (${contentLength}) larger than maxSize (${ + this.#maxSize + })` + ) + } + + if (this.aborted === true) { + return true + } + + return super.onResponseStart(controller, statusCode, headers, statusMessage) + } + + onResponseError (controller, err) { + super.onResponseError(controller, this.aborted === true ? this.reason : err) + } + + onResponseData (controller, chunk) { + this.#size = this.#size + chunk.length + + if (this.#size > this.#maxSize) { + throw new RequestAbortedError( + `Response size (${this.#size}) larger than maxSize (${this.#maxSize})` + ) + } + + if (this.#size === this.#maxSize) { + this.#dumped = true + } + + return true + } + + onResponseEnd (controller, trailers) { + if (this.aborted === true) { + super.onResponseError(controller, this.reason) + return + } + + super.onResponseEnd(controller, this.#dumped ? {} : trailers) + } +} + +function createDumpInterceptor ( + { maxSize: defaultMaxSize } = { + maxSize: 1024 * 1024 + } +) { + return dispatch => { + return function Intercept (opts, handler) { + const { dumpMaxSize = defaultMaxSize } = opts + + const dumpHandler = new DumpHandler({ maxSize: dumpMaxSize, signal: opts.signal }, handler) + + return dispatch(opts, dumpHandler) + } + } +} + +module.exports = createDumpInterceptor diff --git a/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/interceptor/redirect.js b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/interceptor/redirect.js new file mode 100644 index 000000000..b7df18043 --- /dev/null +++ b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/interceptor/redirect.js @@ -0,0 +1,21 @@ +'use strict' + +const RedirectHandler = require('../handler/redirect-handler') + +function createRedirectInterceptor ({ maxRedirections: defaultMaxRedirections } = {}) { + return (dispatch) => { + return function Intercept (opts, handler) { + const { maxRedirections = defaultMaxRedirections, ...rest } = opts + + if (maxRedirections == null || maxRedirections === 0) { + return dispatch(opts, handler) + } + + const dispatchOpts = { ...rest } // Stop sub dispatcher from also redirecting. + const redirectHandler = new RedirectHandler(dispatch, maxRedirections, dispatchOpts, handler) + return dispatch(dispatchOpts, redirectHandler) + } + } +} + +module.exports = createRedirectInterceptor diff --git a/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/interceptor/response-error.js b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/interceptor/response-error.js new file mode 100644 index 000000000..a8105aa14 --- /dev/null +++ b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/interceptor/response-error.js @@ -0,0 +1,95 @@ +'use strict' + +// const { parseHeaders } = require('../core/util') +const DecoratorHandler = require('../handler/decorator-handler') +const { ResponseError } = require('../core/errors') + +class ResponseErrorHandler extends DecoratorHandler { + #statusCode + #contentType + #decoder + #headers + #body + + constructor (_opts, { handler }) { + super(handler) + } + + #checkContentType (contentType) { + return (this.#contentType ?? '').indexOf(contentType) === 0 + } + + onRequestStart (controller, context) { + this.#statusCode = 0 + this.#contentType = null + this.#decoder = null + this.#headers = null + this.#body = '' + + return super.onRequestStart(controller, context) + } + + onResponseStart (controller, statusCode, headers, statusMessage) { + this.#statusCode = statusCode + this.#headers = headers + this.#contentType = headers['content-type'] + + if (this.#statusCode < 400) { + return super.onResponseStart(controller, statusCode, headers, statusMessage) + } + + if (this.#checkContentType('application/json') || this.#checkContentType('text/plain')) { + this.#decoder = new TextDecoder('utf-8') + } + } + + onResponseData (controller, chunk) { + if (this.#statusCode < 400) { + return super.onResponseData(controller, chunk) + } + + this.#body += this.#decoder?.decode(chunk, { stream: true }) ?? '' + } + + onResponseEnd (controller, trailers) { + if (this.#statusCode >= 400) { + this.#body += this.#decoder?.decode(undefined, { stream: false }) ?? '' + + if (this.#checkContentType('application/json')) { + try { + this.#body = JSON.parse(this.#body) + } catch { + // Do nothing... + } + } + + let err + const stackTraceLimit = Error.stackTraceLimit + Error.stackTraceLimit = 0 + try { + err = new ResponseError('Response Error', this.#statusCode, { + body: this.#body, + headers: this.#headers + }) + } finally { + Error.stackTraceLimit = stackTraceLimit + } + + super.onResponseError(controller, err) + } else { + super.onResponseEnd(controller, trailers) + } + } + + onResponseError (controller, err) { + super.onResponseError(controller, err) + } +} + +module.exports = () => { + return (dispatch) => { + return function Intercept (opts, handler) { + return dispatch(opts, new ResponseErrorHandler(opts, { handler })) + } + } +} diff --git a/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/interceptor/retry.js b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/interceptor/retry.js new file mode 100644 index 000000000..1c16fd845 --- /dev/null +++ b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/interceptor/retry.js @@ -0,0 +1,19 @@ +'use strict' +const RetryHandler = require('../handler/retry-handler') + +module.exports = globalOpts => { + return dispatch => { + return function retryInterceptor (opts, handler) { + return dispatch( + opts, + new RetryHandler( + { ...opts, retryOptions: { ...globalOpts, ...opts.retryOptions } }, + { + handler, + dispatch + } + ) + ) + } + } +} diff --git a/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/llhttp/.gitkeep b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/llhttp/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/llhttp/constants.d.ts b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/llhttp/constants.d.ts new file mode 100644 index 000000000..def2436cc --- /dev/null +++ b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/llhttp/constants.d.ts @@ -0,0 +1,195 @@ +export type IntDict = Record; +export declare const ERROR: IntDict; +export declare const TYPE: IntDict; +export declare const FLAGS: IntDict; +export declare const LENIENT_FLAGS: IntDict; +export declare const METHODS: IntDict; +export declare const STATUSES: IntDict; +export declare const FINISH: IntDict; +export declare const HEADER_STATE: IntDict; +export declare const METHODS_HTTP: number[]; +export declare const METHODS_ICE: number[]; +export declare const METHODS_RTSP: number[]; +export declare const METHOD_MAP: IntDict; +export declare const H_METHOD_MAP: { + [k: string]: number; +}; +export declare const STATUSES_HTTP: number[]; +export type CharList = (string | number)[]; +export declare const ALPHA: CharList; +export declare const NUM_MAP: { + 0: number; + 1: number; + 2: number; + 3: number; + 4: number; + 5: number; + 6: number; + 7: number; + 8: number; + 9: number; +}; +export declare const HEX_MAP: { + 0: number; + 1: number; + 2: number; + 3: number; + 4: number; + 5: number; + 6: number; + 7: number; + 8: number; + 9: number; + A: number; + B: number; + C: number; + D: number; + E: number; + F: number; + a: number; + b: number; + c: number; + d: number; + e: number; + f: number; +}; +export declare const NUM: CharList; +export declare const ALPHANUM: CharList; +export declare const MARK: CharList; +export declare const USERINFO_CHARS: CharList; +export declare const URL_CHAR: CharList; +export declare const HEX: CharList; +export declare const TOKEN: CharList; +export declare const HEADER_CHARS: CharList; +export declare const CONNECTION_TOKEN_CHARS: CharList; +export declare const QUOTED_STRING: CharList; +export declare const HTAB_SP_VCHAR_OBS_TEXT: CharList; +export declare const MAJOR: { + 0: number; + 1: number; + 2: number; + 3: number; + 4: number; + 5: number; + 6: number; + 7: number; + 8: number; + 9: number; +}; +export declare const MINOR: { + 0: number; + 1: number; + 2: number; + 3: number; + 4: number; + 5: number; + 6: number; + 7: number; + 8: number; + 9: number; +}; +export declare const SPECIAL_HEADERS: { + connection: number; + 'content-length': number; + 'proxy-connection': number; + 'transfer-encoding': number; + upgrade: number; +}; +declare const _default: { + ERROR: IntDict; + TYPE: IntDict; + FLAGS: IntDict; + LENIENT_FLAGS: IntDict; + METHODS: IntDict; + STATUSES: IntDict; + FINISH: IntDict; + HEADER_STATE: IntDict; + ALPHA: CharList; + NUM_MAP: { + 0: number; + 1: number; + 2: number; + 3: number; + 4: number; + 5: number; + 6: number; + 7: number; + 8: number; + 9: number; + }; + HEX_MAP: { + 0: number; + 1: number; + 2: number; + 3: number; + 4: number; + 5: number; + 6: number; + 7: number; + 8: number; + 9: number; + A: number; + B: number; + C: number; + D: number; + E: number; + F: number; + a: number; + b: number; + c: number; + d: number; + e: number; + f: number; + }; + NUM: CharList; + ALPHANUM: CharList; + MARK: CharList; + USERINFO_CHARS: CharList; + URL_CHAR: CharList; + HEX: CharList; + TOKEN: CharList; + HEADER_CHARS: CharList; + CONNECTION_TOKEN_CHARS: CharList; + QUOTED_STRING: CharList; + HTAB_SP_VCHAR_OBS_TEXT: CharList; + MAJOR: { + 0: number; + 1: number; + 2: number; + 3: number; + 4: number; + 5: number; + 6: number; + 7: number; + 8: number; + 9: number; + }; + MINOR: { + 0: number; + 1: number; + 2: number; + 3: number; + 4: number; + 5: number; + 6: number; + 7: number; + 8: number; + 9: number; + }; + SPECIAL_HEADERS: { + connection: number; + 'content-length': number; + 'proxy-connection': number; + 'transfer-encoding': number; + upgrade: number; + }; + METHODS_HTTP: number[]; + METHODS_ICE: number[]; + METHODS_RTSP: number[]; + METHOD_MAP: IntDict; + H_METHOD_MAP: { + [k: string]: number; + }; + STATUSES_HTTP: number[]; +}; +export default _default; diff --git a/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/llhttp/constants.js b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/llhttp/constants.js new file mode 100644 index 000000000..8b88dfdf6 --- /dev/null +++ b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/llhttp/constants.js @@ -0,0 +1,531 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.SPECIAL_HEADERS = exports.MINOR = exports.MAJOR = exports.HTAB_SP_VCHAR_OBS_TEXT = exports.QUOTED_STRING = exports.CONNECTION_TOKEN_CHARS = exports.HEADER_CHARS = exports.TOKEN = exports.HEX = exports.URL_CHAR = exports.USERINFO_CHARS = exports.MARK = exports.ALPHANUM = exports.NUM = exports.HEX_MAP = exports.NUM_MAP = exports.ALPHA = exports.STATUSES_HTTP = exports.H_METHOD_MAP = exports.METHOD_MAP = exports.METHODS_RTSP = exports.METHODS_ICE = exports.METHODS_HTTP = exports.HEADER_STATE = exports.FINISH = exports.STATUSES = exports.METHODS = exports.LENIENT_FLAGS = exports.FLAGS = exports.TYPE = exports.ERROR = void 0; +const utils_1 = require("./utils"); +// Emums +exports.ERROR = { + OK: 0, + INTERNAL: 1, + STRICT: 2, + CR_EXPECTED: 25, + LF_EXPECTED: 3, + UNEXPECTED_CONTENT_LENGTH: 4, + UNEXPECTED_SPACE: 30, + CLOSED_CONNECTION: 5, + INVALID_METHOD: 6, + INVALID_URL: 7, + INVALID_CONSTANT: 8, + INVALID_VERSION: 9, + INVALID_HEADER_TOKEN: 10, + INVALID_CONTENT_LENGTH: 11, + INVALID_CHUNK_SIZE: 12, + INVALID_STATUS: 13, + INVALID_EOF_STATE: 14, + INVALID_TRANSFER_ENCODING: 15, + CB_MESSAGE_BEGIN: 16, + CB_HEADERS_COMPLETE: 17, + CB_MESSAGE_COMPLETE: 18, + CB_CHUNK_HEADER: 19, + CB_CHUNK_COMPLETE: 20, + PAUSED: 21, + PAUSED_UPGRADE: 22, + PAUSED_H2_UPGRADE: 23, + USER: 24, + CB_URL_COMPLETE: 26, + CB_STATUS_COMPLETE: 27, + CB_METHOD_COMPLETE: 32, + CB_VERSION_COMPLETE: 33, + CB_HEADER_FIELD_COMPLETE: 28, + CB_HEADER_VALUE_COMPLETE: 29, + CB_CHUNK_EXTENSION_NAME_COMPLETE: 34, + CB_CHUNK_EXTENSION_VALUE_COMPLETE: 35, + CB_RESET: 31, + CB_PROTOCOL_COMPLETE: 38, +}; +exports.TYPE = { + BOTH: 0, // default + REQUEST: 1, + RESPONSE: 2, +}; +exports.FLAGS = { + CONNECTION_KEEP_ALIVE: 1 << 0, + CONNECTION_CLOSE: 1 << 1, + CONNECTION_UPGRADE: 1 << 2, + CHUNKED: 1 << 3, + UPGRADE: 1 << 4, + CONTENT_LENGTH: 1 << 5, + SKIPBODY: 1 << 6, + TRAILING: 1 << 7, + // 1 << 8 is unused + TRANSFER_ENCODING: 1 << 9, +}; +exports.LENIENT_FLAGS = { + HEADERS: 1 << 0, + CHUNKED_LENGTH: 1 << 1, + KEEP_ALIVE: 1 << 2, + TRANSFER_ENCODING: 1 << 3, + VERSION: 1 << 4, + DATA_AFTER_CLOSE: 1 << 5, + OPTIONAL_LF_AFTER_CR: 1 << 6, + OPTIONAL_CRLF_AFTER_CHUNK: 1 << 7, + OPTIONAL_CR_BEFORE_LF: 1 << 8, + SPACES_AFTER_CHUNK_SIZE: 1 << 9, +}; +exports.METHODS = { + 'DELETE': 0, + 'GET': 1, + 'HEAD': 2, + 'POST': 3, + 'PUT': 4, + /* pathological */ + 'CONNECT': 5, + 'OPTIONS': 6, + 'TRACE': 7, + /* WebDAV */ + 'COPY': 8, + 'LOCK': 9, + 'MKCOL': 10, + 'MOVE': 11, + 'PROPFIND': 12, + 'PROPPATCH': 13, + 'SEARCH': 14, + 'UNLOCK': 15, + 'BIND': 16, + 'REBIND': 17, + 'UNBIND': 18, + 'ACL': 19, + /* subversion */ + 'REPORT': 20, + 'MKACTIVITY': 21, + 'CHECKOUT': 22, + 'MERGE': 23, + /* upnp */ + 'M-SEARCH': 24, + 'NOTIFY': 25, + 'SUBSCRIBE': 26, + 'UNSUBSCRIBE': 27, + /* RFC-5789 */ + 'PATCH': 28, + 'PURGE': 29, + /* CalDAV */ + 'MKCALENDAR': 30, + /* RFC-2068, section 19.6.1.2 */ + 'LINK': 31, + 'UNLINK': 32, + /* icecast */ + 'SOURCE': 33, + /* RFC-7540, section 11.6 */ + 'PRI': 34, + /* RFC-2326 RTSP */ + 'DESCRIBE': 35, + 'ANNOUNCE': 36, + 'SETUP': 37, + 'PLAY': 38, + 'PAUSE': 39, + 'TEARDOWN': 40, + 'GET_PARAMETER': 41, + 'SET_PARAMETER': 42, + 'REDIRECT': 43, + 'RECORD': 44, + /* RAOP */ + 'FLUSH': 45, + /* DRAFT https://www.ietf.org/archive/id/draft-ietf-httpbis-safe-method-w-body-02.html */ + 'QUERY': 46, +}; +exports.STATUSES = { + CONTINUE: 100, + SWITCHING_PROTOCOLS: 101, + PROCESSING: 102, + EARLY_HINTS: 103, + RESPONSE_IS_STALE: 110, // Unofficial + REVALIDATION_FAILED: 111, // Unofficial + DISCONNECTED_OPERATION: 112, // Unofficial + HEURISTIC_EXPIRATION: 113, // Unofficial + MISCELLANEOUS_WARNING: 199, // Unofficial + OK: 200, + CREATED: 201, + ACCEPTED: 202, + NON_AUTHORITATIVE_INFORMATION: 203, + NO_CONTENT: 204, + RESET_CONTENT: 205, + PARTIAL_CONTENT: 206, + MULTI_STATUS: 207, + ALREADY_REPORTED: 208, + TRANSFORMATION_APPLIED: 214, // Unofficial + IM_USED: 226, + MISCELLANEOUS_PERSISTENT_WARNING: 299, // Unofficial + MULTIPLE_CHOICES: 300, + MOVED_PERMANENTLY: 301, + FOUND: 302, + SEE_OTHER: 303, + NOT_MODIFIED: 304, + USE_PROXY: 305, + SWITCH_PROXY: 306, // No longer used + TEMPORARY_REDIRECT: 307, + PERMANENT_REDIRECT: 308, + BAD_REQUEST: 400, + UNAUTHORIZED: 401, + PAYMENT_REQUIRED: 402, + FORBIDDEN: 403, + NOT_FOUND: 404, + METHOD_NOT_ALLOWED: 405, + NOT_ACCEPTABLE: 406, + PROXY_AUTHENTICATION_REQUIRED: 407, + REQUEST_TIMEOUT: 408, + CONFLICT: 409, + GONE: 410, + LENGTH_REQUIRED: 411, + PRECONDITION_FAILED: 412, + PAYLOAD_TOO_LARGE: 413, + URI_TOO_LONG: 414, + UNSUPPORTED_MEDIA_TYPE: 415, + RANGE_NOT_SATISFIABLE: 416, + EXPECTATION_FAILED: 417, + IM_A_TEAPOT: 418, + PAGE_EXPIRED: 419, // Unofficial + ENHANCE_YOUR_CALM: 420, // Unofficial + MISDIRECTED_REQUEST: 421, + UNPROCESSABLE_ENTITY: 422, + LOCKED: 423, + FAILED_DEPENDENCY: 424, + TOO_EARLY: 425, + UPGRADE_REQUIRED: 426, + PRECONDITION_REQUIRED: 428, + TOO_MANY_REQUESTS: 429, + REQUEST_HEADER_FIELDS_TOO_LARGE_UNOFFICIAL: 430, // Unofficial + REQUEST_HEADER_FIELDS_TOO_LARGE: 431, + LOGIN_TIMEOUT: 440, // Unofficial + NO_RESPONSE: 444, // Unofficial + RETRY_WITH: 449, // Unofficial + BLOCKED_BY_PARENTAL_CONTROL: 450, // Unofficial + UNAVAILABLE_FOR_LEGAL_REASONS: 451, + CLIENT_CLOSED_LOAD_BALANCED_REQUEST: 460, // Unofficial + INVALID_X_FORWARDED_FOR: 463, // Unofficial + REQUEST_HEADER_TOO_LARGE: 494, // Unofficial + SSL_CERTIFICATE_ERROR: 495, // Unofficial + SSL_CERTIFICATE_REQUIRED: 496, // Unofficial + HTTP_REQUEST_SENT_TO_HTTPS_PORT: 497, // Unofficial + INVALID_TOKEN: 498, // Unofficial + CLIENT_CLOSED_REQUEST: 499, // Unofficial + INTERNAL_SERVER_ERROR: 500, + NOT_IMPLEMENTED: 501, + BAD_GATEWAY: 502, + SERVICE_UNAVAILABLE: 503, + GATEWAY_TIMEOUT: 504, + HTTP_VERSION_NOT_SUPPORTED: 505, + VARIANT_ALSO_NEGOTIATES: 506, + INSUFFICIENT_STORAGE: 507, + LOOP_DETECTED: 508, + BANDWIDTH_LIMIT_EXCEEDED: 509, + NOT_EXTENDED: 510, + NETWORK_AUTHENTICATION_REQUIRED: 511, + WEB_SERVER_UNKNOWN_ERROR: 520, // Unofficial + WEB_SERVER_IS_DOWN: 521, // Unofficial + CONNECTION_TIMEOUT: 522, // Unofficial + ORIGIN_IS_UNREACHABLE: 523, // Unofficial + TIMEOUT_OCCURED: 524, // Unofficial + SSL_HANDSHAKE_FAILED: 525, // Unofficial + INVALID_SSL_CERTIFICATE: 526, // Unofficial + RAILGUN_ERROR: 527, // Unofficial + SITE_IS_OVERLOADED: 529, // Unofficial + SITE_IS_FROZEN: 530, // Unofficial + IDENTITY_PROVIDER_AUTHENTICATION_ERROR: 561, // Unofficial + NETWORK_READ_TIMEOUT: 598, // Unofficial + NETWORK_CONNECT_TIMEOUT: 599, // Unofficial +}; +exports.FINISH = { + SAFE: 0, + SAFE_WITH_CB: 1, + UNSAFE: 2, +}; +exports.HEADER_STATE = { + GENERAL: 0, + CONNECTION: 1, + CONTENT_LENGTH: 2, + TRANSFER_ENCODING: 3, + UPGRADE: 4, + CONNECTION_KEEP_ALIVE: 5, + CONNECTION_CLOSE: 6, + CONNECTION_UPGRADE: 7, + TRANSFER_ENCODING_CHUNKED: 8, +}; +// C headers +exports.METHODS_HTTP = [ + exports.METHODS.DELETE, + exports.METHODS.GET, + exports.METHODS.HEAD, + exports.METHODS.POST, + exports.METHODS.PUT, + exports.METHODS.CONNECT, + exports.METHODS.OPTIONS, + exports.METHODS.TRACE, + exports.METHODS.COPY, + exports.METHODS.LOCK, + exports.METHODS.MKCOL, + exports.METHODS.MOVE, + exports.METHODS.PROPFIND, + exports.METHODS.PROPPATCH, + exports.METHODS.SEARCH, + exports.METHODS.UNLOCK, + exports.METHODS.BIND, + exports.METHODS.REBIND, + exports.METHODS.UNBIND, + exports.METHODS.ACL, + exports.METHODS.REPORT, + exports.METHODS.MKACTIVITY, + exports.METHODS.CHECKOUT, + exports.METHODS.MERGE, + exports.METHODS['M-SEARCH'], + exports.METHODS.NOTIFY, + exports.METHODS.SUBSCRIBE, + exports.METHODS.UNSUBSCRIBE, + exports.METHODS.PATCH, + exports.METHODS.PURGE, + exports.METHODS.MKCALENDAR, + exports.METHODS.LINK, + exports.METHODS.UNLINK, + exports.METHODS.PRI, + // TODO(indutny): should we allow it with HTTP? + exports.METHODS.SOURCE, + exports.METHODS.QUERY, +]; +exports.METHODS_ICE = [ + exports.METHODS.SOURCE, +]; +exports.METHODS_RTSP = [ + exports.METHODS.OPTIONS, + exports.METHODS.DESCRIBE, + exports.METHODS.ANNOUNCE, + exports.METHODS.SETUP, + exports.METHODS.PLAY, + exports.METHODS.PAUSE, + exports.METHODS.TEARDOWN, + exports.METHODS.GET_PARAMETER, + exports.METHODS.SET_PARAMETER, + exports.METHODS.REDIRECT, + exports.METHODS.RECORD, + exports.METHODS.FLUSH, + // For AirPlay + exports.METHODS.GET, + exports.METHODS.POST, +]; +exports.METHOD_MAP = (0, utils_1.enumToMap)(exports.METHODS); +exports.H_METHOD_MAP = Object.fromEntries(Object.entries(exports.METHODS).filter(([k]) => k.startsWith('H'))); +exports.STATUSES_HTTP = [ + exports.STATUSES.CONTINUE, + exports.STATUSES.SWITCHING_PROTOCOLS, + exports.STATUSES.PROCESSING, + exports.STATUSES.EARLY_HINTS, + exports.STATUSES.RESPONSE_IS_STALE, + exports.STATUSES.REVALIDATION_FAILED, + exports.STATUSES.DISCONNECTED_OPERATION, + exports.STATUSES.HEURISTIC_EXPIRATION, + exports.STATUSES.MISCELLANEOUS_WARNING, + exports.STATUSES.OK, + exports.STATUSES.CREATED, + exports.STATUSES.ACCEPTED, + exports.STATUSES.NON_AUTHORITATIVE_INFORMATION, + exports.STATUSES.NO_CONTENT, + exports.STATUSES.RESET_CONTENT, + exports.STATUSES.PARTIAL_CONTENT, + exports.STATUSES.MULTI_STATUS, + exports.STATUSES.ALREADY_REPORTED, + exports.STATUSES.TRANSFORMATION_APPLIED, + exports.STATUSES.IM_USED, + exports.STATUSES.MISCELLANEOUS_PERSISTENT_WARNING, + exports.STATUSES.MULTIPLE_CHOICES, + exports.STATUSES.MOVED_PERMANENTLY, + exports.STATUSES.FOUND, + exports.STATUSES.SEE_OTHER, + exports.STATUSES.NOT_MODIFIED, + exports.STATUSES.USE_PROXY, + exports.STATUSES.SWITCH_PROXY, + exports.STATUSES.TEMPORARY_REDIRECT, + exports.STATUSES.PERMANENT_REDIRECT, + exports.STATUSES.BAD_REQUEST, + exports.STATUSES.UNAUTHORIZED, + exports.STATUSES.PAYMENT_REQUIRED, + exports.STATUSES.FORBIDDEN, + exports.STATUSES.NOT_FOUND, + exports.STATUSES.METHOD_NOT_ALLOWED, + exports.STATUSES.NOT_ACCEPTABLE, + exports.STATUSES.PROXY_AUTHENTICATION_REQUIRED, + exports.STATUSES.REQUEST_TIMEOUT, + exports.STATUSES.CONFLICT, + exports.STATUSES.GONE, + exports.STATUSES.LENGTH_REQUIRED, + exports.STATUSES.PRECONDITION_FAILED, + exports.STATUSES.PAYLOAD_TOO_LARGE, + exports.STATUSES.URI_TOO_LONG, + exports.STATUSES.UNSUPPORTED_MEDIA_TYPE, + exports.STATUSES.RANGE_NOT_SATISFIABLE, + exports.STATUSES.EXPECTATION_FAILED, + exports.STATUSES.IM_A_TEAPOT, + exports.STATUSES.PAGE_EXPIRED, + exports.STATUSES.ENHANCE_YOUR_CALM, + exports.STATUSES.MISDIRECTED_REQUEST, + exports.STATUSES.UNPROCESSABLE_ENTITY, + exports.STATUSES.LOCKED, + exports.STATUSES.FAILED_DEPENDENCY, + exports.STATUSES.TOO_EARLY, + exports.STATUSES.UPGRADE_REQUIRED, + exports.STATUSES.PRECONDITION_REQUIRED, + exports.STATUSES.TOO_MANY_REQUESTS, + exports.STATUSES.REQUEST_HEADER_FIELDS_TOO_LARGE_UNOFFICIAL, + exports.STATUSES.REQUEST_HEADER_FIELDS_TOO_LARGE, + exports.STATUSES.LOGIN_TIMEOUT, + exports.STATUSES.NO_RESPONSE, + exports.STATUSES.RETRY_WITH, + exports.STATUSES.BLOCKED_BY_PARENTAL_CONTROL, + exports.STATUSES.UNAVAILABLE_FOR_LEGAL_REASONS, + exports.STATUSES.CLIENT_CLOSED_LOAD_BALANCED_REQUEST, + exports.STATUSES.INVALID_X_FORWARDED_FOR, + exports.STATUSES.REQUEST_HEADER_TOO_LARGE, + exports.STATUSES.SSL_CERTIFICATE_ERROR, + exports.STATUSES.SSL_CERTIFICATE_REQUIRED, + exports.STATUSES.HTTP_REQUEST_SENT_TO_HTTPS_PORT, + exports.STATUSES.INVALID_TOKEN, + exports.STATUSES.CLIENT_CLOSED_REQUEST, + exports.STATUSES.INTERNAL_SERVER_ERROR, + exports.STATUSES.NOT_IMPLEMENTED, + exports.STATUSES.BAD_GATEWAY, + exports.STATUSES.SERVICE_UNAVAILABLE, + exports.STATUSES.GATEWAY_TIMEOUT, + exports.STATUSES.HTTP_VERSION_NOT_SUPPORTED, + exports.STATUSES.VARIANT_ALSO_NEGOTIATES, + exports.STATUSES.INSUFFICIENT_STORAGE, + exports.STATUSES.LOOP_DETECTED, + exports.STATUSES.BANDWIDTH_LIMIT_EXCEEDED, + exports.STATUSES.NOT_EXTENDED, + exports.STATUSES.NETWORK_AUTHENTICATION_REQUIRED, + exports.STATUSES.WEB_SERVER_UNKNOWN_ERROR, + exports.STATUSES.WEB_SERVER_IS_DOWN, + exports.STATUSES.CONNECTION_TIMEOUT, + exports.STATUSES.ORIGIN_IS_UNREACHABLE, + exports.STATUSES.TIMEOUT_OCCURED, + exports.STATUSES.SSL_HANDSHAKE_FAILED, + exports.STATUSES.INVALID_SSL_CERTIFICATE, + exports.STATUSES.RAILGUN_ERROR, + exports.STATUSES.SITE_IS_OVERLOADED, + exports.STATUSES.SITE_IS_FROZEN, + exports.STATUSES.IDENTITY_PROVIDER_AUTHENTICATION_ERROR, + exports.STATUSES.NETWORK_READ_TIMEOUT, + exports.STATUSES.NETWORK_CONNECT_TIMEOUT, +]; +exports.ALPHA = []; +for (let i = 'A'.charCodeAt(0); i <= 'Z'.charCodeAt(0); i++) { + // Upper case + exports.ALPHA.push(String.fromCharCode(i)); + // Lower case + exports.ALPHA.push(String.fromCharCode(i + 0x20)); +} +exports.NUM_MAP = { + 0: 0, 1: 1, 2: 2, 3: 3, 4: 4, + 5: 5, 6: 6, 7: 7, 8: 8, 9: 9, +}; +exports.HEX_MAP = { + 0: 0, 1: 1, 2: 2, 3: 3, 4: 4, + 5: 5, 6: 6, 7: 7, 8: 8, 9: 9, + A: 0XA, B: 0XB, C: 0XC, D: 0XD, E: 0XE, F: 0XF, + a: 0xa, b: 0xb, c: 0xc, d: 0xd, e: 0xe, f: 0xf, +}; +exports.NUM = [ + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', +]; +exports.ALPHANUM = exports.ALPHA.concat(exports.NUM); +exports.MARK = ['-', '_', '.', '!', '~', '*', '\'', '(', ')']; +exports.USERINFO_CHARS = exports.ALPHANUM + .concat(exports.MARK) + .concat(['%', ';', ':', '&', '=', '+', '$', ',']); +// TODO(indutny): use RFC +exports.URL_CHAR = [ + '!', '"', '$', '%', '&', '\'', + '(', ')', '*', '+', ',', '-', '.', '/', + ':', ';', '<', '=', '>', + '@', '[', '\\', ']', '^', '_', + '`', + '{', '|', '}', '~', +].concat(exports.ALPHANUM); +exports.HEX = exports.NUM.concat(['a', 'b', 'c', 'd', 'e', 'f', 'A', 'B', 'C', 'D', 'E', 'F']); +/* Tokens as defined by rfc 2616. Also lowercases them. + * token = 1* + * separators = "(" | ")" | "<" | ">" | "@" + * | "," | ";" | ":" | "\" | <"> + * | "/" | "[" | "]" | "?" | "=" + * | "{" | "}" | SP | HT + */ +exports.TOKEN = [ + '!', '#', '$', '%', '&', '\'', + '*', '+', '-', '.', + '^', '_', '`', + '|', '~', +].concat(exports.ALPHANUM); +/* + * Verify that a char is a valid visible (printable) US-ASCII + * character or %x80-FF + */ +exports.HEADER_CHARS = ['\t']; +for (let i = 32; i <= 255; i++) { + if (i !== 127) { + exports.HEADER_CHARS.push(i); + } +} +// ',' = \x44 +exports.CONNECTION_TOKEN_CHARS = exports.HEADER_CHARS.filter((c) => c !== 44); +exports.QUOTED_STRING = ['\t', ' ']; +for (let i = 0x21; i <= 0xff; i++) { + if (i !== 0x22 && i !== 0x5c) { // All characters in ASCII except \ and " + exports.QUOTED_STRING.push(i); + } +} +exports.HTAB_SP_VCHAR_OBS_TEXT = ['\t', ' ']; +// VCHAR: https://tools.ietf.org/html/rfc5234#appendix-B.1 +for (let i = 0x21; i <= 0x7E; i++) { + exports.HTAB_SP_VCHAR_OBS_TEXT.push(i); +} +// OBS_TEXT: https://datatracker.ietf.org/doc/html/rfc9110#name-collected-abnf +for (let i = 0x80; i <= 0xff; i++) { + exports.HTAB_SP_VCHAR_OBS_TEXT.push(i); +} +exports.MAJOR = exports.NUM_MAP; +exports.MINOR = exports.MAJOR; +exports.SPECIAL_HEADERS = { + 'connection': exports.HEADER_STATE.CONNECTION, + 'content-length': exports.HEADER_STATE.CONTENT_LENGTH, + 'proxy-connection': exports.HEADER_STATE.CONNECTION, + 'transfer-encoding': exports.HEADER_STATE.TRANSFER_ENCODING, + 'upgrade': exports.HEADER_STATE.UPGRADE, +}; +exports.default = { + ERROR: exports.ERROR, + TYPE: exports.TYPE, + FLAGS: exports.FLAGS, + LENIENT_FLAGS: exports.LENIENT_FLAGS, + METHODS: exports.METHODS, + STATUSES: exports.STATUSES, + FINISH: exports.FINISH, + HEADER_STATE: exports.HEADER_STATE, + ALPHA: exports.ALPHA, + NUM_MAP: exports.NUM_MAP, + HEX_MAP: exports.HEX_MAP, + NUM: exports.NUM, + ALPHANUM: exports.ALPHANUM, + MARK: exports.MARK, + USERINFO_CHARS: exports.USERINFO_CHARS, + URL_CHAR: exports.URL_CHAR, + HEX: exports.HEX, + TOKEN: exports.TOKEN, + HEADER_CHARS: exports.HEADER_CHARS, + CONNECTION_TOKEN_CHARS: exports.CONNECTION_TOKEN_CHARS, + QUOTED_STRING: exports.QUOTED_STRING, + HTAB_SP_VCHAR_OBS_TEXT: exports.HTAB_SP_VCHAR_OBS_TEXT, + MAJOR: exports.MAJOR, + MINOR: exports.MINOR, + SPECIAL_HEADERS: exports.SPECIAL_HEADERS, + METHODS_HTTP: exports.METHODS_HTTP, + METHODS_ICE: exports.METHODS_ICE, + METHODS_RTSP: exports.METHODS_RTSP, + METHOD_MAP: exports.METHOD_MAP, + H_METHOD_MAP: exports.H_METHOD_MAP, + STATUSES_HTTP: exports.STATUSES_HTTP, +}; diff --git a/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/llhttp/llhttp-wasm.js b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/llhttp/llhttp-wasm.js new file mode 100644 index 000000000..8e8980635 --- /dev/null +++ b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/llhttp/llhttp-wasm.js @@ -0,0 +1,15 @@ +'use strict' + +const { Buffer } = require('node:buffer') + +const wasmBase64 = 'AGFzbQEAAAABJwdgAX8Bf2ADf39/AX9gAn9/AGABfwBgBH9/f38Bf2AAAGADf39/AALLAQgDZW52GHdhc21fb25faGVhZGVyc19jb21wbGV0ZQAEA2VudhV3YXNtX29uX21lc3NhZ2VfYmVnaW4AAANlbnYLd2FzbV9vbl91cmwAAQNlbnYOd2FzbV9vbl9zdGF0dXMAAQNlbnYUd2FzbV9vbl9oZWFkZXJfZmllbGQAAQNlbnYUd2FzbV9vbl9oZWFkZXJfdmFsdWUAAQNlbnYMd2FzbV9vbl9ib2R5AAEDZW52GHdhc21fb25fbWVzc2FnZV9jb21wbGV0ZQAAAzU0BQYAAAMAAAAAAAADAQMAAwMDAAACAAAAAAICAgICAgICAgIBAQEBAQEBAQEBAwAAAwAAAAQFAXABExMFAwEAAgYIAX8BQcDZBAsHxQcoBm1lbW9yeQIAC19pbml0aWFsaXplAAgZX19pbmRpcmVjdF9mdW5jdGlvbl90YWJsZQEAC2xsaHR0cF9pbml0AAkYbGxodHRwX3Nob3VsZF9rZWVwX2FsaXZlADcMbGxodHRwX2FsbG9jAAsGbWFsbG9jADkLbGxodHRwX2ZyZWUADARmcmVlAAwPbGxodHRwX2dldF90eXBlAA0VbGxodHRwX2dldF9odHRwX21ham9yAA4VbGxodHRwX2dldF9odHRwX21pbm9yAA8RbGxodHRwX2dldF9tZXRob2QAEBZsbGh0dHBfZ2V0X3N0YXR1c19jb2RlABESbGxodHRwX2dldF91cGdyYWRlABIMbGxodHRwX3Jlc2V0ABMObGxodHRwX2V4ZWN1dGUAFBRsbGh0dHBfc2V0dGluZ3NfaW5pdAAVDWxsaHR0cF9maW5pc2gAFgxsbGh0dHBfcGF1c2UAFw1sbGh0dHBfcmVzdW1lABgbbGxodHRwX3Jlc3VtZV9hZnRlcl91cGdyYWRlABkQbGxodHRwX2dldF9lcnJubwAaF2xsaHR0cF9nZXRfZXJyb3JfcmVhc29uABsXbGxodHRwX3NldF9lcnJvcl9yZWFzb24AHBRsbGh0dHBfZ2V0X2Vycm9yX3BvcwAdEWxsaHR0cF9lcnJub19uYW1lAB4SbGxodHRwX21ldGhvZF9uYW1lAB8SbGxodHRwX3N0YXR1c19uYW1lACAabGxodHRw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AXRrQT5qIQILIAEgAjYCHCABQgA3AhAgAkECdEH01wBqIQACQEHI1QAoAgAiA0EBIAJ0IgdxRQRAIAAgATYCAEHI1QAgAyAHcjYCACABIAA2AhggASABNgIIIAEgATYCDAwBCyAEQRkgAkEBdmtBACACQR9HG3QhAiAAKAIAIQACQANAIAAiAygCBEF4cSAERg0BIAJBHXYhACACQQF0IQIgAyAAQQRxakEQaiIHKAIAIgANAAsgByABNgIAIAEgAzYCGCABIAE2AgwgASABNgIIDAELIAMoAggiACABNgIMIAMgATYCCCABQQA2AhggASADNgIMIAEgADYCCAtB5NUAQeTVACgCAEEBayIAQX8gABs2AgALCwcAIAAtACgLBwAgAC0AKgsHACAALQArCwcAIAAtACkLBwAgAC8BNAsHACAALQAwC0ABBH8gACgCGCEBIAAvAS4hAiAALQAoIQMgACgCOCEEIAAQOCAAIAQ2AjggACADOgAoIAAgAjsBLiAAIAE2AhgL5YUCAgd/A34gASACaiEEAkAgACIDKAIMIgANACADKAIEBEAgAyABNgIECyMAQRBrIgkkAAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAn8CQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkAC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C0HjAQwHC0HXAQwGC0HYAQwFC0HZAQwEC0HaAQwDC0HbAQwCC0HdAQwBC0HcAQshAgNAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQCADAn8CQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAn8CQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAn8CQAJAAkACQAJAAkACQAJ/AkACQAJAAn8CQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAIAMCfwJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACfwJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkAg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ogKiAqICogKiAqICogKiAqICogKiAqICogIAogILIAQgAUEBaiIBRw0AC0HJACECDOwCCyABQQFqIQEgA0Evai0AAEEBcQ2lAiADQQA2AhwgAyABNgIUIANBlxA2AhAgA0EKNgIMQQAhAgzrAgsgASAERwRAA0AgAS0AAEEgRw0VIAQgAUEBaiIBRw0AC0H4ACECDOsCC0H4ACECDOoCCyADQQI6ACgMOAtBACECIANBADYCHCADQb8LNgIQIANBAjYCDCADIAFBAWo2AhQM6AILQQAhAgzOAgtBDSECDM0CC0ETIQIMzAILQRUhAgzLAgtBFiECDMoCC0EYIQIMyQILQRkhAgzIAgtBGiECDMcCC0EbIQIMxgILQRwhAgzFAgtBHSECDMQCC0EeIQIMwwILQR8hAgzCAgtBICECDMECC0EiIQIMwAILQSMhAgy/AgtBJSECDL4CC0HlACECDL0CCyADQT02AhwgAyABNgIUIAMgADYCDEEAIQIM1QILIANBGzYCHCADIAE2AhQgA0GkHDYCECADQRU2AgxBACECDNQCCyADQSA2AhwgAyABNgIUIANBmBo2AhAgA0EVNgIMQQAhAgzTAgsgA0ETNgIcIAMgATYCFCADQZgaNgIQIANBFTYCDEEAIQIM0gILIANBCzYCHCADIAE2AhQgA0GYGjYCECADQRU2AgxBACECDNECCyADQRA2AhwgAyABNgIUIANBmBo2AhAgA0EVNgIMQQAhAgzQAgsgA0EgNgIcIAMgATYCFCADQaQcNgIQIANBFTYCDEEAIQIMzwILIANBCzYCHCADIAE2AhQgA0GkHDYCECADQRU2AgxBACECDM4CCyADQQw2AhwgAyABNgIUIANBpBw2AhAgA0EVNgIMQQAhAgzNAgtBACECIANBADYCHCADIAE2AhQgA0HdDjYCECADQRI2AgwMzAILAkADQAJAIAEtAABBCmsOBAACAgACCyAEIAFBAWoiAUcNAAtB/QEhAgzMAgsCQAJAIAMtADZBAUcNAEEAIQACQCADKAI4IgJFDQAgAigCYCICRQ0AIAMgAhEAACEACyAARQ0AIABBFUcNASADQfwBNgIcIAMgATYCFCADQdwZNgIQIANBFTYCDEEAIQIMzQILQdwBIQIMswILIANBADYCHCADIAE2AhQgA0H5CzYCECADQR82AgxBACECDMsCCwJAAkAgAy0AKEEBaw4CBAEAC0HbASECDLICC0HUASECDLECCyADQQI6ADFBACEAAkAgAygCOCICRQ0AIAIoAgAiAkUNACADIAIRAAAhAAsgAEUEQEHdASECDLECCyAAQRVHBEAgA0EANgIcIAMgATYCFCADQbQMNgIQIANBEDYCDEEAIQIMygILIANB+wE2AhwgAyABNgIUIANBgRo2AhAgA0EVNgIMQQAhAgzJAgsgASAERgRAQfoBIQIMyQILIAEtAABByABGDQEgA0EBOgAoC0HAASECDK4CC0HaASECDK0CCyABIARHBEAgA0EMNgIIIAMgATYCBEHZASECDK0CC0H5ASECDMUCCyABIARGBEBB+AEhAgzFAgsgAS0AAEHIAEcNBCABQQFqIQFB2AEhAgyrAgsgASAERgRAQfcBIQIMxAILAkACQCABLQAAQcUAaw4QAAUFBQUFBQUFBQUFBQUFAQULIAFBAWohAUHWASECDKsCCyABQQFqIQFB1wEhAgyqAgtB9gEhAiABIARGDcICIAMoAgAiACAEIAFraiEFIAEgAGtBAmohBgJAA0AgAS0AACAAQbrVAGotAABHDQMgAEECRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAyAFNgIADMMCCyADKAIEIQAgA0IANwMAIAMgACAGQQFqIgEQLiIARQRAQeMBIQIMqgILIANB9QE2AhwgAyABNgIUIAMgADYCDEEAIQIMwgILQfQBIQIgASAERg3BAiADKAIAIgAgBCABa2ohBSABIABrQQFqIQYCQANAIAEtAAAgAEG41QBqLQAARw0CIABBAUYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAzCAgsgA0GBBDsBKCADKAIEIQAgA0IANwMAIAMgACAGQQFqIgEQLiIADQMMAgsgA0EANgIAC0EAIQIgA0EANgIcIAMgATYCFCADQeUfNgIQIANBCDYCDAy/AgtB1QEhAgylAgsgA0HzATYCHCADIAE2AhQgAyAANgIMQQAhAgy9AgtBACEAAkAgAygCOCICRQ0AIAIoAkAiAkUNACADIAIRAAAhAAsgAEUNbiAAQRVHBEAgA0EANgIcIAMgATYCFCADQYIPNgIQIANBIDYCDEEAIQIMvQILIANBjwE2AhwgAyABNgIUIANB7Bs2AhAgA0EVNgIMQQAhAgy8AgsgASAERwRAIANBDTYCCCADIAE2AgRB0wEhAgyjAgtB8gEhAgy7AgsgASAERgRAQfEBIQIMuwILAkACQAJAIAEtAABByABrDgsAAQgICAgICAgIAggLIAFBAWohAUHQASECDKMCCyABQQFqIQFB0QEhAgyiAgsgAUEBaiEBQdIBIQIMoQILQfABIQIgASAERg25AiADKAIAIgAgBCABa2ohBiABIABrQQJqIQUDQCABLQAAIABBtdUAai0AAEcNBCAAQQJGDQMgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAY2AgAMuQILQe8BIQIgASAERg24AiADKAIAIgAgBCABa2ohBiABIABrQQFqIQUDQCABLQAAIABBs9UAai0AAEcNAyAAQQFGDQIgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAY2AgAMuAILQe4BIQIgASAERg23AiADKAIAIgAgBCABa2ohBiABIABrQQJqIQUDQCABLQAAIABBsNUAai0AAEcNAiAAQQJGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAY2AgAMtwILIAMoAgQhACADQgA3AwAgAyAAIAVBAWoiARArIgBFDQIgA0HsATYCHCADIAE2AhQgAyAANgIMQQAhAgy2AgsgA0EANgIACyADKAIEIQAgA0EANgIEIAMgACABECsiAEUNnAIgA0HtATYCHCADIAE2AhQgAyAANgIMQQAhAgy0AgtBzwEhAgyaAgtBACEAAkAgAygCOCICRQ0AIAIoAjQiAkUNACADIAIRAAAhAAsCQCAABEAgAEEVRg0BIANBADYCHCADIAE2AhQgA0HqDTYCECADQSY2AgxBACECDLQCC0HOASECDJoCCyADQesBNgIcIAMgATYCFCADQYAbNgIQIANBFTYCDEEAIQIMsgILIAEgBEYEQEHrASECDLICCyABLQAAQS9GBEAgAUEBaiEBDAELIANBADYCHCADIAE2AhQgA0GyODYCECADQQg2AgxBACECDLECC0HNASECDJcCCyABIARHBEAgA0EONgIIIAMgATYCBEHMASECDJcCC0HqASECDK8CCyABIARGBEBB6QEhAgyvAgsgAS0AAEEwayIAQf8BcUEKSQRAIAMgADoAKiABQQFqIQFBywEhAgyWAgsgAygCBCEAIANBADYCBCADIAAgARAvIgBFDZcCIANB6AE2AhwgAyABNgIUIAMgADYCDEEAIQIMrgILIAEgBEYEQEHnASECDK4CCwJAIAEtAABBLkYEQCABQQFqIQEMAQsgAygCBCEAIANBADYCBCADIAAgARAvIgBFDZgCIANB5gE2AhwgAyABNgIUIAMgADYCDEEAIQIMrgILQcoBIQIMlAILIAEgBEYEQEHlASECDK0CC0EAIQBBASEFQQEhB0EAIQICQAJAAkACQAJAAn8CQAJAAkACQAJAAkACQCABLQAAQTBrDgoKCQABAgMEBQYICwtBAgwGC0EDDAULQQQMBAtBBQwDC0EGDAILQQcMAQtBCAshAkEAIQVBACEHDAILQQkhAkEBIQBBACEFQQAhBwwBC0EAIQVBASECCyADIAI6ACsgAUEBaiEBAkACQCADLQAuQRBxDQACQAJAAkAgAy0AKg4DAQACBAsgB0UNAwwCCyAADQEMAgsgBUUNAQsgAygCBCEAIANBADYCBCADIAAgARAvIgBFDQIgA0HiATYCHCADIAE2AhQgAyAANgIMQQAhAgyvAgsgAygCBCEAIANBADYCBCADIAAgARAvIgBFDZoCIANB4wE2AhwgAyABNgIUIAMgADYCDEEAIQIMrgILIAMoAgQhACADQQA2AgQgAyAAIAEQLyIARQ2YAiADQeQBNgIcIAMgATYCFCADIAA2AgwMrQILQckBIQIMkwILQQAhAAJAIAMoAjgiAkUNACACKAJEIgJFDQAgAyACEQAAIQALAkAgAARAIABBFUYNASADQQA2AhwgAyABNgIUIANBpA02AhAgA0EhNgIMQQAhAgytAgtByAEhAgyTAgsgA0HhATYCHCADIAE2AhQgA0HQGjYCECADQRU2AgxBACECDKsCCyABIARGBEBB4QEhAgyrAgsCQCABLQAAQSBGBEAgA0EAOwE0IAFBAWohAQwBCyADQQA2AhwgAyABNgIUIANBmRE2AhAgA0EJNgIMQQAhAgyrAgtBxwEhAgyRAgsgASAERgRAQeABIQIMqgILAkAgAS0AAEEwa0H/AXEiAkEKSQRAIAFBAWohAQJAIAMvATQiAEGZM0sNACADIABBCmwiADsBNCAAQf7/A3EgAkH//wNzSw0AIAMgACACajsBNAwCC0EAIQIgA0EANgIcIAMgATYCFCADQZUeNgIQIANBDTYCDAyrAgsgA0EANgIcIAMgATYCFCADQZUeNgIQIANBDTYCDEEAIQIMqgILQcYBIQIMkAILIAEgBEYEQEHfASECDKkCCwJAIAEtAABBMGtB/wFxIgJBCkkEQCABQQFqIQECQCADLwE0IgBBmTNLDQAgAyAAQQpsIgA7ATQgAEH+/wNxIAJB//8Dc0sNACADIAAgAmo7ATQMAgtBACECIANBADYCHCADIAE2AhQgA0GVHjYCECADQQ02AgwMqgILIANBADYCHCADIAE2AhQgA0GVHjYCECADQQ02AgxBACECDKkCC0HFASECDI8CCyABIARGBEBB3gEhAgyoAgsCQCABLQAAQTBrQf8BcSICQQpJBEAgAUEBaiEBAkAgAy8BNCIAQZkzSw0AIAMgAEEKbCIAOwE0IABB/v8DcSACQf//A3NLDQAgAyAAIAJqOwE0DAILQQAhAiADQQA2AhwgAyABNgIUIANBlR42AhAgA0ENNgIMDKkCCyADQQA2AhwgAyABNgIUIANBlR42AhAgA0ENNgIMQQAhAgyoAgtBxAEhAgyOAgsgASAERgRAQd0BIQIMpwILAkACQAJAAkAgAS0AAEEKaw4XAgMDAAMDAwMDAwMDAwMDAwMDAwMDAwEDCyABQQFqDAULIAFBAWohAUHDASECDI8CCyABQQFqIQEgA0Evai0AAEEBcQ0IIANBADYCHCADIAE2AhQgA0GNCzYCECADQQ02AgxBACECDKcCCyADQQA2AhwgAyABNgIUIANBjQs2AhAgA0ENNgIMQQAhAgymAgsgASAERwRAIANBDzYCCCADIAE2AgRBASECDI0CC0HcASECDKUCCwJAAkADQAJAIAEtAABBCmsOBAIAAAMACyAEIAFBAWoiAUcNAAtB2wEhAgymAgsgAygCBCEAIANBADYCBCADIAAgARAtIgBFBEAgAUEBaiEBDAQLIANB2gE2AhwgAyAANgIMIAMgAUEBajYCFEEAIQIMpQILIAMoAgQhACADQQA2AgQgAyAAIAEQLSIADQEgAUEBagshAUHBASECDIoCCyADQdkBNgIcIAMgADYCDCADIAFBAWo2AhRBACECDKICC0HCASECDIgCCyADQS9qLQAAQQFxDQEgA0EANgIcIAMgATYCFCADQeQcNgIQIANBGTYCDEEAIQIMoAILIAEgBEYEQEHZASEC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IAFBAWohAUGeASECDOQBCyABQQFqIQFBnwEhAgzjAQtBtwEhAiABIARGDfsBIAMoAgAiACAEIAFraiEFIAEgAGtBA2ohBgJAA0AgAS0AACAAQezUAGotAABHDSggAEEDRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAyAFNgIADPwBCyADQQA2AgAgBkEBaiEBQQ0MKAtBtgEhAiABIARGDfoBIAMoAgAiACAEIAFraiEFIAEgAGtBAmohBgJAA0AgAS0AACAAQaHVAGotAABHDScgAEECRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAyAFNgIADPsBCyADQQA2AgAgBkEBaiEBQQwMJwtBtQEhAiABIARGDfkBIAMoAgAiACAEIAFraiEFIAEgAGtBAWohBgJAA0AgAS0AACAAQerUAGotAABHDSYgAEEBRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAyAFNgIADPoBCyADQQA2AgAgBkEBaiEBQQMMJgtBtAEhAiABIARGDfgBIAMoAgAiACAEIAFraiEFIAEgAGtBAWohBgJAA0AgAS0AACAAQejUAGotAABHDSUgAEEBRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAyAFNgIADPkBCyADQQA2AgAgBkEBaiEBQSYMJQsgASAERgRAQbMBIQIM+AELAkACQCABLQAAQdQAaw4CAAEnCyABQQFqIQFBmQEhAgzfAQsgAUEBaiEBQZoBIQIM3gELQbIBIQIgASAERg32ASADKAIAIgAgBCABa2ohBSABIABrQQFqIQYCQANAIAEtAAAgAEHm1ABqLQAARw0jIABBAUYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAz3AQsgA0EANgIAIAZBAWohAUEnDCMLQbEBIQIgASAERg31ASADKAIAIgAgBCABa2ohBSABIABrQQFqIQYCQANAIAEtAAAgAEHk1ABqLQAARw0iIABBAUYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAz2AQsgA0EANgIAIAZBAWohAUEcDCILQbABIQIgASAERg30ASADKAIAIgAgBCABa2ohBSABIABrQQVqIQYCQANAIAEtAAAgAEHe1ABqLQAARw0hIABBBUYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAz1AQsgA0EANgIAIAZBAWohAUEGDCELQa8BIQIgASAERg3zASADKAIAIgAgBCABa2ohBSABIABrQQRqIQYCQANAIAEtAAAgAEHZ1ABqLQAARw0gIABBBEYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAz0AQsgA0EANgIAIAZBAWohAUEZDCALIAEgBEYEQEGuASECDPMBCwJAAkACQAJAIAEtAABBLWsOIwAkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJAEkJCQkJAIkJCQDJAsgAUEBaiEBQY4BIQIM3AELIAFBAWohAUGPASECDNsBCyABQQFqIQFBlAEhAgzaAQsgAUEBaiEBQZUBIQIM2QELQa0BIQIgASAERg3xASADKAIAIgAgBCABa2ohBSABIABrQQFqIQYCQANAIAEtAAAgAEHX1ABqLQAARw0eIABBAUYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAzyAQsgA0EANgIAIAZBAWohAUELDB4LIAEgBEYEQEGsASECDPEBCwJAAkAgAS0AAEHBAGsOAwAgASALIAFBAWohAUGQASECDNgBCyABQQFqIQFBkwEhAgzXAQsgASAERgRAQasBIQIM8AELAkACQCABLQAAQcEAaw4PAB8fHx8fHx8fHx8fHx8BHwsgAUEBaiEBQZEBIQIM1wELIAFBAWohAUGSASECDNYBCyABIARGBEBBqgEhAgzvAQsgAS0AAEHMAEcNHCABQQFqIQFBCgwbC0GpASECIAEgBEYN7QEgAygCACIAIAQgAWtqIQUgASAAa0EFaiEGAkADQCABLQAAIABB0dQAai0AAEcNGiAAQQVGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAM7gELIANBADYCACAGQQFqIQFBHgwaC0GoASECIAEgBEYN7AEgAygCACIAIAQgAWtqIQUgASAAa0EGaiEGAkADQCABLQAAIABBytQAai0AAEcNGSAAQQZGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAM7QELIANBADYCACAGQQFqIQFBFQwZC0GnASECIAEgBEYN6wEgAygCACIAIAQgAWtqIQUgASAAa0ECaiEGAkADQCABLQAAIABBx9QAai0AAEcNGCAAQQJGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAM7AELIANBADYCACAGQQFqIQFBFwwYC0GmASECIAEgBEYN6gEgAygCACIAIAQgAWtqIQUgASAAa0EFaiEGAkADQCABLQAAIABBwdQAai0AAEcNFyAAQQVGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAM6wELIANBADYCACAGQQFqIQFBGAwXCyABIARGBEBBpQEhAgzqAQsCQAJAIAEtAABByQBrDgcAGRkZGRkBGQsgAUEBaiEBQYsBIQIM0QELIAFBAWohAUGMASECDNABC0GkASECIAEgBEYN6AEgAygCACIAIAQgAWtqIQUgASAAa0EBaiEGAkADQCABLQAAIABBptUAai0AAEcNFSAAQQFGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAM6QELIANBADYCACAGQQFqIQFBCQwVC0GjASECIAEgBEYN5wEgAygCACIAIAQgAWtqIQUgASAAa0EBaiEGAkADQCABLQAAIABBpNUAai0AAEcNFCAAQQFGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAM6AELIANBADYCACAGQQFqIQFBHwwUC0GiASECIAEgBEYN5gEgAygCACIAIAQgAWtqIQUgASAAa0ECaiEGAkADQCABLQAAIABBvtQAai0AAEcNEyAAQQJGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAM5wELIANBADYCACAGQQFqIQFBAgwTC0GhASECIAEgBEYN5QEgAygCACIAIAQgAWtqIQUgASAAa0EBaiEGA0AgAS0AACAAQbzUAGotAABHDREgAEEBRg0CIABBAWohACAEIAFBAWoiAUcNAAsgAyAFNgIADOUBCyABIARGBEBBoAEhAgzlAQtBASABLQAAQd8ARw0RGiABQQFqIQFBhwEhAgzLAQsgA0EANgIAIAZBAWohAUGIASECDMoBC0GfASECIAEgBEYN4gEgAygCACIAIAQgAWtqIQUgASAAa0EIaiEGAkADQCABLQAAIABBhNUAai0AAEcNDyAAQQhGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAM4wELIANBADYCACAGQQFqIQFBKQwPC0GeASECIAEgBEYN4QEgAygCACIAIAQgAWtqIQUgASAAa0EDaiEGAkADQCABLQAAIABBuNQAai0AAEcNDiAAQQNGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAM4gELIANBADYCACAGQQFqIQFBLQwOCyABIARGBEBBnQEhAgzhAQsgAS0AAEHFAEcNDiABQQFqIQFBhAEhAgzHAQsgASAERgRAQZwBIQIM4AELAkACQCABLQAAQcwAaw4IAA8PDw8PDwEPCyABQQFqIQFBggEhAgzHAQsgAUEBaiEBQYMBIQIMxgELQZsBIQIgASAERg3eASADKAIAIgAgBCABa2ohBSABIABrQQRqIQYCQANAIAEtAAAgAEGz1ABqLQAARw0LIABBBEYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAzfAQsgA0EANgIAIAZBAWohAUEjDAsLQZoBIQIgASAERg3dASADKAIAIgAgBCABa2ohBSABIABrQQJqIQYCQANAIAEtAAAgAEGw1ABqLQAARw0KIABBAkYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAzeAQsgA0EANgIAIAZBAWohAUEADAoLIAEgBEYEQEGZASECDN0BCwJAAkAgAS0AAEHIAGsOCAAMDAwMDAwBDAsgAUEBaiEBQf0AIQIMxAELIAFBAWohAUGAASECDMMBCyABIARGBEBBmAEhAgzcAQsCQAJAIAEtAABBzgBrDgMACwELCyABQQFqIQFB/gAhAgzDAQsgAUEBaiEBQf8AIQIMwgELIAEgBEYEQEGXASECDNsBCyABLQAAQdkARw0IIAFBAWohAUEIDAcLQZYBIQIgASAERg3ZASADKAIAIgAgBCABa2ohBSABIABrQQNqIQYCQANAIAEtAAAgAEGs1ABqLQAARw0GIABBA0YNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAzaAQsgA0EANgIAIAZBAWohAUEFDAYLQZUBIQIgASAERg3YASADKAIAIgAgBCABa2ohBSABIABrQQVqIQYCQANAIAEtAAAgAEGm1ABqLQAARw0FIABBBUYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAzZAQsgA0EANgIAIAZBAWohAUEWDAULQZQBIQIgASAERg3XASADKAIAIgAgBCABa2ohBSABIABrQQJqIQYCQANAIAEtAAAgAEGh1QBqLQAARw0EIABBAkYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAzYAQsgA0EANgIAIAZBAWohAUEQDAQLIAEgBEYEQEGTASECDNcBCwJAAkAgAS0AAEHDAGsODAAGBgYGBgYGBgYGAQYLIAFBAWohAUH5ACECDL4BCyABQQFqIQFB+gAhAgy9AQtBkgEhAiABIARGDdUBIAMoAgAiACAEIAFraiEFIAEgAGtBBWohBgJAA0AgAS0AACAAQaDUAGotAABHDQIgAEEFRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAyAFNgIADNYBCyADQQA2AgAgBkEBaiEBQSQMAgsgA0EANgIADAILIAEgBEYEQEGRASECDNQBCyABLQAAQcwARw0BIAFBAWohAUETCzoAKSADKAIEIQAgA0EANgIEIAMgACABEC4iAA0CDAELQQAhAiADQQA2AhwgAyABNgIUIANB/h82AhAgA0EGNgIMDNEBC0H4ACECDLcBCyADQZABNgIcIAMgATYCFCADIAA2AgxBACECDM8BC0EAIQACQCADKAI4IgJFDQAgAigCQCICRQ0AIAMgAhEAACEACyAARQ0AIABBFUYNASADQQA2AhwgAyABNgIUIANBgg82AhAgA0EgNgIMQQAhAgzOAQtB9wAhAgy0AQsgA0GPATYCHCADIAE2AhQgA0HsGzYCECADQRU2AgxBACECDMwBCyABIARGBEBBjwEhAgzMAQsCQCABLQAAQSBGBEAgAUEBaiEBDAELIANBADYCHCADIAE2AhQgA0GbHzYCECADQQY2AgxBACECDMwBC0ECIQIMsgELA0AgAS0AAEEgRw0CIAQgAUEBaiIBRw0AC0GOASECDMoBCyABIARGBEBBjQEhAgzKAQsCQCABLQAAQQlrDgRKAABKAAtB9QAhAgywAQsgAy0AKUEFRgRAQfYAIQIMsAELQfQAIQIMrwELIAEgBEYEQEGMASECDMgBCyADQRA2AgggAyABNgIEDAoLIAEgBEYEQEGLASECDMcBCwJAIAEtAABBCWsOBEcAAEcAC0HzACECDK0BCyABIARHBEAgA0EQNgIIIAMgATYCBEHxACECDK0BC0GKASECDMUBCwJAIAEgBEcEQANAIAEtAABBoNAAai0AACIAQQNHBEACQCAAQQFrDgJJAAQLQfAAIQIMrwELIAQgAUEBaiIBRw0AC0GIASEC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BCABQQFqIgFHDQALQfsAIQIMpwELIAMoAgQhACADQQA2AgQgAyAAIAEQLCIARQ0DIANB2wA2AhwgAyABNgIUIAMgADYCDEEAIQIMpgELIAMoAgQhACADQQA2AgQgAyAAIAEQLCIARQ0DIANB3QA2AhwgAyABNgIUIAMgADYCDEEAIQIMpQELIAMoAgQhACADQQA2AgQgAyAAIAEQLCIARQ0DIANB+gA2AhwgAyABNgIUIAMgADYCDEEAIQIMpAELIANBADYCHCADIAE2AhQgA0G8CjYCECADQQc2AgxBACECDKMBC0HPACECDIkBC0HRACECDIgBC0HnACECDIcBCyABIARGBEBB+gAhAgygAQsCQCABLQAAQQlrDgQgAAAgAAsgAUEBaiEBQeYAIQIMhgELIAEgBEYEQEH5ACECDJ8BCwJAIAEtAABBCWsOBB8AAB8AC0EAIQACQCADKAI4IgJFDQAgAigCOCICRQ0AIAMgAhEAACEACyAARQRAQeIBIQIMhgELIABBFUcEQCADQQA2AhwgAyABNgIUIANByQ02AhAgA0EaNgIMQQAhAgyfAQsgA0H4ADYCHCADIAE2AhQgA0HqGjYCECADQRU2AgxBACECDJ4BCyABIARHBEAgA0ENNgIIIAMgATYCBEHkACECDIUBC0H3ACECDJ0BCyABIARGBEBB9gAhAgydAQsCQAJAAkAgAS0AAEHIAGsOCwABCwsLCwsLCwsCCwsgAUEBaiEBQd0AIQIMhQELIAFBAWohAUHgACECDIQBCyABQQFqIQFB4wAhAgyDAQtB9QAhAiABIARGDZsBIAMoAgAiACAEIAFraiEFIAEgAGtBAmohBgJAA0AgAS0AACAAQbXVAGotAABHDQggAEECRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAyAFNgIADJwBCyADKAIEIQAgA0IANwMAIAMgACAGQQFqIgEQKyIABEAgA0H0ADYCHCADIAE2AhQgAyAANgIMQQAhAgycAQtB4gAhAgyCAQtBACEAAkAgAygCOCICRQ0AIAIoAjQiAkUNACADIAIRAAAhAAsCQCAABEAgAEEVRg0BIANBADYCHCADIAE2AhQgA0HqDTYCECADQSY2AgxBACECDJwBC0HhACECDIIBCyADQfMANgIcIAMgATYCFCADQYAbNgIQIANBFTYCDEEAIQIMmgELIAMtACkiAEEja0ELSQ0JAkAgAEEGSw0AQQEgAHRBygBxRQ0ADAoLQQAhAiADQQA2AhwgAyABNgIUIANB7Qk2AhAgA0EINgIMDJkBC0HyACECIAEgBEYNmAEgAygCACIAIAQgAWtqIQUgASAAa0EBaiEGAkADQCABLQAAIABBs9UAai0AAEcNBSAAQQFGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAMmQELIAMoAgQhACADQgA3AwAgAyAAIAZBAWoiARArIgAEQCADQfEANgIcIAMgATYCFCADIAA2AgxBACECDJkBC0HfACECDH8LQQAhAAJAIAMoAjgiAkUNACACKAI0IgJFDQAgAyACEQAAIQALAkAgAARAIABBFUYNASADQQA2AhwgAyABNgIUIANB6g02AhAgA0EmNgIMQQAhAgyZAQtB3gAhAgx/CyADQfAANgIcIAMgATYCFCADQYAbNgIQIANBFTYCDEEAIQIMlwELIAMtAClBIUYNBiADQQA2AhwgAyABNgIUIANBkQo2AhAgA0EINgIMQQAhAgyWAQtB7wAhAiABIARGDZUBIAMoAgAiACAEIAFraiEFIAEgAGtBAmohBgJAA0AgAS0AACAAQbDVAGotAABHDQIgAEECRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAyAFNgIADJYBCyADKAIEIQAgA0IANwMAIAMgACAGQQFqIgEQKyIARQ0CIANB7QA2AhwgAyABNgIUIAMgADYCDEEAIQIMlQELIANBADYCAAsgAygCBCEAIANBADYCBCADIAAgARArIgBFDYABIANB7gA2AhwgAyABNgIUIAMgADYCDEEAIQIMkwELQdwAIQIMeQtBACEAAkAgAygCOCICRQ0AIAIoAjQiAkUNACADIAIRAAAhAAsCQCAABEAgAEEVRg0BIANBADYCHCADIAE2AhQgA0HqDTYCECADQSY2AgxBACECDJMBC0HbACECDHkLIANB7AA2AhwgAyABNgIUIANBgBs2AhAgA0EVNgIMQQAhAgyRAQsgAy0AKSIAQSNJDQAgAEEuRg0AIANBADYCHCADIAE2AhQgA0HJCTYCECADQQg2AgxBACECDJABC0HaACECDHYLIAEgBEYEQEHrACECDI8BCwJAIAEtAABBL0YEQCABQQFqIQEMAQsgA0EANgIcIAMgATYCFCADQbI4NgIQIANBCDYCDEEAIQIMjwELQdkAIQIMdQsgASAERwRAIANBDjYCCCADIAE2AgRB2AAhAgx1C0HqACECDI0BCyABIARGBEBB6QAhAgyNAQsgAS0AAEEwayIAQf8BcUEKSQRAIAMgADoAKiABQQFqIQFB1wAhAgx0CyADKAIEIQAgA0EANgIEIAMgACABEC8iAEUNeiADQegANgIcIAMgATYCFCADIAA2AgxBACECDIwBCyABIARGBEBB5wAhAgyMAQsCQCABLQAAQS5GBEAgAUEBaiEBDAELIAMoAgQhACADQQA2AgQgAyAAIAEQLyIARQ17IANB5gA2AhwgAyABNgIUIAMgADYCDEEAIQIMjAELQdYAIQIMcgsgASAERgRAQeUAIQIMiwELQQAhAEEBIQVBASEHQQAhAgJAAkACQAJAAkACfwJAAkACQAJAAkACQAJAIAEtAABBMGsOCgoJAAECAwQFBggLC0ECDAYLQQMMBQtBBAwEC0EFDAMLQQYMAgtBBwwBC0EICyECQQAhBUEAIQcMAgtBCSECQQEhAEEAIQVBACEHDAELQQAhBUEBIQILIAMgAjoAKyABQQFqIQECQAJAIAMtAC5BEHENAAJAAkACQCADLQAqDgMBAAIECyAHRQ0DDAILIAANAQwCCyAFRQ0BCyADKAIEIQAgA0EANgIEIAMgACABEC8iAEUNAiADQeIANgIcIAMgATYCFCADIAA2AgxBACECDI0BCyADKAIEIQAgA0EANgIEIAMgACABEC8iAEUNfSADQeMANgIcIAMgATYCFCADIAA2AgxBACECDIwBCyADKAIEIQAgA0EANgIEIAMgACABEC8iAEUNeyADQeQANgIcIAMgATYCFCADIAA2AgwMiwELQdQAIQIMcQsgAy0AKUEiRg2GAUHTACECDHALQQAhAAJAIAMoAjgiAkUNACACKAJEIgJFDQAgAyACEQAAIQALIABFBEBB1QAhAgxwCyAAQRVHBEAgA0EANgIcIAMgATYCFCADQaQNNgIQIANBITYCDEEAIQIMiQELIANB4QA2AhwgAyABNgIUIANB0Bo2AhAgA0EVNgIMQQAhAgyIAQsgASAERgRAQeAAIQIMiAELAkACQAJAAkACQCABLQAAQQprDgQBBAQABAsgAUEBaiEBDAELIAFBAWohASADQS9qLQAAQQFxRQ0BC0HSACECDHALIANBADYCHCADIAE2AhQgA0G2ETYCECADQQk2AgxBACECDIgBCyADQQA2AhwgAyABNgIUIANBthE2AhAgA0EJNgIMQQAhAgyHAQsgASAERgRAQd8AIQIMhwELIAEtAABBCkYEQCABQQFqIQEMCQsgAy0ALkHAAHENCCADQQA2AhwgAyABNgIUIANBthE2AhAgA0ECNgIMQQAhAgyGAQsgASAERgRAQd0AIQIMhgELIAEtAAAiAkENRgRAIAFBAWohAUHQACECDG0LIAEhACACQQlrDgQFAQEFAQsgBCABIgBGBEBB3AAhAgyFAQsgAC0AAEEKRw0AIABBAWoMAgtBACECIANBADYCHCADIAA2AhQgA0HKLTYCECADQQc2AgwMgwELIAEgBEYEQEHbACECDIMBCwJAIAEtAABBCWsOBAMAAAMACyABQQFqCyEBQc4AIQIMaAsgASAERgRAQdoAIQIMgQELIAEtAABBCWsOBAABAQABC0EAIQIgA0EANgIcIANBmhI2AhAgA0EHNgIMIAMgAUEBajYCFAx/CyADQYASOwEqQQAhAAJAIAMoAjgiAkUNACACKAI4IgJFDQAgAyACEQAAIQALIABFDQAgAEEVRw0BIANB2QA2AhwgAyABNgIUIANB6ho2AhAgA0EVNgIMQQAhAgx+C0HNACECDGQLIANBADYCHCADIAE2AhQgA0HJDTYCECADQRo2AgxBACECDHwLIAEgBEYEQEHZACECDHwLIAEtAABBIEcNPSABQQFqIQEgAy0ALkEBcQ09IANBADYCHCADIAE2AhQgA0HCHDYCECADQR42AgxBACECDHsLIAEgBEYEQEHYACECDHsLAkACQAJAAkACQCABLQAAIgBBCmsOBAIDAwABCyABQQFqIQFBLCECDGULIABBOkcNASADQQA2AhwgAyABNgIUIANB5xE2AhAgA0EKNgIMQQAhAgx9CyABQQFqIQEgA0Evai0AAEEBcUUNcyADLQAyQYABcUUEQCADQTJqIQIgAxA1QQAhAAJAIAMoAjgiBkUNACAGKAIoIgZFDQAgAyAGEQAAIQALAkACQCAADhZNTEsBAQEBAQEBAQEBAQEBAQEBAQEAAQsgA0EpNgIcIAMgATYCFCADQawZNgIQIANBFTYCDEEAIQIMfgsg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c3BvbnNlIHN0YXR1cwBFeHBlY3RlZCBMRiBhZnRlciBoZWFkZXJzAEludmFsaWQgY2hhcmFjdGVyIGluIGNodW5rIGV4dGVuc2lvbnMAVXNlciBjYWxsYmFjayBlcnJvcgBgb25fcmVzZXRgIGNhbGxiYWNrIGVycm9yAGBvbl9jaHVua19oZWFkZXJgIGNhbGxiYWNrIGVycm9yAGBvbl9tZXNzYWdlX2JlZ2luYCBjYWxsYmFjayBlcnJvcgBgb25fY2h1bmtfZXh0ZW5zaW9uX3ZhbHVlYCBjYWxsYmFjayBlcnJvcgBgb25fc3RhdHVzX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25fdmVyc2lvbl9jb21wbGV0ZWAgY2FsbGJhY2sgZXJyb3IAYG9uX3VybF9jb21wbGV0ZWAgY2FsbGJhY2sgZXJyb3IAYG9uX3Byb3RvY29sX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25fY2h1bmtfY29tcGxldGVgIGNhbGxiYWNrIGVycm9yAGBvbl9oZWFkZXJfdmFsdWVfY29tcGxldGVgIGNhbGxiYWNrIGVycm9yAGBvbl9tZXNzYWdlX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25fbWV0aG9kX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25faGVhZGVyX2ZpZWxkX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25fY2h1bmtfZXh0ZW5zaW9uX25hbWVgIGNhbGxiYWNrIGVycm9yAFVuZXhwZWN0ZWQgY2hhciBpbiB1cmwgc2VydmVyAEludmFsaWQgaGVhZGVyIHZhbHVlIGNoYXIASW52YWxpZCBoZWFkZXIgZmllbGQgY2hhcgBTcGFuIGNhbGxiYWNrIGVycm9yIGluIG9uX3ZlcnNpb24ASW52YWxpZCBtaW5vciB2ZXJzaW9uAEludmFsaWQgbWFqb3IgdmVyc2lvbgBFeHBlY3RlZCBzcGFjZSBhZnRlciB2ZXJzaW9uAEV4cGVjdGVkIENSTEYgYWZ0ZXIgdmVyc2lvbgBJbnZhbGlkIEhUVFAgdmVyc2lvbgBJbnZhbGlkIGhlYWRlciB0b2tlbgBTcGFuIGNhbGxiYWNrIGVycm9yIGluIG9uX3VybABJbnZhbGlkIGNoYXJhY3RlcnMgaW4gdXJsAFVuZXhwZWN0ZWQgc3RhcnQgY2hhciBpbiB1cmwARG91YmxlIEAgaW4gdXJsAFNwYW4gY2FsbGJhY2sgZXJyb3IgaW4gb25fcHJvdG9jb2wARW1wdHkgQ29udGVudC1MZW5ndGgASW52YWxpZCBjaGFyYWN0ZXIgaW4gQ29udGVudC1MZW5ndGgAVHJhbnNmZXItRW5jb2RpbmcgY2FuJ3QgYmUgcHJlc2VudCB3aXRoIENvbnRlbnQtTGVuZ3RoAER1cGxpY2F0ZSBDb250ZW50LUxlbmd0aABJbnZhbGlkIGNoYXIgaW4gdXJsIHBhdGgAQ29udGVudC1MZW5ndGggY2FuJ3QgYmUgcHJlc2VudCB3aXRoIFRyYW5zZmVyLUVuY29kaW5nAE1pc3NpbmcgZXhwZWN0ZWQgQ1IgYWZ0ZXIgY2h1bmsgc2l6ZQBFeHBlY3RlZCBMRiBhZnRlciBjaHVuayBzaXplAEludmFsaWQgY2hhcmFjdGVyIGluIGNodW5rIHNpemUAU3BhbiBjYWxsYmFjayBlcnJvciBpbiBvbl9oZWFkZXJfdmFsdWUAU3BhbiBjYWxsYmFjayBlcnJvciBpbiBvbl9jaHVua19leHRlbnNpb25fdmFsdWUASW52YWxpZCBjaGFyYWN0ZXIgaW4gY2h1bmsgZXh0ZW5zaW9ucyB2YWx1ZQBVbmV4cGVjdGVkIHdoaXRlc3BhY2UgYWZ0ZXIgaGVhZGVyIHZhbHVlAE1pc3NpbmcgZXhwZWN0ZWQgQ1IgYWZ0ZXIgaGVhZGVyIHZhbHVlAE1pc3NpbmcgZXhwZWN0ZWQgTEYgYWZ0ZXIgaGVhZGVyIHZhbHVlAEludmFsaWQgYFRyYW5zZmVyLUVuY29kaW5nYCBoZWFkZXIgdmFsdWUATWlzc2luZyBleHBlY3RlZCBDUiBhZnRlciBjaHVuayBleHRlbnNpb24gdmFsdWUASW52YWxpZCBjaGFyYWN0ZXIgaW4gY2h1bmsgZXh0ZW5zaW9ucyBxdW90ZSB2YWx1ZQBJbnZhbGlkIHF1b3RlZC1wYWlyIGluIGNodW5rIGV4dGVuc2lvbnMgcXVvdGVkIHZhbHVlAEludmFsaWQgY2hhcmFjdGVyIGluIGNodW5rIGV4dGVuc2lvbnMgcXVvdGVkIHZhbHVlAFBhdXNlZCBieSBvbl9oZWFkZXJzX2NvbXBsZXRlAEludmFsaWQgRU9GIHN0YXRlAG9uX3Jlc2V0IHBhdXNlAG9uX2NodW5rX2hlYWRlciBwYXVzZQBvbl9tZXNzYWdlX2JlZ2luIHBhdXNlAG9uX2NodW5rX2V4dGVuc2lvbl92YWx1ZSBwYXVzZQBvbl9zdGF0dXNfY29tcGxldGUgcGF1c2UAb25fdmVyc2lvbl9jb21wbGV0ZSBwYXVzZQBvbl91cmxfY29tcGxldGUgcGF1c2UAb25fcHJvdG9jb2xfY29tcGxldGUgcGF1c2UAb25fY2h1bmtfY29tcGxldGUgcGF1c2UAb25faGVhZGVyX3ZhbHVlX2NvbXBsZXRlIHBhdXNlAG9uX21lc3NhZ2VfY29tcGxldGUgcGF1c2UAb25fbWV0aG9kX2NvbXBsZXRlIHBhdXNlAG9uX2hlYWRlcl9maWVsZF9jb21wbGV0ZSBwYXVzZQBvbl9jaHVua19leHRlbnNpb25fbmFtZSBwYXVzZQBVbmV4cGVjdGVkIHNwYWNlIGFmdGVyIHN0YXJ0IGxpbmUATWlzc2luZyBleHBlY3RlZCBDUiBhZnRlciByZXNwb25zZSBsaW5lAFNwYW4gY2FsbGJhY2sgZXJyb3IgaW4gb25fY2h1bmtfZXh0ZW5zaW9uX25hbWUASW52YWxpZCBjaGFyYWN0ZXIgaW4gY2h1bmsgZXh0ZW5zaW9ucyBuYW1lAE1pc3NpbmcgZXhwZWN0ZWQgQ1IgYWZ0ZXIgY2h1bmsgZXh0ZW5zaW9uIG5hbWUASW52YWxpZCBzdGF0dXMgY29kZQBQYXVzZSBvbiBDT05ORUNUL1VwZ3JhZGUAUGF1c2Ugb24gUFJJL1VwZ3JhZGUARXhwZWN0ZWQgSFRUUC8yIENvbm5lY3Rpb24gUHJlZmFjZQBTcGFuIGNhbGxiYWNrIGVycm9yIGluIG9uX21ldGhvZABFeHBlY3RlZCBzcGFjZSBhZnRlciBtZXRob2QAU3BhbiBjYWxsYmFjayBlcnJvciBpbiBvbl9oZWFkZXJfZmllbGQAUGF1c2VkAEludmFsaWQgd29yZCBlbmNvdW50ZXJlZABJbnZhbGlkIG1ldGhvZCBlbmNvdW50ZXJlZABNaXNzaW5nIGV4cGVjdGVkIENSIGFmdGVyIGNodW5rIGRhdGEARXhwZWN0ZWQgTEYgYWZ0ZXIgY2h1bmsgZGF0YQBVbmV4cGVjdGVkIGNoYXIgaW4gdXJsIHNjaGVtYQBSZXF1ZXN0IGhhcyBpbnZhbGlkIGBUcmFuc2Zlci1FbmNvZGluZ2AARGF0YSBhZnRlciBgQ29ubmVjdGlvbjogY2xvc2VgAFNXSVRDSF9QUk9YWQBVU0VfUFJPWFkATUtBQ1RJVklUWQBVTlBST0NFU1NBQkxFX0VOVElUWQBRVUVSWQBDT1BZAE1PVkVEX1BFUk1BTkVOVExZAFRPT19FQVJMWQBOT1RJRlkARkFJTEVEX0RFUEVOREVOQ1kAQkFEX0dBVEVXQVkAUExBWQBQVVQAQ0hFQ0tPVVQAR0FURVdBWV9USU1FT1VUAFJFUVVFU1RfVElNRU9VVABORVRXT1JLX0NPTk5FQ1RfVElNRU9VVABDT05ORUNUSU9OX1RJTUVPVVQATE9HSU5fVElNRU9VVABORVRXT1JLX1JFQURfVElNRU9VVABQT1NUAE1JU0RJUkVDVEVEX1JFUVVFU1QAQ0xJRU5UX0NMT1NFRF9SRVFVRVNUAENMSUVOVF9DTE9TRURfTE9BRF9CQUxBTkNFRF9SRVFVRVNUAEJBRF9SRVFVRVNUAEhUVFBfUkVRVUVTVF9TRU5UX1RPX0hUVFBTX1BPUlQAUkVQT1JUAElNX0FfVEVBUE9UAFJFU0VUX0NPTlRFTlQATk9fQ09OVEVOVABQQVJUSUFMX0NPTlRFTlQASFBFX0lOVkFMSURfQ09OU1RBTlQASFBFX0NCX1JFU0VUAEdFVABIUEVfU1RSSUNUAENPTkZMSUNUAFRFTVBPUkFSWV9SRURJUkVDVABQRVJNQU5FTlRfUkVESVJFQ1QAQ09OTkVDVABNVUxUSV9TVEFUVVMASFBFX0lOVkFMSURfU1RBVFVTAFRPT19NQU5ZX1JFUVVFU1RTAEVBUkxZX0hJTlRTAFVOQVZBSUxBQkxFX0ZPUl9MRUdBTF9SRUFTT05TAE9QVElPTlMAU1dJVENISU5HX1BST1RPQ09MUwBWQVJJQU5UX0FMU09fTkVHT1RJQVRFUwBNVUxUSVBMRV9DSE9JQ0VTAElOVEVSTkFMX1NFUlZFUl9FUlJPUgBXRUJfU0VSVkVSX1VOS05PV05fRVJST1IAUkFJTEdVTl9FUlJPUgBJREVOVElUWV9QUk9WSURFUl9BVVRIRU5USUNBVElPTl9FUlJPUgBTU0xfQ0VSVElGSUNBVEVfRVJST1IASU5WQUxJRF9YX0ZPUldBUkRFRF9GT1IAU0VUX1BBUkFNRVRFUgBHRVRfUEFSQU1FVEVSAEhQRV9VU0VSAFNFRV9PVEhFUgBIUEVfQ0JfQ0hVTktfSEVBREVSAEV4cGVjdGVkIExGIGFmdGVyIENSAE1LQ0FMRU5EQVIAU0VUVVAAV0VCX1NFUlZFUl9JU19ET1dOAFRFQVJET1dOAEhQRV9DTE9TRURfQ09OTkVDVElPTgBIRVVSSVNUSUNfRVhQSVJBVElPTgBESVNDT05ORUNURURfT1BFUkFUSU9OAE5PTl9BVVRIT1JJVEFUSVZFX0lORk9STUFUSU9OAEhQRV9JTlZBTElEX1ZFUlNJT04ASFBFX0NCX01FU1NBR0VfQkVHSU4AU0lURV9JU19GUk9aRU4ASFBFX0lOVkFMSURfSEVBREVSX1RPS0VOAElOVkFMSURfVE9LRU4ARk9SQklEREVOAEVOSEFOQ0VfWU9VUl9DQUxNAEhQRV9JTlZBTElEX1VSTABCTE9DS0VEX0JZX1BBUkVOVEFMX0NPTlRST0wATUtDT0wAQUNMAEhQRV9JTlRFUk5BTABSRVFVRVNUX0hFQURFUl9GSUVMRFNfVE9PX0xBUkdFX1VOT0ZGSUNJQUwASFBFX09LAFVOTElOSwBVTkxPQ0sAUFJJAFJFVFJZX1dJVEgASFBFX0lOVkFMSURfQ09OVEVOVF9MRU5HVEgASFBFX1VORVhQRUNURURfQ09OVEVOVF9MRU5HVEgARkxVU0gAUFJPUFBBVENIAE0tU0VBUkNIAFVSSV9UT09fTE9ORwBQUk9DRVNTSU5HAE1JU0NFTExBTkVPVVNfUEVSU0lTVEVOVF9XQVJOSU5HAE1JU0NFTExBTkVPVVNfV0FSTklORwBIUEVfSU5WQUxJRF9UUkFOU0ZFUl9FTkNPRElORwBFeHBlY3RlZCBDUkxGAEhQRV9JTlZBTElEX0NIVU5LX1NJWkUATU9WRQBDT05USU5VRQBIUEVfQ0JfU1RBVFVTX0NPTVBMRVRFAEhQRV9DQl9IRUFERVJTX0NPTVBMRVRFAEhQRV9DQl9WRVJTSU9OX0NPTVBMRVRFAEhQRV9DQl9VUkxfQ09NUExFVEUASFBFX0NCX1BST1RPQ09MX0NPTVBMRVRFAEhQRV9DQl9DSFVOS19DT01QTEVURQBIUEVfQ0JfSEVBREVSX1ZBTFVFX0NPTVBMRVRFAEhQRV9DQl9DSFVOS19FWFRFTlNJT05fVkFMVUVfQ09NUExFVEUASFBFX0NCX0NIVU5LX0VYVEVOU0lPTl9OQU1FX0NPTVBMRVRFAEhQRV9DQl9NRVNTQUdFX0NPTVBMRVRFAEhQRV9DQl9NRVRIT0RfQ09NUExFVEUASFBFX0NCX0hFQURFUl9GSUVMRF9DT01QTEVURQBERUxFVEUASFBFX0lOVkFMSURfRU9GX1NUQVRFAElOVkFMSURfU1NMX0NFUlRJRklDQVRFAFBBVVNFAE5PX1JFU1BPTlNFAFVOU1VQUE9SVEVEX01FRElBX1RZUEUAR09ORQBOT1RfQUNDRVBUQUJMRQBTRVJWSUNFX1VOQVZBSUxBQkxFAFJBTkdFX05PVF9TQVRJU0ZJQUJMRQBPUklHSU5fSVNfVU5SRUFDSEFCTEUAUkVTUE9OU0VfSVNfU1RBTEUAUFVSR0UATUVSR0UAUkVRVUVTVF9IRUFERVJfRklFTERTX1RPT19MQVJHRQBSRVFVRVNUX0hFQURFUl9UT09fTEFSR0UAUEFZTE9BRF9UT09fTEFSR0UASU5TVUZGSUNJRU5UX1NUT1JBR0UASFBFX1BBVVNFRF9VUEdSQURFAEhQRV9QQVVTRURfSDJfVVBHUkFERQBTT1VSQ0UAQU5OT1VOQ0UAVFJBQ0UASFBFX1VORVhQRUNURURfU1BBQ0UAREVTQ1JJQkUAVU5TVUJTQ1JJQkUAUkVDT1JEAEhQRV9JTlZBTElEX01FVEhPRABOT1RfRk9VTkQAUFJPUEZJTkQAVU5CSU5EAFJFQklORABVTkFVVEhPUklaRUQATUVUSE9EX05PVF9BTExPV0VEAEhUVFBfVkVSU0lPTl9OT1RfU1VQUE9SVEVEAEFMUkVBRFlfUkVQT1JURUQAQUNDRVBURUQATk9UX0lNUExFTUVOVEVEAExPT1BfREVURUNURUQASFBFX0NSX0VYUEVDVEVEAEhQRV9MRl9FWFBFQ1RFRABDUkVBVEVEAElNX1VTRUQASFBFX1BBVVNFRABUSU1FT1VUX09DQ1VSRUQAUEFZTUVOVF9SRVFVSVJFRABQUkVDT05ESVRJT05fUkVRVUlSRUQAUFJPWFlfQVVUSEVOVElDQVRJT05fUkVRVUlSRUQATkVUV09SS19BVVRIRU5USUNBVElPTl9SRVFVSVJFRABMRU5HVEhfUkVRVUlSRUQAU1NMX0NFUlRJRklDQVRFX1JFUVVJUkVEAFVQR1JBREVfUkVRVUlSRUQAUEFHRV9FWFBJUkVEAFBSRUNPTkRJVElPTl9GQUlMRUQARVhQRUNUQVRJT05fRkFJTEVEAFJFVkFMSURBVElPTl9GQUlMRUQAU1NMX0hBTkRTSEFLRV9GQUlMRUQATE9DS0VEAFRSQU5TRk9STUFUSU9OX0FQUExJRUQATk9UX01PRElGSUVEAE5PVF9FWFRFTkRFRABCQU5EV0lEVEhfTElNSVRfRVhDRUVERUQAU0lURV9JU19PVkVSTE9BREVEAEhFQUQARXhwZWN0ZWQgSFRUUC8sIFJUU1AvIG9yIElDRS8A5xUAAK8VAACkEgAAkhoAACYWAACeFAAA2xkAAHkVAAB+EgAA/hQAADYVAAALFgAA2BYAAPMSAABCGAAArBYAABIVAAAUFwAA7xcAAEgUAABxFwAAshoAAGsZAAB+GQAANRQAAIIaAABEFwAA/RYAAB4YAACHFwAAqhkAAJMSAAAHGAAALBcAAMoXAACkFwAA5xUAAOcVAABYFwAAOxgAAKASAAAtHAAAwxEAAEgRAADeEgAAQhMAAKQZAAD9EAAA9xUAAKUVAADvFgAA+BkAAEoWAABWFgAA9RUAAAoaAAAIGgAAARoAAKsVAABCEgAA1xAAAEwRAAAFGQAAVBYAAB4RAADKGQAAyBkAAE4WAAD/GAAAcRQAAPAVAADuFQAAlBkAAPwVAAC/GQAAmxkAAHwUAABDEQAAcBgAAJUUAAAnFAAAGRQAANUSAADUGQAARBYAAPcQAEG5OwsBAQBB0DsL4AEBAQIBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEDAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQBBuj0LBAEAAAIAQdE9C14DBAMDAwMDAAADAwADAwADAwMDAwMDAwMDAAUAAAAAAAMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAAAAAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMAAwADAEG6PwsEAQAAAgBB0T8LXgMAAwMDAwMAAAMDAAMDAAMDAwMDAwMDAwMABAAFAAAAAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMAAAADAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwADAAMAQbDBAAsNbG9zZWVlcC1hbGl2ZQBBycEACwEBAEHgwQAL4AEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQBBycMACwEBAEHgwwAL5wEBAQEBAQEBAQEBAQECAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAWNodW5rZWQAQfHFAAteAQABAQEBAQAAAQEAAQEAAQEBAQEBAQEBAQAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEAAQBB0McACyFlY3Rpb25lbnQtbGVuZ3Rob25yb3h5LWNvbm5lY3Rpb24AQYDIAAsgcmFuc2Zlci1lbmNvZGluZ3BncmFkZQ0KDQpTTQ0KDQoAQanIAAsFAQIAAQMAQcDIAAtfBAUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUAQanKAAsFAQIAAQMAQcDKAAtfBAUFBgUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUAQanMAAsEAQAAAQBBwcwAC14CAgACAgICAgICAgICAgICAgICAgICAgICAgICAgIAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAEGpzgALBQECAAEDAEHAzgALXwQFAAAFBQUFBQUFBQUFBQYFBQUFBQUFBQUFBQUABQAHCAUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQAFAAUABQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUAAAAFAEGp0AALBQEBAAEBAEHA0AALAQEAQdrQAAtBAgAAAAAAAAMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAAAAAAAAAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMAQanSAAsFAQEAAQEAQcDSAAsBAQBBytIACwYCAAAAAAIAQeHSAAs6AwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMAAAAAAAADAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwBBoNQAC50BTk9VTkNFRUNLT1VUTkVDVEVURUNSSUJFTFVTSEVURUFEU0VBUkNIUkdFQ1RJVklUWUxFTkRBUlZFT1RJRllQVElPTlNDSFNFQVlTVEFUQ0hHRVVFUllPUkRJUkVDVE9SVFJDSFBBUkFNRVRFUlVSQ0VCU0NSSUJFQVJET1dOQUNFSU5ETktDS1VCU0NSSUJFVFRQQ0VUU1BBRFRQLw==' + +let wasmBuffer + +Object.defineProperty(module, 'exports', { + get: () => { + return wasmBuffer + ? wasmBuffer + : (wasmBuffer = Buffer.from(wasmBase64, 'base64')) + } +}) diff --git a/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/llhttp/llhttp_simd-wasm.js b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/llhttp/llhttp_simd-wasm.js new file mode 100644 index 000000000..4508cb14d --- /dev/null +++ b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/llhttp/llhttp_simd-wasm.js @@ -0,0 +1,15 @@ +'use strict' + +const { Buffer } = require('node:buffer') + +const wasmBase64 = 'AGFzbQEAAAABJwdgAX8Bf2ADf39/AX9gAn9/AGABfwBgBH9/f38Bf2AAAGADf39/AALLAQgDZW52GHdhc21fb25faGVhZGVyc19jb21wbGV0ZQAEA2VudhV3YXNtX29uX21lc3NhZ2VfYmVnaW4AAANlbnYLd2FzbV9vbl91cmwAAQNlbnYOd2FzbV9vbl9zdGF0dXMAAQNlbnYUd2FzbV9vbl9oZWFkZXJfZmllbGQAAQNlbnYUd2FzbV9vbl9oZWFkZXJfdmFsdWUAAQNlbnYMd2FzbV9vbl9ib2R5AAEDZW52GHdhc21fb25fbWVzc2FnZV9jb21wbGV0ZQAAAzU0BQYAAAMAAAAAAAADAQMAAwMDAAACAAAAAAICAgICAgICAgIBAQEBAQEBAQEBAwAAAwAAAAQFAXABExMFAwEAAgYIAX8BQcDZBAsHxQcoBm1lbW9yeQIAC19pbml0aWFsaXplAAgZX19pbmRpcmVjdF9mdW5jdGlvbl90YWJsZQEAC2xsaHR0cF9pbml0AAkYbGxodHRwX3Nob3VsZF9rZWVwX2FsaXZlADcMbGxodHRwX2FsbG9jAAsGbWFsbG9jADkLbGxodHRwX2ZyZWUADARmcmVlAAwPbGxodHRwX2dldF90eXBlAA0VbGxodHRwX2dldF9odHRwX21ham9yAA4VbGxodHRwX2dldF9odHRwX21pbm9yAA8RbGxodHRwX2dldF9tZXRob2QAEBZsbGh0dHBfZ2V0X3N0YXR1c19jb2RlABESbGxodHRwX2dldF91cGdyYWRlABIMbGxodHRwX3Jlc2V0ABMObGxodHRwX2V4ZWN1dGUAFBRsbGh0dHBfc2V0dGluZ3NfaW5pdAAVDWxsaHR0cF9maW5pc2gAFgxsbGh0dHBfcGF1c2UAFw1sbGh0dHBfcmVzdW1lABgbbGxodHRwX3Jlc3VtZV9hZnRlcl91cGdyYWRlABkQbGxodHRwX2dldF9lcnJubwAaF2xsaHR0cF9nZXRfZXJyb3JfcmVhc29uABsXbGxodHRwX3NldF9lcnJvcl9yZWFzb24AHBRsbGh0dHBfZ2V0X2Vycm9yX3BvcwAdEWxsaHR0cF9lcnJub19uYW1lAB4SbGxodHRwX21ldGhvZF9uYW1lAB8SbGxodHRwX3N0YXR1c19uYW1lACAabGxodHRw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AXRrQT5qIQILIAEgAjYCHCABQgA3AhAgAkECdEH01wBqIQACQEHI1QAoAgAiA0EBIAJ0IgdxRQRAIAAgATYCAEHI1QAgAyAHcjYCACABIAA2AhggASABNgIIIAEgATYCDAwBCyAEQRkgAkEBdmtBACACQR9HG3QhAiAAKAIAIQACQANAIAAiAygCBEF4cSAERg0BIAJBHXYhACACQQF0IQIgAyAAQQRxakEQaiIHKAIAIgANAAsgByABNgIAIAEgAzYCGCABIAE2AgwgASABNgIIDAELIAMoAggiACABNgIMIAMgATYCCCABQQA2AhggASADNgIMIAEgADYCCAtB5NUAQeTVACgCAEEBayIAQX8gABs2AgALCwcAIAAtACgLBwAgAC0AKgsHACAALQArCwcAIAAtACkLBwAgAC8BNAsHACAALQAwC0ABBH8gACgCGCEBIAAvAS4hAiAALQAoIQMgACgCOCEEIAAQOCAAIAQ2AjggACADOgAoIAAgAjsBLiAAIAE2AhgLhocCAwd/A34BeyABIAJqIQQCQCAAIgMoAgwiAA0AIAMoAgQEQCADIAE2AgQLIwBBEGsiCSQAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACfwJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkAC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DAgLQeMBDAcLQdcBDAYLQdgBDAULQdkBDAQLQdoBDAMLQdsBDAILQd0BDAELQdwBCyECA0ACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAIAMCfwJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACfwJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACfwJAAkACQAJAAkACQAJAAn8CQAJAAkACfwJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkAgAwJ/AkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJ/AkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkAC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A0EXNgIcIAMgADYCDCADIAFBAWo2AhQM+AILIABBFUYNI0EAIQIgA0EANgIcIAMgATYCFCADQdYMNgIQIANBIzYCDAz3AgsgAygCBCEAQQAhAiADQQA2AgQgAyAAIAEQNCIARQRAIAFBAWohAQwdCyADQRk2AhwgAyAANgIMIAMgAUEBajYCFAz2AgsgAygCBCEAQQAhAiADQQA2AgQgAyAAIAEQNCIARQRAIAFBAWohAQzvAgsgA0EaNgIcIAMgADYCDCADIAFBAWo2AhQM9QILIABBFUYNH0EAIQIgA0EANgIcIAMgATYCFCADQdAPNgIQIANBIjYCDAz0AgsgAygCBCEAIANBADYCBCADIAAgARAzIgBFBEAgAUEBaiEBDBsLIANBHDYCHCADIAA2AgwgAyABQQFqNgIUQQAhAgzzAgsgAygCBCEAIANBADYCBCADIAAgARAzIgBFBEAgAUEBaiEBDOsCCyADQR02AhwgAyAANgIMIAMgAUEBajYCFEEAIQIM8gILIABBO0cNASABQQFqIQELQSYhAgzXAgtBACECIANBADYCHCADIAE2AhQgA0GfFTYCECADQQw2AgwM7wILIAEgBEcEQANAIAEtAABBIEcNhAIgBCABQQFqIgFHDQALQSwhAgzvAgtBLCECDO4CCyABIARGBEBBNCECDO4CCwJAAkADQAJAIAEtAABBCmsOBAIAAAMACyAEIAFBAWoiAUcNAAtBNCECDO8CCyADKAIEIQAgA0EANgIEIAMgACABEDEiAEUNnwIgA0EyNgIcIAMgATYCFCADIAA2AgxBACECDO4CCyADKAIEIQAgA0EANgIEIAMgACABEDEiAEUEQCABQQFqIQEMnwILIANBMjYCHCADIAA2AgwgAyABQQFqNgIUQQAhAgztAgsgASAERwRAAkADQCABLQAAQTBrIgBB/wFxQQpPBEBBOiECDNcCCyADKQMgIgtCmbPmzJmz5swZVg0BIAMgC0IKfiIKNwMgIAogAK1C/wGDIgtCf4VWDQEgAyAKIAt8NwMgIAQgAUEBaiIBRw0AC0HAACECDO4CCyADKAIEIQAgA0EANgIEIAMgACABQQFqIgEQMSIADRcM4gILQcAAIQIM7AILIAEgBEYEQEHJACECDOwCCwJAA0ACQCABLQAAQQlrDhgAAqICogKpAqICogKiAqICogKiAqICogKiAqICogKiAqICogKiAqICogKiAgCiAgsgBCABQQFqIgFHDQALQckAIQIM7AILIAFBAWohASADQS9qLQAAQQFxDaUCIANBADYCHCADIAE2AhQgA0GXEDYCECADQQo2AgxBACECDOsCCyABIARHBEADQCABLQAAQSBHDRUgBCABQQFqIgFHDQALQfgAIQIM6wILQfgAIQIM6gILIANBAjoAKAw4C0EAIQIgA0EANgIcIANBvws2AhAgA0ECNgIMIAMgAUEBajYCFAzoAgtBACECDM4CC0ENIQIMzQILQRMhAgzMAgtBFSECDMsCC0EWIQIMygILQRghAgzJAgtBGSECDMgCC0EaIQIMxwILQRshAgzGAgtBHCECDMUCC0EdIQIMxAILQR4hAgzDAgtBHyECDMICC0EgIQIMwQILQSIhAgzAAgtBIyECDL8CC0ElIQIMvgILQeUAIQIMvQILIANBPTYCHCADIAE2AhQgAyAANgIMQQAhAgzVAgsgA0EbNgIcIAMgATYCFCADQaQcNgIQIANBFTYCDEEAIQIM1AILIANBIDYCHCADIAE2AhQgA0GYGjYCECADQRU2AgxBACECDNMCCyADQRM2AhwgAyABNgIUIANBmBo2AhAgA0EVNgIMQQAhAgzSAgsgA0ELNgIcIAMgATYCFCADQZgaNgIQIANBFTYCDEEAIQIM0QILIANBEDYCHCADIAE2AhQgA0GYGjYCECADQRU2AgxBACECDNACCyADQSA2AhwgAyABNgIUIANBpBw2AhAgA0EVNgIMQQAhAgzPAgsgA0ELNgIcIAMgATYCFCADQaQcNgIQIANBFTYCDEEAIQIMzgILIANBDDYCHCADIAE2AhQgA0GkHDYCECADQRU2AgxBACECDM0CC0EAIQIgA0EANgIcIAMgATYCFCADQd0ONgIQIANBEjYCDAzMAgsCQANAAkAgAS0AAEEKaw4EAAICAAILIAQgAUEBaiIBRw0AC0H9ASECDMwCCwJAAkAgAy0ANkEBRw0AQQAhAAJAIAMoAjgiAkUNACACKAJgIgJFDQAgAyACEQAAIQALIABFDQAgAEEVRw0BIANB/AE2AhwgAyABNgIUIANB3Bk2AhAgA0EVNgIMQQAhAgzNAgtB3AEhAgyzAgsgA0EANgIcIAMgATYCFCADQfkLNgIQIANBHzYCDEEAIQIMywILAkACQCADLQAoQQFrDgIEAQALQdsBIQIMsgILQdQBIQIMsQILIANBAjoAMUEAIQACQCADKAI4IgJFDQAgAigCACICRQ0AIAMgAhEAACEACyAARQRAQd0BIQIMsQILIABBFUcEQCADQQA2AhwgAyABNgIUIANBtAw2AhAgA0EQNgIMQQAhAgzKAgsgA0H7ATYCHCADIAE2AhQgA0GBGjYCECADQRU2AgxBACECDMkCCyABIARGBEBB+gEhAgzJAgsgAS0AAEHIAEYNASADQQE6ACgLQcABIQIMrgILQdoBIQIMrQILIAEgBEcEQCADQQw2AgggAyABNgIEQdkBIQIMrQILQfkBIQIMxQILIAEgBEYEQEH4ASECDMUCCyABLQAAQcgARw0EIAFBAWohAUHYASECDKsCCyABIARGBEBB9wEhAgzEAgsCQAJAIAEtAABBxQBrDhAABQUFBQUFBQUFBQUFBQUBBQsgAUEBaiEBQdYBIQIMqwILIAFBAWohAUHXASECDKoCC0H2ASECIAEgBEYNwgIgAygCACIAIAQgAWtqIQUgASAAa0ECaiEGAkADQCABLQAAIABButUAai0AAEcNAyAAQQJGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAMwwILIAMoAgQhACADQgA3AwAgAyAAIAZBAWoiARAuIgBFBEBB4wEhAgyqAgsgA0H1ATYCHCADIAE2AhQgAyAANgIMQQAhAgzCAgtB9AEhAiABIARGDcECIAMoAgAiACAEIAFraiEFIAEgAGtBAWohBgJAA0AgAS0AACAAQbjVAGotAABHDQIgAEEBRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAyAFNgIADMICCyADQYEEOwEoIAMoAgQhACADQgA3AwAgAyAAIAZBAWoiARAuIgANAwwCCyADQQA2AgALQQAhAiADQQA2AhwgAyABNgIUIANB5R82AhAgA0EINgIMDL8CC0HVASECDKUCCyADQfMBNgIcIAMgATYCFCADIAA2AgxBACECDL0CC0EAIQACQCADKAI4IgJFDQAgAigCQCICRQ0AIAMgAhEAACEACyAARQ1uIABBFUcEQCADQQA2AhwgAyABNgIUIANBgg82AhAgA0EgNgIMQQAhAgy9AgsgA0GPATYCHCADIAE2AhQgA0HsGzYCECADQRU2AgxBACECDLwCCyABIARHBEAgA0ENNgIIIAMgATYCBEHTASECDKMCC0HyASECDLsCCyABIARGBEBB8QEhAgy7AgsCQAJAAkAgAS0AAEHIAGsOCwABCAgICAgICAgCCAsgAUEBaiEBQdABIQIMowILIAFBAWohAUHRASECDKICCyABQQFqIQFB0gEhAgyhAgtB8AEhAiABIARGDbkCIAMoAgAiACAEIAFraiEGIAEgAGtBAmohBQNAIAEtAAAgAEG11QBqLQAARw0EIABBAkYNAyAAQQFqIQAgBCABQQFqIgFHDQALIAMgBjYCAAy5AgtB7wEhAiABIARGDbgCIAMoAgAiACAEIAFraiEGIAEgAGtBAWohBQNAIAEtAAAgAEGz1QBqLQAARw0DIABBAUYNAiAAQQFqIQAgBCABQQFqIgFHDQALIAMgBjYCAAy4AgtB7gEhAiABIARGDbcCIAMoAgAiACAEIAFraiEGIAEgAGtBAmohBQNAIAEtAAAgAEGw1QBqLQAARw0CIABBAkYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBjYCAAy3AgsgAygCBCEAIANCADcDACADIAAgBUEBaiIBECsiAEUNAiADQewBNgIcIAMgATYCFCADIAA2AgxBACECDLYCCyADQQA2AgALIAMoAgQhACADQQA2AgQgAyAAIAEQKyIARQ2cAiADQe0BNgIcIAMgATYCFCADIAA2AgxBACECDLQCC0HPASECDJoCC0EAIQACQCADKAI4IgJFDQAgAigCNCICRQ0AIAMgAhEAACEACwJAIAAEQCAAQRVGDQEgA0EANgIcIAMgATYCFCADQeoNNgIQIANBJjYCDEEAIQIMtAILQc4BIQIMmgILIANB6wE2AhwgAyABNgIUIANBgBs2AhAgA0EVNgIMQQAhAgyyAgsgASAERgRAQesBIQIMsgILIAEtAABBL0YEQCABQQFqIQEMAQsgA0EANgIcIAMgATYCFCADQbI4NgIQIANBCDYCDEEAIQIMsQILQc0BIQIMlwILIAEgBEcEQCADQQ42AgggAyABNgIEQcwBIQIMlwILQeoBIQIMrwILIAEgBEYEQEHpASECDK8CCyABLQAAQTBrIgBB/wFxQQpJBEAgAyAAOgAqIAFBAWohAUHLASECDJYCCyADKAIEIQAgA0EANgIEIAMgACABEC8iAEUNlwIgA0HoATYCHCADIAE2AhQgAyAANgIMQQAhAgyuAgsgASAERgRAQecBIQIMrgILAkAgAS0AAEEuRgRAIAFBAWohAQwBCyADKAIEIQAgA0EANgIEIAMgACABEC8iAEUNmAIgA0HmATYCHCADIAE2AhQgAyAANgIMQQAhAgyuAgtBygEhAgyUAgsgASAERgRAQeUBIQIMrQILQQAhAEEBIQVBASEHQQAhAgJAAkACQAJAAkACfwJAAkACQAJAAkACQAJAIAEtAABBMGsOCgoJAAECAwQFBggLC0ECDAYLQQMMBQtBBAwEC0EFDAMLQQYMAgtBBwwBC0EICyECQQAhBUEAIQcMAgtBCSECQQEhAEEAIQVBACEHDAELQQAhBUEBIQILIAMgAjoAKyABQQFqIQECQAJAIAMtAC5BEHENAAJAAkACQCADLQAqDgMBAAIECyAHRQ0DDAILIAANAQwCCyAFRQ0BCyADKAIEIQAgA0EANgIEIAMgACABEC8iAEUNAiADQeIBNgIcIAMgATYCFCADIAA2AgxBACECDK8CCyADKAIEIQAgA0EANgIEIAMgACABEC8iAEUNmgIgA0HjATYCHCADIAE2AhQgAyAANgIMQQAhAgyuAgsgAygCBCEAIANBADYCBCADIAAgARAvIgBFDZgCIANB5AE2AhwgAyABNgIUIAMgADYCDAytAgtByQEhAgyTAgtBACEAAkAgAygCOCICRQ0AIAIoAkQiAkUNACADIAIRAAAhAAsCQCAABEAgAEEVRg0BIANBADYCHCADIAE2AhQgA0GkDTYCECADQSE2AgxBACECDK0CC0HIASECDJMCCyADQeEBNgIcIAMgATYCFCADQdAaNgIQIANBFTYCDEEAIQIMqwILIAEgBEYEQEHhASECDKsCCwJAIAEtAABBIEYEQCADQQA7ATQgAUEBaiEBDAELIANBADYCHCADIAE2AhQgA0GZETYCECADQQk2AgxBACECDKsCC0HHASECDJECCyABIARGBEBB4AEhAgyqAgsCQCABLQAAQTBrQf8BcSICQQpJBEAgAUEBaiEBAkAgAy8BNCIAQZkzSw0AIAMgAEEKbCIAOwE0IABB/v8DcSACQf//A3NLDQAgAyAAIAJqOwE0DAILQQAhAiADQQA2AhwgAyABNgIUIANBlR42AhAgA0ENNgIMDKsCCyADQQA2AhwgAyABNgIUIANBlR42AhAgA0ENNgIMQQAhAgyqAgtBxgEhAgyQAgsgASAERgRAQd8BIQIMqQILAkAgAS0AAEEwa0H/AXEiAkEKSQRAIAFBAWohAQJAIAMvATQiAEGZM0sNACADIABBCmwiADsBNCAAQf7/A3EgAkH//wNzSw0AIAMgACACajsBNAwCC0EAIQIgA0EANgIcIAMgATYCFCADQZUeNgIQIANBDTYCDAyqAgsgA0EANgIcIAMgATYCFCADQZUeNgIQIANBDTYCDEEAIQIMqQILQcUBIQIMjwILIAEgBEYEQEHeASECDKgCCwJAIAEtAABBMGtB/wFxIgJBCkkEQCABQQFqIQECQCADLwE0IgBBmTNLDQAgAyAAQQpsIgA7ATQgAEH+/wNxIAJB//8Dc0sNACADIAAgAmo7ATQMAgtBACECIANBADYCHCADIAE2AhQgA0GVHjYCECADQQ02AgwMqQILIANBADYCHCADIAE2AhQgA0GVHjYCECADQQ02AgxBACECDKgCC0HEASECDI4CCyABIARGBEBB3QEhAgynAgsCQAJAAkACQCABLQAAQQprDhcCAwMAAwMDAwMDAwMDAwMDAwMDAwMDAQMLIAFBAWoMBQsgAUEBaiEBQcMBIQIMjwILIAFBAWohASADQS9qLQAAQQFxDQggA0EANgIcIAMgATYCFCADQY0LNgIQIANBDTYCDEEAIQIMpwILIANBADYCHCADIAE2AhQgA0GNCzYCECADQQ02AgxBACECDKYCCyABIARHBEAgA0EPNgIIIAMgATYCBEEBIQIMjQILQdwBIQIMpQILAkACQANAAkAgAS0AAEEKaw4EAgAAAwALIAQgAUEBaiIBRw0AC0HbASECDKYCCyADKAIEIQAgA0EANgIEIAMgACABEC0iAEUEQCABQQFqIQEMBAsgA0HaATYCHCADIAA2AgwgAyABQQFqNgIUQQAhAgylAgsgAygCBCEAIANBADYCBCADIAAgARAtIgANASABQQFqCyEBQcEBIQIMigILIANB2QE2AhwgAyAANgIMIAMgAUEBajYCFEEAIQIMogILQcIBIQIMiAILIANBL2otAABBAXENASADQQA2AhwgAyABNgIUIANB5Bw2AhAgA0EZNgIMQQAhAgygAgsgASAERgRAQdkB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CyADIAU2AgAMkAILIANBADYCACAGQQFqIQFBGgw8C0HKASECIAEgBEYNjgIgAygCACIAIAQgAWtqIQUgASAAa0EDaiEGAkADQCABLQAAIABBjdUAai0AAEcNOyAAQQNGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAMjwILIANBADYCACAGQQFqIQFBIQw7CyABIARGBEBByQEhAgyOAgsCQAJAIAEtAABBwQBrDhQAPT09PT09PT09PT09PT09PT09AT0LIAFBAWohAUGtASECDPUBCyABQQFqIQFBsAEhAgz0AQsgASAERgRAQcgBIQIMjQILAkACQCABLQAAQdUAaw4LADw8PDw8PDw8PAE8CyABQQFqIQFBrgEhAgz0AQsgAUEBaiEBQa8BIQIM8wELQccBIQIgASAERg2LAiADKAIAIgAgBCABa2ohBSABIABrQQhqIQYCQANAIAEtAAAgAEGE1QBqLQAARw04IABBCEYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAyMAgsgA0EANgIAIAZBAWohAUEqDDgLIAEgBEYEQEHGASECDIsCCyABLQAAQdAARw04IAFBAWohAUElDDcLQcUBIQIgASAERg2JAiADKAIAIgAgBCABa2ohBSABIABrQQJqIQYCQANAIAEtAAAgAEGB1QBqLQAARw02IABBAkYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAyKAgsgA0EANgIAIAZBAWohAUEODDYLIAEgBEYEQEHEASECDIkCCyABLQAAQcUARw02IAFBAWohAUGrASECDO8BCyABIARGBEBBwwEhAgyIAgsCQAJAAkACQCABLQAAQcIAaw4PAAECOTk5OTk5OTk5OTkDOQsgAUEBaiEBQacBIQIM8QELIAFBAWohAUGoASECDPABCyABQQFqIQFBqQEhAgzvAQsgAUEBaiEBQaoBIQIM7gELQcIBIQIgASAERg2GAiADKAIAIgAgBCABa2ohBSABIABrQQJqIQYCQANAIAEtAAAgAEH+1ABqLQAARw0zIABBAkYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAyHAgsgA0EANgIAIAZBAWohAUEUDDMLQcEBIQIgASAERg2FAiADKAIAIgAgBCABa2ohBSABIABrQQRqIQYCQANA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BTYCAAzsAQsgA0EANgIAIAZBAWohAUEXDBgLQaYBIQIgASAERg3qASADKAIAIgAgBCABa2ohBSABIABrQQVqIQYCQANAIAEtAAAgAEHB1ABqLQAARw0XIABBBUYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAzrAQsgA0EANgIAIAZBAWohAUEYDBcLIAEgBEYEQEGlASECDOoBCwJAAkAgAS0AAEHJAGsOBwAZGRkZGQEZCyABQQFqIQFBiwEhAgzRAQsgAUEBaiEBQYwBIQIM0AELQaQBIQIgASAERg3oASADKAIAIgAgBCABa2ohBSABIABrQQFqIQYCQANAIAEtAAAgAEGm1QBqLQAARw0VIABBAUYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAzpAQsgA0EANgIAIAZBAWohAUEJDBULQaMBIQIgASAERg3nASADKAIAIgAgBCABa2ohBSABIABrQQFqIQYCQANAIAEtAAAgAEGk1QBqLQAARw0UIABBAUYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAzoAQsgA0EANgIAIAZBAWohAUEfDBQLQaIBIQIgASAERg3mASADKAIAIgAgBCABa2ohBSABIABrQQJqIQYCQANAIAEtAAAgAEG+1ABqLQAARw0TIABBAkYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAznAQsgA0EANgIAIAZBAWohAUECDBMLQaEBIQIgASAERg3lASADKAIAIgAgBCABa2ohBSABIABrQQFqIQYDQCABLQAAIABBvNQAai0AAEcNESAAQQFGDQIgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAM5QELIAEgBEYEQEGgASECDOUBC0EBIAEtAABB3wBHDREaIAFBAWohAUGHASECDMsBCyADQQA2AgAgBkEBaiEBQYgBIQIMygELQZ8BIQIgASAERg3iASADKAIAIgAgBCABa2ohBSABIABrQQhqIQYCQANAIAEtAAAgAEGE1QBqLQAARw0PIABBCEYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAzjAQsgA0EANgIAIAZBAWohAUEpDA8LQZ4BIQIgASAERg3hASADKAIAIgAgBCABa2ohBSABIABrQQNqIQYCQANAIAEtAAAgAEG41ABqLQAARw0OIABBA0YNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAziAQsgA0EANgIAIAZBAWohAUEtDA4LIAEgBEYEQEGdASECDOEBCyABLQAAQcUARw0OIAFBAWohAUGEASECDMcBCyABIARGBEBBnAEhAgzgAQsCQAJAIAEtAABBzABrDggADw8PDw8PAQ8LIAFBAWohAUGCASECDMcBCyABQQFqIQFBgwEhAgzGAQtBmwEhAiABIARGDd4BIAMoAgAiACAEIAFraiEFIAEgAGtBBGohBgJAA0AgAS0AACAAQbPUAGotAABHDQsgAEEERg0BIABBAWohACAEIAFBAWoiAUcNAAsgAyAFNgIADN8BCyADQQA2AgAgBkEBaiEBQSMMCwtBmgEhAiABIARGDd0BIAMoAgAiACAEIAFraiEFIAEgAGtBAmohBgJAA0AgAS0AACAAQbDUAGotAABHDQogAEECRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAyAFNgIADN4BCyADQQA2AgAgBkEBaiEBQQAMCgsgASAERgRAQZkBIQIM3QELAkACQCABLQAAQcgAaw4IAAwMDAwMDAEMCyABQQFqIQFB/QAhAgzEAQsgAUEBaiEBQYABIQIMwwELIAEgBEYEQEGYASECDNwBCwJAAkAgAS0AAEHOAGsOAwALAQsLIAFBAWohAUH+ACECDMMBCyABQQFqIQFB/wAhAgzCAQsgASAERgRAQZcBIQIM2wELIAEtAABB2QBHDQggAUEBaiEBQQgMBwtBlgEhAiABIARGDdkBIAMoAgAiACAEIAFraiEFIAEgAGtBA2ohBgJAA0AgAS0AACAAQazUAGotAABHDQYgAEEDRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAyAFNgIADNoBCyADQQA2AgAgBkEBaiEBQQUMBgtBlQEhAiABIARGDdgBIAMoAgAiACAEIAFraiEFIAEgAGtBBWohBgJAA0AgAS0AACAAQabUAGotAABHDQUgAEEFRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAyAFNgIADNkBCyADQQA2AgAgBkEBaiEBQRYMBQtBlAEhAiABIARGDdcBIAMoAgAiACAEIAFraiEFIAEgAGtBAmohBgJAA0AgAS0AACAAQaHVAGotAABHDQQgAEECRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAyAFNgIADNgBCyADQQA2AgAgBkEBaiEBQRAMBAsgASAERgRAQZMBIQIM1wELAkACQCABLQAAQcMAaw4MAAYGBgYGBgYGBgYBBgsgAUEBaiEBQfkAIQIMvgELIAFBAWohAUH6ACECDL0BC0GSASECIAEgBEYN1QEgAygCACIAIAQgAWtqIQUgASAAa0EFaiEGAkADQCABLQAAIABBoNQAai0AAEcNAiAAQQVGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAM1gELIANBADYCACAGQQFqIQFBJAwCCyADQQA2AgAMAgsgASAERgRAQZEBIQIM1AELIAEtAABBzABHDQEgAUEBaiEBQRMLOgApIAMoAgQhACADQQA2AgQgAyAAIAEQLiIADQIMAQtBACECIANBADYCHCADIAE2AhQgA0H+HzYCECADQQY2AgwM0QELQfgAIQIMtwELIANBkAE2AhwgAyABNgIUIAMgADYCDEEAIQIMzwELQQAhAAJAIAMoAjgiAkUNACACKAJAIgJFDQAgAyACEQAAIQALIABFDQAgAEEVRg0BIANBADYCHCADIAE2AhQgA0GCDzYCECADQSA2AgxBACECDM4BC0H3ACECDLQBCyADQY8BNgIcIAMgATYCFCADQewbNgIQIANBFTYCDEEAIQIMzAELIAEgBEYEQEGPASECDMwBCwJAIAEtAABBIEYEQCABQQFqIQEMAQsgA0EANgIcIAMgATYCFCADQZsfNgIQIANBBjYCDEEAIQIMzAELQQIhAgyyAQsDQCABLQAAQSBHDQIgBCABQQFqIgFHDQALQY4BIQIMygELIAEgBEYEQEGNASECDMoBCwJAIAEtAABBCWsOBEoAAEoAC0H1ACECDLABCyADLQApQQVGBEBB9gAhAgywAQtB9AAhAgyvAQsgASAERgRAQYwBIQIMyAELIANBEDYCCCADIAE2AgQMCgsgASAERgRAQYsBIQIMxwELAkAgAS0AAEEJaw4ERwAARwALQfMAIQIMrQELIAEgBEcEQCADQRA2AgggAyABNgIEQfEAIQIMrQELQYoBIQIMxQELAkAgASAERwRAA0AgAS0AAEGg0ABqLQAAIgBBA0cEQAJAIABBAWsOAkkABAtB8AAhAgyvAQsgBCABQQFqIgFHDQALQYgBIQIMxgELQYgBIQIMxQELIANBADYCHCADIAE2AhQgA0HbIDYCECADQQc2AgxBACECDMQBCyABIARGBEBBiQEhAgzEAQsCQAJAAkAgAS0AAEGg0gBqLQAAQQFrDgNGAgABC0HyACECDKwBCyADQQA2AhwgAyABNgIUIANBtBI2AhAgA0EHNgIMQQAhAgzEAQtB6gAhAgyqAQsgASAERwRAIAFBAWohAUHvACECDKoBC0GHASECDMIBCyAEIAEiAEYEQEGGASECDMIBCyAALQAAIgFBL0YEQCAAQQFqIQFB7gAhAgypAQsgAUEJayICQRdLDQEgACEBQQEgAnRBm4CABHENQQwBCyAEIAEiAEYEQEGFASECDMEBCyAALQAAQS9HDQAgAEEBaiEBDAMLQQAhAiADQQA2AhwgAyAANgIUIANB2yA2AhAgA0EHNgIMDL8BCwJAAkACQAJAAkADQCABLQAAQaDOAGotAAAiAEEFRwRAAkACQCAAQQFrDghHBQYHCAAEAQgLQesAIQIMrQELIAFBAWohAUHtACECDKwBCyAEIAFBAWoiAUcNAAtBhAEhAgzDAQsgAUEBagwUCyADKAIEIQAgA0EANgIEIAMgACABECwiAEUNHiADQdsANgIcIAMgATYCFCADIAA2AgxBACECDMEBCyADKAIEIQAgA0EANgIEIAMgACABECwiAEUNHiADQd0ANgIcIAMgATYCFCADIAA2AgxBACECDMABCyADKAIEIQAgA0EANgIEIAMgACABECwiAEUNHiADQfoANgIcIAMgATYCFCADIAA2AgxBACECDL8BCyADQQA2AhwgAyABNgIUIANB+Q82AhAgA0EHNgIMQQAhAgy+AQsgASAERgRAQYMBIQIMvgELAkAgAS0AAEGgzgBqLQAAQQFrDgg+BAUGAAgCAwcLIAFBAWohAQtBAyECDKMBCyABQQFqDA0LQQAhAiADQQA2AhwgA0HREjYCECADQQc2AgwgAyABQQFqNgIUDLoBCyADKAIEIQAgA0EANgIEIAMgACABECwiAEUNFiADQdsANgIcIAMgATYCFCADIAA2AgxBACECDLkBCyADKAIEIQAgA0EANgIEIAMgACABECwiAEUNFiADQd0ANgIcIAMgATYCFCADIAA2AgxBACECDLgBCyADKAIEIQAgA0EANgIEIAMgACABECwiAEUNFiADQfoANgIcIAMgATYCFCADIAA2AgxBACECDLcBCyADQQA2AhwgAyABNgIUIANB+Q82AhAgA0EHNgIMQQAhAgy2AQtB7AAhAgycAQsgASAERgRAQYIBIQIMtQELIAFBAWoMAgsgASAERgRAQYEBIQIMtAELIAFBAWoMAQsgASAERg0BIAFBAWoLIQFBBCECDJgBC0GAASECDLABCwNAIAEtAABBoMwAai0AACIAQQJHBEAgAEEBRwRAQekAIQIMmQELDDELIAQgAUEBaiIBRw0AC0H/ACECDK8BCyABIARGBEBB/gAhAgyvAQsCQCABLQAAQQlrDjcvAwYvBAYGBgYGBgYGBgYGBgYGBgYGBgUGBgIGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYABgsgAUEBagshAUEFIQIMlAELIAFBAWoMBgsgAygCBCEAIANBADYCBCADIAAgARAsIgBFDQggA0HbADYCHCADIAE2AhQgAyAANgIMQQAhAgyrAQsgAygCBCEAIANBADYCBCADIAAgARAsIgBFDQggA0HdADYCHCADIAE2AhQgAyAANgIMQQAhAgyqAQsgAygCBCEAIANBADYCBCADIAAgARAsIgBFDQggA0H6ADYCHCADIAE2AhQgAyAANgIMQQAhAgypAQsgA0EANgIcIAMgATYCFCADQY0UNgIQIANBBzYCDEEAIQIMqAELAkACQAJAAkADQCABLQAAQaDKAGotAAAiAEEFRwRAAkAgAEEBaw4GLgMEBQYABgtB6AAhAgyUAQsgBCABQQFqIgFHDQALQf0AIQIMqwELIAMoAgQhACADQQA2AgQgAyAAIAEQLCIARQ0HIANB2wA2AhwgAyABNgIUIAMgADYCDEEAIQIMqgELIAMoAgQhACADQQA2AgQgAyAAIAEQLCIARQ0HIANB3QA2AhwgAyABNgIUIAMgADYCDEEAIQIMqQELIAMoAgQhACADQQA2AgQgAyAAIAEQLCIARQ0HIANB+gA2AhwgAyABNgIUIAMgADYCDEEAIQIMqAELIANBADYCHCADIAE2AhQgA0HkCDYCECADQQc2AgxBACECDKcBCyABIARGDQEgAUEBagshAUEGIQIMjAELQfwAIQIMpAELAkACQAJAAkADQCABLQAAQaDIAGotAAAiAEEFRwRAIABBAWsOBCkCAwQFCyAEIAFBAWoiAUcNAAtB+wAhAgynAQsgAygCBCEAIANBADYCBCADIAAgARAsIgBFDQMgA0HbADYCHCADIAE2AhQgAyAANgIMQQAhAgymAQsgAygCBCEAIANBADYCBCADIAAgARAsIgBFDQMgA0HdADYCHCADIAE2AhQgAyAANgIMQQAhAgylAQsgAygCBCEAIANBADYCBCADIAAgARAsIgBFDQMgA0H6ADYCHCADIAE2AhQgAyAANgIMQQAhAgykAQsgA0EANgIcIAMgATYCFCADQbwKNgIQIANBBzYCDEEAIQIMowELQc8AIQIMiQELQdEAIQIMiAELQecAIQIMhwELIAEgBEYEQEH6ACECDKABCwJAIAEtAABBCWsOBCAAACAACyABQQFqIQFB5gAhAgyGAQsgASAERgRAQfkAIQIMnwELAkAgAS0AAEEJaw4EHwAAHwALQQAhAAJAIAMoAjgiAkUNACACKAI4IgJFDQAgAyACEQAAIQALIABFBEBB4gEhAgyGAQsgAEEVRwRAIANBADYCHCADIAE2AhQgA0HJDTYCECADQRo2AgxBACECDJ8BCyADQfgANgIcIAMgATYCFCADQeoaNgIQIANBFTYCDEEAIQIMngELIAEgBEcEQCADQQ02AgggAyABNgIEQeQAIQIMhQELQfcAIQIMnQELIAEgBEYEQEH2ACECDJ0BCwJAAkACQCABLQAAQcgAaw4LAAELCwsLCwsLCwILCyABQQFqIQFB3QAhAgyFAQsgAUEBaiEBQeAAIQIMhAELIAFBAWohAUHjACECDIMBC0H1ACECIAEgBEYNmwEgAygCACIAIAQgAWtqIQUgASAAa0ECaiEGAkADQCABLQAAIABBtdUAai0AAEcNCCAAQQJGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAMnAELIAMoAgQhACADQgA3AwAgAyAAIAZBAWoiARArIgAEQCADQfQANgIcIAMgATYCFCADIAA2AgxBACECDJwBC0HiACECDIIBC0EAIQACQCADKAI4IgJFDQAgAigCNCICRQ0AIAMgAhEAACEACwJAIAAEQCAAQRVGDQEgA0EANgIcIAMgATYCFCADQeoNNgIQIANBJjYCDEEAIQIMnAELQeEAIQIMggELIANB8wA2AhwgAyABNgIUIANBgBs2AhAgA0EVNgIMQQAhAgyaAQsgAy0AKSIAQSNrQQtJDQkCQCAAQQZLDQBBASAAdEHKAHFFDQAMCgtBACECIANBADYCHCADIAE2AhQgA0HtCTYCECADQQg2AgwMmQELQfIAIQIgASAERg2YASADKAIAIgAgBCABa2ohBSABIABrQQFqIQYCQANAIAEtAAAgAEGz1QBqLQAARw0FIABBAUYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAyZAQsgAygCBCEAIANCADcDACADIAAgBkEBaiIBECsiAARAIANB8QA2AhwgAyABNgIUIAMgADYCDEEAIQIMmQELQd8AIQIMfwtBACEAAkAgAygCOCICRQ0AIAIoAjQiAkUNACADIAIRAAAhAAsCQCAABEAgAEEVRg0BIANBADYCHCADIAE2AhQgA0HqDTYCECADQSY2AgxBACECDJkBC0HeACECDH8LIANB8AA2AhwgAyABNgIUIANBgBs2AhAgA0EVNgIMQQAhAgyXAQsgAy0AKUEhRg0GIANBADYCHCADIAE2AhQgA0GRCjYCECADQQg2AgxBACECDJYBC0HvACECIAEgBEYNlQEgAygCACIAIAQgAWtqIQUgASAAa0ECaiEGAkADQCABLQAAIABBsNUAai0AAEcNAiAAQQJGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAMlgELIAMoAgQhACADQgA3AwAgAyAAIAZBAWoiARArIgBFDQIgA0HtADYCHCADIAE2AhQgAyAANgIMQQAhAgyVAQsgA0EANgIACyADKAIEIQAgA0EANgIEIAMgACABECsiAEUNgAEgA0HuADYCHCADIAE2AhQgAyAANgIMQQAhAgyTAQtB3AAhAgx5C0EAIQACQCADKAI4IgJFDQAgAigCNCICRQ0AIAMgAhEAACEACwJAIAAEQCAAQRVGDQEgA0EANgIcIAMgATYCFCADQeoNNgIQIANBJjYCDEEAIQIMkwELQdsAIQIMeQsgA0HsADYCHCADIAE2AhQgA0GAGzYCECADQRU2AgxBACECDJEBCyADLQApIgBBI0kNACAAQS5GDQAgA0EANgIcIAMgATYCFCADQckJNgIQIANBCDYCDEEAIQIMkAELQdoAIQIMdgsgASAERgRAQesAIQIMjwELAkAgAS0AAEEvRgRAIAFBAWohAQwBCyAD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ciBIVFRQL3gueCByZXF1ZXN0AEludmFsaWQgbWV0aG9kIGZvciBSVFNQL3gueCByZXF1ZXN0AEV4cGVjdGVkIFNPVVJDRSBtZXRob2QgZm9yIElDRS94LnggcmVxdWVzdABJbnZhbGlkIGNoYXIgaW4gdXJsIGZyYWdtZW50IHN0YXJ0AEV4cGVjdGVkIGRvdABTcGFuIGNhbGxiYWNrIGVycm9yIGluIG9uX3N0YXR1cwBJbnZhbGlkIHJlc3BvbnNlIHN0YXR1cwBFeHBlY3RlZCBMRiBhZnRlciBoZWFkZXJzAEludmFsaWQgY2hhcmFjdGVyIGluIGNodW5rIGV4dGVuc2lvbnMAVXNlciBjYWxsYmFjayBlcnJvcgBgb25fcmVzZXRgIGNhbGxiYWNrIGVycm9yAGBvbl9jaHVua19oZWFkZXJgIGNhbGxiYWNrIGVycm9yAGBvbl9tZXNzYWdlX2JlZ2luYCBjYWxsYmFjayBlcnJvcgBgb25fY2h1bmtfZXh0ZW5zaW9uX3ZhbHVlYCBjYWxsYmFjayBlcnJvcgBgb25fc3RhdHVzX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25fdmVyc2lvbl9jb21wbGV0ZWAgY2FsbGJhY2sgZXJyb3IAYG9uX3VybF9jb21wbGV0ZWAgY2FsbGJhY2sgZXJyb3IAYG9uX3Byb3RvY29sX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25fY2h1bmtfY29tcGxldGVgIGNhbGxiYWNrIGVycm9yAGBvbl9oZWFkZXJfdmFsdWVfY29tcGxldGVgIGNhbGxiYWNrIGVycm9yAGBvbl9tZXNzYWdlX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25fbWV0aG9kX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25faGVhZGVyX2ZpZWxkX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25fY2h1bmtfZXh0ZW5zaW9uX25hbWVgIGNhbGxiYWNrIGVycm9yAFVuZXhwZWN0ZWQgY2hhciBpbiB1cmwgc2VydmVyAEludmFsaWQgaGVhZGVyIHZhbHVlIGNoYXIASW52YWxpZCBoZWFkZXIgZmllbGQgY2hhcgBTcGFuIGNhbGxiYWNrIGVycm9yIGluIG9uX3ZlcnNpb24ASW52YWxpZCBtaW5vciB2ZXJzaW9uAEludmFsaWQgbWFqb3IgdmVyc2lvbgBFeHBlY3RlZCBzcGFjZSBhZnRlciB2ZXJzaW9uAEV4cGVjdGVkIENSTEYgYWZ0ZXIgdmVyc2lvbgBJbnZhbGlkIEhUVFAgdmVyc2lvbgBJbnZhbGlkIGhlYWRlciB0b2tlbgBTcGFuIGNhbGxiYWNrIGVycm9yIGluIG9uX3VybABJbnZhbGlkIGNoYXJhY3RlcnMgaW4gdXJsAFVuZXhwZWN0ZWQgc3RhcnQgY2hhciBpbiB1cmwARG91YmxlIEAgaW4gdXJsAFNwYW4gY2FsbGJhY2sgZXJyb3IgaW4gb25fcHJvdG9jb2wARW1wdHkgQ29udGVudC1MZW5ndGgASW52YWxpZCBjaGFyYWN0ZXIgaW4gQ29udGVudC1MZW5ndGgAVHJhbnNmZXItRW5jb2RpbmcgY2FuJ3QgYmUgcHJlc2VudCB3aXRoIENvbnRlbnQtTGVuZ3RoAER1cGxpY2F0ZSBDb250ZW50LUxlbmd0aABJbnZhbGlkIGNoYXIgaW4gdXJsIHBhdGgAQ29udGVudC1MZW5ndGggY2FuJ3QgYmUgcHJlc2VudCB3aXRoIFRyYW5zZmVyLUVuY29kaW5nAE1pc3NpbmcgZXhwZWN0ZWQgQ1IgYWZ0ZXIgY2h1bmsgc2l6ZQBFeHBlY3RlZCBMRiBhZnRlciBjaHVuayBzaXplAEludmFsaWQgY2hhcmFjdGVyIGluIGNodW5rIHNpemUAU3BhbiBjYWxsYmFjayBlcnJvciBpbiBvbl9oZWFkZXJfdmFsdWUAU3BhbiBjYWxsYmFjayBlcnJvciBpbiBvbl9jaHVua19leHRlbnNpb25fdmFsdWUASW52YWxpZCBjaGFyYWN0ZXIgaW4gY2h1bmsgZXh0ZW5zaW9ucyB2YWx1ZQBVbmV4cGVjdGVkIHdoaXRlc3BhY2UgYWZ0ZXIgaGVhZGVyIHZhbHVlAE1pc3NpbmcgZXhwZWN0ZWQgQ1IgYWZ0ZXIgaGVhZGVyIHZhbHVlAE1pc3NpbmcgZXhwZWN0ZWQgTEYgYWZ0ZXIgaGVhZGVyIHZhbHVlAEludmFsaWQgYFRyYW5zZmVyLUVuY29kaW5nYCBoZWFkZXIgdmFsdWUATWlzc2luZyBleHBlY3RlZCBDUiBhZnRlciBjaHVuayBleHRlbnNpb24gdmFsdWUASW52YWxpZCBjaGFyYWN0ZXIgaW4gY2h1bmsgZXh0ZW5zaW9ucyBxdW90ZSB2YWx1ZQBJbnZhbGlkIHF1b3RlZC1wYWlyIGluIGNodW5rIGV4dGVuc2lvbnMgcXVvdGVkIHZhbHVlAEludmFsaWQgY2hhcmFjdGVyIGluIGNodW5rIGV4dGVuc2lvbnMgcXVvdGVkIHZhbHVlAFBhdXNlZCBieSBvbl9oZWFkZXJzX2NvbXBsZXRlAEludmFsaWQgRU9GIHN0YXRlAG9uX3Jlc2V0IHBhdXNlAG9uX2NodW5rX2hlYWRlciBwYXVzZQBvbl9tZXNzYWdlX2JlZ2luIHBhdXNlAG9uX2NodW5rX2V4dGVuc2lvbl92YWx1ZSBwYXVzZQBvbl9zdGF0dXNfY29tcGxldGUgcGF1c2UAb25fdmVyc2lvbl9jb21wbGV0ZSBwYXVzZQBvbl91cmxfY29tcGxldGUgcGF1c2UAb25fcHJvdG9jb2xfY29tcGxldGUgcGF1c2UAb25fY2h1bmtfY29tcGxldGUgcGF1c2UAb25faGVhZGVyX3ZhbHVlX2NvbXBsZXRlIHBhdXNlAG9uX21lc3NhZ2VfY29tcGxldGUgcGF1c2UAb25fbWV0aG9kX2NvbXBsZXRlIHBhdXNlAG9uX2hlYWRlcl9maWVsZF9jb21wbGV0ZSBwYXVzZQBvbl9jaHVua19leHRlbnNpb25fbmFtZSBwYXVzZQBVbmV4cGVjdGVkIHNwYWNlIGFmdGVyIHN0YXJ0IGxpbmUATWlzc2luZyBleHBlY3RlZCBDUiBhZnRlciByZXNwb25zZSBsaW5lAFNwYW4gY2FsbGJhY2sgZXJyb3IgaW4gb25fY2h1bmtfZXh0ZW5zaW9uX25hbWUASW52YWxpZCBjaGFyYWN0ZXIgaW4gY2h1bmsgZXh0ZW5zaW9ucyBuYW1lAE1pc3NpbmcgZXhwZWN0ZWQgQ1IgYWZ0ZXIgY2h1bmsgZXh0ZW5zaW9uIG5hbWUASW52YWxpZCBzdGF0dXMgY29kZQBQYXVzZSBvbiBDT05ORUNUL1VwZ3JhZGUAUGF1c2Ugb24gUFJJL1VwZ3JhZGUARXhwZWN0ZWQgSFRUUC8yIENvbm5lY3Rpb24gUHJlZmFjZQBTcGFuIGNhbGxiYWNrIGVycm9yIGluIG9uX21ldGhvZABFeHBlY3RlZCBzcGFjZSBhZnRlciBtZXRob2QAU3BhbiBjYWxsYmFjayBlcnJvciBpbiBvbl9oZWFkZXJfZmllbGQAUGF1c2VkAEludmFsaWQgd29yZCBlbmNvdW50ZXJlZABJbnZhbGlkIG1ldGhvZCBlbmNvdW50ZXJlZABNaXNzaW5nIGV4cGVjdGVkIENSIGFmdGVyIGNodW5rIGRhdGEARXhwZWN0ZWQgTEYgYWZ0ZXIgY2h1bmsgZGF0YQBVbmV4cGVjdGVkIGNoYXIgaW4gdXJsIHNjaGVtYQBSZXF1ZXN0IGhhcyBpbnZhbGlkIGBUcmFuc2Zlci1FbmNvZGluZ2AARGF0YSBhZnRlciBgQ29ubmVjdGlvbjogY2xvc2VgAFNXSVRDSF9QUk9YWQBVU0VfUFJPWFkATUtBQ1RJVklUWQBVTlBST0NFU1NBQkxFX0VOVElUWQBRVUVSWQBDT1BZAE1PVkVEX1BFUk1BTkVOVExZAFRPT19FQVJMWQBOT1RJRlkARkFJTEVEX0RFUEVOREVOQ1kAQkFEX0dBVEVXQVkAUExBWQBQVVQAQ0hFQ0tPVVQAR0FURVdBWV9USU1FT1VUAFJFUVVFU1RfVElNRU9VVABORVRXT1JLX0NPTk5FQ1RfVElNRU9VVABDT05ORUNUSU9OX1RJTUVPVVQATE9HSU5fVElNRU9VVABORVRXT1JLX1JFQURfVElNRU9VVABQT1NUAE1JU0RJUkVDVEVEX1JFUVVFU1QAQ0xJRU5UX0NMT1NFRF9SRVFVRVNUAENMSUVOVF9DTE9TRURfTE9BRF9CQUxBTkNFRF9SRVFVRVNUAEJBRF9SRVFVRVNUAEhUVFBfUkVRVUVTVF9TRU5UX1RPX0hUVFBTX1BPUlQAUkVQT1JUAElNX0FfVEVBUE9UAFJFU0VUX0NPTlRFTlQATk9fQ09OVEVOVABQQVJUSUFMX0NPTlRFTlQASFBFX0lOVkFMSURfQ09OU1RBTlQASFBFX0NCX1JFU0VUAEdFVABIUEVfU1RSSUNUAENPTkZMSUNUAFRFTVBPUkFSWV9SRURJUkVDVABQRVJNQU5FTlRfUkVESVJFQ1QAQ09OTkVDVABNVUxUSV9TVEFUVVMASFBFX0lOVkFMSURfU1RBVFVTAFRPT19NQU5ZX1JFUVVFU1RTAEVBUkxZX0hJTlRTAFVOQVZBSUxBQkxFX0ZPUl9MRUdBTF9SRUFTT05TAE9QVElPTlMAU1dJVENISU5HX1BST1RPQ09MUwBWQVJJQU5UX0FMU09fTkVHT1RJQVRFUwBNVUxUSVBMRV9DSE9JQ0VTAElOVEVSTkFMX1NFUlZFUl9FUlJPUgBXRUJfU0VSVkVSX1VOS05PV05fRVJST1IAUkFJTEdVTl9FUlJPUgBJREVOVElUWV9QUk9WSURFUl9BVVRIRU5USUNBVElPTl9FUlJPUgBTU0xfQ0VSVElGSUNBVEVfRVJST1IASU5WQUxJRF9YX0ZPUldBUkRFRF9GT1IAU0VUX1BBUkFNRVRFUgBHRVRfUEFSQU1FVEVSAEhQRV9VU0VSAFNFRV9PVEhFUgBIUEVfQ0JfQ0hVTktfSEVBREVSAEV4cGVjdGVkIExGIGFmdGVyIENSAE1LQ0FMRU5EQVIAU0VUVVAAV0VCX1NFUlZFUl9JU19ET1dOAFRFQVJET1dOAEhQRV9DTE9TRURfQ09OTkVDVElPTgBIRVVSSVNUSUNfRVhQSVJBVElPTgBESVNDT05ORUNURURfT1BFUkFUSU9OAE5PTl9BVVRIT1JJVEFUSVZFX0lORk9STUFUSU9OAEhQRV9JTlZBTElEX1ZFUlNJT04ASFBFX0NCX01FU1NBR0VfQkVHSU4AU0lURV9JU19GUk9aRU4ASFBFX0lOVkFMSURfSEVBREVSX1RPS0VOAElOVkFMSURfVE9LRU4ARk9SQklEREVOAEVOSEFOQ0VfWU9VUl9DQUxNAEhQRV9JTlZBTElEX1VSTABCTE9DS0VEX0JZX1BBUkVOVEFMX0NPTlRST0wATUtDT0wAQUNMAEhQRV9JTlRFUk5BTABSRVFVRVNUX0hFQURFUl9GSUVMRFNfVE9PX0xBUkdFX1VOT0ZGSUNJQUwASFBFX09LAFVOTElOSwBVTkxPQ0sAUFJJAFJFVFJZX1dJVEgASFBFX0lOVkFMSURfQ09OVEVOVF9MRU5HVEgASFBFX1VORVhQRUNURURfQ09OVEVOVF9MRU5HVEgARkxVU0gAUFJPUFBBVENIAE0tU0VBUkNIAFVSSV9UT09fTE9ORwBQUk9DRVNTSU5HAE1JU0NFTExBTkVPVVNfUEVSU0lTVEVOVF9XQVJOSU5HAE1JU0NFTExBTkVPVVNfV0FSTklORwBIUEVfSU5WQUxJRF9UUkFOU0ZFUl9FTkNPRElORwBFeHBlY3RlZCBDUkxGAEhQRV9JTlZBTElEX0NIVU5LX1NJWkUATU9WRQBDT05USU5VRQBIUEVfQ0JfU1RBVFVTX0NPTVBMRVRFAEhQRV9DQl9IRUFERVJTX0NPTVBMRVRFAEhQRV9DQl9WRVJTSU9OX0NPTVBMRVRFAEhQRV9DQl9VUkxfQ09NUExFVEUASFBFX0NCX1BST1RPQ09MX0NPTVBMRVRFAEhQRV9DQl9DSFVOS19DT01QTEVURQBIUEVfQ0JfSEVBREVSX1ZBTFVFX0NPTVBMRVRFAEhQRV9DQl9DSFVOS19FWFRFTlNJT05fVkFMVUVfQ09NUExFVEUASFBFX0NCX0NIVU5LX0VYVEVOU0lPTl9OQU1FX0NPTVBMRVRFAEhQRV9DQl9NRVNTQUdFX0NPTVBMRVRFAEhQRV9DQl9NRVRIT0RfQ09NUExFVEUASFBFX0NCX0hFQURFUl9GSUVMRF9DT01QTEVURQBERUxFVEUASFBFX0lOVkFMSURfRU9GX1NUQVRFAElOVkFMSURfU1NMX0NFUlRJRklDQVRFAFBBVVNFAE5PX1JFU1BPTlNFAFVOU1VQUE9SVEVEX01FRElBX1RZUEUAR09ORQBOT1RfQUNDRVBUQUJMRQBTRVJWSUNFX1VOQVZBSUxBQkxFAFJBTkdFX05PVF9TQVRJU0ZJQUJMRQBPUklHSU5fSVNfVU5SRUFDSEFCTEUAUkVTUE9OU0VfSVNfU1RBTEUAUFVSR0UATUVSR0UAUkVRVUVTVF9IRUFERVJfRklFTERTX1RPT19MQVJHRQBSRVFVRVNUX0hFQURFUl9UT09fTEFSR0UAUEFZTE9BRF9UT09fTEFSR0UASU5TVUZGSUNJRU5UX1NUT1JBR0UASFBFX1BBVVNFRF9VUEdSQURFAEhQRV9QQVVTRURfSDJfVVBHUkFERQBTT1VSQ0UAQU5OT1VOQ0UAVFJBQ0UASFBFX1VORVhQRUNURURfU1BBQ0UAREVTQ1JJQkUAVU5TVUJTQ1JJQkUAUkVDT1JEAEhQRV9JTlZBTElEX01FVEhPRABOT1RfRk9VTkQAUFJPUEZJTkQAVU5CSU5EAFJFQklORABVTkFVVEhPUklaRUQATUVUSE9EX05PVF9BTExPV0VEAEhUVFBfVkVSU0lPTl9OT1RfU1VQUE9SVEVEAEFMUkVBRFlfUkVQT1JURUQAQUNDRVBURUQATk9UX0lNUExFTUVOVEVEAExPT1BfREVURUNURUQASFBFX0NSX0VYUEVDVEVEAEhQRV9MRl9FWFBFQ1RFRABDUkVBVEVEAElNX1VTRUQASFBFX1BBVVNFRABUSU1FT1VUX09DQ1VSRUQAUEFZTUVOVF9SRVFVSVJFRABQUkVDT05ESVRJT05fUkVRVUlSRUQAUFJPWFlfQVVUSEVOVElDQVRJT05fUkVRVUlSRUQATkVUV09SS19BVVRIRU5USUNBVElPTl9SRVFVSVJFRABMRU5HVEhfUkVRVUlSRUQAU1NMX0NFUlRJRklDQVRFX1JFUVVJUkVEAFVQR1JBREVfUkVRVUlSRUQAUEFHRV9FWFBJUkVEAFBSRUNPTkRJVElPTl9GQUlMRUQARVhQRUNUQVRJT05fRkFJTEVEAFJFVkFMSURBVElPTl9GQUlMRUQAU1NMX0hBTkRTSEFLRV9GQUlMRUQATE9DS0VEAFRSQU5TRk9STUFUSU9OX0FQUExJRUQATk9UX01PRElGSUVEAE5PVF9FWFRFTkRFRABCQU5EV0lEVEhfTElNSVRfRVhDRUVERUQAU0lURV9JU19PVkVSTE9BREVEAEhFQUQARXhwZWN0ZWQgSFRUUC8sIFJUU1AvIG9yIElDRS8A5xUAAK8VAACkEgAAkhoAACYWAACeFAAA2xkAAHkVAAB+EgAA/hQAADYVAAALFgAA2BYAAPMSAABCGAAArBYAABIVAAAUFwAA7xcAAEgUAABxFwAAshoAAGsZAAB+GQAANRQAAIIaAABEFwAA/RYAAB4YAACHFwAAqhkAAJMSAAAHGAAALBcAAMoXAACkFwAA5xUAAOcVAABYFwAAOxgAAKASAAAtHAAAwxEAAEgRAADeEgAAQhMAAKQZAAD9EAAA9xUAAKUVAADvFgAA+BkAAEoWAABWFgAA9RUAAAoaAAAIGgAAARoAAKsVAABCEgAA1xAAAEwRAAAFGQAAVBYAAB4RAADKGQAAyBkAAE4WAAD/GAAAcRQAAPAVAADuFQAAlBkAAPwVAAC/GQAAmxkAAHwUAABDEQAAcBgAAJUUAAAnFAAAGRQAANUSAADUGQAARBYAAPcQAEG5OwsBAQBB0DsL4AEBAQIBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEDAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQBBuj0LBAEAAAIAQdE9C14DBAMDAwMDAAADAwADAwADAwMDAwMDAwMDAAUAAAAAAAMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAAAAAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMAAwADAEG6PwsEAQAAAgBB0T8LXgMAAwMDAwMAAAMDAAMDAAMDAwMDAwMDAwMABAAFAAAAAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMAAAADAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwADAAMAQbDBAAsNbG9zZWVlcC1hbGl2ZQBBycEACwEBAEHgwQAL4AEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQBBycMACwEBAEHgwwAL5wEBAQEBAQEBAQEBAQECAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAWNodW5rZWQAQfHFAAteAQABAQEBAQAAAQEAAQEAAQEBAQEBAQEBAQAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEAAQBB0McACyFlY3Rpb25lbnQtbGVuZ3Rob25yb3h5LWNvbm5lY3Rpb24AQYDIAAsgcmFuc2Zlci1lbmNvZGluZ3BncmFkZQ0KDQpTTQ0KDQoAQanIAAsFAQIAAQMAQcDIAAtfBAUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUAQanKAAsFAQIAAQMAQcDKAAtfBAUFBgUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUAQanMAAsEAQAAAQBBwcwAC14CAgACAgICAgICAgICAgICAgICAgICAgICAgICAgIAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAEGpzgALBQECAAEDAEHAzgALXwQFAAAFBQUFBQUFBQUFBQYFBQUFBQUFBQUFBQUABQAHCAUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQAFAAUABQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUAAAAFAEGp0AALBQEBAAEBAEHA0AALAQEAQdrQAAtBAgAAAAAAAAMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAAAAAAAAAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMAQanSAAsFAQEAAQEAQcDSAAsBAQBBytIACwYCAAAAAAIAQeHSAAs6AwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMAAAAAAAADAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwBBoNQAC50BTk9VTkNFRUNLT1VUTkVDVEVURUNSSUJFTFVTSEVURUFEU0VBUkNIUkdFQ1RJVklUWUxFTkRBUlZFT1RJRllQVElPTlNDSFNFQVlTVEFUQ0hHRVVFUllPUkRJUkVDVE9SVFJDSFBBUkFNRVRFUlVSQ0VCU0NSSUJFQVJET1dOQUNFSU5ETktDS1VCU0NSSUJFVFRQQ0VUU1BBRFRQLw==' + +let wasmBuffer + +Object.defineProperty(module, 'exports', { + get: () => { + return wasmBuffer + ? wasmBuffer + : (wasmBuffer = Buffer.from(wasmBase64, 'base64')) + } +}) diff --git a/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/llhttp/utils.d.ts b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/llhttp/utils.d.ts new file mode 100644 index 000000000..681b22b94 --- /dev/null +++ b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/llhttp/utils.d.ts @@ -0,0 +1,2 @@ +import type { IntDict } from './constants'; +export declare function enumToMap(obj: IntDict, filter?: readonly number[], exceptions?: readonly number[]): IntDict; diff --git a/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/llhttp/utils.js b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/llhttp/utils.js new file mode 100644 index 000000000..95081ead5 --- /dev/null +++ b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/llhttp/utils.js @@ -0,0 +1,12 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.enumToMap = enumToMap; +function enumToMap(obj, filter = [], exceptions = []) { + const emptyFilter = (filter?.length ?? 0) === 0; + const emptyExceptions = (exceptions?.length ?? 0) === 0; + return Object.fromEntries(Object.entries(obj).filter(([, value]) => { + return (typeof value === 'number' && + (emptyFilter || filter.includes(value)) && + (emptyExceptions || !exceptions.includes(value))); + })); +} diff --git a/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/mock/mock-agent.js b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/mock/mock-agent.js new file mode 100644 index 000000000..61449e077 --- /dev/null +++ b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/mock/mock-agent.js @@ -0,0 +1,232 @@ +'use strict' + +const { kClients } = require('../core/symbols') +const Agent = require('../dispatcher/agent') +const { + kAgent, + kMockAgentSet, + kMockAgentGet, + kDispatches, + kIsMockActive, + kNetConnect, + kGetNetConnect, + kOptions, + kFactory, + kMockAgentRegisterCallHistory, + kMockAgentIsCallHistoryEnabled, + kMockAgentAddCallHistoryLog, + kMockAgentMockCallHistoryInstance, + kMockAgentAcceptsNonStandardSearchParameters, + kMockCallHistoryAddLog, + kIgnoreTrailingSlash +} = require('./mock-symbols') +const MockClient = require('./mock-client') +const MockPool = require('./mock-pool') +const { matchValue, normalizeSearchParams, buildAndValidateMockOptions, normalizeOrigin } = require('./mock-utils') +const { InvalidArgumentError, UndiciError } = require('../core/errors') +const Dispatcher = require('../dispatcher/dispatcher') +const PendingInterceptorsFormatter = require('./pending-interceptors-formatter') +const { MockCallHistory } = require('./mock-call-history') + +class MockAgent extends Dispatcher { + constructor (opts = {}) { + super(opts) + + const mockOptions = buildAndValidateMockOptions(opts) + + this[kNetConnect] = true + this[kIsMockActive] = true + this[kMockAgentIsCallHistoryEnabled] = mockOptions.enableCallHistory ?? false + this[kMockAgentAcceptsNonStandardSearchParameters] = mockOptions.acceptNonStandardSearchParameters ?? false + this[kIgnoreTrailingSlash] = mockOptions.ignoreTrailingSlash ?? false + + // Instantiate Agent and encapsulate + if (opts?.agent && typeof opts.agent.dispatch !== 'function') { + throw new InvalidArgumentError('Argument opts.agent must implement Agent') + } + const agent = opts?.agent ? opts.agent : new Agent(opts) + this[kAgent] = agent + + this[kClients] = agent[kClients] + this[kOptions] = mockOptions + + if (this[kMockAgentIsCallHistoryEnabled]) { + this[kMockAgentRegisterCallHistory]() + } + } + + get (origin) { + // Normalize origin to handle URL objects and case-insensitive hostnames + const normalizedOrigin = normalizeOrigin(origin) + const originKey = this[kIgnoreTrailingSlash] ? normalizedOrigin.replace(/\/$/, '') : normalizedOrigin + + let dispatcher = this[kMockAgentGet](originKey) + + if (!dispatcher) { + dispatcher = this[kFactory](originKey) + this[kMockAgentSet](originKey, dispatcher) + } + return dispatcher + } + + dispatch (opts, handler) { + opts.origin = normalizeOrigin(opts.origin) + + // Call MockAgent.get to perform additional setup before dispatching as normal + this.get(opts.origin) + + this[kMockAgentAddCallHistoryLog](opts) + + const acceptNonStandardSearchParameters = this[kMockAgentAcceptsNonStandardSearchParameters] + + const dispatchOpts = { ...opts } + + if (acceptNonStandardSearchParameters && dispatchOpts.path) { + const [path, searchParams] = dispatchOpts.path.split('?') + const normalizedSearchParams = normalizeSearchParams(searchParams, acceptNonStandardSearchParameters) + dispatchOpts.path = `${path}?${normalizedSearchParams}` + } + + return this[kAgent].dispatch(dispatchOpts, handler) + } + + async close () { + this.clearCallHistory() + await this[kAgent].close() + this[kClients].clear() + } + + deactivate () { + this[kIsMockActive] = false + } + + activate () { + this[kIsMockActive] = true + } + + enableNetConnect (matcher) { + if (typeof matcher === 'string' || typeof matcher === 'function' || matcher instanceof RegExp) { + if (Array.isArray(this[kNetConnect])) { + this[kNetConnect].push(matcher) + } else { + this[kNetConnect] = [matcher] + } + } else if (typeof matcher === 'undefined') { + this[kNetConnect] = true + } else { + throw new InvalidArgumentError('Unsupported matcher. Must be one of String|Function|RegExp.') + } + } + + disableNetConnect () { + this[kNetConnect] = false + } + + enableCallHistory () { + this[kMockAgentIsCallHistoryEnabled] = true + + return this + } + + disableCallHistory () { + this[kMockAgentIsCallHistoryEnabled] = false + + return this + } + + getCallHistory () { + return this[kMockAgentMockCallHistoryInstance] + } + + clearCallHistory () { + if (this[kMockAgentMockCallHistoryInstance] !== undefined) { + this[kMockAgentMockCallHistoryInstance].clear() + } + } + + // This is required to bypass issues caused by using global symbols - see: + // https://github.com/nodejs/undici/issues/1447 + get isMockActive () { + return this[kIsMockActive] + } + + [kMockAgentRegisterCallHistory] () { + if (this[kMockAgentMockCallHistoryInstance] === undefined) { + this[kMockAgentMockCallHistoryInstance] = new MockCallHistory() + } + } + + [kMockAgentAddCallHistoryLog] (opts) { + if (this[kMockAgentIsCallHistoryEnabled]) { + // additional setup when enableCallHistory class method is used after mockAgent instantiation + this[kMockAgentRegisterCallHistory]() + + // add call history log on every call (intercepted or not) + this[kMockAgentMockCallHistoryInstance][kMockCallHistoryAddLog](opts) + } + } + + [kMockAgentSet] (origin, dispatcher) { + this[kClients].set(origin, { count: 0, dispatcher }) + } + + [kFactory] (origin) { + const mockOptions = Object.assign({ agent: this }, this[kOptions]) + return this[kOptions] && this[kOptions].connections === 1 + ? new MockClient(origin, mockOptions) + : new MockPool(origin, mockOptions) + } + + [kMockAgentGet] (origin) { + // First check if we can immediately find it + const result = this[kClients].get(origin) + if (result?.dispatcher) { + return result.dispatcher + } + + // If the origin is not a string create a dummy parent pool and return to user + if (typeof origin !== 'string') { + const dispatcher = this[kFactory]('http://localhost:9999') + this[kMockAgentSet](origin, dispatcher) + return dispatcher + } + + // If we match, create a pool and assign the same dispatches + for (const [keyMatcher, result] of Array.from(this[kClients])) { + if (result && typeof keyMatcher !== 'string' && matchValue(keyMatcher, origin)) { + const dispatcher = this[kFactory](origin) + this[kMockAgentSet](origin, dispatcher) + dispatcher[kDispatches] = result.dispatcher[kDispatches] + return dispatcher + } + } + } + + [kGetNetConnect] () { + return this[kNetConnect] + } + + pendingInterceptors () { + const mockAgentClients = this[kClients] + + return Array.from(mockAgentClients.entries()) + .flatMap(([origin, result]) => result.dispatcher[kDispatches].map(dispatch => ({ ...dispatch, origin }))) + .filter(({ pending }) => pending) + } + + assertNoPendingInterceptors ({ pendingInterceptorsFormatter = new PendingInterceptorsFormatter() } = {}) { + const pending = this.pendingInterceptors() + + if (pending.length === 0) { + return + } + + throw new UndiciError( + pending.length === 1 + ? `1 interceptor is pending:\n\n${pendingInterceptorsFormatter.format(pending)}`.trim() + : `${pending.length} interceptors are pending:\n\n${pendingInterceptorsFormatter.format(pending)}`.trim() + ) + } +} + +module.exports = MockAgent diff --git a/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/mock/mock-call-history.js b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/mock/mock-call-history.js new file mode 100644 index 000000000..74de68247 --- /dev/null +++ b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/mock/mock-call-history.js @@ -0,0 +1,248 @@ +'use strict' + +const { kMockCallHistoryAddLog } = require('./mock-symbols') +const { InvalidArgumentError } = require('../core/errors') + +function handleFilterCallsWithOptions (criteria, options, handler, store, allLogs) { + switch (options.operator) { + case 'OR': + store.push(...handler(criteria, allLogs)) + + return store + case 'AND': + return handler(criteria, store) + default: + // guard -- should never happens because buildAndValidateFilterCallsOptions is called before + throw new InvalidArgumentError('options.operator must to be a case insensitive string equal to \'OR\' or \'AND\'') + } +} + +function buildAndValidateFilterCallsOptions (options = {}) { + const finalOptions = {} + + if ('operator' in options) { + if (typeof options.operator !== 'string' || (options.operator.toUpperCase() !== 'OR' && options.operator.toUpperCase() !== 'AND')) { + throw new InvalidArgumentError('options.operator must to be a case insensitive string equal to \'OR\' or \'AND\'') + } + + return { + ...finalOptions, + operator: options.operator.toUpperCase() + } + } + + return finalOptions +} + +function makeFilterCalls (parameterName) { + return (parameterValue, logs) => { + if (typeof parameterValue === 'string' || parameterValue == null) { + return logs.filter((log) => { + return log[parameterName] === parameterValue + }) + } + if (parameterValue instanceof RegExp) { + return logs.filter((log) => { + return parameterValue.test(log[parameterName]) + }) + } + + throw new InvalidArgumentError(`${parameterName} parameter should be one of string, regexp, undefined or null`) + } +} +function computeUrlWithMaybeSearchParameters (requestInit) { + // path can contains query url parameters + // or query can contains query url parameters + try { + const url = new URL(requestInit.path, requestInit.origin) + + // requestInit.path contains query url parameters + // requestInit.query is then undefined + if (url.search.length !== 0) { + return url + } + + // requestInit.query can be populated here + url.search = new URLSearchParams(requestInit.query).toString() + + return url + } catch (error) { + throw new InvalidArgumentError('An error occurred when computing MockCallHistoryLog.url', { cause: error }) + } +} + +class MockCallHistoryLog { + constructor (requestInit = {}) { + this.body = requestInit.body + this.headers = requestInit.headers + this.method = requestInit.method + + const url = computeUrlWithMaybeSearchParameters(requestInit) + + this.fullUrl = url.toString() + this.origin = url.origin + this.path = url.pathname + this.searchParams = Object.fromEntries(url.searchParams) + this.protocol = url.protocol + this.host = url.host + this.port = url.port + this.hash = url.hash + } + + toMap () { + return new Map([ + ['protocol', this.protocol], + ['host', this.host], + ['port', this.port], + ['origin', this.origin], + ['path', this.path], + ['hash', this.hash], + ['searchParams', this.searchParams], + ['fullUrl', this.fullUrl], + ['method', this.method], + ['body', this.body], + ['headers', this.headers]] + ) + } + + toString () { + const options = { betweenKeyValueSeparator: '->', betweenPairSeparator: '|' } + let result = '' + + this.toMap().forEach((value, key) => { + if (typeof value === 'string' || value === undefined || value === null) { + result = `${result}${key}${options.betweenKeyValueSeparator}${value}${options.betweenPairSeparator}` + } + if ((typeof value === 'object' && value !== null) || Array.isArray(value)) { + result = `${result}${key}${options.betweenKeyValueSeparator}${JSON.stringify(value)}${options.betweenPairSeparator}` + } + // maybe miss something for non Record / Array headers and searchParams here + }) + + // delete last betweenPairSeparator + return result.slice(0, -1) + } +} + +class MockCallHistory { + logs = [] + + calls () { + return this.logs + } + + firstCall () { + return this.logs.at(0) + } + + lastCall () { + return this.logs.at(-1) + } + + nthCall (number) { + if (typeof number !== 'number') { + throw new InvalidArgumentError('nthCall must be called with a number') + } + if (!Number.isInteger(number)) { + throw new InvalidArgumentError('nthCall must be called with an integer') + } + if (Math.sign(number) !== 1) { + throw new InvalidArgumentError('nthCall must be called with a positive value. use firstCall or lastCall instead') + } + + // non zero based index. this is more human readable + return this.logs.at(number - 1) + } + + filterCalls (criteria, options) { + // perf + if (this.logs.length === 0) { + return this.logs + } + if (typeof criteria === 'function') { + return this.logs.filter(criteria) + } + if (criteria instanceof RegExp) { + return this.logs.filter((log) => { + return criteria.test(log.toString()) + }) + } + if (typeof criteria === 'object' && criteria !== null) { + // no criteria - returning all logs + if (Object.keys(criteria).length === 0) { + return this.logs + } + + const finalOptions = { operator: 'OR', ...buildAndValidateFilterCallsOptions(options) } + + let maybeDuplicatedLogsFiltered = finalOptions.operator === 'AND' ? this.logs : [] + if ('protocol' in criteria) { + maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.protocol, finalOptions, this.filterCallsByProtocol, maybeDuplicatedLogsFiltered, this.logs) + } + if ('host' in criteria) { + maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.host, finalOptions, this.filterCallsByHost, maybeDuplicatedLogsFiltered, this.logs) + } + if ('port' in criteria) { + maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.port, finalOptions, this.filterCallsByPort, maybeDuplicatedLogsFiltered, this.logs) + } + if ('origin' in criteria) { + maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.origin, finalOptions, this.filterCallsByOrigin, maybeDuplicatedLogsFiltered, this.logs) + } + if ('path' in criteria) { + maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.path, finalOptions, this.filterCallsByPath, maybeDuplicatedLogsFiltered, this.logs) + } + if ('hash' in criteria) { + maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.hash, finalOptions, this.filterCallsByHash, maybeDuplicatedLogsFiltered, this.logs) + } + if ('fullUrl' in criteria) { + maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.fullUrl, finalOptions, this.filterCallsByFullUrl, maybeDuplicatedLogsFiltered, this.logs) + } + if ('method' in criteria) { + maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.method, finalOptions, this.filterCallsByMethod, maybeDuplicatedLogsFiltered, this.logs) + } + + const uniqLogsFiltered = [...new Set(maybeDuplicatedLogsFiltered)] + + return uniqLogsFiltered + } + + throw new InvalidArgumentError('criteria parameter should be one of function, regexp, or object') + } + + filterCallsByProtocol = makeFilterCalls.call(this, 'protocol') + + filterCallsByHost = makeFilterCalls.call(this, 'host') + + filterCallsByPort = makeFilterCalls.call(this, 'port') + + filterCallsByOrigin = makeFilterCalls.call(this, 'origin') + + filterCallsByPath = makeFilterCalls.call(this, 'path') + + filterCallsByHash = makeFilterCalls.call(this, 'hash') + + filterCallsByFullUrl = makeFilterCalls.call(this, 'fullUrl') + + filterCallsByMethod = makeFilterCalls.call(this, 'method') + + clear () { + this.logs = [] + } + + [kMockCallHistoryAddLog] (requestInit) { + const log = new MockCallHistoryLog(requestInit) + + this.logs.push(log) + + return log + } + + * [Symbol.iterator] () { + for (const log of this.calls()) { + yield log + } + } +} + +module.exports.MockCallHistory = MockCallHistory +module.exports.MockCallHistoryLog = MockCallHistoryLog diff --git a/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/mock/mock-client.js b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/mock/mock-client.js new file mode 100644 index 000000000..b3be7ab3b --- /dev/null +++ b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/mock/mock-client.js @@ -0,0 +1,68 @@ +'use strict' + +const { promisify } = require('node:util') +const Client = require('../dispatcher/client') +const { buildMockDispatch } = require('./mock-utils') +const { + kDispatches, + kMockAgent, + kClose, + kOriginalClose, + kOrigin, + kOriginalDispatch, + kConnected, + kIgnoreTrailingSlash +} = require('./mock-symbols') +const { MockInterceptor } = require('./mock-interceptor') +const Symbols = require('../core/symbols') +const { InvalidArgumentError } = require('../core/errors') + +/** + * MockClient provides an API that extends the Client to influence the mockDispatches. + */ +class MockClient extends Client { + constructor (origin, opts) { + if (!opts || !opts.agent || typeof opts.agent.dispatch !== 'function') { + throw new InvalidArgumentError('Argument opts.agent must implement Agent') + } + + super(origin, opts) + + this[kMockAgent] = opts.agent + this[kOrigin] = origin + this[kIgnoreTrailingSlash] = opts.ignoreTrailingSlash ?? false + this[kDispatches] = [] + this[kConnected] = 1 + this[kOriginalDispatch] = this.dispatch + this[kOriginalClose] = this.close.bind(this) + + this.dispatch = buildMockDispatch.call(this) + this.close = this[kClose] + } + + get [Symbols.kConnected] () { + return this[kConnected] + } + + /** + * Sets up the base interceptor for mocking replies from undici. + */ + intercept (opts) { + return new MockInterceptor( + opts && { ignoreTrailingSlash: this[kIgnoreTrailingSlash], ...opts }, + this[kDispatches] + ) + } + + cleanMocks () { + this[kDispatches] = [] + } + + async [kClose] () { + await promisify(this[kOriginalClose])() + this[kConnected] = 0 + this[kMockAgent][Symbols.kClients].delete(this[kOrigin]) + } +} + +module.exports = MockClient diff --git a/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/mock/mock-errors.js b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/mock/mock-errors.js new file mode 100644 index 000000000..69e4f9cc3 --- /dev/null +++ b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/mock/mock-errors.js @@ -0,0 +1,29 @@ +'use strict' + +const { UndiciError } = require('../core/errors') + +const kMockNotMatchedError = Symbol.for('undici.error.UND_MOCK_ERR_MOCK_NOT_MATCHED') + +/** + * The request does not match any registered mock dispatches. + */ +class MockNotMatchedError extends UndiciError { + constructor (message) { + super(message) + this.name = 'MockNotMatchedError' + this.message = message || 'The request does not match any registered mock dispatches' + this.code = 'UND_MOCK_ERR_MOCK_NOT_MATCHED' + } + + static [Symbol.hasInstance] (instance) { + return instance && instance[kMockNotMatchedError] === true + } + + get [kMockNotMatchedError] () { + return true + } +} + +module.exports = { + MockNotMatchedError +} diff --git a/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/mock/mock-interceptor.js b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/mock/mock-interceptor.js new file mode 100644 index 000000000..1ea7aac48 --- /dev/null +++ b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/mock/mock-interceptor.js @@ -0,0 +1,209 @@ +'use strict' + +const { getResponseData, buildKey, addMockDispatch } = require('./mock-utils') +const { + kDispatches, + kDispatchKey, + kDefaultHeaders, + kDefaultTrailers, + kContentLength, + kMockDispatch, + kIgnoreTrailingSlash +} = require('./mock-symbols') +const { InvalidArgumentError } = require('../core/errors') +const { serializePathWithQuery } = require('../core/util') + +/** + * Defines the scope API for an interceptor reply + */ +class MockScope { + constructor (mockDispatch) { + this[kMockDispatch] = mockDispatch + } + + /** + * Delay a reply by a set amount in ms. + */ + delay (waitInMs) { + if (typeof waitInMs !== 'number' || !Number.isInteger(waitInMs) || waitInMs <= 0) { + throw new InvalidArgumentError('waitInMs must be a valid integer > 0') + } + + this[kMockDispatch].delay = waitInMs + return this + } + + /** + * For a defined reply, never mark as consumed. + */ + persist () { + this[kMockDispatch].persist = true + return this + } + + /** + * Allow one to define a reply for a set amount of matching requests. + */ + times (repeatTimes) { + if (typeof repeatTimes !== 'number' || !Number.isInteger(repeatTimes) || repeatTimes <= 0) { + throw new InvalidArgumentError('repeatTimes must be a valid integer > 0') + } + + this[kMockDispatch].times = repeatTimes + return this + } +} + +/** + * Defines an interceptor for a Mock + */ +class MockInterceptor { + constructor (opts, mockDispatches) { + if (typeof opts !== 'object') { + throw new InvalidArgumentError('opts must be an object') + } + if (typeof opts.path === 'undefined') { + throw new InvalidArgumentError('opts.path must be defined') + } + if (typeof opts.method === 'undefined') { + opts.method = 'GET' + } + // See https://github.com/nodejs/undici/issues/1245 + // As per RFC 3986, clients are not supposed to send URI + // fragments to servers when they retrieve a document, + if (typeof opts.path === 'string') { + if (opts.query) { + opts.path = serializePathWithQuery(opts.path, opts.query) + } else { + // Matches https://github.com/nodejs/undici/blob/main/lib/web/fetch/index.js#L1811 + const parsedURL = new URL(opts.path, 'data://') + opts.path = parsedURL.pathname + parsedURL.search + } + } + if (typeof opts.method === 'string') { + opts.method = opts.method.toUpperCase() + } + + this[kDispatchKey] = buildKey(opts) + this[kDispatches] = mockDispatches + this[kIgnoreTrailingSlash] = opts.ignoreTrailingSlash ?? false + this[kDefaultHeaders] = {} + this[kDefaultTrailers] = {} + this[kContentLength] = false + } + + createMockScopeDispatchData ({ statusCode, data, responseOptions }) { + const responseData = getResponseData(data) + const contentLength = this[kContentLength] ? { 'content-length': responseData.length } : {} + const headers = { ...this[kDefaultHeaders], ...contentLength, ...responseOptions.headers } + const trailers = { ...this[kDefaultTrailers], ...responseOptions.trailers } + + return { statusCode, data, headers, trailers } + } + + validateReplyParameters (replyParameters) { + if (typeof replyParameters.statusCode === 'undefined') { + throw new InvalidArgumentError('statusCode must be defined') + } + if (typeof replyParameters.responseOptions !== 'object' || replyParameters.responseOptions === null) { + throw new InvalidArgumentError('responseOptions must be an object') + } + } + + /** + * Mock an undici request with a defined reply. + */ + reply (replyOptionsCallbackOrStatusCode) { + // Values of reply aren't available right now as they + // can only be available when the reply callback is invoked. + if (typeof replyOptionsCallbackOrStatusCode === 'function') { + // We'll first wrap the provided callback in another function, + // this function will properly resolve the data from the callback + // when invoked. + const wrappedDefaultsCallback = (opts) => { + // Our reply options callback contains the parameter for statusCode, data and options. + const resolvedData = replyOptionsCallbackOrStatusCode(opts) + + // Check if it is in the right format + if (typeof resolvedData !== 'object' || resolvedData === null) { + throw new InvalidArgumentError('reply options callback must return an object') + } + + const replyParameters = { data: '', responseOptions: {}, ...resolvedData } + this.validateReplyParameters(replyParameters) + // Since the values can be obtained immediately we return them + // from this higher order function that will be resolved later. + return { + ...this.createMockScopeDispatchData(replyParameters) + } + } + + // Add usual dispatch data, but this time set the data parameter to function that will eventually provide data. + const newMockDispatch = addMockDispatch(this[kDispatches], this[kDispatchKey], wrappedDefaultsCallback, { ignoreTrailingSlash: this[kIgnoreTrailingSlash] }) + return new MockScope(newMockDispatch) + } + + // We can have either one or three parameters, if we get here, + // we should have 1-3 parameters. So we spread the arguments of + // this function to obtain the parameters, since replyData will always + // just be the statusCode. + const replyParameters = { + statusCode: replyOptionsCallbackOrStatusCode, + data: arguments[1] === undefined ? '' : arguments[1], + responseOptions: arguments[2] === undefined ? {} : arguments[2] + } + this.validateReplyParameters(replyParameters) + + // Send in-already provided data like usual + const dispatchData = this.createMockScopeDispatchData(replyParameters) + const newMockDispatch = addMockDispatch(this[kDispatches], this[kDispatchKey], dispatchData, { ignoreTrailingSlash: this[kIgnoreTrailingSlash] }) + return new MockScope(newMockDispatch) + } + + /** + * Mock an undici request with a defined error. + */ + replyWithError (error) { + if (typeof error === 'undefined') { + throw new InvalidArgumentError('error must be defined') + } + + const newMockDispatch = addMockDispatch(this[kDispatches], this[kDispatchKey], { error }, { ignoreTrailingSlash: this[kIgnoreTrailingSlash] }) + return new MockScope(newMockDispatch) + } + + /** + * Set default reply headers on the interceptor for subsequent replies + */ + defaultReplyHeaders (headers) { + if (typeof headers === 'undefined') { + throw new InvalidArgumentError('headers must be defined') + } + + this[kDefaultHeaders] = headers + return this + } + + /** + * Set default reply trailers on the interceptor for subsequent replies + */ + defaultReplyTrailers (trailers) { + if (typeof trailers === 'undefined') { + throw new InvalidArgumentError('trailers must be defined') + } + + this[kDefaultTrailers] = trailers + return this + } + + /** + * Set reply content length header for replies on the interceptor + */ + replyContentLength () { + this[kContentLength] = true + return this + } +} + +module.exports.MockInterceptor = MockInterceptor +module.exports.MockScope = MockScope diff --git a/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/mock/mock-pool.js b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/mock/mock-pool.js new file mode 100644 index 000000000..2121e3c99 --- /dev/null +++ b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/mock/mock-pool.js @@ -0,0 +1,68 @@ +'use strict' + +const { promisify } = require('node:util') +const Pool = require('../dispatcher/pool') +const { buildMockDispatch } = require('./mock-utils') +const { + kDispatches, + kMockAgent, + kClose, + kOriginalClose, + kOrigin, + kOriginalDispatch, + kConnected, + kIgnoreTrailingSlash +} = require('./mock-symbols') +const { MockInterceptor } = require('./mock-interceptor') +const Symbols = require('../core/symbols') +const { InvalidArgumentError } = require('../core/errors') + +/** + * MockPool provides an API that extends the Pool to influence the mockDispatches. + */ +class MockPool extends Pool { + constructor (origin, opts) { + if (!opts || !opts.agent || typeof opts.agent.dispatch !== 'function') { + throw new InvalidArgumentError('Argument opts.agent must implement Agent') + } + + super(origin, opts) + + this[kMockAgent] = opts.agent + this[kOrigin] = origin + this[kIgnoreTrailingSlash] = opts.ignoreTrailingSlash ?? false + this[kDispatches] = [] + this[kConnected] = 1 + this[kOriginalDispatch] = this.dispatch + this[kOriginalClose] = this.close.bind(this) + + this.dispatch = buildMockDispatch.call(this) + this.close = this[kClose] + } + + get [Symbols.kConnected] () { + return this[kConnected] + } + + /** + * Sets up the base interceptor for mocking replies from undici. + */ + intercept (opts) { + return new MockInterceptor( + opts && { ignoreTrailingSlash: this[kIgnoreTrailingSlash], ...opts }, + this[kDispatches] + ) + } + + cleanMocks () { + this[kDispatches] = [] + } + + async [kClose] () { + await promisify(this[kOriginalClose])() + this[kConnected] = 0 + this[kMockAgent][Symbols.kClients].delete(this[kOrigin]) + } +} + +module.exports = MockPool diff --git a/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/mock/mock-symbols.js b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/mock/mock-symbols.js new file mode 100644 index 000000000..9b23e8e3c --- /dev/null +++ b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/mock/mock-symbols.js @@ -0,0 +1,32 @@ +'use strict' + +module.exports = { + kAgent: Symbol('agent'), + kOptions: Symbol('options'), + kFactory: Symbol('factory'), + kDispatches: Symbol('dispatches'), + kDispatchKey: Symbol('dispatch key'), + kDefaultHeaders: Symbol('default headers'), + kDefaultTrailers: Symbol('default trailers'), + kContentLength: Symbol('content length'), + kMockAgent: Symbol('mock agent'), + kMockAgentSet: Symbol('mock agent set'), + kMockAgentGet: Symbol('mock agent get'), + kMockDispatch: Symbol('mock dispatch'), + kClose: Symbol('close'), + kOriginalClose: Symbol('original agent close'), + kOriginalDispatch: Symbol('original dispatch'), + kOrigin: Symbol('origin'), + kIsMockActive: Symbol('is mock active'), + kNetConnect: Symbol('net connect'), + kGetNetConnect: Symbol('get net connect'), + kConnected: Symbol('connected'), + kIgnoreTrailingSlash: Symbol('ignore trailing slash'), + kMockAgentMockCallHistoryInstance: Symbol('mock agent mock call history name'), + kMockAgentRegisterCallHistory: Symbol('mock agent register mock call history'), + kMockAgentAddCallHistoryLog: Symbol('mock agent add call history log'), + kMockAgentIsCallHistoryEnabled: Symbol('mock agent is call history enabled'), + kMockAgentAcceptsNonStandardSearchParameters: Symbol('mock agent accepts non standard search parameters'), + kMockCallHistoryAddLog: Symbol('mock call history add log'), + kTotalDispatchCount: Symbol('total dispatch count') +} diff --git a/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/mock/mock-utils.js b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/mock/mock-utils.js new file mode 100644 index 000000000..a51816f40 --- /dev/null +++ b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/mock/mock-utils.js @@ -0,0 +1,486 @@ +'use strict' + +const { MockNotMatchedError } = require('./mock-errors') +const { + kDispatches, + kMockAgent, + kOriginalDispatch, + kOrigin, + kGetNetConnect, + kTotalDispatchCount +} = require('./mock-symbols') +const { serializePathWithQuery } = require('../core/util') +const { STATUS_CODES } = require('node:http') +const { + types: { + isPromise + } +} = require('node:util') +const { InvalidArgumentError } = require('../core/errors') + +function matchValue (match, value) { + if (typeof match === 'string') { + return match === value + } + if (match instanceof RegExp) { + return match.test(value) + } + if (typeof match === 'function') { + return match(value) === true + } + return false +} + +function lowerCaseEntries (headers) { + return Object.fromEntries( + Object.entries(headers).map(([headerName, headerValue]) => { + return [headerName.toLocaleLowerCase(), headerValue] + }) + ) +} + +/** + * @param {import('../../index').Headers|string[]|Record} headers + * @param {string} key + */ +function getHeaderByName (headers, key) { + if (Array.isArray(headers)) { + for (let i = 0; i < headers.length; i += 2) { + if (headers[i].toLocaleLowerCase() === key.toLocaleLowerCase()) { + return headers[i + 1] + } + } + + return undefined + } else if (typeof headers.get === 'function') { + return headers.get(key) + } else { + return lowerCaseEntries(headers)[key.toLocaleLowerCase()] + } +} + +/** @param {string[]} headers */ +function buildHeadersFromArray (headers) { // fetch HeadersList + const clone = headers.slice() + const entries = [] + for (let index = 0; index < clone.length; index += 2) { + entries.push([clone[index], clone[index + 1]]) + } + return Object.fromEntries(entries) +} + +function matchHeaders (mockDispatch, headers) { + if (typeof mockDispatch.headers === 'function') { + if (Array.isArray(headers)) { // fetch HeadersList + headers = buildHeadersFromArray(headers) + } + return mockDispatch.headers(headers ? lowerCaseEntries(headers) : {}) + } + if (typeof mockDispatch.headers === 'undefined') { + return true + } + if (typeof headers !== 'object' || typeof mockDispatch.headers !== 'object') { + return false + } + + for (const [matchHeaderName, matchHeaderValue] of Object.entries(mockDispatch.headers)) { + const headerValue = getHeaderByName(headers, matchHeaderName) + + if (!matchValue(matchHeaderValue, headerValue)) { + return false + } + } + return true +} + +function normalizeSearchParams (query) { + if (typeof query !== 'string') { + return query + } + + const originalQp = new URLSearchParams(query) + const normalizedQp = new URLSearchParams() + + for (let [key, value] of originalQp.entries()) { + key = key.replace('[]', '') + + const valueRepresentsString = /^(['"]).*\1$/.test(value) + if (valueRepresentsString) { + normalizedQp.append(key, value) + continue + } + + if (value.includes(',')) { + const values = value.split(',') + for (const v of values) { + normalizedQp.append(key, v) + } + continue + } + + normalizedQp.append(key, value) + } + + return normalizedQp +} + +function safeUrl (path) { + if (typeof path !== 'string') { + return path + } + const pathSegments = path.split('?', 3) + if (pathSegments.length !== 2) { + return path + } + + const qp = new URLSearchParams(pathSegments.pop()) + qp.sort() + return [...pathSegments, qp.toString()].join('?') +} + +function matchKey (mockDispatch, { path, method, body, headers }) { + const pathMatch = matchValue(mockDispatch.path, path) + const methodMatch = matchValue(mockDispatch.method, method) + const bodyMatch = typeof mockDispatch.body !== 'undefined' ? matchValue(mockDispatch.body, body) : true + const headersMatch = matchHeaders(mockDispatch, headers) + return pathMatch && methodMatch && bodyMatch && headersMatch +} + +function getResponseData (data) { + if (Buffer.isBuffer(data)) { + return data + } else if (data instanceof Uint8Array) { + return data + } else if (data instanceof ArrayBuffer) { + return data + } else if (typeof data === 'object') { + return JSON.stringify(data) + } else if (data) { + return data.toString() + } else { + return '' + } +} + +function getMockDispatch (mockDispatches, key) { + const basePath = key.query ? serializePathWithQuery(key.path, key.query) : key.path + const resolvedPath = typeof basePath === 'string' ? safeUrl(basePath) : basePath + + const resolvedPathWithoutTrailingSlash = removeTrailingSlash(resolvedPath) + + // Match path + let matchedMockDispatches = mockDispatches + .filter(({ consumed }) => !consumed) + .filter(({ path, ignoreTrailingSlash }) => { + return ignoreTrailingSlash + ? matchValue(removeTrailingSlash(safeUrl(path)), resolvedPathWithoutTrailingSlash) + : matchValue(safeUrl(path), resolvedPath) + }) + if (matchedMockDispatches.length === 0) { + throw new MockNotMatchedError(`Mock dispatch not matched for path '${resolvedPath}'`) + } + + // Match method + matchedMockDispatches = matchedMockDispatches.filter(({ method }) => matchValue(method, key.method)) + if (matchedMockDispatches.length === 0) { + throw new MockNotMatchedError(`Mock dispatch not matched for method '${key.method}' on path '${resolvedPath}'`) + } + + // Match body + matchedMockDispatches = matchedMockDispatches.filter(({ body }) => typeof body !== 'undefined' ? matchValue(body, key.body) : true) + if (matchedMockDispatches.length === 0) { + throw new MockNotMatchedError(`Mock dispatch not matched for body '${key.body}' on path '${resolvedPath}'`) + } + + // Match headers + matchedMockDispatches = matchedMockDispatches.filter((mockDispatch) => matchHeaders(mockDispatch, key.headers)) + if (matchedMockDispatches.length === 0) { + const headers = typeof key.headers === 'object' ? JSON.stringify(key.headers) : key.headers + throw new MockNotMatchedError(`Mock dispatch not matched for headers '${headers}' on path '${resolvedPath}'`) + } + + return matchedMockDispatches[0] +} + +function addMockDispatch (mockDispatches, key, data, opts) { + const baseData = { timesInvoked: 0, times: 1, persist: false, consumed: false, ...opts } + const replyData = typeof data === 'function' ? { callback: data } : { ...data } + const newMockDispatch = { ...baseData, ...key, pending: true, data: { error: null, ...replyData } } + mockDispatches.push(newMockDispatch) + // Track total number of intercepts ever registered for better error messages + mockDispatches[kTotalDispatchCount] = (mockDispatches[kTotalDispatchCount] || 0) + 1 + return newMockDispatch +} + +function deleteMockDispatch (mockDispatches, key) { + const index = mockDispatches.findIndex(dispatch => { + if (!dispatch.consumed) { + return false + } + return matchKey(dispatch, key) + }) + if (index !== -1) { + mockDispatches.splice(index, 1) + } +} + +/** + * @param {string} path Path to remove trailing slash from + */ +function removeTrailingSlash (path) { + while (path.endsWith('/')) { + path = path.slice(0, -1) + } + + if (path.length === 0) { + path = '/' + } + + return path +} + +function buildKey (opts) { + const { path, method, body, headers, query } = opts + + return { + path, + method, + body, + headers, + query + } +} + +function generateKeyValues (data) { + const keys = Object.keys(data) + const result = [] + for (let i = 0; i < keys.length; ++i) { + const key = keys[i] + const value = data[key] + const name = Buffer.from(`${key}`) + if (Array.isArray(value)) { + for (let j = 0; j < value.length; ++j) { + result.push(name, Buffer.from(`${value[j]}`)) + } + } else { + result.push(name, Buffer.from(`${value}`)) + } + } + return result +} + +/** + * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Status + * @param {number} statusCode + */ +function getStatusText (statusCode) { + return STATUS_CODES[statusCode] || 'unknown' +} + +async function getResponse (body) { + const buffers = [] + for await (const data of body) { + buffers.push(data) + } + return Buffer.concat(buffers).toString('utf8') +} + +/** + * Mock dispatch function used to simulate undici dispatches + */ +function mockDispatch (opts, handler) { + // Get mock dispatch from built key + const key = buildKey(opts) + const mockDispatch = getMockDispatch(this[kDispatches], key) + + mockDispatch.timesInvoked++ + + // Here's where we resolve a callback if a callback is present for the dispatch data. + if (mockDispatch.data.callback) { + mockDispatch.data = { ...mockDispatch.data, ...mockDispatch.data.callback(opts) } + } + + // Parse mockDispatch data + const { data: { statusCode, data, headers, trailers, error }, delay, persist } = mockDispatch + const { timesInvoked, times } = mockDispatch + + // If it's used up and not persistent, mark as consumed + mockDispatch.consumed = !persist && timesInvoked >= times + mockDispatch.pending = timesInvoked < times + + // If specified, trigger dispatch error + if (error !== null) { + deleteMockDispatch(this[kDispatches], key) + handler.onError(error) + return true + } + + // Track whether the request has been aborted + let aborted = false + let timer = null + + function abort (err) { + if (aborted) { + return + } + aborted = true + + // Clear the pending delayed response if any + if (timer !== null) { + clearTimeout(timer) + timer = null + } + + // Notify the handler of the abort + handler.onError(err) + } + + // Call onConnect to allow the handler to register the abort callback + handler.onConnect?.(abort, null) + + // Handle the request with a delay if necessary + if (typeof delay === 'number' && delay > 0) { + timer = setTimeout(() => { + timer = null + handleReply(this[kDispatches]) + }, delay) + } else { + handleReply(this[kDispatches]) + } + + function handleReply (mockDispatches, _data = data) { + // Don't send response if the request was aborted + if (aborted) { + return + } + + // fetch's HeadersList is a 1D string array + const optsHeaders = Array.isArray(opts.headers) + ? buildHeadersFromArray(opts.headers) + : opts.headers + const body = typeof _data === 'function' + ? _data({ ...opts, headers: optsHeaders }) + : _data + + // util.types.isPromise is likely needed for jest. + if (isPromise(body)) { + // If handleReply is asynchronous, throwing an error + // in the callback will reject the promise, rather than + // synchronously throw the error, which breaks some tests. + // Rather, we wait for the callback to resolve if it is a + // promise, and then re-run handleReply with the new body. + return body.then((newData) => handleReply(mockDispatches, newData)) + } + + // Check again if aborted after async body resolution + if (aborted) { + return + } + + const responseData = getResponseData(body) + const responseHeaders = generateKeyValues(headers) + const responseTrailers = generateKeyValues(trailers) + + handler.onHeaders?.(statusCode, responseHeaders, resume, getStatusText(statusCode)) + handler.onData?.(Buffer.from(responseData)) + handler.onComplete?.(responseTrailers) + deleteMockDispatch(mockDispatches, key) + } + + function resume () {} + + return true +} + +function buildMockDispatch () { + const agent = this[kMockAgent] + const origin = this[kOrigin] + const originalDispatch = this[kOriginalDispatch] + + return function dispatch (opts, handler) { + if (agent.isMockActive) { + try { + mockDispatch.call(this, opts, handler) + } catch (error) { + if (error.code === 'UND_MOCK_ERR_MOCK_NOT_MATCHED') { + const netConnect = agent[kGetNetConnect]() + const totalInterceptsCount = this[kDispatches][kTotalDispatchCount] || this[kDispatches].length + const pendingInterceptsCount = this[kDispatches].filter(({ consumed }) => !consumed).length + const interceptsMessage = `, ${pendingInterceptsCount} interceptor(s) remaining out of ${totalInterceptsCount} defined` + if (netConnect === false) { + throw new MockNotMatchedError(`${error.message}: subsequent request to origin ${origin} was not allowed (net.connect disabled)${interceptsMessage}`) + } + if (checkNetConnect(netConnect, origin)) { + originalDispatch.call(this, opts, handler) + } else { + throw new MockNotMatchedError(`${error.message}: subsequent request to origin ${origin} was not allowed (net.connect is not enabled for this origin)${interceptsMessage}`) + } + } else { + throw error + } + } + } else { + originalDispatch.call(this, opts, handler) + } + } +} + +function checkNetConnect (netConnect, origin) { + const url = new URL(origin) + if (netConnect === true) { + return true + } else if (Array.isArray(netConnect) && netConnect.some((matcher) => matchValue(matcher, url.host))) { + return true + } + return false +} + +function normalizeOrigin (origin) { + if (typeof origin !== 'string' && !(origin instanceof URL)) { + return origin + } + + if (origin instanceof URL) { + return origin.origin + } + + return origin.toLowerCase() +} + +function buildAndValidateMockOptions (opts) { + const { agent, ...mockOptions } = opts + + if ('enableCallHistory' in mockOptions && typeof mockOptions.enableCallHistory !== 'boolean') { + throw new InvalidArgumentError('options.enableCallHistory must to be a boolean') + } + + if ('acceptNonStandardSearchParameters' in mockOptions && typeof mockOptions.acceptNonStandardSearchParameters !== 'boolean') { + throw new InvalidArgumentError('options.acceptNonStandardSearchParameters must to be a boolean') + } + + if ('ignoreTrailingSlash' in mockOptions && typeof mockOptions.ignoreTrailingSlash !== 'boolean') { + throw new InvalidArgumentError('options.ignoreTrailingSlash must to be a boolean') + } + + return mockOptions +} + +module.exports = { + getResponseData, + getMockDispatch, + addMockDispatch, + deleteMockDispatch, + buildKey, + generateKeyValues, + matchValue, + getResponse, + getStatusText, + mockDispatch, + buildMockDispatch, + checkNetConnect, + buildAndValidateMockOptions, + getHeaderByName, + buildHeadersFromArray, + normalizeSearchParams, + normalizeOrigin +} diff --git a/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/mock/pending-interceptors-formatter.js b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/mock/pending-interceptors-formatter.js new file mode 100644 index 000000000..ccca95119 --- /dev/null +++ b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/mock/pending-interceptors-formatter.js @@ -0,0 +1,43 @@ +'use strict' + +const { Transform } = require('node:stream') +const { Console } = require('node:console') + +const PERSISTENT = process.versions.icu ? '✅' : 'Y ' +const NOT_PERSISTENT = process.versions.icu ? '❌' : 'N ' + +/** + * Gets the output of `console.table(…)` as a string. + */ +module.exports = class PendingInterceptorsFormatter { + constructor ({ disableColors } = {}) { + this.transform = new Transform({ + transform (chunk, _enc, cb) { + cb(null, chunk) + } + }) + + this.logger = new Console({ + stdout: this.transform, + inspectOptions: { + colors: !disableColors && !process.env.CI + } + }) + } + + format (pendingInterceptors) { + const withPrettyHeaders = pendingInterceptors.map( + ({ method, path, data: { statusCode }, persist, times, timesInvoked, origin }) => ({ + Method: method, + Origin: origin, + Path: path, + 'Status code': statusCode, + Persistent: persist ? PERSISTENT : NOT_PERSISTENT, + Invocations: timesInvoked, + Remaining: persist ? Infinity : times - timesInvoked + })) + + this.logger.table(withPrettyHeaders) + return this.transform.read().toString() + } +} diff --git a/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/mock/snapshot-agent.js b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/mock/snapshot-agent.js new file mode 100644 index 000000000..802801119 --- /dev/null +++ b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/mock/snapshot-agent.js @@ -0,0 +1,353 @@ +'use strict' + +const Agent = require('../dispatcher/agent') +const MockAgent = require('./mock-agent') +const { SnapshotRecorder } = require('./snapshot-recorder') +const WrapHandler = require('../handler/wrap-handler') +const { InvalidArgumentError, UndiciError } = require('../core/errors') +const { validateSnapshotMode } = require('./snapshot-utils') + +const kSnapshotRecorder = Symbol('kSnapshotRecorder') +const kSnapshotMode = Symbol('kSnapshotMode') +const kSnapshotPath = Symbol('kSnapshotPath') +const kSnapshotLoaded = Symbol('kSnapshotLoaded') +const kRealAgent = Symbol('kRealAgent') + +// Static flag to ensure warning is only emitted once per process +let warningEmitted = false + +class SnapshotAgent extends MockAgent { + constructor (opts = {}) { + // Emit experimental warning only once + if (!warningEmitted) { + process.emitWarning( + 'SnapshotAgent is experimental and subject to change', + 'ExperimentalWarning' + ) + warningEmitted = true + } + + const { + mode = 'record', + snapshotPath = null, + ...mockAgentOpts + } = opts + + super(mockAgentOpts) + + validateSnapshotMode(mode) + + // Validate snapshotPath is provided when required + if ((mode === 'playback' || mode === 'update') && !snapshotPath) { + throw new InvalidArgumentError(`snapshotPath is required when mode is '${mode}'`) + } + + this[kSnapshotMode] = mode + this[kSnapshotPath] = snapshotPath + + this[kSnapshotRecorder] = new SnapshotRecorder({ + snapshotPath: this[kSnapshotPath], + mode: this[kSnapshotMode], + maxSnapshots: opts.maxSnapshots, + autoFlush: opts.autoFlush, + flushInterval: opts.flushInterval, + matchHeaders: opts.matchHeaders, + ignoreHeaders: opts.ignoreHeaders, + excludeHeaders: opts.excludeHeaders, + matchBody: opts.matchBody, + matchQuery: opts.matchQuery, + caseSensitive: opts.caseSensitive, + shouldRecord: opts.shouldRecord, + shouldPlayback: opts.shouldPlayback, + excludeUrls: opts.excludeUrls + }) + this[kSnapshotLoaded] = false + + // For recording/update mode, we need a real agent to make actual requests + // For playback mode, we need a real agent if there are excluded URLs + if (this[kSnapshotMode] === 'record' || this[kSnapshotMode] === 'update' || + (this[kSnapshotMode] === 'playback' && opts.excludeUrls && opts.excludeUrls.length > 0)) { + this[kRealAgent] = new Agent(opts) + } + + // Auto-load snapshots in playback/update mode + if ((this[kSnapshotMode] === 'playback' || this[kSnapshotMode] === 'update') && this[kSnapshotPath]) { + this.loadSnapshots().catch(() => { + // Ignore load errors - file might not exist yet + }) + } + } + + dispatch (opts, handler) { + handler = WrapHandler.wrap(handler) + const mode = this[kSnapshotMode] + + // Check if URL should be excluded (pass through without mocking/recording) + if (this[kSnapshotRecorder].isUrlExcluded(opts)) { + // Real agent is guaranteed by constructor when excludeUrls is configured + return this[kRealAgent].dispatch(opts, handler) + } + + if (mode === 'playback' || mode === 'update') { + // Ensure snapshots are loaded + if (!this[kSnapshotLoaded]) { + // Need to load asynchronously, delegate to async version + return this.#asyncDispatch(opts, handler) + } + + // Try to find existing snapshot (synchronous) + const snapshot = this[kSnapshotRecorder].findSnapshot(opts) + + if (snapshot) { + // Use recorded response (synchronous) + return this.#replaySnapshot(snapshot, handler) + } else if (mode === 'update') { + // Make real request and record it (async required) + return this.#recordAndReplay(opts, handler) + } else { + // Playback mode but no snapshot found + const error = new UndiciError(`No snapshot found for ${opts.method || 'GET'} ${opts.path}`) + if (handler.onError) { + handler.onError(error) + return + } + throw error + } + } else if (mode === 'record') { + // Record mode - make real request and save response (async required) + return this.#recordAndReplay(opts, handler) + } + } + + /** + * Async version of dispatch for when we need to load snapshots first + */ + async #asyncDispatch (opts, handler) { + await this.loadSnapshots() + return this.dispatch(opts, handler) + } + + /** + * Records a real request and replays the response + */ + #recordAndReplay (opts, handler) { + const responseData = { + statusCode: null, + headers: {}, + trailers: {}, + body: [] + } + + const self = this // Capture 'this' context for use within nested handler callbacks + + const recordingHandler = { + onRequestStart (controller, context) { + return handler.onRequestStart(controller, { ...context, history: this.history }) + }, + + onRequestUpgrade (controller, statusCode, headers, socket) { + return handler.onRequestUpgrade(controller, statusCode, headers, socket) + }, + + onResponseStart (controller, statusCode, headers, statusMessage) { + responseData.statusCode = statusCode + responseData.headers = headers + return handler.onResponseStart(controller, statusCode, headers, statusMessage) + }, + + onResponseData (controller, chunk) { + responseData.body.push(chunk) + return handler.onResponseData(controller, chunk) + }, + + onResponseEnd (controller, trailers) { + responseData.trailers = trailers + + // Record the interaction using captured 'self' context (fire and forget) + const responseBody = Buffer.concat(responseData.body) + self[kSnapshotRecorder].record(opts, { + statusCode: responseData.statusCode, + headers: responseData.headers, + body: responseBody, + trailers: responseData.trailers + }) + .then(() => handler.onResponseEnd(controller, trailers)) + .catch((error) => handler.onResponseError(controller, error)) + } + } + + // Use composed agent if available (includes interceptors), otherwise use real agent + const agent = this[kRealAgent] + return agent.dispatch(opts, recordingHandler) + } + + /** + * Replays a recorded response + * + * @param {Object} snapshot - The recorded snapshot to replay. + * @param {Object} handler - The handler to call with the response data. + * @returns {void} + */ + #replaySnapshot (snapshot, handler) { + try { + const { response } = snapshot + + const controller = { + pause () { }, + resume () { }, + abort (reason) { + this.aborted = true + this.reason = reason + }, + + aborted: false, + paused: false + } + + handler.onRequestStart(controller) + + handler.onResponseStart(controller, response.statusCode, response.headers) + + // Body is always stored as base64 string + const body = Buffer.from(response.body, 'base64') + handler.onResponseData(controller, body) + + handler.onResponseEnd(controller, response.trailers) + } catch (error) { + handler.onError?.(error) + } + } + + /** + * Loads snapshots from file + * + * @param {string} [filePath] - Optional file path to load snapshots from. + * @returns {Promise} - Resolves when snapshots are loaded. + */ + async loadSnapshots (filePath) { + await this[kSnapshotRecorder].loadSnapshots(filePath || this[kSnapshotPath]) + this[kSnapshotLoaded] = true + + // In playback mode, set up MockAgent interceptors for all snapshots + if (this[kSnapshotMode] === 'playback') { + this.#setupMockInterceptors() + } + } + + /** + * Saves snapshots to file + * + * @param {string} [filePath] - Optional file path to save snapshots to. + * @returns {Promise} - Resolves when snapshots are saved. + */ + async saveSnapshots (filePath) { + return this[kSnapshotRecorder].saveSnapshots(filePath || this[kSnapshotPath]) + } + + /** + * Sets up MockAgent interceptors based on recorded snapshots. + * + * This method creates MockAgent interceptors for each recorded snapshot, + * allowing the SnapshotAgent to fall back to MockAgent's standard intercept + * mechanism in playback mode. Each interceptor is configured to persist + * (remain active for multiple requests) and responds with the recorded + * response data. + * + * Called automatically when loading snapshots in playback mode. + * + * @returns {void} + */ + #setupMockInterceptors () { + for (const snapshot of this[kSnapshotRecorder].getSnapshots()) { + const { request, responses, response } = snapshot + const url = new URL(request.url) + + const mockPool = this.get(url.origin) + + // Handle both new format (responses array) and legacy format (response object) + const responseData = responses ? responses[0] : response + if (!responseData) continue + + mockPool.intercept({ + path: url.pathname + url.search, + method: request.method, + headers: request.headers, + body: request.body + }).reply(responseData.statusCode, responseData.body, { + headers: responseData.headers, + trailers: responseData.trailers + }).persist() + } + } + + /** + * Gets the snapshot recorder + * @return {SnapshotRecorder} - The snapshot recorder instance + */ + getRecorder () { + return this[kSnapshotRecorder] + } + + /** + * Gets the current mode + * @return {import('./snapshot-utils').SnapshotMode} - The current snapshot mode + */ + getMode () { + return this[kSnapshotMode] + } + + /** + * Clears all snapshots + * @returns {void} + */ + clearSnapshots () { + this[kSnapshotRecorder].clear() + } + + /** + * Resets call counts for all snapshots (useful for test cleanup) + * @returns {void} + */ + resetCallCounts () { + this[kSnapshotRecorder].resetCallCounts() + } + + /** + * Deletes a specific snapshot by request options + * @param {import('./snapshot-recorder').SnapshotRequestOptions} requestOpts - Request options to identify the snapshot + * @return {Promise} - Returns true if the snapshot was deleted, false if not found + */ + deleteSnapshot (requestOpts) { + return this[kSnapshotRecorder].deleteSnapshot(requestOpts) + } + + /** + * Gets information about a specific snapshot + * @returns {import('./snapshot-recorder').SnapshotInfo|null} - Snapshot information or null if not found + */ + getSnapshotInfo (requestOpts) { + return this[kSnapshotRecorder].getSnapshotInfo(requestOpts) + } + + /** + * Replaces all snapshots with new data (full replacement) + * @param {Array<{hash: string; snapshot: import('./snapshot-recorder').SnapshotEntryshotEntry}>|Record} snapshotData - New snapshot data to replace existing snapshots + * @returns {void} + */ + replaceSnapshots (snapshotData) { + this[kSnapshotRecorder].replaceSnapshots(snapshotData) + } + + /** + * Closes the agent, saving snapshots and cleaning up resources. + * + * @returns {Promise} + */ + async close () { + await this[kSnapshotRecorder].close() + await this[kRealAgent]?.close() + await super.close() + } +} + +module.exports = SnapshotAgent diff --git a/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/mock/snapshot-recorder.js b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/mock/snapshot-recorder.js new file mode 100644 index 000000000..b5d07fae3 --- /dev/null +++ b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/mock/snapshot-recorder.js @@ -0,0 +1,588 @@ +'use strict' + +const { writeFile, readFile, mkdir } = require('node:fs/promises') +const { dirname, resolve } = require('node:path') +const { setTimeout, clearTimeout } = require('node:timers') +const { InvalidArgumentError, UndiciError } = require('../core/errors') +const { hashId, isUrlExcludedFactory, normalizeHeaders, createHeaderFilters } = require('./snapshot-utils') + +/** + * @typedef {Object} SnapshotRequestOptions + * @property {string} method - HTTP method (e.g. 'GET', 'POST', etc.) + * @property {string} path - Request path + * @property {string} origin - Request origin (base URL) + * @property {import('./snapshot-utils').Headers|import('./snapshot-utils').UndiciHeaders} headers - Request headers + * @property {import('./snapshot-utils').NormalizedHeaders} _normalizedHeaders - Request headers as a lowercase object + * @property {string|Buffer} [body] - Request body (optional) + */ + +/** + * @typedef {Object} SnapshotEntryRequest + * @property {string} method - HTTP method (e.g. 'GET', 'POST', etc.) + * @property {string} url - Full URL of the request + * @property {import('./snapshot-utils').NormalizedHeaders} headers - Normalized headers as a lowercase object + * @property {string|Buffer} [body] - Request body (optional) + */ + +/** + * @typedef {Object} SnapshotEntryResponse + * @property {number} statusCode - HTTP status code of the response + * @property {import('./snapshot-utils').NormalizedHeaders} headers - Normalized response headers as a lowercase object + * @property {string} body - Response body as a base64url encoded string + * @property {Object} [trailers] - Optional response trailers + */ + +/** + * @typedef {Object} SnapshotEntry + * @property {SnapshotEntryRequest} request - The request object + * @property {Array} responses - Array of response objects + * @property {number} callCount - Number of times this snapshot has been called + * @property {string} timestamp - ISO timestamp of when the snapshot was created + */ + +/** + * @typedef {Object} SnapshotRecorderMatchOptions + * @property {Array} [matchHeaders=[]] - Headers to match (empty array means match all headers) + * @property {Array} [ignoreHeaders=[]] - Headers to ignore for matching + * @property {Array} [excludeHeaders=[]] - Headers to exclude from matching + * @property {boolean} [matchBody=true] - Whether to match request body + * @property {boolean} [matchQuery=true] - Whether to match query properties + * @property {boolean} [caseSensitive=false] - Whether header matching is case-sensitive + */ + +/** + * @typedef {Object} SnapshotRecorderOptions + * @property {string} [snapshotPath] - Path to save/load snapshots + * @property {import('./snapshot-utils').SnapshotMode} [mode='record'] - Mode: 'record' or 'playback' + * @property {number} [maxSnapshots=Infinity] - Maximum number of snapshots to keep + * @property {boolean} [autoFlush=false] - Whether to automatically flush snapshots to disk + * @property {number} [flushInterval=30000] - Auto-flush interval in milliseconds (default: 30 seconds) + * @property {Array} [excludeUrls=[]] - URLs to exclude from recording + * @property {function} [shouldRecord=null] - Function to filter requests for recording + * @property {function} [shouldPlayback=null] - Function to filter requests + */ + +/** + * @typedef {Object} SnapshotFormattedRequest + * @property {string} method - HTTP method (e.g. 'GET', 'POST', etc.) + * @property {string} url - Full URL of the request (with query parameters if matchQuery is true) + * @property {import('./snapshot-utils').NormalizedHeaders} headers - Normalized headers as a lowercase object + * @property {string} body - Request body (optional, only if matchBody is true) + */ + +/** + * @typedef {Object} SnapshotInfo + * @property {string} hash - Hash key for the snapshot + * @property {SnapshotEntryRequest} request - The request object + * @property {number} responseCount - Number of responses recorded for this request + * @property {number} callCount - Number of times this snapshot has been called + * @property {string} timestamp - ISO timestamp of when the snapshot was created + */ + +/** + * Formats a request for consistent snapshot storage + * Caches normalized headers to avoid repeated processing + * + * @param {SnapshotRequestOptions} opts - Request options + * @param {import('./snapshot-utils').HeaderFilters} headerFilters - Cached header sets for performance + * @param {SnapshotRecorderMatchOptions} [matchOptions] - Matching options for headers and body + * @returns {SnapshotFormattedRequest} - Formatted request object + */ +function formatRequestKey (opts, headerFilters, matchOptions = {}) { + const url = new URL(opts.path, opts.origin) + + // Cache normalized headers if not already done + const normalized = opts._normalizedHeaders || normalizeHeaders(opts.headers) + if (!opts._normalizedHeaders) { + opts._normalizedHeaders = normalized + } + + return { + method: opts.method || 'GET', + url: matchOptions.matchQuery !== false ? url.toString() : `${url.origin}${url.pathname}`, + headers: filterHeadersForMatching(normalized, headerFilters, matchOptions), + body: matchOptions.matchBody !== false && opts.body ? String(opts.body) : '' + } +} + +/** + * Filters headers based on matching configuration + * + * @param {import('./snapshot-utils').Headers} headers - Headers to filter + * @param {import('./snapshot-utils').HeaderFilters} headerFilters - Cached sets for ignore, exclude, and match headers + * @param {SnapshotRecorderMatchOptions} [matchOptions] - Matching options for headers + */ +function filterHeadersForMatching (headers, headerFilters, matchOptions = {}) { + if (!headers || typeof headers !== 'object') return {} + + const { + caseSensitive = false + } = matchOptions + + const filtered = {} + const { ignore, exclude, match } = headerFilters + + for (const [key, value] of Object.entries(headers)) { + const headerKey = caseSensitive ? key : key.toLowerCase() + + // Skip if in exclude list (for security) + if (exclude.has(headerKey)) continue + + // Skip if in ignore list (for matching) + if (ignore.has(headerKey)) continue + + // If matchHeaders is specified, only include those headers + if (match.size !== 0) { + if (!match.has(headerKey)) continue + } + + filtered[headerKey] = value + } + + return filtered +} + +/** + * Filters headers for storage (only excludes sensitive headers) + * + * @param {import('./snapshot-utils').Headers} headers - Headers to filter + * @param {import('./snapshot-utils').HeaderFilters} headerFilters - Cached sets for ignore, exclude, and match headers + * @param {SnapshotRecorderMatchOptions} [matchOptions] - Matching options for headers + */ +function filterHeadersForStorage (headers, headerFilters, matchOptions = {}) { + if (!headers || typeof headers !== 'object') return {} + + const { + caseSensitive = false + } = matchOptions + + const filtered = {} + const { exclude: excludeSet } = headerFilters + + for (const [key, value] of Object.entries(headers)) { + const headerKey = caseSensitive ? key : key.toLowerCase() + + // Skip if in exclude list (for security) + if (excludeSet.has(headerKey)) continue + + filtered[headerKey] = value + } + + return filtered +} + +/** + * Creates a hash key for request matching + * Properly orders headers to avoid conflicts and uses crypto hashing when available + * + * @param {SnapshotFormattedRequest} formattedRequest - Request object + * @returns {string} - Base64url encoded hash of the request + */ +function createRequestHash (formattedRequest) { + const parts = [ + formattedRequest.method, + formattedRequest.url + ] + + // Process headers in a deterministic way to avoid conflicts + if (formattedRequest.headers && typeof formattedRequest.headers === 'object') { + const headerKeys = Object.keys(formattedRequest.headers).sort() + for (const key of headerKeys) { + const values = Array.isArray(formattedRequest.headers[key]) + ? formattedRequest.headers[key] + : [formattedRequest.headers[key]] + + // Add header name + parts.push(key) + + // Add all values for this header, sorted for consistency + for (const value of values.sort()) { + parts.push(String(value)) + } + } + } + + // Add body + parts.push(formattedRequest.body) + + const content = parts.join('|') + + return hashId(content) +} + +class SnapshotRecorder { + /** @type {NodeJS.Timeout | null} */ + #flushTimeout + + /** @type {import('./snapshot-utils').IsUrlExcluded} */ + #isUrlExcluded + + /** @type {Map} */ + #snapshots = new Map() + + /** @type {string|undefined} */ + #snapshotPath + + /** @type {number} */ + #maxSnapshots = Infinity + + /** @type {boolean} */ + #autoFlush = false + + /** @type {import('./snapshot-utils').HeaderFilters} */ + #headerFilters + + /** + * Creates a new SnapshotRecorder instance + * @param {SnapshotRecorderOptions&SnapshotRecorderMatchOptions} [options={}] - Configuration options for the recorder + */ + constructor (options = {}) { + this.#snapshotPath = options.snapshotPath + this.#maxSnapshots = options.maxSnapshots || Infinity + this.#autoFlush = options.autoFlush || false + this.flushInterval = options.flushInterval || 30000 // 30 seconds default + this._flushTimer = null + + // Matching configuration + /** @type {Required} */ + this.matchOptions = { + matchHeaders: options.matchHeaders || [], // empty means match all headers + ignoreHeaders: options.ignoreHeaders || [], + excludeHeaders: options.excludeHeaders || [], + matchBody: options.matchBody !== false, // default: true + matchQuery: options.matchQuery !== false, // default: true + caseSensitive: options.caseSensitive || false + } + + // Cache processed header sets to avoid recreating them on every request + this.#headerFilters = createHeaderFilters(this.matchOptions) + + // Request filtering callbacks + this.shouldRecord = options.shouldRecord || (() => true) // function(requestOpts) -> boolean + this.shouldPlayback = options.shouldPlayback || (() => true) // function(requestOpts) -> boolean + + // URL pattern filtering + this.#isUrlExcluded = isUrlExcludedFactory(options.excludeUrls) // Array of regex patterns or strings + + // Start auto-flush timer if enabled + if (this.#autoFlush && this.#snapshotPath) { + this.#startAutoFlush() + } + } + + /** + * Records a request-response interaction + * @param {SnapshotRequestOptions} requestOpts - Request options + * @param {SnapshotEntryResponse} response - Response data to record + * @return {Promise} - Resolves when the recording is complete + */ + async record (requestOpts, response) { + // Check if recording should be filtered out + if (!this.shouldRecord(requestOpts)) { + return // Skip recording + } + + // Check URL exclusion patterns + if (this.isUrlExcluded(requestOpts)) { + return // Skip recording + } + + const request = formatRequestKey(requestOpts, this.#headerFilters, this.matchOptions) + const hash = createRequestHash(request) + + // Extract response data - always store body as base64 + const normalizedHeaders = normalizeHeaders(response.headers) + + /** @type {SnapshotEntryResponse} */ + const responseData = { + statusCode: response.statusCode, + headers: filterHeadersForStorage(normalizedHeaders, this.#headerFilters, this.matchOptions), + body: Buffer.isBuffer(response.body) + ? response.body.toString('base64') + : Buffer.from(String(response.body || '')).toString('base64'), + trailers: response.trailers + } + + // Remove oldest snapshot if we exceed maxSnapshots limit + if (this.#snapshots.size >= this.#maxSnapshots && !this.#snapshots.has(hash)) { + const oldestKey = this.#snapshots.keys().next().value + this.#snapshots.delete(oldestKey) + } + + // Support sequential responses - if snapshot exists, add to responses array + const existingSnapshot = this.#snapshots.get(hash) + if (existingSnapshot && existingSnapshot.responses) { + existingSnapshot.responses.push(responseData) + existingSnapshot.timestamp = new Date().toISOString() + } else { + this.#snapshots.set(hash, { + request, + responses: [responseData], // Always store as array for consistency + callCount: 0, + timestamp: new Date().toISOString() + }) + } + + // Auto-flush if enabled + if (this.#autoFlush && this.#snapshotPath) { + this.#scheduleFlush() + } + } + + /** + * Checks if a URL should be excluded from recording/playback + * @param {SnapshotRequestOptions} requestOpts - Request options to check + * @returns {boolean} - True if URL is excluded + */ + isUrlExcluded (requestOpts) { + const url = new URL(requestOpts.path, requestOpts.origin).toString() + return this.#isUrlExcluded(url) + } + + /** + * Finds a matching snapshot for the given request + * Returns the appropriate response based on call count for sequential responses + * + * @param {SnapshotRequestOptions} requestOpts - Request options to match + * @returns {SnapshotEntry&Record<'response', SnapshotEntryResponse>|undefined} - Matching snapshot response or undefined if not found + */ + findSnapshot (requestOpts) { + // Check if playback should be filtered out + if (!this.shouldPlayback(requestOpts)) { + return undefined // Skip playback + } + + // Check URL exclusion patterns + if (this.isUrlExcluded(requestOpts)) { + return undefined // Skip playback + } + + const request = formatRequestKey(requestOpts, this.#headerFilters, this.matchOptions) + const hash = createRequestHash(request) + const snapshot = this.#snapshots.get(hash) + + if (!snapshot) return undefined + + // Handle sequential responses + const currentCallCount = snapshot.callCount || 0 + const responseIndex = Math.min(currentCallCount, snapshot.responses.length - 1) + snapshot.callCount = currentCallCount + 1 + + return { + ...snapshot, + response: snapshot.responses[responseIndex] + } + } + + /** + * Loads snapshots from file + * @param {string} [filePath] - Optional file path to load snapshots from + * @return {Promise} - Resolves when snapshots are loaded + */ + async loadSnapshots (filePath) { + const path = filePath || this.#snapshotPath + if (!path) { + throw new InvalidArgumentError('Snapshot path is required') + } + + try { + const data = await readFile(resolve(path), 'utf8') + const parsed = JSON.parse(data) + + // Convert array format back to Map + if (Array.isArray(parsed)) { + this.#snapshots.clear() + for (const { hash, snapshot } of parsed) { + this.#snapshots.set(hash, snapshot) + } + } else { + // Legacy object format + this.#snapshots = new Map(Object.entries(parsed)) + } + } catch (error) { + if (error.code === 'ENOENT') { + // File doesn't exist yet - that's ok for recording mode + this.#snapshots.clear() + } else { + throw new UndiciError(`Failed to load snapshots from ${path}`, { cause: error }) + } + } + } + + /** + * Saves snapshots to file + * + * @param {string} [filePath] - Optional file path to save snapshots + * @returns {Promise} - Resolves when snapshots are saved + */ + async saveSnapshots (filePath) { + const path = filePath || this.#snapshotPath + if (!path) { + throw new InvalidArgumentError('Snapshot path is required') + } + + const resolvedPath = resolve(path) + + // Ensure directory exists + await mkdir(dirname(resolvedPath), { recursive: true }) + + // Convert Map to serializable format + const data = Array.from(this.#snapshots.entries()).map(([hash, snapshot]) => ({ + hash, + snapshot + })) + + await writeFile(resolvedPath, JSON.stringify(data, null, 2), { flush: true }) + } + + /** + * Clears all recorded snapshots + * @returns {void} + */ + clear () { + this.#snapshots.clear() + } + + /** + * Gets all recorded snapshots + * @return {Array} - Array of all recorded snapshots + */ + getSnapshots () { + return Array.from(this.#snapshots.values()) + } + + /** + * Gets snapshot count + * @return {number} - Number of recorded snapshots + */ + size () { + return this.#snapshots.size + } + + /** + * Resets call counts for all snapshots (useful for test cleanup) + * @returns {void} + */ + resetCallCounts () { + for (const snapshot of this.#snapshots.values()) { + snapshot.callCount = 0 + } + } + + /** + * Deletes a specific snapshot by request options + * @param {SnapshotRequestOptions} requestOpts - Request options to match + * @returns {boolean} - True if snapshot was deleted, false if not found + */ + deleteSnapshot (requestOpts) { + const request = formatRequestKey(requestOpts, this.#headerFilters, this.matchOptions) + const hash = createRequestHash(request) + return this.#snapshots.delete(hash) + } + + /** + * Gets information about a specific snapshot + * @param {SnapshotRequestOptions} requestOpts - Request options to match + * @returns {SnapshotInfo|null} - Snapshot information or null if not found + */ + getSnapshotInfo (requestOpts) { + const request = formatRequestKey(requestOpts, this.#headerFilters, this.matchOptions) + const hash = createRequestHash(request) + const snapshot = this.#snapshots.get(hash) + + if (!snapshot) return null + + return { + hash, + request: snapshot.request, + responseCount: snapshot.responses ? snapshot.responses.length : (snapshot.response ? 1 : 0), // .response for legacy snapshots + callCount: snapshot.callCount || 0, + timestamp: snapshot.timestamp + } + } + + /** + * Replaces all snapshots with new data (full replacement) + * @param {Array<{hash: string; snapshot: SnapshotEntry}>|Record} snapshotData - New snapshot data to replace existing ones + * @returns {void} + */ + replaceSnapshots (snapshotData) { + this.#snapshots.clear() + + if (Array.isArray(snapshotData)) { + for (const { hash, snapshot } of snapshotData) { + this.#snapshots.set(hash, snapshot) + } + } else if (snapshotData && typeof snapshotData === 'object') { + // Legacy object format + this.#snapshots = new Map(Object.entries(snapshotData)) + } + } + + /** + * Starts the auto-flush timer + * @returns {void} + */ + #startAutoFlush () { + return this.#scheduleFlush() + } + + /** + * Stops the auto-flush timer + * @returns {void} + */ + #stopAutoFlush () { + if (this.#flushTimeout) { + clearTimeout(this.#flushTimeout) + // Ensure any pending flush is completed + this.saveSnapshots().catch(() => { + // Ignore flush errors + }) + this.#flushTimeout = null + } + } + + /** + * Schedules a flush (debounced to avoid excessive writes) + */ + #scheduleFlush () { + this.#flushTimeout = setTimeout(() => { + this.saveSnapshots().catch(() => { + // Ignore flush errors + }) + if (this.#autoFlush) { + this.#flushTimeout?.refresh() + } else { + this.#flushTimeout = null + } + }, 1000) // 1 second debounce + } + + /** + * Cleanup method to stop timers + * @returns {void} + */ + destroy () { + this.#stopAutoFlush() + if (this.#flushTimeout) { + clearTimeout(this.#flushTimeout) + this.#flushTimeout = null + } + } + + /** + * Async close method that saves all recordings and performs cleanup + * @returns {Promise} + */ + async close () { + // Save any pending recordings if we have a snapshot path + if (this.#snapshotPath && this.#snapshots.size !== 0) { + await this.saveSnapshots() + } + + // Perform cleanup + this.destroy() + } +} + +module.exports = { SnapshotRecorder, formatRequestKey, createRequestHash, filterHeadersForMatching, filterHeadersForStorage, createHeaderFilters } diff --git a/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/mock/snapshot-utils.js b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/mock/snapshot-utils.js new file mode 100644 index 000000000..a14b69c15 --- /dev/null +++ b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/mock/snapshot-utils.js @@ -0,0 +1,158 @@ +'use strict' + +const { InvalidArgumentError } = require('../core/errors') +const { runtimeFeatures } = require('../util/runtime-features.js') + +/** + * @typedef {Object} HeaderFilters + * @property {Set} ignore - Set of headers to ignore for matching + * @property {Set} exclude - Set of headers to exclude from matching + * @property {Set} match - Set of headers to match (empty means match + */ + +/** + * Creates cached header sets for performance + * + * @param {import('./snapshot-recorder').SnapshotRecorderMatchOptions} matchOptions - Matching options for headers + * @returns {HeaderFilters} - Cached sets for ignore, exclude, and match headers + */ +function createHeaderFilters (matchOptions = {}) { + const { ignoreHeaders = [], excludeHeaders = [], matchHeaders = [], caseSensitive = false } = matchOptions + + return { + ignore: new Set(ignoreHeaders.map(header => caseSensitive ? header : header.toLowerCase())), + exclude: new Set(excludeHeaders.map(header => caseSensitive ? header : header.toLowerCase())), + match: new Set(matchHeaders.map(header => caseSensitive ? header : header.toLowerCase())) + } +} + +const crypto = runtimeFeatures.has('crypto') + ? require('node:crypto') + : null + +/** + * @callback HashIdFunction + * @param {string} value - The value to hash + * @returns {string} - The base64url encoded hash of the value + */ + +/** + * Generates a hash for a given value + * @type {HashIdFunction} + */ +const hashId = crypto?.hash + ? (value) => crypto.hash('sha256', value, 'base64url') + : (value) => Buffer.from(value).toString('base64url') + +/** + * @typedef {(url: string) => boolean} IsUrlExcluded Checks if a URL matches any of the exclude patterns + */ + +/** @typedef {{[key: Lowercase]: string}} NormalizedHeaders */ +/** @typedef {Array} UndiciHeaders */ +/** @typedef {Record} Headers */ + +/** + * @param {*} headers + * @returns {headers is UndiciHeaders} + */ +function isUndiciHeaders (headers) { + return Array.isArray(headers) && (headers.length & 1) === 0 +} + +/** + * Factory function to create a URL exclusion checker + * @param {Array} [excludePatterns=[]] - Array of patterns to exclude + * @returns {IsUrlExcluded} - A function that checks if a URL matches any of the exclude patterns + */ +function isUrlExcludedFactory (excludePatterns = []) { + if (excludePatterns.length === 0) { + return () => false + } + + return function isUrlExcluded (url) { + let urlLowerCased + + for (const pattern of excludePatterns) { + if (typeof pattern === 'string') { + if (!urlLowerCased) { + // Convert URL to lowercase only once + urlLowerCased = url.toLowerCase() + } + // Simple string match (case-insensitive) + if (urlLowerCased.includes(pattern.toLowerCase())) { + return true + } + } else if (pattern instanceof RegExp) { + // Regex pattern match + if (pattern.test(url)) { + return true + } + } + } + + return false + } +} + +/** + * Normalizes headers for consistent comparison + * + * @param {Object|UndiciHeaders} headers - Headers to normalize + * @returns {NormalizedHeaders} - Normalized headers as a lowercase object + */ +function normalizeHeaders (headers) { + /** @type {NormalizedHeaders} */ + const normalizedHeaders = {} + + if (!headers) return normalizedHeaders + + // Handle array format (undici internal format: [name, value, name, value, ...]) + if (isUndiciHeaders(headers)) { + for (let i = 0; i < headers.length; i += 2) { + const key = headers[i] + const value = headers[i + 1] + if (key && value !== undefined) { + // Convert Buffers to strings if needed + const keyStr = Buffer.isBuffer(key) ? key.toString() : key + const valueStr = Buffer.isBuffer(value) ? value.toString() : value + normalizedHeaders[keyStr.toLowerCase()] = valueStr + } + } + return normalizedHeaders + } + + // Handle object format + if (headers && typeof headers === 'object') { + for (const [key, value] of Object.entries(headers)) { + if (key && typeof key === 'string') { + normalizedHeaders[key.toLowerCase()] = Array.isArray(value) ? value.join(', ') : String(value) + } + } + } + + return normalizedHeaders +} + +const validSnapshotModes = /** @type {const} */ (['record', 'playback', 'update']) + +/** @typedef {typeof validSnapshotModes[number]} SnapshotMode */ + +/** + * @param {*} mode - The snapshot mode to validate + * @returns {asserts mode is SnapshotMode} + */ +function validateSnapshotMode (mode) { + if (!validSnapshotModes.includes(mode)) { + throw new InvalidArgumentError(`Invalid snapshot mode: ${mode}. Must be one of: ${validSnapshotModes.join(', ')}`) + } +} + +module.exports = { + createHeaderFilters, + hashId, + isUndiciHeaders, + normalizeHeaders, + isUrlExcludedFactory, + validateSnapshotMode +} diff --git a/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/util/cache.js b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/util/cache.js new file mode 100644 index 000000000..da8ae1228 --- /dev/null +++ b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/util/cache.js @@ -0,0 +1,676 @@ +'use strict' + +const { + safeHTTPMethods, + pathHasQueryOrFragment, + hasSafeIterator, + isValidHTTPToken +} = require('../core/util') + +const { serializePathWithQuery } = require('../core/util') + +const MAX_DELTA_SECONDS = 2147483647 +const RESTRICTIVE_DIRECTIVE_NAMES = ['no-store', 'private', 'no-cache'] +const kInvalidCacheControlDirectives = Symbol('invalid cache-control directives') + +function trimOWS (value) { + return value.replace(/^[\t ]+|[\t ]+$/g, '') +} + +function arrayIncludes (array, value) { + for (let i = 0; i < array.length; i++) { + if (array[i] === value) { + return true + } + } + + return false +} + +function trimOWSStart (value) { + return value.replace(/^[\t ]+/, '') +} + +function trimOWSEnd (value) { + return value.replace(/[\t ]+$/, '') +} + +function findUnescapedQuote (value, start) { + let escaped = false + for (let i = start; i < value.length; i++) { + if (escaped) { + escaped = false + } else if (value[i] === '\\') { + escaped = true + } else if (value[i] === '"') { + return i + } + } + + return -1 +} + +function splitCacheControlHeaderValue (value) { + const directives = [] + let start = 0 + let quoteStart = -1 + let inQuote = false + let escaped = false + + for (let i = 0; i < value.length; i++) { + if (inQuote) { + if (escaped) { + escaped = false + } else if (value[i] === '\\') { + escaped = true + } else if (value[i] === '"') { + inQuote = false + quoteStart = -1 + } + } else if (value[i] === '"') { + inQuote = true + quoteStart = i + } else if (value[i] === ',') { + directives.push({ value: value.substring(start, i), fromMalformedQuote: false }) + start = i + 1 + } + } + + if (!inQuote) { + directives.push({ value: value.substring(start), fromMalformedQuote: false }) + return directives + } + + const tail = value.substring(start) + const quoteOffset = quoteStart - start + let tailStart = 0 + for (let i = 0; i < tail.length; i++) { + if (tail[i] === ',') { + directives.push({ + value: tail.substring(tailStart, i), + fromMalformedQuote: tailStart > quoteOffset + }) + tailStart = i + 1 + } + } + + directives.push({ + value: tail.substring(tailStart), + fromMalformedQuote: tailStart > quoteOffset + }) + return directives +} + +function markInvalidCacheControlDirective (directives, key) { + let invalidDirectives = directives[kInvalidCacheControlDirectives] + + if (invalidDirectives === undefined) { + invalidDirectives = new Set() + Object.defineProperty(directives, kInvalidCacheControlDirectives, { + value: invalidDirectives + }) + } + + invalidDirectives.add(key) +} + +function hasInvalidCacheControlDirective (directives, key) { + return directives[kInvalidCacheControlDirectives]?.has(key) === true +} + +function getMalformedRestrictiveDirectiveName (key) { + for (const directiveName of RESTRICTIVE_DIRECTIVE_NAMES) { + if ( + key.startsWith(directiveName) && + key.length > directiveName.length && + !isValidHTTPToken(key[directiveName.length]) + ) { + return directiveName + } + } + + let tokenOnlyKey = '' + let hasInvalidTokenChar = false + for (let i = 0; i < key.length; i++) { + if (isValidHTTPToken(key[i])) { + tokenOnlyKey += key[i] + } else { + hasInvalidTokenChar = true + } + } + + if (hasInvalidTokenChar && arrayIncludes(RESTRICTIVE_DIRECTIVE_NAMES, tokenOnlyKey)) { + return tokenOnlyKey + } +} + +/** + * @param {import('../../types/dispatcher.d.ts').default.DispatchOptions} opts + */ +function makeCacheKey (opts) { + if (!opts.origin) { + throw new Error('opts.origin is undefined') + } + + let fullPath = opts.path || '/' + + if (opts.query && !pathHasQueryOrFragment(fullPath)) { + fullPath = serializePathWithQuery(fullPath, opts.query) + } + + return { + origin: opts.origin.toString(), + method: opts.method, + path: fullPath, + headers: opts.headers + } +} + +function appendHeader (headers, key, val) { + const headerName = key.toLowerCase() + const current = headers[headerName] + const values = Array.isArray(val) ? val : [val] + + if (current === undefined) { + headers[headerName] = Array.isArray(val) ? val.slice() : val + } else if (Array.isArray(current)) { + current.push(...values) + } else { + headers[headerName] = [current, ...values] + } +} + +/** + * @param {Record} + * @returns {Record} + */ +function normalizeHeaders (opts) { + let headers + if (opts.headers == null) { + headers = {} + } else if (typeof opts.headers === 'object') { + headers = {} + + if (hasSafeIterator(opts.headers)) { + for (const x of opts.headers) { + if (!Array.isArray(x)) { + throw new Error('opts.headers is not a valid header map') + } + const [key, val] = x + if (typeof key !== 'string' || typeof val !== 'string') { + throw new Error('opts.headers is not a valid header map') + } + appendHeader(headers, key, val) + } + } else { + for (const key of Object.keys(opts.headers)) { + appendHeader(headers, key, opts.headers[key]) + } + } + } else { + throw new Error('opts.headers is not an object') + } + + return headers +} + +/** + * @param {any} key + */ +function assertCacheKey (key) { + if (typeof key !== 'object') { + throw new TypeError(`expected key to be object, got ${typeof key}`) + } + + for (const property of ['origin', 'method', 'path']) { + if (typeof key[property] !== 'string') { + throw new TypeError(`expected key.${property} to be string, got ${typeof key[property]}`) + } + } + + if (key.headers !== undefined && typeof key.headers !== 'object') { + throw new TypeError(`expected headers to be object, got ${typeof key}`) + } +} + +/** + * @param {any} value + */ +function assertCacheValue (value) { + if (typeof value !== 'object') { + throw new TypeError(`expected value to be object, got ${typeof value}`) + } + + for (const property of ['statusCode', 'cachedAt', 'staleAt', 'deleteAt']) { + if (typeof value[property] !== 'number') { + throw new TypeError(`expected value.${property} to be number, got ${typeof value[property]}`) + } + } + + if (typeof value.statusMessage !== 'string') { + throw new TypeError(`expected value.statusMessage to be string, got ${typeof value.statusMessage}`) + } + + if (value.headers != null && typeof value.headers !== 'object') { + throw new TypeError(`expected value.rawHeaders to be object, got ${typeof value.headers}`) + } + + if (value.vary !== undefined && typeof value.vary !== 'object') { + throw new TypeError(`expected value.vary to be object, got ${typeof value.vary}`) + } + + if (value.etag !== undefined && typeof value.etag !== 'string') { + throw new TypeError(`expected value.etag to be string, got ${typeof value.etag}`) + } +} + +/** + * @see https://www.rfc-editor.org/rfc/rfc9111.html#name-cache-control + * @see https://www.iana.org/assignments/http-cache-directives/http-cache-directives.xhtml + + * @param {string | string[]} header + * @returns {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives} + */ +function parseCacheControlHeader (header) { + /** + * @type {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives} + */ + const output = {} + const invalidNumericDirectives = new Set() + const invalidNoArgumentDirectives = new Set() + + const directives = splitCacheControlHeaderValue(Array.isArray(header) ? header.join(',') : header) + + for (let i = 0; i < directives.length; i++) { + const directiveRecord = directives[i] + const directive = directiveRecord.value.toLowerCase() + const fromMalformedQuote = directiveRecord.fromMalformedQuote + const keyValueDelimiter = directive.indexOf('=') + + let key + let value + let keyHasTrailingWhitespace = false + let valueHasLeadingWhitespace = false + if (keyValueDelimiter !== -1) { + const rawKey = directive.substring(0, keyValueDelimiter) + const rawValue = directive.substring(keyValueDelimiter + 1) + + keyHasTrailingWhitespace = trimOWSEnd(rawKey) !== rawKey + valueHasLeadingWhitespace = trimOWSStart(rawValue) !== rawValue + key = trimOWS(rawKey) + value = trimOWSStart(rawValue) + } else { + key = trimOWS(directive) + } + + const malformedRestrictiveDirectiveName = getMalformedRestrictiveDirectiveName(key) + if (malformedRestrictiveDirectiveName !== undefined) { + output[malformedRestrictiveDirectiveName] = true + continue + } + + switch (key) { + case 'min-fresh': + case 'max-stale': + case 'max-age': + case 's-maxage': + case 'stale-while-revalidate': + case 'stale-if-error': { + if (fromMalformedQuote || invalidNumericDirectives.has(key)) { + continue + } + + if (value === undefined || keyHasTrailingWhitespace || valueHasLeadingWhitespace) { + delete output[key] + invalidNumericDirectives.add(key) + markInvalidCacheControlDirective(output, key) + continue + } + + if ( + value.length >= 2 && + value[0] === '"' && + value[value.length - 1] === '"' + ) { + value = value.substring(1, value.length - 1) + } + + if (!/^[0-9]+$/.test(value)) { + delete output[key] + invalidNumericDirectives.add(key) + markInvalidCacheControlDirective(output, key) + continue + } + + const parsedValue = Math.min(parseInt(value, 10), MAX_DELTA_SECONDS) + + if (key === 'min-fresh') { + if (!(key in output) || output[key] < parsedValue) { + output[key] = parsedValue + } + } else if (!(key in output) || output[key] > parsedValue) { + output[key] = parsedValue + } + + break + } + case 'private': + case 'no-cache': { + if (fromMalformedQuote) { + output[key] = true + break + } + + if (value !== undefined && value.length === 0) { + output[key] = true + break + } + + if (value) { + // The private and no-cache directives can be unqualified (aka just + // `private` or `no-cache`) or qualified (w/ a value). When they're + // qualified, it's a list of headers like `no-cache=header1`, + // `no-cache="header1"`, or `no-cache="header1, header2"` + // If we're given multiple headers, the comma messes us up since + // we split the full header by commas. So, let's loop through the + // remaining parts in front of us until we find one that contains a + // closing quote. We can then skip the consumed quoted-list fragments and + // continue parsing like normal. + // https://www.rfc-editor.org/rfc/rfc9111.html#name-no-cache-2 + if (value[0] === '"') { + // Something like `no-cache="some-header"` OR `no-cache="some-header, another-header"`. + value = trimOWSEnd(value) + + let fieldList = '' + let lastQuotedPart = i + let foundEndingQuote = false + const closingQuote = findUnescapedQuote(value, 1) + + if (closingQuote !== -1) { + fieldList = value.substring(1, closingQuote) + foundEndingQuote = true + } else { + // Something like `no-cache="some-header, another-header"` + // This can still be something invalid, e.g. `no-cache="some-header, ...` + const fieldListParts = [value.substring(1)] + + for (let j = i + 1; j < directives.length; j++) { + const nextPart = trimOWS(directives[j].value) + const closingQuote = findUnescapedQuote(nextPart, 0) + + lastQuotedPart = j + + if (closingQuote !== -1) { + fieldListParts.push(nextPart.substring(0, closingQuote)) + foundEndingQuote = true + break + } + + fieldListParts.push(nextPart) + } + + fieldList = fieldListParts.join(',') + } + + if (!foundEndingQuote) { + output[key] = true + break + } + + i = lastQuotedPart + + const headers = fieldList.split(',') + let validFieldNames = true + for (let j = 0; j < headers.length; j++) { + headers[j] = trimOWS(headers[j]) + if (!isValidHTTPToken(headers[j])) { + validFieldNames = false + } + } + + if (!validFieldNames) { + output[key] = true + } else if (output[key] !== true) { + if (key in output) { + output[key] = output[key].concat(headers) + } else { + output[key] = headers + } + } + } else { + // Something like `no-cache=some-header` + const fieldName = trimOWS(value) + + if (!isValidHTTPToken(fieldName)) { + output[key] = true + } else if (output[key] !== true) { + if (key in output) { + output[key] = output[key].concat(fieldName) + } else { + output[key] = [fieldName] + } + } + } + + break + } + } + // eslint-disable-next-line no-fallthrough + case 'public': + case 'must-revalidate': + case 'proxy-revalidate': + case 'immutable': + case 'no-transform': + case 'must-understand': + case 'only-if-cached': + if (fromMalformedQuote || invalidNoArgumentDirectives.has(key)) { + continue + } + + if (value !== undefined) { + // These are qualified (something like `public=...`) when they aren't + // allowed to be, skip all instances of the malformed directive. + delete output[key] + invalidNoArgumentDirectives.add(key) + continue + } + + output[key] = true + break + case 'no-store': + output[key] = true + break + default: + // Ignore unknown directives as per https://www.rfc-editor.org/rfc/rfc9111.html#section-5.2.3-1 + continue + } + } + + return output +} + +/** + * @param {string | string[]} varyHeader Vary header from the server + * @returns {string[]} + */ +function splitVaryHeader (varyHeader) { + const values = Array.isArray(varyHeader) ? varyHeader : [varyHeader] + const output = [] + + for (let i = 0; i < values.length; i++) { + const parts = values[i].split(',') + for (let j = 0; j < parts.length; j++) { + output.push(parts[j]) + } + } + + return output +} + +/** + * @param {string | string[]} varyHeader Vary header from the server + * @returns {boolean} + */ +function hasVaryStar (varyHeader) { + const values = splitVaryHeader(varyHeader) + for (let i = 0; i < values.length; i++) { + if (trimOWS(values[i]).indexOf('*') !== -1) { + return true + } + } + + return false +} + +/** + * @param {string | string[]} varyHeader Vary header from the server + * @param {Record} headers Request headers + * @returns {Record | undefined} + */ +function parseVaryHeader (varyHeader, headers) { + if (hasVaryStar(varyHeader)) { + return headers + } + + const output = /** @type {Record} */ ({}) + + const varyingHeaders = splitVaryHeader(varyHeader) + + for (const header of varyingHeaders) { + const trimmedHeader = trimOWS(header).toLowerCase() + + if (trimmedHeader.length === 0) { + continue + } + + if (!isValidHTTPToken(trimmedHeader)) { + return undefined + } + + const headerValue = headers[trimmedHeader] + output[trimmedHeader] = Array.isArray(headerValue) ? headerValue.slice() : headerValue ?? null + } + + return output +} + +/** + * @param {string | string[]} varyHeader Vary header from the server + * @returns {boolean} + */ +function isInvalidOrWildcardVaryHeader (varyHeader) { + return hasVaryStar(varyHeader) || parseVaryHeader(varyHeader, {}) === undefined +} + +/** + * Note: this deviates from the spec a little. Empty etags ("", W/"") are valid, + * however, including them in cached resposnes serves little to no purpose. + * + * @see https://www.rfc-editor.org/rfc/rfc9110.html#name-etag + * + * @param {string} etag + * @returns {boolean} + */ +function isEtagUsable (etag) { + if (etag.length <= 2) { + // Shortest an etag can be is two chars (just ""). This is where we deviate + // from the spec requiring a min of 3 chars however + return false + } + + if (etag[0] === '"' && etag[etag.length - 1] === '"') { + // ETag: ""asd123"" or ETag: "W/"asd123"", kinda undefined behavior in the + // spec. Some servers will accept these while others don't. + // ETag: "asd123" + return !(etag[1] === '"' || etag.startsWith('"W/')) + } + + if (etag.startsWith('W/"') && etag[etag.length - 1] === '"') { + // ETag: W/"", also where we deviate from the spec & require a min of 3 + // chars + // ETag: for W/"", W/"asd123" + return etag.length !== 4 + } + + // Anything else + return false +} + +/** + * @param {unknown} store + * @returns {asserts store is import('../../types/cache-interceptor.d.ts').default.CacheStore} + */ +function assertCacheStore (store, name = 'CacheStore') { + if (typeof store !== 'object' || store === null) { + throw new TypeError(`expected type of ${name} to be a CacheStore, got ${store === null ? 'null' : typeof store}`) + } + + for (const fn of ['get', 'createWriteStream', 'delete']) { + if (typeof store[fn] !== 'function') { + throw new TypeError(`${name} needs to have a \`${fn}()\` function`) + } + } +} +/** + * @param {unknown} methods + * @returns {asserts methods is import('../../types/cache-interceptor.d.ts').default.CacheMethods[]} + */ +function assertCacheMethods (methods, name = 'CacheMethods') { + if (!Array.isArray(methods)) { + throw new TypeError(`expected type of ${name} needs to be an array, got ${methods === null ? 'null' : typeof methods}`) + } + + if (methods.length === 0) { + throw new TypeError(`${name} needs to have at least one method`) + } + + for (const method of methods) { + if (!arrayIncludes(safeHTTPMethods, method)) { + throw new TypeError(`element of ${name}-array needs to be one of following values: ${safeHTTPMethods.join(', ')}, got ${method}`) + } + } +} + +/** + * Creates a string key for request deduplication purposes. + * This key is used to identify in-flight requests that can be shared. + * @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} cacheKey + * @param {Set} [excludeHeaders] Set of lowercase header names to exclude from the key + * @returns {string} + */ +function makeDeduplicationKey (cacheKey, excludeHeaders) { + // Use JSON.stringify to produce a collision-resistant key. + // Previous format used `:` and `=` delimiters without escaping, which + // allowed different header sets to produce identical keys (e.g. + // {a:"x:b=y"} vs {a:"x", b:"y"}). See: https://github.com/nodejs/undici/issues/5012 + const headers = {} + + if (cacheKey.headers) { + const sortedHeaders = Object.keys(cacheKey.headers).sort() + for (const header of sortedHeaders) { + // Skip excluded headers + if (excludeHeaders?.has(header.toLowerCase())) { + continue + } + headers[header] = cacheKey.headers[header] + } + } + + return JSON.stringify([cacheKey.origin, cacheKey.method, cacheKey.path, headers]) +} + +module.exports = { + makeCacheKey, + normalizeHeaders, + assertCacheKey, + assertCacheValue, + parseCacheControlHeader, + hasInvalidCacheControlDirective, + parseVaryHeader, + hasVaryStar, + isInvalidOrWildcardVaryHeader, + isEtagUsable, + assertCacheMethods, + assertCacheStore, + makeDeduplicationKey +} diff --git a/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/util/date.js b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/util/date.js new file mode 100644 index 000000000..a1862a07d --- /dev/null +++ b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/util/date.js @@ -0,0 +1,670 @@ +'use strict' + +/** + * @see https://www.rfc-editor.org/rfc/rfc9110.html#name-date-time-formats + * + * @param {string} date + * @returns {Date | undefined} + */ +function parseHttpDate (date) { + // Sun, 06 Nov 1994 08:49:37 GMT ; IMF-fixdate + // Sun Nov 6 08:49:37 1994 ; ANSI C's asctime() format + // Sunday, 06-Nov-94 08:49:37 GMT ; obsolete RFC 850 format + + switch (date[3]) { + case ',': return parseImfDate(date) + case ' ': return parseAscTimeDate(date) + default: return parseRfc850Date(date) + } +} + +function makeDate (year, monthIdx, day, hour, minute, second, weekday) { + const result = new Date(Date.UTC(year, monthIdx, day, hour, minute, second)) + + // Date.UTC treats years 0-99 as 1900-1999. Reset the full year so component + // checks below validate the HTTP date as written. + if (year >= 0 && year <= 99) { + result.setUTCFullYear(year) + } + + return result.getUTCFullYear() === year && + result.getUTCMonth() === monthIdx && + result.getUTCDate() === day && + result.getUTCHours() === hour && + result.getUTCMinutes() === minute && + result.getUTCSeconds() === second && + result.getUTCDay() === weekday + ? result + : undefined +} + +/** + * @see https://httpwg.org/specs/rfc9110.html#preferred.date.format + * + * @param {string} date + * @returns {Date | undefined} + */ +function parseImfDate (date) { + if ( + date.length !== 29 || + date[4] !== ' ' || + date[7] !== ' ' || + date[11] !== ' ' || + date[16] !== ' ' || + date[19] !== ':' || + date[22] !== ':' || + date[25] !== ' ' || + date[26] !== 'G' || + date[27] !== 'M' || + date[28] !== 'T' + ) { + return undefined + } + + let weekday = -1 + if (date[0] === 'S' && date[1] === 'u' && date[2] === 'n') { // Sunday + weekday = 0 + } else if (date[0] === 'M' && date[1] === 'o' && date[2] === 'n') { // Monday + weekday = 1 + } else if (date[0] === 'T' && date[1] === 'u' && date[2] === 'e') { // Tuesday + weekday = 2 + } else if (date[0] === 'W' && date[1] === 'e' && date[2] === 'd') { // Wednesday + weekday = 3 + } else if (date[0] === 'T' && date[1] === 'h' && date[2] === 'u') { // Thursday + weekday = 4 + } else if (date[0] === 'F' && date[1] === 'r' && date[2] === 'i') { // Friday + weekday = 5 + } else if (date[0] === 'S' && date[1] === 'a' && date[2] === 't') { // Saturday + weekday = 6 + } else { + return undefined // Not a valid day of the week + } + + let day = 0 + if (date[5] === '0') { + // Single digit day, e.g. "Sun Nov 6 08:49:37 1994" + const code = date.charCodeAt(6) + if (code < 49 || code > 57) { + return undefined // Not a digit + } + day = code - 48 // Convert ASCII code to number + } else { + const code1 = date.charCodeAt(5) + if (code1 < 49 || code1 > 51) { + return undefined // Not a digit between 1 and 3 + } + const code2 = date.charCodeAt(6) + if (code2 < 48 || code2 > 57) { + return undefined // Not a digit + } + day = (code1 - 48) * 10 + (code2 - 48) // Convert ASCII codes to number + } + + let monthIdx = -1 + if ( + (date[8] === 'J' && date[9] === 'a' && date[10] === 'n') + ) { + monthIdx = 0 // Jan + } else if ( + (date[8] === 'F' && date[9] === 'e' && date[10] === 'b') + ) { + monthIdx = 1 // Feb + } else if ( + (date[8] === 'M' && date[9] === 'a') + ) { + if (date[10] === 'r') { + monthIdx = 2 // Mar + } else if (date[10] === 'y') { + monthIdx = 4 // May + } else { + return undefined // Invalid month + } + } else if ( + (date[8] === 'J') + ) { + if (date[9] === 'a' && date[10] === 'n') { + monthIdx = 0 // Jan + } else if (date[9] === 'u') { + if (date[10] === 'n') { + monthIdx = 5 // Jun + } else if (date[10] === 'l') { + monthIdx = 6 // Jul + } else { + return undefined // Invalid month + } + } else { + return undefined // Invalid month + } + } else if ( + (date[8] === 'A') + ) { + if (date[9] === 'p' && date[10] === 'r') { + monthIdx = 3 // Apr + } else if (date[9] === 'u' && date[10] === 'g') { + monthIdx = 7 // Aug + } else { + return undefined // Invalid month + } + } else if ( + (date[8] === 'S' && date[9] === 'e' && date[10] === 'p') + ) { + monthIdx = 8 // Sep + } else if ( + (date[8] === 'O' && date[9] === 'c' && date[10] === 't') + ) { + monthIdx = 9 // Oct + } else if ( + (date[8] === 'N' && date[9] === 'o' && date[10] === 'v') + ) { + monthIdx = 10 // Nov + } else if ( + (date[8] === 'D' && date[9] === 'e' && date[10] === 'c') + ) { + monthIdx = 11 // Dec + } else { + // Not a valid month + return undefined + } + + const yearDigit1 = date.charCodeAt(12) + if (yearDigit1 < 48 || yearDigit1 > 57) { + return undefined // Not a digit + } + const yearDigit2 = date.charCodeAt(13) + if (yearDigit2 < 48 || yearDigit2 > 57) { + return undefined // Not a digit + } + const yearDigit3 = date.charCodeAt(14) + if (yearDigit3 < 48 || yearDigit3 > 57) { + return undefined // Not a digit + } + const yearDigit4 = date.charCodeAt(15) + if (yearDigit4 < 48 || yearDigit4 > 57) { + return undefined // Not a digit + } + const year = (yearDigit1 - 48) * 1000 + (yearDigit2 - 48) * 100 + (yearDigit3 - 48) * 10 + (yearDigit4 - 48) + + let hour = 0 + if (date[17] === '0') { + const code = date.charCodeAt(18) + if (code < 48 || code > 57) { + return undefined // Not a digit + } + hour = code - 48 // Convert ASCII code to number + } else { + const code1 = date.charCodeAt(17) + if (code1 < 48 || code1 > 50) { + return undefined // Not a digit between 0 and 2 + } + const code2 = date.charCodeAt(18) + if (code2 < 48 || code2 > 57) { + return undefined // Not a digit + } + if (code1 === 50 && code2 > 51) { + return undefined // Hour cannot be greater than 23 + } + hour = (code1 - 48) * 10 + (code2 - 48) // Convert ASCII codes to number + } + + let minute = 0 + if (date[20] === '0') { + const code = date.charCodeAt(21) + if (code < 48 || code > 57) { + return undefined // Not a digit + } + minute = code - 48 // Convert ASCII code to number + } else { + const code1 = date.charCodeAt(20) + if (code1 < 48 || code1 > 53) { + return undefined // Not a digit between 0 and 5 + } + const code2 = date.charCodeAt(21) + if (code2 < 48 || code2 > 57) { + return undefined // Not a digit + } + minute = (code1 - 48) * 10 + (code2 - 48) // Convert ASCII codes to number + } + + let second = 0 + if (date[23] === '0') { + const code = date.charCodeAt(24) + if (code < 48 || code > 57) { + return undefined // Not a digit + } + second = code - 48 // Convert ASCII code to number + } else { + const code1 = date.charCodeAt(23) + if (code1 < 48 || code1 > 53) { + return undefined // Not a digit between 0 and 5 + } + const code2 = date.charCodeAt(24) + if (code2 < 48 || code2 > 57) { + return undefined // Not a digit + } + second = (code1 - 48) * 10 + (code2 - 48) // Convert ASCII codes to number + } + + return makeDate(year, monthIdx, day, hour, minute, second, weekday) +} + +/** + * @see https://httpwg.org/specs/rfc9110.html#obsolete.date.formats + * + * @param {string} date + * @returns {Date | undefined} + */ +function parseAscTimeDate (date) { + // This is assumed to be in UTC + + if ( + date.length !== 24 || + date[7] !== ' ' || + date[10] !== ' ' || + date[19] !== ' ' + ) { + return undefined + } + + let weekday = -1 + if (date[0] === 'S' && date[1] === 'u' && date[2] === 'n') { // Sunday + weekday = 0 + } else if (date[0] === 'M' && date[1] === 'o' && date[2] === 'n') { // Monday + weekday = 1 + } else if (date[0] === 'T' && date[1] === 'u' && date[2] === 'e') { // Tuesday + weekday = 2 + } else if (date[0] === 'W' && date[1] === 'e' && date[2] === 'd') { // Wednesday + weekday = 3 + } else if (date[0] === 'T' && date[1] === 'h' && date[2] === 'u') { // Thursday + weekday = 4 + } else if (date[0] === 'F' && date[1] === 'r' && date[2] === 'i') { // Friday + weekday = 5 + } else if (date[0] === 'S' && date[1] === 'a' && date[2] === 't') { // Saturday + weekday = 6 + } else { + return undefined // Not a valid day of the week + } + + let monthIdx = -1 + if ( + (date[4] === 'J' && date[5] === 'a' && date[6] === 'n') + ) { + monthIdx = 0 // Jan + } else if ( + (date[4] === 'F' && date[5] === 'e' && date[6] === 'b') + ) { + monthIdx = 1 // Feb + } else if ( + (date[4] === 'M' && date[5] === 'a') + ) { + if (date[6] === 'r') { + monthIdx = 2 // Mar + } else if (date[6] === 'y') { + monthIdx = 4 // May + } else { + return undefined // Invalid month + } + } else if ( + (date[4] === 'J') + ) { + if (date[5] === 'a' && date[6] === 'n') { + monthIdx = 0 // Jan + } else if (date[5] === 'u') { + if (date[6] === 'n') { + monthIdx = 5 // Jun + } else if (date[6] === 'l') { + monthIdx = 6 // Jul + } else { + return undefined // Invalid month + } + } else { + return undefined // Invalid month + } + } else if ( + (date[4] === 'A') + ) { + if (date[5] === 'p' && date[6] === 'r') { + monthIdx = 3 // Apr + } else if (date[5] === 'u' && date[6] === 'g') { + monthIdx = 7 // Aug + } else { + return undefined // Invalid month + } + } else if ( + (date[4] === 'S' && date[5] === 'e' && date[6] === 'p') + ) { + monthIdx = 8 // Sep + } else if ( + (date[4] === 'O' && date[5] === 'c' && date[6] === 't') + ) { + monthIdx = 9 // Oct + } else if ( + (date[4] === 'N' && date[5] === 'o' && date[6] === 'v') + ) { + monthIdx = 10 // Nov + } else if ( + (date[4] === 'D' && date[5] === 'e' && date[6] === 'c') + ) { + monthIdx = 11 // Dec + } else { + // Not a valid month + return undefined + } + + let day = 0 + if (date[8] === ' ') { + // Single digit day, e.g. "Sun Nov 6 08:49:37 1994" + const code = date.charCodeAt(9) + if (code < 49 || code > 57) { + return undefined // Not a digit + } + day = code - 48 // Convert ASCII code to number + } else { + const code1 = date.charCodeAt(8) + if (code1 < 49 || code1 > 51) { + return undefined // Not a digit between 1 and 3 + } + const code2 = date.charCodeAt(9) + if (code2 < 48 || code2 > 57) { + return undefined // Not a digit + } + day = (code1 - 48) * 10 + (code2 - 48) // Convert ASCII codes to number + } + + let hour = 0 + if (date[11] === '0') { + const code = date.charCodeAt(12) + if (code < 48 || code > 57) { + return undefined // Not a digit + } + hour = code - 48 // Convert ASCII code to number + } else { + const code1 = date.charCodeAt(11) + if (code1 < 48 || code1 > 50) { + return undefined // Not a digit between 0 and 2 + } + const code2 = date.charCodeAt(12) + if (code2 < 48 || code2 > 57) { + return undefined // Not a digit + } + if (code1 === 50 && code2 > 51) { + return undefined // Hour cannot be greater than 23 + } + hour = (code1 - 48) * 10 + (code2 - 48) // Convert ASCII codes to number + } + + let minute = 0 + if (date[14] === '0') { + const code = date.charCodeAt(15) + if (code < 48 || code > 57) { + return undefined // Not a digit + } + minute = code - 48 // Convert ASCII code to number + } else { + const code1 = date.charCodeAt(14) + if (code1 < 48 || code1 > 53) { + return undefined // Not a digit between 0 and 5 + } + const code2 = date.charCodeAt(15) + if (code2 < 48 || code2 > 57) { + return undefined // Not a digit + } + minute = (code1 - 48) * 10 + (code2 - 48) // Convert ASCII codes to number + } + + let second = 0 + if (date[17] === '0') { + const code = date.charCodeAt(18) + if (code < 48 || code > 57) { + return undefined // Not a digit + } + second = code - 48 // Convert ASCII code to number + } else { + const code1 = date.charCodeAt(17) + if (code1 < 48 || code1 > 53) { + return undefined // Not a digit between 0 and 5 + } + const code2 = date.charCodeAt(18) + if (code2 < 48 || code2 > 57) { + return undefined // Not a digit + } + second = (code1 - 48) * 10 + (code2 - 48) // Convert ASCII codes to number + } + + const yearDigit1 = date.charCodeAt(20) + if (yearDigit1 < 48 || yearDigit1 > 57) { + return undefined // Not a digit + } + const yearDigit2 = date.charCodeAt(21) + if (yearDigit2 < 48 || yearDigit2 > 57) { + return undefined // Not a digit + } + const yearDigit3 = date.charCodeAt(22) + if (yearDigit3 < 48 || yearDigit3 > 57) { + return undefined // Not a digit + } + const yearDigit4 = date.charCodeAt(23) + if (yearDigit4 < 48 || yearDigit4 > 57) { + return undefined // Not a digit + } + const year = (yearDigit1 - 48) * 1000 + (yearDigit2 - 48) * 100 + (yearDigit3 - 48) * 10 + (yearDigit4 - 48) + + return makeDate(year, monthIdx, day, hour, minute, second, weekday) +} + +/** + * @see https://httpwg.org/specs/rfc9110.html#obsolete.date.formats + * + * @param {string} date + * @returns {Date | undefined} + */ +function parseRfc850Date (date) { + let commaIndex = -1 + + let weekday = -1 + if (date[0] === 'S') { + if (date[1] === 'u' && date[2] === 'n' && date[3] === 'd' && date[4] === 'a' && date[5] === 'y') { + weekday = 0 // Sunday + commaIndex = 6 + } else if (date[1] === 'a' && date[2] === 't' && date[3] === 'u' && date[4] === 'r' && date[5] === 'd' && date[6] === 'a' && date[7] === 'y') { + weekday = 6 // Saturday + commaIndex = 8 + } + } else if (date[0] === 'M' && date[1] === 'o' && date[2] === 'n' && date[3] === 'd' && date[4] === 'a' && date[5] === 'y') { + weekday = 1 // Monday + commaIndex = 6 + } else if (date[0] === 'T') { + if (date[1] === 'u' && date[2] === 'e' && date[3] === 's' && date[4] === 'd' && date[5] === 'a' && date[6] === 'y') { + weekday = 2 // Tuesday + commaIndex = 7 + } else if (date[1] === 'h' && date[2] === 'u' && date[3] === 'r' && date[4] === 's' && date[5] === 'd' && date[6] === 'a' && date[7] === 'y') { + weekday = 4 // Thursday + commaIndex = 8 + } + } else if (date[0] === 'W' && date[1] === 'e' && date[2] === 'd' && date[3] === 'n' && date[4] === 'e' && date[5] === 's' && date[6] === 'd' && date[7] === 'a' && date[8] === 'y') { + weekday = 3 // Wednesday + commaIndex = 9 + } else if (date[0] === 'F' && date[1] === 'r' && date[2] === 'i' && date[3] === 'd' && date[4] === 'a' && date[5] === 'y') { + weekday = 5 // Friday + commaIndex = 6 + } else { + // Not a valid day name + return undefined + } + + if ( + date[commaIndex] !== ',' || + (date.length - commaIndex - 1) !== 23 || + date[commaIndex + 1] !== ' ' || + date[commaIndex + 4] !== '-' || + date[commaIndex + 8] !== '-' || + date[commaIndex + 11] !== ' ' || + date[commaIndex + 14] !== ':' || + date[commaIndex + 17] !== ':' || + date[commaIndex + 20] !== ' ' || + date[commaIndex + 21] !== 'G' || + date[commaIndex + 22] !== 'M' || + date[commaIndex + 23] !== 'T' + ) { + return undefined + } + + let day = 0 + if (date[commaIndex + 2] === '0') { + // Single digit day, e.g. "Sun Nov 6 08:49:37 1994" + const code = date.charCodeAt(commaIndex + 3) + if (code < 49 || code > 57) { + return undefined // Not a digit + } + day = code - 48 // Convert ASCII code to number + } else { + const code1 = date.charCodeAt(commaIndex + 2) + if (code1 < 49 || code1 > 51) { + return undefined // Not a digit between 1 and 3 + } + const code2 = date.charCodeAt(commaIndex + 3) + if (code2 < 48 || code2 > 57) { + return undefined // Not a digit + } + day = (code1 - 48) * 10 + (code2 - 48) // Convert ASCII codes to number + } + + let monthIdx = -1 + if ( + (date[commaIndex + 5] === 'J' && date[commaIndex + 6] === 'a' && date[commaIndex + 7] === 'n') + ) { + monthIdx = 0 // Jan + } else if ( + (date[commaIndex + 5] === 'F' && date[commaIndex + 6] === 'e' && date[commaIndex + 7] === 'b') + ) { + monthIdx = 1 // Feb + } else if ( + (date[commaIndex + 5] === 'M' && date[commaIndex + 6] === 'a' && date[commaIndex + 7] === 'r') + ) { + monthIdx = 2 // Mar + } else if ( + (date[commaIndex + 5] === 'A' && date[commaIndex + 6] === 'p' && date[commaIndex + 7] === 'r') + ) { + monthIdx = 3 // Apr + } else if ( + (date[commaIndex + 5] === 'M' && date[commaIndex + 6] === 'a' && date[commaIndex + 7] === 'y') + ) { + monthIdx = 4 // May + } else if ( + (date[commaIndex + 5] === 'J' && date[commaIndex + 6] === 'u' && date[commaIndex + 7] === 'n') + ) { + monthIdx = 5 // Jun + } else if ( + (date[commaIndex + 5] === 'J' && date[commaIndex + 6] === 'u' && date[commaIndex + 7] === 'l') + ) { + monthIdx = 6 // Jul + } else if ( + (date[commaIndex + 5] === 'A' && date[commaIndex + 6] === 'u' && date[commaIndex + 7] === 'g') + ) { + monthIdx = 7 // Aug + } else if ( + (date[commaIndex + 5] === 'S' && date[commaIndex + 6] === 'e' && date[commaIndex + 7] === 'p') + ) { + monthIdx = 8 // Sep + } else if ( + (date[commaIndex + 5] === 'O' && date[commaIndex + 6] === 'c' && date[commaIndex + 7] === 't') + ) { + monthIdx = 9 // Oct + } else if ( + (date[commaIndex + 5] === 'N' && date[commaIndex + 6] === 'o' && date[commaIndex + 7] === 'v') + ) { + monthIdx = 10 // Nov + } else if ( + (date[commaIndex + 5] === 'D' && date[commaIndex + 6] === 'e' && date[commaIndex + 7] === 'c') + ) { + monthIdx = 11 // Dec + } else { + // Not a valid month + return undefined + } + + const yearDigit1 = date.charCodeAt(commaIndex + 9) + if (yearDigit1 < 48 || yearDigit1 > 57) { + return undefined // Not a digit + } + const yearDigit2 = date.charCodeAt(commaIndex + 10) + if (yearDigit2 < 48 || yearDigit2 > 57) { + return undefined // Not a digit + } + + let year = (yearDigit1 - 48) * 10 + (yearDigit2 - 48) // Convert ASCII codes to number + + // RFC 6265 states that the year is in the range 1970-2069. + // @see https://datatracker.ietf.org/doc/html/rfc6265#section-5.1.1 + // + // 3. If the year-value is greater than or equal to 70 and less than or + // equal to 99, increment the year-value by 1900. + // 4. If the year-value is greater than or equal to 0 and less than or + // equal to 69, increment the year-value by 2000. + year += year < 70 ? 2000 : 1900 + + let hour = 0 + if (date[commaIndex + 12] === '0') { + const code = date.charCodeAt(commaIndex + 13) + if (code < 48 || code > 57) { + return undefined // Not a digit + } + hour = code - 48 // Convert ASCII code to number + } else { + const code1 = date.charCodeAt(commaIndex + 12) + if (code1 < 48 || code1 > 50) { + return undefined // Not a digit between 0 and 2 + } + const code2 = date.charCodeAt(commaIndex + 13) + if (code2 < 48 || code2 > 57) { + return undefined // Not a digit + } + if (code1 === 50 && code2 > 51) { + return undefined // Hour cannot be greater than 23 + } + hour = (code1 - 48) * 10 + (code2 - 48) // Convert ASCII codes to number + } + + let minute = 0 + if (date[commaIndex + 15] === '0') { + const code = date.charCodeAt(commaIndex + 16) + if (code < 48 || code > 57) { + return undefined // Not a digit + } + minute = code - 48 // Convert ASCII code to number + } else { + const code1 = date.charCodeAt(commaIndex + 15) + if (code1 < 48 || code1 > 53) { + return undefined // Not a digit between 0 and 5 + } + const code2 = date.charCodeAt(commaIndex + 16) + if (code2 < 48 || code2 > 57) { + return undefined // Not a digit + } + minute = (code1 - 48) * 10 + (code2 - 48) // Convert ASCII codes to number + } + + let second = 0 + if (date[commaIndex + 18] === '0') { + const code = date.charCodeAt(commaIndex + 19) + if (code < 48 || code > 57) { + return undefined // Not a digit + } + second = code - 48 // Convert ASCII code to number + } else { + const code1 = date.charCodeAt(commaIndex + 18) + if (code1 < 48 || code1 > 53) { + return undefined // Not a digit between 0 and 5 + } + const code2 = date.charCodeAt(commaIndex + 19) + if (code2 < 48 || code2 > 57) { + return undefined // Not a digit + } + second = (code1 - 48) * 10 + (code2 - 48) // Convert ASCII codes to number + } + + return makeDate(year, monthIdx, day, hour, minute, second, weekday) +} + +module.exports = { + parseHttpDate +} diff --git a/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/util/promise.js b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/util/promise.js new file mode 100644 index 000000000..048f86e34 --- /dev/null +++ b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/util/promise.js @@ -0,0 +1,28 @@ +'use strict' + +/** + * @template {*} T + * @typedef {Object} DeferredPromise + * @property {Promise} promise + * @property {(value?: T) => void} resolve + * @property {(reason?: any) => void} reject + */ + +/** + * @template {*} T + * @returns {DeferredPromise} An object containing a promise and its resolve/reject methods. + */ +function createDeferredPromise () { + let res + let rej + const promise = new Promise((resolve, reject) => { + res = resolve + rej = reject + }) + + return { promise, resolve: res, reject: rej } +} + +module.exports = { + createDeferredPromise +} diff --git a/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/util/runtime-features.js b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/util/runtime-features.js new file mode 100644 index 000000000..3e62dc004 --- /dev/null +++ b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/util/runtime-features.js @@ -0,0 +1,124 @@ +'use strict' + +/** @typedef {`node:${string}`} NodeModuleName */ + +/** @type {Record any>} */ +const lazyLoaders = { + __proto__: null, + 'node:crypto': () => require('node:crypto'), + 'node:sqlite': () => require('node:sqlite'), + 'node:worker_threads': () => require('node:worker_threads'), + 'node:zlib': () => require('node:zlib') +} + +/** + * @param {NodeModuleName} moduleName + * @returns {boolean} + */ +function detectRuntimeFeatureByNodeModule (moduleName) { + try { + lazyLoaders[moduleName]() + return true + } catch (err) { + if (err.code !== 'ERR_UNKNOWN_BUILTIN_MODULE' && err.code !== 'ERR_NO_CRYPTO') { + throw err + } + return false + } +} + +/** + * @param {NodeModuleName} moduleName + * @param {string} property + * @returns {boolean} + */ +function detectRuntimeFeatureByExportedProperty (moduleName, property) { + const module = lazyLoaders[moduleName]() + return typeof module[property] !== 'undefined' +} + +const runtimeFeaturesByExportedProperty = /** @type {const} */ (['markAsUncloneable', 'zstd']) + +/** @type {Record} */ +const exportedPropertyLookup = { + markAsUncloneable: ['node:worker_threads', 'markAsUncloneable'], + zstd: ['node:zlib', 'createZstdDecompress'] +} + +/** @typedef {typeof runtimeFeaturesByExportedProperty[number]} RuntimeFeatureByExportedProperty */ + +const runtimeFeaturesAsNodeModule = /** @type {const} */ (['crypto', 'sqlite']) +/** @typedef {typeof runtimeFeaturesAsNodeModule[number]} RuntimeFeatureByNodeModule */ + +const features = /** @type {const} */ ([ + ...runtimeFeaturesAsNodeModule, + ...runtimeFeaturesByExportedProperty +]) + +/** @typedef {typeof features[number]} Feature */ + +/** + * @param {Feature} feature + * @returns {boolean} + */ +function detectRuntimeFeature (feature) { + if (runtimeFeaturesAsNodeModule.includes(/** @type {RuntimeFeatureByNodeModule} */ (feature))) { + return detectRuntimeFeatureByNodeModule(`node:${feature}`) + } else if (runtimeFeaturesByExportedProperty.includes(/** @type {RuntimeFeatureByExportedProperty} */ (feature))) { + const [moduleName, property] = exportedPropertyLookup[feature] + return detectRuntimeFeatureByExportedProperty(moduleName, property) + } + throw new TypeError(`unknown feature: ${feature}`) +} + +/** + * @class + * @name RuntimeFeatures + */ +class RuntimeFeatures { + /** @type {Map} */ + #map = new Map() + + /** + * Clears all cached feature detections. + */ + clear () { + this.#map.clear() + } + + /** + * @param {Feature} feature + * @returns {boolean} + */ + has (feature) { + return ( + this.#map.get(feature) ?? this.#detectRuntimeFeature(feature) + ) + } + + /** + * @param {Feature} feature + * @param {boolean} value + */ + set (feature, value) { + if (features.includes(feature) === false) { + throw new TypeError(`unknown feature: ${feature}`) + } + this.#map.set(feature, value) + } + + /** + * @param {Feature} feature + * @returns {boolean} + */ + #detectRuntimeFeature (feature) { + const result = detectRuntimeFeature(feature) + this.#map.set(feature, result) + return result + } +} + +const instance = new RuntimeFeatures() + +module.exports.runtimeFeatures = instance +module.exports.default = instance diff --git a/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/util/stats.js b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/util/stats.js new file mode 100644 index 000000000..a13132e4e --- /dev/null +++ b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/util/stats.js @@ -0,0 +1,32 @@ +'use strict' + +const { + kConnected, + kPending, + kRunning, + kSize, + kFree, + kQueued +} = require('../core/symbols') + +class ClientStats { + constructor (client) { + this.connected = client[kConnected] + this.pending = client[kPending] + this.running = client[kRunning] + this.size = client[kSize] + } +} + +class PoolStats { + constructor (pool) { + this.connected = pool[kConnected] + this.free = pool[kFree] + this.pending = pool[kPending] + this.queued = pool[kQueued] + this.running = pool[kRunning] + this.size = pool[kSize] + } +} + +module.exports = { ClientStats, PoolStats } diff --git a/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/util/timers.js b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/util/timers.js new file mode 100644 index 000000000..14984d42e --- /dev/null +++ b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/util/timers.js @@ -0,0 +1,425 @@ +'use strict' + +/** + * This module offers an optimized timer implementation designed for scenarios + * where high precision is not critical. + * + * The timer achieves faster performance by using a low-resolution approach, + * with an accuracy target of within 500ms. This makes it particularly useful + * for timers with delays of 1 second or more, where exact timing is less + * crucial. + * + * It's important to note that Node.js timers are inherently imprecise, as + * delays can occur due to the event loop being blocked by other operations. + * Consequently, timers may trigger later than their scheduled time. + */ + +/** + * The fastNow variable contains the internal fast timer clock value. + * + * @type {number} + */ +let fastNow = 0 + +/** + * RESOLUTION_MS represents the target resolution time in milliseconds. + * + * @type {number} + * @default 1000 + */ +const RESOLUTION_MS = 1e3 + +/** + * TICK_MS defines the desired interval in milliseconds between each tick. + * The target value is set to half the resolution time, minus 1 ms, to account + * for potential event loop overhead. + * + * @type {number} + * @default 499 + */ +const TICK_MS = (RESOLUTION_MS >> 1) - 1 + +/** + * fastNowTimeout is a Node.js timer used to manage and process + * the FastTimers stored in the `fastTimers` array. + * + * @type {NodeJS.Timeout} + */ +let fastNowTimeout + +/** + * The kFastTimer symbol is used to identify FastTimer instances. + * + * @type {Symbol} + */ +const kFastTimer = Symbol('kFastTimer') + +/** + * The fastTimers array contains all active FastTimers. + * + * @type {FastTimer[]} + */ +const fastTimers = [] + +/** + * These constants represent the various states of a FastTimer. + */ + +/** + * The `NOT_IN_LIST` constant indicates that the FastTimer is not included + * in the `fastTimers` array. Timers with this status will not be processed + * during the next tick by the `onTick` function. + * + * A FastTimer can be re-added to the `fastTimers` array by invoking the + * `refresh` method on the FastTimer instance. + * + * @type {-2} + */ +const NOT_IN_LIST = -2 + +/** + * The `TO_BE_CLEARED` constant indicates that the FastTimer is scheduled + * for removal from the `fastTimers` array. A FastTimer in this state will + * be removed in the next tick by the `onTick` function and will no longer + * be processed. + * + * This status is also set when the `clear` method is called on the FastTimer instance. + * + * @type {-1} + */ +const TO_BE_CLEARED = -1 + +/** + * The `PENDING` constant signifies that the FastTimer is awaiting processing + * in the next tick by the `onTick` function. Timers with this status will have + * their `_idleStart` value set and their status updated to `ACTIVE` in the next tick. + * + * @type {0} + */ +const PENDING = 0 + +/** + * The `ACTIVE` constant indicates that the FastTimer is active and waiting + * for its timer to expire. During the next tick, the `onTick` function will + * check if the timer has expired, and if so, it will execute the associated callback. + * + * @type {1} + */ +const ACTIVE = 1 + +/** + * The onTick function processes the fastTimers array. + * + * @returns {void} + */ +function onTick () { + /** + * Increment the fastNow value by the TICK_MS value, despite the actual time + * that has passed since the last tick. This approach ensures independence + * from the system clock and delays caused by a blocked event loop. + * + * @type {number} + */ + fastNow += TICK_MS + + /** + * The `idx` variable is used to iterate over the `fastTimers` array. + * Expired timers are removed by replacing them with the last element in the array. + * Consequently, `idx` is only incremented when the current element is not removed. + * + * @type {number} + */ + let idx = 0 + + /** + * The len variable will contain the length of the fastTimers array + * and will be decremented when a FastTimer should be removed from the + * fastTimers array. + * + * @type {number} + */ + let len = fastTimers.length + + while (idx < len) { + /** + * @type {FastTimer} + */ + const timer = fastTimers[idx] + + // If the timer is in the ACTIVE state and the timer has expired, it will + // be processed in the next tick. + if (timer._state === PENDING) { + // Set the _idleStart value to the fastNow value minus the TICK_MS value + // to account for the time the timer was in the PENDING state. + timer._idleStart = fastNow - TICK_MS + timer._state = ACTIVE + } else if ( + timer._state === ACTIVE && + fastNow >= timer._idleStart + timer._idleTimeout + ) { + timer._state = TO_BE_CLEARED + timer._idleStart = -1 + timer._onTimeout(timer._timerArg) + } + + if (timer._state === TO_BE_CLEARED) { + timer._state = NOT_IN_LIST + + // Move the last element to the current index and decrement len if it is + // not the only element in the array. + if (--len !== 0) { + fastTimers[idx] = fastTimers[len] + } + } else { + ++idx + } + } + + // Set the length of the fastTimers array to the new length and thus + // removing the excess FastTimers elements from the array. + fastTimers.length = len + + // If there are still active FastTimers in the array, refresh the Timer. + // If there are no active FastTimers, the timer will be refreshed again + // when a new FastTimer is instantiated. + if (fastTimers.length !== 0) { + refreshTimeout() + } +} + +function refreshTimeout () { + // If the fastNowTimeout is already set and the Timer has the refresh()- + // method available, call it to refresh the timer. + // Some timer objects returned by setTimeout may not have a .refresh() + // method (e.g. mocked timers in tests). + if (fastNowTimeout?.refresh) { + fastNowTimeout.refresh() + // fastNowTimeout is not instantiated yet or refresh is not availabe, + // create a new Timer. + } else { + clearTimeout(fastNowTimeout) + fastNowTimeout = setTimeout(onTick, TICK_MS) + // If the Timer has an unref method, call it to allow the process to exit, + // if there are no other active handles. When using fake timers or mocked + // environments (like Jest), .unref() may not be defined, + fastNowTimeout?.unref() + } +} + +/** + * The `FastTimer` class is a data structure designed to store and manage + * timer information. + */ +class FastTimer { + [kFastTimer] = true + + /** + * The state of the timer, which can be one of the following: + * - NOT_IN_LIST (-2) + * - TO_BE_CLEARED (-1) + * - PENDING (0) + * - ACTIVE (1) + * + * @type {-2|-1|0|1} + * @private + */ + _state = NOT_IN_LIST + + /** + * The number of milliseconds to wait before calling the callback. + * + * @type {number} + * @private + */ + _idleTimeout = -1 + + /** + * The time in milliseconds when the timer was started. This value is used to + * calculate when the timer should expire. + * + * @type {number} + * @default -1 + * @private + */ + _idleStart = -1 + + /** + * The function to be executed when the timer expires. + * @type {Function} + * @private + */ + _onTimeout + + /** + * The argument to be passed to the callback when the timer expires. + * + * @type {*} + * @private + */ + _timerArg + + /** + * @constructor + * @param {Function} callback A function to be executed after the timer + * expires. + * @param {number} delay The time, in milliseconds that the timer should wait + * before the specified function or code is executed. + * @param {*} arg + */ + constructor (callback, delay, arg) { + this._onTimeout = callback + this._idleTimeout = delay + this._timerArg = arg + + this.refresh() + } + + /** + * Sets the timer's start time to the current time, and reschedules the timer + * to call its callback at the previously specified duration adjusted to the + * current time. + * Using this on a timer that has already called its callback will reactivate + * the timer. + * + * @returns {void} + */ + refresh () { + // In the special case that the timer is not in the list of active timers, + // add it back to the array to be processed in the next tick by the onTick + // function. + if (this._state === NOT_IN_LIST) { + fastTimers.push(this) + } + + // If the timer is the only active timer, refresh the fastNowTimeout for + // better resolution. + if (!fastNowTimeout || fastTimers.length === 1) { + refreshTimeout() + } + + // Setting the state to PENDING will cause the timer to be reset in the + // next tick by the onTick function. + this._state = PENDING + } + + /** + * The `clear` method cancels the timer, preventing it from executing. + * + * @returns {void} + * @private + */ + clear () { + // Set the state to TO_BE_CLEARED to mark the timer for removal in the next + // tick by the onTick function. + this._state = TO_BE_CLEARED + + // Reset the _idleStart value to -1 to indicate that the timer is no longer + // active. + this._idleStart = -1 + } +} + +/** + * This module exports a setTimeout and clearTimeout function that can be + * used as a drop-in replacement for the native functions. + */ +module.exports = { + /** + * The setTimeout() method sets a timer which executes a function once the + * timer expires. + * @param {Function} callback A function to be executed after the timer + * expires. + * @param {number} delay The time, in milliseconds that the timer should + * wait before the specified function or code is executed. + * @param {*} [arg] An optional argument to be passed to the callback function + * when the timer expires. + * @returns {NodeJS.Timeout|FastTimer} + */ + setTimeout (callback, delay, arg) { + // If the delay is less than or equal to the RESOLUTION_MS value return a + // native Node.js Timer instance. + return delay <= RESOLUTION_MS + ? setTimeout(callback, delay, arg) + : new FastTimer(callback, delay, arg) + }, + /** + * The clearTimeout method cancels an instantiated Timer previously created + * by calling setTimeout. + * + * @param {NodeJS.Timeout|FastTimer} timeout + */ + clearTimeout (timeout) { + // If the timeout is a FastTimer, call its own clear method. + if (timeout[kFastTimer]) { + /** + * @type {FastTimer} + */ + timeout.clear() + // Otherwise it is an instance of a native NodeJS.Timeout, so call the + // Node.js native clearTimeout function. + } else { + clearTimeout(timeout) + } + }, + /** + * The setFastTimeout() method sets a fastTimer which executes a function once + * the timer expires. + * @param {Function} callback A function to be executed after the timer + * expires. + * @param {number} delay The time, in milliseconds that the timer should + * wait before the specified function or code is executed. + * @param {*} [arg] An optional argument to be passed to the callback function + * when the timer expires. + * @returns {FastTimer} + */ + setFastTimeout (callback, delay, arg) { + return new FastTimer(callback, delay, arg) + }, + /** + * The clearTimeout method cancels an instantiated FastTimer previously + * created by calling setFastTimeout. + * + * @param {FastTimer} timeout + */ + clearFastTimeout (timeout) { + timeout.clear() + }, + /** + * The now method returns the value of the internal fast timer clock. + * + * @returns {number} + */ + now () { + return fastNow + }, + /** + * Trigger the onTick function to process the fastTimers array. + * Exported for testing purposes only. + * Marking as deprecated to discourage any use outside of testing. + * @deprecated + * @param {number} [delay=0] The delay in milliseconds to add to the now value. + */ + tick (delay = 0) { + fastNow += delay - RESOLUTION_MS + 1 + onTick() + onTick() + }, + /** + * Reset FastTimers. + * Exported for testing purposes only. + * Marking as deprecated to discourage any use outside of testing. + * @deprecated + */ + reset () { + fastNow = 0 + fastTimers.length = 0 + clearTimeout(fastNowTimeout) + fastNowTimeout = null + }, + /** + * Exporting for testing purposes only. + * Marking as deprecated to discourage any use outside of testing. + * @deprecated + */ + kFastTimer +} diff --git a/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/web/cache/cache.js b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/web/cache/cache.js new file mode 100644 index 000000000..10decbede --- /dev/null +++ b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/web/cache/cache.js @@ -0,0 +1,864 @@ +'use strict' + +const assert = require('node:assert') + +const { kConstruct } = require('../../core/symbols') +const { urlEquals, getFieldValues } = require('./util') +const { kEnumerableProperty, isDisturbed } = require('../../core/util') +const { webidl } = require('../webidl') +const { cloneResponse, fromInnerResponse, getResponseState } = require('../fetch/response') +const { Request, fromInnerRequest, getRequestState } = require('../fetch/request') +const { fetching } = require('../fetch/index') +const { urlIsHttpHttpsScheme, readAllBytes } = require('../fetch/util') +const { createDeferredPromise } = require('../../util/promise') + +/** + * @see https://w3c.github.io/ServiceWorker/#dfn-cache-batch-operation + * @typedef {Object} CacheBatchOperation + * @property {'delete' | 'put'} type + * @property {any} request + * @property {any} response + * @property {import('../../../types/cache').CacheQueryOptions} options + */ + +/** + * @see https://w3c.github.io/ServiceWorker/#dfn-request-response-list + * @typedef {[any, any][]} requestResponseList + */ + +class Cache { + /** + * @see https://w3c.github.io/ServiceWorker/#dfn-relevant-request-response-list + * @type {requestResponseList} + */ + #relevantRequestResponseList + + constructor () { + if (arguments[0] !== kConstruct) { + webidl.illegalConstructor() + } + + webidl.util.markAsUncloneable(this) + this.#relevantRequestResponseList = arguments[1] + } + + async match (request, options = {}) { + webidl.brandCheck(this, Cache) + + const prefix = 'Cache.match' + webidl.argumentLengthCheck(arguments, 1, prefix) + + request = webidl.converters.RequestInfo(request) + options = webidl.converters.CacheQueryOptions(options, prefix, 'options') + + const p = this.#internalMatchAll(request, options, 1) + + if (p.length === 0) { + return + } + + return p[0] + } + + async matchAll (request = undefined, options = {}) { + webidl.brandCheck(this, Cache) + + const prefix = 'Cache.matchAll' + if (request !== undefined) request = webidl.converters.RequestInfo(request) + options = webidl.converters.CacheQueryOptions(options, prefix, 'options') + + return this.#internalMatchAll(request, options) + } + + async add (request) { + webidl.brandCheck(this, Cache) + + const prefix = 'Cache.add' + webidl.argumentLengthCheck(arguments, 1, prefix) + + request = webidl.converters.RequestInfo(request) + + // 1. + const requests = [request] + + // 2. + const responseArrayPromise = this.addAll(requests) + + // 3. + return await responseArrayPromise + } + + async addAll (requests) { + webidl.brandCheck(this, Cache) + + const prefix = 'Cache.addAll' + webidl.argumentLengthCheck(arguments, 1, prefix) + + // 1. + const responsePromises = [] + + // 2. + const requestList = [] + + // 3. + for (let request of requests) { + if (request === undefined) { + throw webidl.errors.conversionFailed({ + prefix, + argument: 'Argument 1', + types: ['undefined is not allowed'] + }) + } + + request = webidl.converters.RequestInfo(request) + + if (typeof request === 'string') { + continue + } + + // 3.1 + const r = getRequestState(request) + + // 3.2 + if (!urlIsHttpHttpsScheme(r.url) || r.method !== 'GET') { + throw webidl.errors.exception({ + header: prefix, + message: 'Expected http/s scheme when method is not GET.' + }) + } + } + + // 4. + /** @type {ReturnType[]} */ + const fetchControllers = [] + + // 5. + for (const request of requests) { + // 5.1 + const r = getRequestState(new Request(request)) + + // 5.2 + if (!urlIsHttpHttpsScheme(r.url)) { + throw webidl.errors.exception({ + header: prefix, + message: 'Expected http/s scheme.' + }) + } + + // 5.4 + r.initiator = 'fetch' + r.destination = 'subresource' + + // 5.5 + requestList.push(r) + + // 5.6 + const responsePromise = createDeferredPromise() + + // 5.7 + fetchControllers.push(fetching({ + request: r, + processResponse (response) { + // 1. + if (response.type === 'error' || response.status === 206 || response.status < 200 || response.status > 299) { + responsePromise.reject(webidl.errors.exception({ + header: 'Cache.addAll', + message: 'Received an invalid status code or the request failed.' + })) + } else if (response.headersList.contains('vary')) { // 2. + // 2.1 + const fieldValues = getFieldValues(response.headersList.get('vary')) + + // 2.2 + for (const fieldValue of fieldValues) { + // 2.2.1 + if (fieldValue === '*') { + responsePromise.reject(webidl.errors.exception({ + header: 'Cache.addAll', + message: 'invalid vary field value' + })) + + for (const controller of fetchControllers) { + controller.abort() + } + + return + } + } + } + }, + processResponseEndOfBody (response) { + // 1. + if (response.aborted) { + responsePromise.reject(new DOMException('aborted', 'AbortError')) + return + } + + // 2. + responsePromise.resolve(response) + } + })) + + // 5.8 + responsePromises.push(responsePromise.promise) + } + + // 6. + const p = Promise.all(responsePromises) + + // 7. + const responses = await p + + // 7.1 + const operations = [] + + // 7.2 + let index = 0 + + // 7.3 + for (const response of responses) { + // 7.3.1 + /** @type {CacheBatchOperation} */ + const operation = { + type: 'put', // 7.3.2 + request: requestList[index], // 7.3.3 + response // 7.3.4 + } + + operations.push(operation) // 7.3.5 + + index++ // 7.3.6 + } + + // 7.5 + const cacheJobPromise = createDeferredPromise() + + // 7.6.1 + let errorData = null + + // 7.6.2 + try { + this.#batchCacheOperations(operations) + } catch (e) { + errorData = e + } + + // 7.6.3 + queueMicrotask(() => { + // 7.6.3.1 + if (errorData === null) { + cacheJobPromise.resolve(undefined) + } else { + // 7.6.3.2 + cacheJobPromise.reject(errorData) + } + }) + + // 7.7 + return cacheJobPromise.promise + } + + async put (request, response) { + webidl.brandCheck(this, Cache) + + const prefix = 'Cache.put' + webidl.argumentLengthCheck(arguments, 2, prefix) + + request = webidl.converters.RequestInfo(request) + response = webidl.converters.Response(response, prefix, 'response') + + // 1. + let innerRequest = null + + // 2. + if (webidl.is.Request(request)) { + innerRequest = getRequestState(request) + } else { // 3. + innerRequest = getRequestState(new Request(request)) + } + + // 4. + if (!urlIsHttpHttpsScheme(innerRequest.url) || innerRequest.method !== 'GET') { + throw webidl.errors.exception({ + header: prefix, + message: 'Expected an http/s scheme when method is not GET' + }) + } + + // 5. + const innerResponse = getResponseState(response) + + // 6. + if (innerResponse.status === 206) { + throw webidl.errors.exception({ + header: prefix, + message: 'Got 206 status' + }) + } + + // 7. + if (innerResponse.headersList.contains('vary')) { + // 7.1. + const fieldValues = getFieldValues(innerResponse.headersList.get('vary')) + + // 7.2. + for (const fieldValue of fieldValues) { + // 7.2.1 + if (fieldValue === '*') { + throw webidl.errors.exception({ + header: prefix, + message: 'Got * vary field value' + }) + } + } + } + + // 8. + if (innerResponse.body && (isDisturbed(innerResponse.body.stream) || innerResponse.body.stream.locked)) { + throw webidl.errors.exception({ + header: prefix, + message: 'Response body is locked or disturbed' + }) + } + + // 9. + const clonedResponse = cloneResponse(innerResponse) + + // 10. + const bodyReadPromise = createDeferredPromise() + + // 11. + if (innerResponse.body != null) { + // 11.1 + const stream = innerResponse.body.stream + + // 11.2 + const reader = stream.getReader() + + // 11.3 + readAllBytes(reader, bodyReadPromise.resolve, bodyReadPromise.reject) + } else { + bodyReadPromise.resolve(undefined) + } + + // 12. + /** @type {CacheBatchOperation[]} */ + const operations = [] + + // 13. + /** @type {CacheBatchOperation} */ + const operation = { + type: 'put', // 14. + request: innerRequest, // 15. + response: clonedResponse // 16. + } + + // 17. + operations.push(operation) + + // 19. + const bytes = await bodyReadPromise.promise + + if (clonedResponse.body != null) { + clonedResponse.body.source = bytes + } + + // 19.1 + const cacheJobPromise = createDeferredPromise() + + // 19.2.1 + let errorData = null + + // 19.2.2 + try { + this.#batchCacheOperations(operations) + } catch (e) { + errorData = e + } + + // 19.2.3 + queueMicrotask(() => { + // 19.2.3.1 + if (errorData === null) { + cacheJobPromise.resolve() + } else { // 19.2.3.2 + cacheJobPromise.reject(errorData) + } + }) + + return cacheJobPromise.promise + } + + async delete (request, options = {}) { + webidl.brandCheck(this, Cache) + + const prefix = 'Cache.delete' + webidl.argumentLengthCheck(arguments, 1, prefix) + + request = webidl.converters.RequestInfo(request) + options = webidl.converters.CacheQueryOptions(options, prefix, 'options') + + /** + * @type {Request} + */ + let r = null + + if (webidl.is.Request(request)) { + r = getRequestState(request) + + if (r.method !== 'GET' && !options.ignoreMethod) { + return false + } + } else { + assert(typeof request === 'string') + + r = getRequestState(new Request(request)) + } + + /** @type {CacheBatchOperation[]} */ + const operations = [] + + /** @type {CacheBatchOperation} */ + const operation = { + type: 'delete', + request: r, + options + } + + operations.push(operation) + + const cacheJobPromise = createDeferredPromise() + + let errorData = null + let requestResponses + + try { + requestResponses = this.#batchCacheOperations(operations) + } catch (e) { + errorData = e + } + + queueMicrotask(() => { + if (errorData === null) { + cacheJobPromise.resolve(!!requestResponses?.length) + } else { + cacheJobPromise.reject(errorData) + } + }) + + return cacheJobPromise.promise + } + + /** + * @see https://w3c.github.io/ServiceWorker/#dom-cache-keys + * @param {any} request + * @param {import('../../../types/cache').CacheQueryOptions} options + * @returns {Promise} + */ + async keys (request = undefined, options = {}) { + webidl.brandCheck(this, Cache) + + const prefix = 'Cache.keys' + + if (request !== undefined) request = webidl.converters.RequestInfo(request) + options = webidl.converters.CacheQueryOptions(options, prefix, 'options') + + // 1. + let r = null + + // 2. + if (request !== undefined) { + // 2.1 + if (webidl.is.Request(request)) { + // 2.1.1 + r = getRequestState(request) + + // 2.1.2 + if (r.method !== 'GET' && !options.ignoreMethod) { + return [] + } + } else if (typeof request === 'string') { // 2.2 + r = getRequestState(new Request(request)) + } + } + + // 4. + const promise = createDeferredPromise() + + // 5. + // 5.1 + const requests = [] + + // 5.2 + if (request === undefined) { + // 5.2.1 + for (const requestResponse of this.#relevantRequestResponseList) { + // 5.2.1.1 + requests.push(requestResponse[0]) + } + } else { // 5.3 + // 5.3.1 + const requestResponses = this.#queryCache(r, options) + + // 5.3.2 + for (const requestResponse of requestResponses) { + // 5.3.2.1 + requests.push(requestResponse[0]) + } + } + + // 5.4 + queueMicrotask(() => { + // 5.4.1 + const requestList = [] + + // 5.4.2 + for (const request of requests) { + const requestObject = fromInnerRequest( + request, + undefined, + new AbortController().signal, + 'immutable' + ) + // 5.4.2.1 + requestList.push(requestObject) + } + + // 5.4.3 + promise.resolve(Object.freeze(requestList)) + }) + + return promise.promise + } + + /** + * @see https://w3c.github.io/ServiceWorker/#batch-cache-operations-algorithm + * @param {CacheBatchOperation[]} operations + * @returns {requestResponseList} + */ + #batchCacheOperations (operations) { + // 1. + const cache = this.#relevantRequestResponseList + + // 2. + const backupCache = [...cache] + + // 3. + const addedItems = [] + + // 4.1 + const resultList = [] + + try { + // 4.2 + for (const operation of operations) { + // 4.2.1 + if (operation.type !== 'delete' && operation.type !== 'put') { + throw webidl.errors.exception({ + header: 'Cache.#batchCacheOperations', + message: 'operation type does not match "delete" or "put"' + }) + } + + // 4.2.2 + if (operation.type === 'delete' && operation.response != null) { + throw webidl.errors.exception({ + header: 'Cache.#batchCacheOperations', + message: 'delete operation should not have an associated response' + }) + } + + // 4.2.3 + if (this.#queryCache(operation.request, operation.options, addedItems).length) { + throw new DOMException('???', 'InvalidStateError') + } + + // 4.2.4 + let requestResponses + + // 4.2.5 + if (operation.type === 'delete') { + // 4.2.5.1 + requestResponses = this.#queryCache(operation.request, operation.options) + + // TODO: the spec is wrong, this is needed to pass WPTs + if (requestResponses.length === 0) { + return [] + } + + // 4.2.5.2 + for (const requestResponse of requestResponses) { + const idx = cache.indexOf(requestResponse) + assert(idx !== -1) + + // 4.2.5.2.1 + cache.splice(idx, 1) + } + } else if (operation.type === 'put') { // 4.2.6 + // 4.2.6.1 + if (operation.response == null) { + throw webidl.errors.exception({ + header: 'Cache.#batchCacheOperations', + message: 'put operation should have an associated response' + }) + } + + // 4.2.6.2 + const r = operation.request + + // 4.2.6.3 + if (!urlIsHttpHttpsScheme(r.url)) { + throw webidl.errors.exception({ + header: 'Cache.#batchCacheOperations', + message: 'expected http or https scheme' + }) + } + + // 4.2.6.4 + if (r.method !== 'GET') { + throw webidl.errors.exception({ + header: 'Cache.#batchCacheOperations', + message: 'not get method' + }) + } + + // 4.2.6.5 + if (operation.options != null) { + throw webidl.errors.exception({ + header: 'Cache.#batchCacheOperations', + message: 'options must not be defined' + }) + } + + // 4.2.6.6 + requestResponses = this.#queryCache(operation.request) + + // 4.2.6.7 + for (const requestResponse of requestResponses) { + const idx = cache.indexOf(requestResponse) + assert(idx !== -1) + + // 4.2.6.7.1 + cache.splice(idx, 1) + } + + // 4.2.6.8 + cache.push([operation.request, operation.response]) + + // 4.2.6.10 + addedItems.push([operation.request, operation.response]) + } + + // 4.2.7 + resultList.push([operation.request, operation.response]) + } + + // 4.3 + return resultList + } catch (e) { // 5. + // 5.1 + this.#relevantRequestResponseList.length = 0 + + // 5.2 + this.#relevantRequestResponseList = backupCache + + // 5.3 + throw e + } + } + + /** + * @see https://w3c.github.io/ServiceWorker/#query-cache + * @param {any} requestQuery + * @param {import('../../../types/cache').CacheQueryOptions} options + * @param {requestResponseList} targetStorage + * @returns {requestResponseList} + */ + #queryCache (requestQuery, options, targetStorage) { + /** @type {requestResponseList} */ + const resultList = [] + + const storage = targetStorage ?? this.#relevantRequestResponseList + + for (const requestResponse of storage) { + const [cachedRequest, cachedResponse] = requestResponse + if (this.#requestMatchesCachedItem(requestQuery, cachedRequest, cachedResponse, options)) { + resultList.push(requestResponse) + } + } + + return resultList + } + + /** + * @see https://w3c.github.io/ServiceWorker/#request-matches-cached-item-algorithm + * @param {any} requestQuery + * @param {any} request + * @param {any | null} response + * @param {import('../../../types/cache').CacheQueryOptions | undefined} options + * @returns {boolean} + */ + #requestMatchesCachedItem (requestQuery, request, response = null, options) { + // if (options?.ignoreMethod === false && request.method === 'GET') { + // return false + // } + + const queryURL = new URL(requestQuery.url) + + const cachedURL = new URL(request.url) + + if (options?.ignoreSearch) { + cachedURL.search = '' + + queryURL.search = '' + } + + if (!urlEquals(queryURL, cachedURL, true)) { + return false + } + + if ( + response == null || + options?.ignoreVary || + !response.headersList.contains('vary') + ) { + return true + } + + const fieldValues = getFieldValues(response.headersList.get('vary')) + + for (const fieldValue of fieldValues) { + if (fieldValue === '*') { + return false + } + + const requestValue = request.headersList.get(fieldValue) + const queryValue = requestQuery.headersList.get(fieldValue) + + // If one has the header and the other doesn't, or one has + // a different value than the other, return false + if (requestValue !== queryValue) { + return false + } + } + + return true + } + + #internalMatchAll (request, options, maxResponses = Infinity) { + // 1. + let r = null + + // 2. + if (request !== undefined) { + if (webidl.is.Request(request)) { + // 2.1.1 + r = getRequestState(request) + + // 2.1.2 + if (r.method !== 'GET' && !options.ignoreMethod) { + return [] + } + } else if (typeof request === 'string') { + // 2.2.1 + r = getRequestState(new Request(request)) + } + } + + // 5. + // 5.1 + const responses = [] + + // 5.2 + if (request === undefined) { + // 5.2.1 + for (const requestResponse of this.#relevantRequestResponseList) { + responses.push(requestResponse[1]) + } + } else { // 5.3 + // 5.3.1 + const requestResponses = this.#queryCache(r, options) + + // 5.3.2 + for (const requestResponse of requestResponses) { + responses.push(requestResponse[1]) + } + } + + // 5.4 + // We don't implement CORs so we don't need to loop over the responses, yay! + + // 5.5.1 + const responseList = [] + + // 5.5.2 + for (const response of responses) { + // 5.5.2.1 + const responseObject = fromInnerResponse(cloneResponse(response), 'immutable') + + responseList.push(responseObject) + + if (responseList.length >= maxResponses) { + break + } + } + + // 6. + return Object.freeze(responseList) + } +} + +Object.defineProperties(Cache.prototype, { + [Symbol.toStringTag]: { + value: 'Cache', + configurable: true + }, + match: kEnumerableProperty, + matchAll: kEnumerableProperty, + add: kEnumerableProperty, + addAll: kEnumerableProperty, + put: kEnumerableProperty, + delete: kEnumerableProperty, + keys: kEnumerableProperty +}) + +const cacheQueryOptionConverters = [ + { + key: 'ignoreSearch', + converter: webidl.converters.boolean, + defaultValue: () => false + }, + { + key: 'ignoreMethod', + converter: webidl.converters.boolean, + defaultValue: () => false + }, + { + key: 'ignoreVary', + converter: webidl.converters.boolean, + defaultValue: () => false + } +] + +webidl.converters.CacheQueryOptions = webidl.dictionaryConverter(cacheQueryOptionConverters) + +webidl.converters.MultiCacheQueryOptions = webidl.dictionaryConverter([ + ...cacheQueryOptionConverters, + { + key: 'cacheName', + converter: webidl.converters.DOMString + } +]) + +webidl.converters.Response = webidl.interfaceConverter( + webidl.is.Response, + 'Response' +) + +webidl.converters['sequence'] = webidl.sequenceConverter( + webidl.converters.RequestInfo +) + +module.exports = { + Cache +} diff --git a/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/web/cache/cachestorage.js b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/web/cache/cachestorage.js new file mode 100644 index 000000000..c49b1e82e --- /dev/null +++ b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/web/cache/cachestorage.js @@ -0,0 +1,152 @@ +'use strict' + +const { Cache } = require('./cache') +const { webidl } = require('../webidl') +const { kEnumerableProperty } = require('../../core/util') +const { kConstruct } = require('../../core/symbols') + +class CacheStorage { + /** + * @see https://w3c.github.io/ServiceWorker/#dfn-relevant-name-to-cache-map + * @type {Map} + */ + async has (cacheName) { + webidl.brandCheck(this, CacheStorage) + + const prefix = 'CacheStorage.has' + webidl.argumentLengthCheck(arguments, 1, prefix) + + cacheName = webidl.converters.DOMString(cacheName, prefix, 'cacheName') + + // 2.1.1 + // 2.2 + return this.#caches.has(cacheName) + } + + /** + * @see https://w3c.github.io/ServiceWorker/#dom-cachestorage-open + * @param {string} cacheName + * @returns {Promise} + */ + async open (cacheName) { + webidl.brandCheck(this, CacheStorage) + + const prefix = 'CacheStorage.open' + webidl.argumentLengthCheck(arguments, 1, prefix) + + cacheName = webidl.converters.DOMString(cacheName, prefix, 'cacheName') + + // 2.1 + if (this.#caches.has(cacheName)) { + // await caches.open('v1') !== await caches.open('v1') + + // 2.1.1 + const cache = this.#caches.get(cacheName) + + // 2.1.1.1 + return new Cache(kConstruct, cache) + } + + // 2.2 + const cache = [] + + // 2.3 + this.#caches.set(cacheName, cache) + + // 2.4 + return new Cache(kConstruct, cache) + } + + /** + * @see https://w3c.github.io/ServiceWorker/#cache-storage-delete + * @param {string} cacheName + * @returns {Promise} + */ + async delete (cacheName) { + webidl.brandCheck(this, CacheStorage) + + const prefix = 'CacheStorage.delete' + webidl.argumentLengthCheck(arguments, 1, prefix) + + cacheName = webidl.converters.DOMString(cacheName, prefix, 'cacheName') + + return this.#caches.delete(cacheName) + } + + /** + * @see https://w3c.github.io/ServiceWorker/#cache-storage-keys + * @returns {Promise} + */ + async keys () { + webidl.brandCheck(this, CacheStorage) + + // 2.1 + const keys = this.#caches.keys() + + // 2.2 + return [...keys] + } +} + +Object.defineProperties(CacheStorage.prototype, { + [Symbol.toStringTag]: { + value: 'CacheStorage', + configurable: true + }, + match: kEnumerableProperty, + has: kEnumerableProperty, + open: kEnumerableProperty, + delete: kEnumerableProperty, + keys: kEnumerableProperty +}) + +module.exports = { + CacheStorage +} diff --git a/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/web/cache/util.js b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/web/cache/util.js new file mode 100644 index 000000000..5ac9d846d --- /dev/null +++ b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/web/cache/util.js @@ -0,0 +1,45 @@ +'use strict' + +const assert = require('node:assert') +const { URLSerializer } = require('../fetch/data-url') +const { isValidHeaderName } = require('../fetch/util') + +/** + * @see https://url.spec.whatwg.org/#concept-url-equals + * @param {URL} A + * @param {URL} B + * @param {boolean | undefined} excludeFragment + * @returns {boolean} + */ +function urlEquals (A, B, excludeFragment = false) { + const serializedA = URLSerializer(A, excludeFragment) + + const serializedB = URLSerializer(B, excludeFragment) + + return serializedA === serializedB +} + +/** + * @see https://github.com/chromium/chromium/blob/694d20d134cb553d8d89e5500b9148012b1ba299/content/browser/cache_storage/cache_storage_cache.cc#L260-L262 + * @param {string} header + */ +function getFieldValues (header) { + assert(header !== null) + + const values = [] + + for (let value of header.split(',')) { + value = value.trim() + + if (isValidHeaderName(value)) { + values.push(value) + } + } + + return values +} + +module.exports = { + urlEquals, + getFieldValues +} diff --git a/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/web/cookies/constants.js b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/web/cookies/constants.js new file mode 100644 index 000000000..85f1fec0e --- /dev/null +++ b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/web/cookies/constants.js @@ -0,0 +1,12 @@ +'use strict' + +// https://wicg.github.io/cookie-store/#cookie-maximum-attribute-value-size +const maxAttributeValueSize = 1024 + +// https://wicg.github.io/cookie-store/#cookie-maximum-name-value-pair-size +const maxNameValuePairSize = 4096 + +module.exports = { + maxAttributeValueSize, + maxNameValuePairSize +} diff --git a/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/web/cookies/index.js b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/web/cookies/index.js new file mode 100644 index 000000000..be99f6f3d --- /dev/null +++ b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/web/cookies/index.js @@ -0,0 +1,199 @@ +'use strict' + +const { parseSetCookie } = require('./parse') +const { stringify } = require('./util') +const { webidl } = require('../webidl') +const { Headers } = require('../fetch/headers') + +const brandChecks = webidl.brandCheckMultiple([Headers, globalThis.Headers].filter(Boolean)) + +/** + * @typedef {Object} Cookie + * @property {string} name + * @property {string} value + * @property {Date|number} [expires] + * @property {number} [maxAge] + * @property {string} [domain] + * @property {string} [path] + * @property {boolean} [secure] + * @property {boolean} [httpOnly] + * @property {'Strict'|'Lax'|'None'} [sameSite] + * @property {string[]} [unparsed] + */ + +/** + * @param {Headers} headers + * @returns {Record} + */ +function getCookies (headers) { + webidl.argumentLengthCheck(arguments, 1, 'getCookies') + + brandChecks(headers) + + const cookie = headers.get('cookie') + + /** @type {Record} */ + const out = {} + + if (!cookie) { + return out + } + + for (const piece of cookie.split(';')) { + const [name, ...value] = piece.split('=') + + out[name.trim()] = value.join('=') + } + + return out +} + +/** + * @param {Headers} headers + * @param {string} name + * @param {{ path?: string, domain?: string }|undefined} attributes + * @returns {void} + */ +function deleteCookie (headers, name, attributes) { + brandChecks(headers) + + const prefix = 'deleteCookie' + webidl.argumentLengthCheck(arguments, 2, prefix) + + name = webidl.converters.DOMString(name, prefix, 'name') + attributes = webidl.converters.DeleteCookieAttributes(attributes) + + // Matches behavior of + // https://github.com/denoland/deno_std/blob/63827b16330b82489a04614027c33b7904e08be5/http/cookie.ts#L278 + setCookie(headers, { + name, + value: '', + expires: new Date(0), + ...attributes + }) +} + +/** + * @param {Headers} headers + * @returns {Cookie[]} + */ +function getSetCookies (headers) { + webidl.argumentLengthCheck(arguments, 1, 'getSetCookies') + + brandChecks(headers) + + const cookies = headers.getSetCookie() + + if (!cookies) { + return [] + } + + return cookies.map((pair) => parseSetCookie(pair)) +} + +/** + * Parses a cookie string + * @param {string} cookie + */ +function parseCookie (cookie) { + cookie = webidl.converters.DOMString(cookie) + + return parseSetCookie(cookie) +} + +/** + * @param {Headers} headers + * @param {Cookie} cookie + * @returns {void} + */ +function setCookie (headers, cookie) { + webidl.argumentLengthCheck(arguments, 2, 'setCookie') + + brandChecks(headers) + + cookie = webidl.converters.Cookie(cookie) + + const str = stringify(cookie) + + if (str) { + headers.append('set-cookie', str, true) + } +} + +webidl.converters.DeleteCookieAttributes = webidl.dictionaryConverter([ + { + converter: webidl.nullableConverter(webidl.converters.DOMString), + key: 'path', + defaultValue: () => null + }, + { + converter: webidl.nullableConverter(webidl.converters.DOMString), + key: 'domain', + defaultValue: () => null + } +]) + +webidl.converters.Cookie = webidl.dictionaryConverter([ + { + converter: webidl.converters.DOMString, + key: 'name' + }, + { + converter: webidl.converters.DOMString, + key: 'value' + }, + { + converter: webidl.nullableConverter((value) => { + if (typeof value === 'number') { + return webidl.converters['unsigned long long'](value) + } + + return new Date(value) + }), + key: 'expires', + defaultValue: () => null + }, + { + converter: webidl.nullableConverter(webidl.converters['long long']), + key: 'maxAge', + defaultValue: () => null + }, + { + converter: webidl.nullableConverter(webidl.converters.DOMString), + key: 'domain', + defaultValue: () => null + }, + { + converter: webidl.nullableConverter(webidl.converters.DOMString), + key: 'path', + defaultValue: () => null + }, + { + converter: webidl.nullableConverter(webidl.converters.boolean), + key: 'secure', + defaultValue: () => null + }, + { + converter: webidl.nullableConverter(webidl.converters.boolean), + key: 'httpOnly', + defaultValue: () => null + }, + { + converter: webidl.converters.USVString, + key: 'sameSite', + allowedValues: ['Strict', 'Lax', 'None'] + }, + { + converter: webidl.sequenceConverter(webidl.converters.DOMString), + key: 'unparsed', + defaultValue: () => [] + } +]) + +module.exports = { + getCookies, + deleteCookie, + getSetCookies, + setCookie, + parseCookie +} diff --git a/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/web/cookies/parse.js b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/web/cookies/parse.js new file mode 100644 index 000000000..0e5249080 --- /dev/null +++ b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/web/cookies/parse.js @@ -0,0 +1,314 @@ +'use strict' + +const { collectASequenceOfCodePointsFast } = require('../infra') +const { maxNameValuePairSize, maxAttributeValueSize } = require('./constants') +const { isCTLExcludingHtab } = require('./util') +const assert = require('node:assert') + +/** + * @description Parses the field-value attributes of a set-cookie header string. + * @see https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4 + * @param {string} header + * @returns {import('./index').Cookie|null} if the header is invalid, null will be returned + */ +function parseSetCookie (header) { + // 1. If the set-cookie-string contains a %x00-08 / %x0A-1F / %x7F + // character (CTL characters excluding HTAB): Abort these steps and + // ignore the set-cookie-string entirely. + if (isCTLExcludingHtab(header)) { + return null + } + + let nameValuePair = '' + let unparsedAttributes = '' + let name = '' + let value = '' + + // 2. If the set-cookie-string contains a %x3B (";") character: + if (header.includes(';')) { + // 1. The name-value-pair string consists of the characters up to, + // but not including, the first %x3B (";"), and the unparsed- + // attributes consist of the remainder of the set-cookie-string + // (including the %x3B (";") in question). + const position = { position: 0 } + + nameValuePair = collectASequenceOfCodePointsFast(';', header, position) + unparsedAttributes = header.slice(position.position) + } else { + // Otherwise: + + // 1. The name-value-pair string consists of all the characters + // contained in the set-cookie-string, and the unparsed- + // attributes is the empty string. + nameValuePair = header + } + + // 3. If the name-value-pair string lacks a %x3D ("=") character, then + // the name string is empty, and the value string is the value of + // name-value-pair. + if (!nameValuePair.includes('=')) { + value = nameValuePair + } else { + // Otherwise, the name string consists of the characters up to, but + // not including, the first %x3D ("=") character, and the (possibly + // empty) value string consists of the characters after the first + // %x3D ("=") character. + const position = { position: 0 } + name = collectASequenceOfCodePointsFast( + '=', + nameValuePair, + position + ) + value = nameValuePair.slice(position.position + 1) + } + + // 4. Remove any leading or trailing WSP characters from the name + // string and the value string. + name = name.trim() + value = value.trim() + + // 5. If the sum of the lengths of the name string and the value string + // is more than 4096 octets, abort these steps and ignore the set- + // cookie-string entirely. + if (name.length + value.length > maxNameValuePairSize) { + return null + } + + // 6. The cookie-name is the name string, and the cookie-value is the + // value string. + // https://datatracker.ietf.org/doc/html/rfc6265 + // To maximize compatibility with user agents, servers that wish to + // store arbitrary data in a cookie-value SHOULD encode that data, for + // example, using Base64 [RFC4648]. + return { + name, value, ...parseUnparsedAttributes(unparsedAttributes) + } +} + +/** + * Parses the remaining attributes of a set-cookie header + * @see https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4 + * @param {string} unparsedAttributes + * @param {Object.} [cookieAttributeList={}] + */ +function parseUnparsedAttributes (unparsedAttributes, cookieAttributeList = {}) { + // 1. If the unparsed-attributes string is empty, skip the rest of + // these steps. + if (unparsedAttributes.length === 0) { + return cookieAttributeList + } + + // 2. Discard the first character of the unparsed-attributes (which + // will be a %x3B (";") character). + assert(unparsedAttributes[0] === ';') + unparsedAttributes = unparsedAttributes.slice(1) + + let cookieAv = '' + + // 3. If the remaining unparsed-attributes contains a %x3B (";") + // character: + if (unparsedAttributes.includes(';')) { + // 1. Consume the characters of the unparsed-attributes up to, but + // not including, the first %x3B (";") character. + cookieAv = collectASequenceOfCodePointsFast( + ';', + unparsedAttributes, + { position: 0 } + ) + unparsedAttributes = unparsedAttributes.slice(cookieAv.length) + } else { + // Otherwise: + + // 1. Consume the remainder of the unparsed-attributes. + cookieAv = unparsedAttributes + unparsedAttributes = '' + } + + // Let the cookie-av string be the characters consumed in this step. + + let attributeName = '' + let attributeValue = '' + + // 4. If the cookie-av string contains a %x3D ("=") character: + if (cookieAv.includes('=')) { + // 1. The (possibly empty) attribute-name string consists of the + // characters up to, but not including, the first %x3D ("=") + // character, and the (possibly empty) attribute-value string + // consists of the characters after the first %x3D ("=") + // character. + const position = { position: 0 } + + attributeName = collectASequenceOfCodePointsFast( + '=', + cookieAv, + position + ) + attributeValue = cookieAv.slice(position.position + 1) + } else { + // Otherwise: + + // 1. The attribute-name string consists of the entire cookie-av + // string, and the attribute-value string is empty. + attributeName = cookieAv + } + + // 5. Remove any leading or trailing WSP characters from the attribute- + // name string and the attribute-value string. + attributeName = attributeName.trim() + attributeValue = attributeValue.trim() + + // 6. If the attribute-value is longer than 1024 octets, ignore the + // cookie-av string and return to Step 1 of this algorithm. + if (attributeValue.length > maxAttributeValueSize) { + return parseUnparsedAttributes(unparsedAttributes, cookieAttributeList) + } + + // 7. Process the attribute-name and attribute-value according to the + // requirements in the following subsections. (Notice that + // attributes with unrecognized attribute-names are ignored.) + const attributeNameLowercase = attributeName.toLowerCase() + + // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.1 + // If the attribute-name case-insensitively matches the string + // "Expires", the user agent MUST process the cookie-av as follows. + if (attributeNameLowercase === 'expires') { + // 1. Let the expiry-time be the result of parsing the attribute-value + // as cookie-date (see Section 5.1.1). + const expiryTime = new Date(attributeValue) + + // 2. If the attribute-value failed to parse as a cookie date, ignore + // the cookie-av. + + cookieAttributeList.expires = expiryTime + } else if (attributeNameLowercase === 'max-age') { + // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.2 + // If the attribute-name case-insensitively matches the string "Max- + // Age", the user agent MUST process the cookie-av as follows. + + // 1. If the first character of the attribute-value is not a DIGIT or a + // "-" character, ignore the cookie-av. + const charCode = attributeValue.charCodeAt(0) + + if ((charCode < 48 || charCode > 57) && attributeValue[0] !== '-') { + return parseUnparsedAttributes(unparsedAttributes, cookieAttributeList) + } + + // 2. If the remainder of attribute-value contains a non-DIGIT + // character, ignore the cookie-av. + if (!/^\d+$/.test(attributeValue)) { + return parseUnparsedAttributes(unparsedAttributes, cookieAttributeList) + } + + // 3. Let delta-seconds be the attribute-value converted to an integer. + const deltaSeconds = Number(attributeValue) + + // 4. Let cookie-age-limit be the maximum age of the cookie (which + // SHOULD be 400 days or less, see Section 4.1.2.2). + + // 5. Set delta-seconds to the smaller of its present value and cookie- + // age-limit. + // deltaSeconds = Math.min(deltaSeconds * 1000, maxExpiresMs) + + // 6. If delta-seconds is less than or equal to zero (0), let expiry- + // time be the earliest representable date and time. Otherwise, let + // the expiry-time be the current date and time plus delta-seconds + // seconds. + // const expiryTime = deltaSeconds <= 0 ? Date.now() : Date.now() + deltaSeconds + + // 7. Append an attribute to the cookie-attribute-list with an + // attribute-name of Max-Age and an attribute-value of expiry-time. + cookieAttributeList.maxAge = deltaSeconds + } else if (attributeNameLowercase === 'domain') { + // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.3 + // If the attribute-name case-insensitively matches the string "Domain", + // the user agent MUST process the cookie-av as follows. + + // 1. Let cookie-domain be the attribute-value. + let cookieDomain = attributeValue + + // 2. If cookie-domain starts with %x2E ("."), let cookie-domain be + // cookie-domain without its leading %x2E ("."). + if (cookieDomain[0] === '.') { + cookieDomain = cookieDomain.slice(1) + } + + // 3. Convert the cookie-domain to lower case. + cookieDomain = cookieDomain.toLowerCase() + + // 4. Append an attribute to the cookie-attribute-list with an + // attribute-name of Domain and an attribute-value of cookie-domain. + cookieAttributeList.domain = cookieDomain + } else if (attributeNameLowercase === 'path') { + // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.4 + // If the attribute-name case-insensitively matches the string "Path", + // the user agent MUST process the cookie-av as follows. + + // 1. If the attribute-value is empty or if the first character of the + // attribute-value is not %x2F ("/"): + let cookiePath = '' + if (attributeValue.length === 0 || attributeValue[0] !== '/') { + // 1. Let cookie-path be the default-path. + cookiePath = '/' + } else { + // Otherwise: + + // 1. Let cookie-path be the attribute-value. + cookiePath = attributeValue + } + + // 2. Append an attribute to the cookie-attribute-list with an + // attribute-name of Path and an attribute-value of cookie-path. + cookieAttributeList.path = cookiePath + } else if (attributeNameLowercase === 'secure') { + // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.5 + // If the attribute-name case-insensitively matches the string "Secure", + // the user agent MUST append an attribute to the cookie-attribute-list + // with an attribute-name of Secure and an empty attribute-value. + + cookieAttributeList.secure = true + } else if (attributeNameLowercase === 'httponly') { + // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.6 + // If the attribute-name case-insensitively matches the string + // "HttpOnly", the user agent MUST append an attribute to the cookie- + // attribute-list with an attribute-name of HttpOnly and an empty + // attribute-value. + + cookieAttributeList.httpOnly = true + } else if (attributeNameLowercase === 'samesite') { + // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.7 + // If the attribute-name case-insensitively matches the string + // "SameSite", the user agent MUST process the cookie-av as follows: + + const attributeValueLowercase = attributeValue.toLowerCase() + + // 1. If cookie-av's attribute-value is a case-insensitive match for + // "None", append an attribute to the cookie-attribute-list with an + // attribute-name of "SameSite" and an attribute-value of "None". + if (attributeValueLowercase === 'none') { + cookieAttributeList.sameSite = 'None' + } else if (attributeValueLowercase === 'strict') { + // 2. If cookie-av's attribute-value is a case-insensitive match for + // "Strict", append an attribute to the cookie-attribute-list with + // an attribute-name of "SameSite" and an attribute-value of + // "Strict". + cookieAttributeList.sameSite = 'Strict' + } else if (attributeValueLowercase === 'lax') { + // 3. If cookie-av's attribute-value is a case-insensitive match for + // "Lax", append an attribute to the cookie-attribute-list with an + // attribute-name of "SameSite" and an attribute-value of "Lax". + cookieAttributeList.sameSite = 'Lax' + } + } else { + cookieAttributeList.unparsed ??= [] + + cookieAttributeList.unparsed.push(`${attributeName}=${attributeValue}`) + } + + // 8. Return to Step 1 of this algorithm. + return parseUnparsedAttributes(unparsedAttributes, cookieAttributeList) +} + +module.exports = { + parseSetCookie, + parseUnparsedAttributes +} diff --git a/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/web/cookies/util.js b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/web/cookies/util.js new file mode 100644 index 000000000..ca408e153 --- /dev/null +++ b/tests/fixtures/npm/cases/undici7/node_modules/undici/lib/web/cookies/util.js @@ -0,0 +1,352 @@ +'use strict' + +/** + * @param {string} value + * @returns {boolean} + */ +function isCTLExcludingHtab (value) { + for (let i = 0; i < value.length; ++i) { + const code = value.charCodeAt(i) + + if ( + (code >= 0x00 && code <= 0x08) || + (code >= 0x0A && code <= 0x1F) || + code === 0x7F + ) { + return true + } + } + return false +} + +/** + CHAR = + token = 1* + separators = "(" | ")" | "<" | ">" | "@" + | "," | ";" | ":" | "\" | <"> + | "/" | "[" | "]" | "?" | "=" + | "{" | "}" | SP | HT + * @param {string} name + */ +function validateCookieName (name) { + for (let i = 0; i < name.length; ++i) { + const code = name.charCodeAt(i) + + if ( + code < 0x21 || // exclude CTLs (0-31), SP and HT + code > 0x7E || // exclude non-ascii and DEL + code === 0x22 || // " + code === 0x28 || // ( + code === 0x29 || // ) + code === 0x3C || // < + code === 0x3E || // > + code === 0x40 || // @ + code === 0x2C || // , + code === 0x3B || // ; + code === 0x3A || // : + code === 0x5C || // \ + code === 0x2F || // / + code === 0x5B || // [ + code === 0x5D || // ] + code === 0x3F || // ? + code === 0x3D || // = + code === 0x7B || // { + code === 0x7D // } + ) { + throw new Error('Invalid cookie name') + } + } +} + +/** + cookie-value = *cookie-octet / ( DQUOTE *cookie-octet DQUOTE ) + cookie-octet = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E + ; US-ASCII characters excluding CTLs, + ; whitespace DQUOTE, comma, semicolon, + ; and backslash + * @param {string} value + */ +function validateCookieValue (value) { + let len = value.length + let i = 0 + + // if the value is wrapped in DQUOTE + if (value[0] === '"') { + if (len === 1 || value[len - 1] !== '"') { + throw new Error('Invalid cookie value') + } + --len + ++i + } + + while (i < len) { + const code = value.charCodeAt(i++) + + if ( + code < 0x21 || // exclude CTLs (0-31) + code > 0x7E || // non-ascii and DEL (127) + code === 0x22 || // " + code === 0x2C || // , + code === 0x3B || // ; + code === 0x5C // \ + ) { + throw new Error('Invalid cookie value') + } + } +} + +/** + * path-value = + * @param {string} path + */ +function validateCookiePath (path) { + for (let i = 0; i < path.length; ++i) { + const code = path.charCodeAt(i) + + if ( + code < 0x20 || // exclude CTLs (0-31) + code > 0x7E || // exclude DEL and non-ascii + code === 0x3B // ; + ) { + throw new Error('Invalid cookie path') + } + } +} + +/** + * ::= | + * + * ::= any one of the 52 alphabetic characters A through Z in + * upper case and a through z in lower case + * + * ::= any one of the ten digits 0 through 9r + * + * @see https://www.rfc-editor.org/rfc/rfc1034#section-3.5 + * @param {number} code + */ +function isLetterOrDigit (code) { + return ( + (code >= 0x30 && code <= 0x39) || // 0-9 + (code >= 0x41 && code <= 0x5A) || // A-Z + (code >= 0x61 && code <= 0x7A) // a-z + ) +} + +/** + * Validates a cookie domain against the "preferred name syntax". + * + * ::= | " " + * ::=