diff --git a/.gitignore b/.gitignore index 7b7bc57f2..fda9414aa 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,8 @@ node_modules/ !tests/fixtures/npm-static/nested/inner/node_modules/ !tests/fixtures/npm-static/node_modules/*/node_modules/ !tests/fixtures/npm/store/*/node_modules/ +!tests/fixtures/npm/node_modules/*/node_modules/ +!tests/fixtures/npm/workspace/*/node_modules/ !tests/fixtures/npm/cases/*/node_modules/ !tests/library-mode/*/node_modules/ !tests/fixtures/provenance/node_modules/ @@ -49,3 +51,6 @@ tmp-*/ .env.local .env* .env*.local +# the vendored fs-extra corpus package: its lib/output module and jsonfile's own universalify +!tests/corpus/fs-extra-package/node_modules/fs-extra/lib/output/ +!tests/corpus/fs-extra-package/node_modules/jsonfile/node_modules/ diff --git a/benchmarks/runtime/README.md b/benchmarks/runtime/README.md index f8552b003..92eb099db 100644 --- a/benchmarks/runtime/README.md +++ b/benchmarks/runtime/README.md @@ -4,33 +4,34 @@ Application-shaped workloads for measuring the speed of compiled executables. Ea | Workload | Category | Exercises | | --- | --- | --- | -| `json-records` | cpu | `JSON.stringify`, typed `JSON.parse`, Map aggregation | -| `regex-logs` | cpu | regex `exec` with groups, `test`, global `replace` | -| `ast-interp` | cpu | tokenizer, recursive-descent parser, class dispatch | -| `records-sort` | cpu | comparator sorts, group-by, `reduce`/`filter` | -| `template-render` | cpu | template literals, escaping, `map`/`join` | -| `functional-pipeline` | cpu | `filter`/`map`/`reduce`/`flatMap` chains, closures, small records | -| `async-pipeline` | cpu | `async`/`await`, `Promise.all` fan-out | -| `word-graph` | cpu | string-keyed Map/Set, BFS | | `alloc-trees` | cpu | allocation-heavy trees, immutable object-spread updates | -| `cyclic-graphs` | cpu | object graphs with back references built and dropped: trees with parent and sibling links, rings of records sharing a long-lived registry, closures capturing their owner | -| `numeric-kernels` | cpu | sieve, hashing, `number[][]` matmul, `Float64Array` | -| `typed-array-scan`, `typed-array-scan-workers` | cpu | element-by-element scan loops over `Uint8Array`, `Uint16Array`, `Int32Array` and `Float64Array`; the second entry is a worker-enabled executable (it can start a worker), so each access also checks for shared storage | -| `intern-table`, `intern-table-map`, `intern-table-1`, `intern-table-map-1` | cpu | worker threads interning the same type-like objects under string keys and records under number keys, then hits and misses: one shared `InternTable` per kind of key, or a module-level `Map` per thread (the `-map` entries), with 8 threads or 1 (the `-1` entries); under Node.js every thread has its own tables | -| `thread-slots`, `thread-slots-cache` | cpu | worker threads reading one published type graph from an `InternTable` and keeping per-thread state for each type they visit: a `ThreadSlots` family, or a direct-mapped cache by id in front of a `Map` by id (the `-cache` entry); under Node.js both are per thread | -| `channel-messages` | cpu | 200,000 MessageChannel messages in one thread, half numbers and half small objects, all posted before the port delivers any | +| `ast-interp` | cpu | tokenizer, recursive-descent parser, class dispatch | +| `async-pipeline` | cpu | `async`/`await`, `Promise.all` fan-out | | `atomics-contended` | cpu | four worker threads updating `SharedArrayBuffer` counters with `Atomics`: one shared counter, one counter per worker, a `compareExchange` loop, and `Atomics.wait`/`notify` handoffs | -| `value-objects` | cpu | small immutable records and class instances: vector helpers, method chains, particle updates | -| `value-records` | cpu | records that hold strings, spread state updates and destructured tuples: token helpers, an immutable reducer, tuple returns | -| `region-builds` | cpu | a watch-mode build loop over `LifetimeRegion`: each build binds a program in a new region and releases the previous one, which a cache keeps referenced for two builds | -| `optional-fields` | cpu | records and class instances with `number \| undefined` and other scalar-union fields | -| `record-boundaries` | cpu | records built from unchecked array reads passed to and returned from annotated functions | -| `number-maps` | cpu | number-keyed Map/Set: a dense-id table, sparse ids in many small maps, get/has/set mixes | -| `published-graphs` | cpu | syntax trees with parent links and a symbol table, published with `@scriptc/threads` and then read | -| `validate-errors` | cpu | throw/catch on a fraction of inputs, small call chains | +| `channel-messages` | cpu | 200,000 MessageChannel messages in one thread, half numbers and half small objects, all posted before the port delivers any | | `csv-numbers` | cpu | `split`, `Number`, `parseInt`, `toFixed` | +| `cyclic-graphs` | cpu | object graphs with back references built and dropped: trees with parent and sibling links, rings of records sharing a long-lived registry, closures capturing their owner | +| `functional-pipeline` | cpu | `filter`/`map`/`reduce`/`flatMap` chains, closures, small records | +| `intern-table`, `intern-table-map`, `intern-table-1`, `intern-table-map-1` | cpu | worker threads interning the same type-like objects under string keys and records under number keys, then hits and misses: one shared `InternTable` per kind of key, or a module-level `Map` per thread (the `-map` entries), with 8 threads or 1 (the `-1` entries); under Node.js every thread has its own tables | +| `json-records` | cpu | `JSON.stringify`, typed `JSON.parse`, Map aggregation | | `log-lines` | cpu | 200k immediate `console.log`/`process.stdout.write` lines into the runner's pipe | | `log-summary`, `inventory-report` | cpu | the multi-module applications from `benchmarks/builds`, on 300k-line inputs | +| `number-maps` | cpu | number-keyed Map/Set: a dense-id table, sparse ids in many small maps, get/has/set mixes | +| `numeric-kernels` | cpu | sieve, hashing, `number[][]` matmul, `Float64Array` | +| `optional-fields` | cpu | records and class instances with `number \| undefined` and other scalar-union fields | +| `published-graphs` | cpu | syntax trees with parent links and a symbol table, published with `@scriptc/threads` and then read | +| `record-boundaries` | cpu | records built from unchecked array reads passed to and returned from annotated functions | +| `records-sort` | cpu | comparator sorts, group-by, `reduce`/`filter` | +| `regex-logs` | cpu | regex `exec` with groups, `test`, global `replace` | +| `region-builds` | cpu | a watch-mode build loop over `LifetimeRegion`: each build binds a program in a new region and releases the previous one, which a cache keeps referenced for two builds | +| `template-render` | cpu | template literals, escaping, `map`/`join` | +| `thread-slots`, `thread-slots-cache` | cpu | worker threads reading one published type graph from an `InternTable` and keeping per-thread state for each type they visit: a `ThreadSlots` family, or a direct-mapped cache by id in front of a `Map` by id (the `-cache` entry); under Node.js both are per thread | +| `typed-array-scan`, `typed-array-scan-workers` | cpu | element-by-element scan loops over `Uint8Array`, `Uint16Array`, `Int32Array` and `Float64Array`; the second entry is a worker-enabled executable (it can start a worker), so each access also checks for shared storage | +| `unicode-lexer` | cpu | a hand-written lexer over a 2.7M-unit bundled module whose comments and strings are in several languages: `charCodeAt` scans, line starts | +| `validate-errors` | cpu | throw/catch on a fraction of inputs, small call chains | +| `value-objects` | cpu | small immutable records and class instances: vector helpers, method chains, particle updates | +| `value-records` | cpu | records that hold strings, spread state updates and destructured tuples: token helpers, an immutable reducer, tuple returns | +| `word-graph` | cpu | string-keyed Map/Set, BFS | | `cli-config` | startup | a `deploy-plan` CLI: read a ~110 KB JSON manifest, validate, interpolate env values, order 80 services into dependency waves, print a summary | | `http-api` | server | a `node:http` JSON API: routing, query parsing, a seeded catalog, per-cart state, typed JSON bodies, `ETag`/`If-None-Match`, auth and request-id headers | @@ -46,7 +47,7 @@ Node is timed in the same interleaved loop and every row reports candidate/Node; ## Workload kinds -Entries in `workloads.json` are process workloads unless they set `kind`. +Each workload is a JSON file in `workloads/`, named after the workload, that holds its `entry`, `args` and options; adding a workload adds its file and its row in the table above, kept in category and then name order. Workloads are process workloads unless they set `kind`. Process workloads time whole launches (wall time, startup included). An optional `launches` count makes each sample the median of that many sequential launches; `cli-config` uses 20 so that a startup-dominated command (a few milliseconds compiled, tens of milliseconds under Node) is measured with low noise. Its table row is labeled `(per launch)`. On a heavily loaded macOS host, whole batches of launches can run several times slower (samples become bimodal), so judge startup changes on the Linux lane or a quiet machine. diff --git a/benchmarks/runtime/workloads.json b/benchmarks/runtime/workloads.json deleted file mode 100644 index 9a3f1bc2f..000000000 --- a/benchmarks/runtime/workloads.json +++ /dev/null @@ -1,283 +0,0 @@ -{ - "workloads": [ - { - "name": "json-records", - "entry": "workloads/json-records.ts", - "args": [ - "4" - ] - }, - { - "name": "regex-logs", - "entry": "workloads/regex-logs.ts", - "args": [ - "1" - ] - }, - { - "name": "ast-interp", - "entry": "workloads/ast-interp.ts", - "args": [ - "5" - ] - }, - { - "name": "records-sort", - "entry": "workloads/records-sort.ts", - "args": [ - "1" - ] - }, - { - "name": "template-render", - "entry": "workloads/template-render.ts", - "args": [ - "1" - ] - }, - { - "name": "functional-pipeline", - "entry": "workloads/functional-pipeline.ts", - "args": [ - "1" - ] - }, - { - "name": "async-pipeline", - "entry": "workloads/async-pipeline.ts", - "args": [ - "1" - ] - }, - { - "name": "word-graph", - "entry": "workloads/word-graph.ts", - "args": [ - "2" - ] - }, - { - "name": "alloc-trees", - "entry": "workloads/alloc-trees.ts", - "args": [ - "1" - ] - }, - { - "name": "cyclic-graphs", - "entry": "workloads/cyclic-graphs.ts", - "args": [ - "3" - ] - }, - { - "name": "numeric-kernels", - "entry": "workloads/numeric-kernels.ts", - "args": [ - "0.5" - ] - }, - { - "name": "typed-array-scan", - "entry": "workloads/typed-array-scan.ts", - "args": [ - "3" - ] - }, - { - "name": "typed-array-scan-workers", - "entry": "workloads/typed-array-scan-workers.ts", - "args": [ - "3" - ] - }, - { - "name": "intern-table", - "entry": "workloads/intern-table.ts", - "args": [ - "table", - "8", - "100000" - ] - }, - { - "name": "intern-table-map", - "entry": "workloads/intern-table.ts", - "args": [ - "map", - "8", - "100000" - ] - }, - { - "name": "intern-table-1", - "entry": "workloads/intern-table.ts", - "args": [ - "table", - "1", - "100000" - ] - }, - { - "name": "intern-table-map-1", - "entry": "workloads/intern-table.ts", - "args": [ - "map", - "1", - "100000" - ] - }, - { - "name": "thread-slots", - "entry": "workloads/thread-slots.ts", - "args": [ - "slots", - "8", - "50000", - "20" - ] - }, - { - "name": "thread-slots-cache", - "entry": "workloads/thread-slots.ts", - "args": [ - "cache", - "8", - "50000", - "20" - ] - }, - { - "name": "channel-messages", - "entry": "workloads/channel-messages.ts", - "args": [ - "200000" - ] - }, - { - "name": "atomics-contended", - "entry": "workloads/atomics-contended.ts", - "args": [ - "2" - ] - }, - { - "name": "value-objects", - "entry": "workloads/value-objects.ts", - "args": [ - "2" - ] - }, - { - "name": "value-records", - "entry": "workloads/value-records.ts", - "args": [ - "1" - ] - }, - { - "name": "region-builds", - "entry": "workloads/region-builds.ts", - "args": [ - "1" - ] - }, - { - "name": "optional-fields", - "entry": "workloads/optional-fields.ts", - "args": [ - "10" - ] - }, - { - "name": "record-boundaries", - "entry": "workloads/record-boundaries.ts", - "args": [ - "8" - ] - }, - { - "name": "number-maps", - "entry": "workloads/number-maps.ts", - "args": [ - "4" - ] - }, - { - "name": "published-graphs", - "entry": "workloads/published-graphs.ts", - "args": [ - "1" - ] - }, - { - "name": "validate-errors", - "entry": "workloads/validate-errors.ts", - "args": [ - "1" - ] - }, - { - "name": "csv-numbers", - "entry": "workloads/csv-numbers.ts", - "args": [ - "1" - ] - }, - { - "name": "log-lines", - "entry": "workloads/log-lines.ts", - "args": [ - "4" - ] - }, - { - "name": "log-summary", - "entry": "../builds/log-summary/main.ts", - "input": "log-summary", - "args": [ - "{input}", - "--min-status", - "200", - "--limit", - "12" - ] - }, - { - "name": "inventory-report", - "entry": "../builds/inventory-report/main.ts", - "input": "inventory-report", - "args": [ - "{input}" - ] - }, - { - "name": "cli-config", - "entry": "workloads/cli-config.ts", - "input": "cli-config", - "launches": 20, - "args": [ - "{input}", - "--env", - "production", - "--max-wave", - "12" - ] - }, - { - "name": "http-api", - "kind": "server", - "entry": "workloads/http-api.ts", - "args": [], - "client": { - "script": "clients/http-load.mjs", - "args": [ - "--connections=64", - "--threads=2", - "--warmup=4000", - "--requests=40000" - ] - } - } - ] -} diff --git a/benchmarks/runtime/workloads.mjs b/benchmarks/runtime/workloads.mjs new file mode 100644 index 000000000..e63ee98eb --- /dev/null +++ b/benchmarks/runtime/workloads.mjs @@ -0,0 +1,33 @@ +/* The runtime benchmark workloads: one JSON file per workload in + * workloads/, named after the workload, next to the entry programs. A file + * holds the workload's entry, args and options; changes that add + * different workloads add different files. */ +import { existsSync, readdirSync, readFileSync } from "node:fs"; +import { join } from "node:path"; + +const CATEGORIES = ["cpu", "startup", "server"]; + +/* Server workloads run a listening program; startup-bound ones time + * several sequential launches per sample. */ +export const workloadCategory = (w) => + w.kind === "server" ? "server" : (w.launches ?? 1) > 1 ? "startup" : "cpu"; + +/* The workloads of the suite at `suite` (a benchmarks/runtime directory), + * by category and then name. A checkout from before the per-workload files + * lists them in workloads.json, in its own order. */ +export function readWorkloads(suite) { + const legacy = join(suite, "workloads.json"); + if (existsSync(legacy)) return JSON.parse(readFileSync(legacy, "utf8")).workloads; + const dir = join(suite, "workloads"); + return readdirSync(dir) + .filter((file) => file.endsWith(".json")) + .map((file) => ({ + name: file.slice(0, -".json".length), + ...JSON.parse(readFileSync(join(dir, file), "utf8")), + })) + .sort( + (a, b) => + CATEGORIES.indexOf(workloadCategory(a)) - CATEGORIES.indexOf(workloadCategory(b)) || + (a.name < b.name ? -1 : a.name > b.name ? 1 : 0), + ); +} diff --git a/benchmarks/runtime/workloads/alloc-trees.json b/benchmarks/runtime/workloads/alloc-trees.json new file mode 100644 index 000000000..d4ae2ae72 --- /dev/null +++ b/benchmarks/runtime/workloads/alloc-trees.json @@ -0,0 +1,6 @@ +{ + "entry": "workloads/alloc-trees.ts", + "args": [ + "1" + ] +} diff --git a/benchmarks/runtime/workloads/ast-interp.json b/benchmarks/runtime/workloads/ast-interp.json new file mode 100644 index 000000000..1817541ac --- /dev/null +++ b/benchmarks/runtime/workloads/ast-interp.json @@ -0,0 +1,6 @@ +{ + "entry": "workloads/ast-interp.ts", + "args": [ + "5" + ] +} diff --git a/benchmarks/runtime/workloads/async-pipeline.json b/benchmarks/runtime/workloads/async-pipeline.json new file mode 100644 index 000000000..610f62793 --- /dev/null +++ b/benchmarks/runtime/workloads/async-pipeline.json @@ -0,0 +1,6 @@ +{ + "entry": "workloads/async-pipeline.ts", + "args": [ + "1" + ] +} diff --git a/benchmarks/runtime/workloads/atomics-contended.json b/benchmarks/runtime/workloads/atomics-contended.json new file mode 100644 index 000000000..7f78d265b --- /dev/null +++ b/benchmarks/runtime/workloads/atomics-contended.json @@ -0,0 +1,6 @@ +{ + "entry": "workloads/atomics-contended.ts", + "args": [ + "2" + ] +} diff --git a/benchmarks/runtime/workloads/channel-messages.json b/benchmarks/runtime/workloads/channel-messages.json new file mode 100644 index 000000000..56b84dda4 --- /dev/null +++ b/benchmarks/runtime/workloads/channel-messages.json @@ -0,0 +1,6 @@ +{ + "entry": "workloads/channel-messages.ts", + "args": [ + "200000" + ] +} diff --git a/benchmarks/runtime/workloads/cli-config.json b/benchmarks/runtime/workloads/cli-config.json new file mode 100644 index 000000000..db7a4c645 --- /dev/null +++ b/benchmarks/runtime/workloads/cli-config.json @@ -0,0 +1,12 @@ +{ + "entry": "workloads/cli-config.ts", + "input": "cli-config", + "launches": 20, + "args": [ + "{input}", + "--env", + "production", + "--max-wave", + "12" + ] +} diff --git a/benchmarks/runtime/workloads/csv-numbers.json b/benchmarks/runtime/workloads/csv-numbers.json new file mode 100644 index 000000000..acb3c21d7 --- /dev/null +++ b/benchmarks/runtime/workloads/csv-numbers.json @@ -0,0 +1,6 @@ +{ + "entry": "workloads/csv-numbers.ts", + "args": [ + "1" + ] +} diff --git a/benchmarks/runtime/workloads/cyclic-graphs.json b/benchmarks/runtime/workloads/cyclic-graphs.json new file mode 100644 index 000000000..2e62f7fdf --- /dev/null +++ b/benchmarks/runtime/workloads/cyclic-graphs.json @@ -0,0 +1,6 @@ +{ + "entry": "workloads/cyclic-graphs.ts", + "args": [ + "3" + ] +} diff --git a/benchmarks/runtime/workloads/functional-pipeline.json b/benchmarks/runtime/workloads/functional-pipeline.json new file mode 100644 index 000000000..5ee3af902 --- /dev/null +++ b/benchmarks/runtime/workloads/functional-pipeline.json @@ -0,0 +1,6 @@ +{ + "entry": "workloads/functional-pipeline.ts", + "args": [ + "1" + ] +} diff --git a/benchmarks/runtime/workloads/http-api.json b/benchmarks/runtime/workloads/http-api.json new file mode 100644 index 000000000..145166945 --- /dev/null +++ b/benchmarks/runtime/workloads/http-api.json @@ -0,0 +1,14 @@ +{ + "kind": "server", + "entry": "workloads/http-api.ts", + "args": [], + "client": { + "script": "clients/http-load.mjs", + "args": [ + "--connections=64", + "--threads=2", + "--warmup=4000", + "--requests=40000" + ] + } +} diff --git a/benchmarks/runtime/workloads/intern-table-1.json b/benchmarks/runtime/workloads/intern-table-1.json new file mode 100644 index 000000000..8a2b253f6 --- /dev/null +++ b/benchmarks/runtime/workloads/intern-table-1.json @@ -0,0 +1,8 @@ +{ + "entry": "workloads/intern-table.ts", + "args": [ + "table", + "1", + "100000" + ] +} diff --git a/benchmarks/runtime/workloads/intern-table-map-1.json b/benchmarks/runtime/workloads/intern-table-map-1.json new file mode 100644 index 000000000..fefafac67 --- /dev/null +++ b/benchmarks/runtime/workloads/intern-table-map-1.json @@ -0,0 +1,8 @@ +{ + "entry": "workloads/intern-table.ts", + "args": [ + "map", + "1", + "100000" + ] +} diff --git a/benchmarks/runtime/workloads/intern-table-map.json b/benchmarks/runtime/workloads/intern-table-map.json new file mode 100644 index 000000000..136c6c31e --- /dev/null +++ b/benchmarks/runtime/workloads/intern-table-map.json @@ -0,0 +1,8 @@ +{ + "entry": "workloads/intern-table.ts", + "args": [ + "map", + "8", + "100000" + ] +} diff --git a/benchmarks/runtime/workloads/intern-table.json b/benchmarks/runtime/workloads/intern-table.json new file mode 100644 index 000000000..86fa11413 --- /dev/null +++ b/benchmarks/runtime/workloads/intern-table.json @@ -0,0 +1,8 @@ +{ + "entry": "workloads/intern-table.ts", + "args": [ + "table", + "8", + "100000" + ] +} diff --git a/benchmarks/runtime/workloads/inventory-report.json b/benchmarks/runtime/workloads/inventory-report.json new file mode 100644 index 000000000..e75f5e3e5 --- /dev/null +++ b/benchmarks/runtime/workloads/inventory-report.json @@ -0,0 +1,7 @@ +{ + "entry": "../builds/inventory-report/main.ts", + "input": "inventory-report", + "args": [ + "{input}" + ] +} diff --git a/benchmarks/runtime/workloads/json-records.json b/benchmarks/runtime/workloads/json-records.json new file mode 100644 index 000000000..3263ac068 --- /dev/null +++ b/benchmarks/runtime/workloads/json-records.json @@ -0,0 +1,6 @@ +{ + "entry": "workloads/json-records.ts", + "args": [ + "4" + ] +} diff --git a/benchmarks/runtime/workloads/log-lines.json b/benchmarks/runtime/workloads/log-lines.json new file mode 100644 index 000000000..635bcc49f --- /dev/null +++ b/benchmarks/runtime/workloads/log-lines.json @@ -0,0 +1,6 @@ +{ + "entry": "workloads/log-lines.ts", + "args": [ + "4" + ] +} diff --git a/benchmarks/runtime/workloads/log-summary.json b/benchmarks/runtime/workloads/log-summary.json new file mode 100644 index 000000000..274a1cff6 --- /dev/null +++ b/benchmarks/runtime/workloads/log-summary.json @@ -0,0 +1,11 @@ +{ + "entry": "../builds/log-summary/main.ts", + "input": "log-summary", + "args": [ + "{input}", + "--min-status", + "200", + "--limit", + "12" + ] +} diff --git a/benchmarks/runtime/workloads/number-maps.json b/benchmarks/runtime/workloads/number-maps.json new file mode 100644 index 000000000..2adfbcc47 --- /dev/null +++ b/benchmarks/runtime/workloads/number-maps.json @@ -0,0 +1,6 @@ +{ + "entry": "workloads/number-maps.ts", + "args": [ + "4" + ] +} diff --git a/benchmarks/runtime/workloads/numeric-kernels.json b/benchmarks/runtime/workloads/numeric-kernels.json new file mode 100644 index 000000000..04718346b --- /dev/null +++ b/benchmarks/runtime/workloads/numeric-kernels.json @@ -0,0 +1,6 @@ +{ + "entry": "workloads/numeric-kernels.ts", + "args": [ + "0.5" + ] +} diff --git a/benchmarks/runtime/workloads/optional-fields.json b/benchmarks/runtime/workloads/optional-fields.json new file mode 100644 index 000000000..c54d87437 --- /dev/null +++ b/benchmarks/runtime/workloads/optional-fields.json @@ -0,0 +1,6 @@ +{ + "entry": "workloads/optional-fields.ts", + "args": [ + "10" + ] +} diff --git a/benchmarks/runtime/workloads/published-graphs.json b/benchmarks/runtime/workloads/published-graphs.json new file mode 100644 index 000000000..b22f5a0db --- /dev/null +++ b/benchmarks/runtime/workloads/published-graphs.json @@ -0,0 +1,6 @@ +{ + "entry": "workloads/published-graphs.ts", + "args": [ + "1" + ] +} diff --git a/benchmarks/runtime/workloads/record-boundaries.json b/benchmarks/runtime/workloads/record-boundaries.json new file mode 100644 index 000000000..a35c346eb --- /dev/null +++ b/benchmarks/runtime/workloads/record-boundaries.json @@ -0,0 +1,6 @@ +{ + "entry": "workloads/record-boundaries.ts", + "args": [ + "8" + ] +} diff --git a/benchmarks/runtime/workloads/records-sort.json b/benchmarks/runtime/workloads/records-sort.json new file mode 100644 index 000000000..fd0097fe2 --- /dev/null +++ b/benchmarks/runtime/workloads/records-sort.json @@ -0,0 +1,6 @@ +{ + "entry": "workloads/records-sort.ts", + "args": [ + "1" + ] +} diff --git a/benchmarks/runtime/workloads/regex-logs.json b/benchmarks/runtime/workloads/regex-logs.json new file mode 100644 index 000000000..636db124e --- /dev/null +++ b/benchmarks/runtime/workloads/regex-logs.json @@ -0,0 +1,6 @@ +{ + "entry": "workloads/regex-logs.ts", + "args": [ + "1" + ] +} diff --git a/benchmarks/runtime/workloads/region-builds.json b/benchmarks/runtime/workloads/region-builds.json new file mode 100644 index 000000000..dc53dbc36 --- /dev/null +++ b/benchmarks/runtime/workloads/region-builds.json @@ -0,0 +1,6 @@ +{ + "entry": "workloads/region-builds.ts", + "args": [ + "1" + ] +} diff --git a/benchmarks/runtime/workloads/template-render.json b/benchmarks/runtime/workloads/template-render.json new file mode 100644 index 000000000..9897494f6 --- /dev/null +++ b/benchmarks/runtime/workloads/template-render.json @@ -0,0 +1,6 @@ +{ + "entry": "workloads/template-render.ts", + "args": [ + "1" + ] +} diff --git a/benchmarks/runtime/workloads/thread-slots-cache.json b/benchmarks/runtime/workloads/thread-slots-cache.json new file mode 100644 index 000000000..0eb6e11ce --- /dev/null +++ b/benchmarks/runtime/workloads/thread-slots-cache.json @@ -0,0 +1,9 @@ +{ + "entry": "workloads/thread-slots.ts", + "args": [ + "cache", + "8", + "50000", + "20" + ] +} diff --git a/benchmarks/runtime/workloads/thread-slots.json b/benchmarks/runtime/workloads/thread-slots.json new file mode 100644 index 000000000..b8ed5b140 --- /dev/null +++ b/benchmarks/runtime/workloads/thread-slots.json @@ -0,0 +1,9 @@ +{ + "entry": "workloads/thread-slots.ts", + "args": [ + "slots", + "8", + "50000", + "20" + ] +} diff --git a/benchmarks/runtime/workloads/typed-array-scan-workers.json b/benchmarks/runtime/workloads/typed-array-scan-workers.json new file mode 100644 index 000000000..c01893a2c --- /dev/null +++ b/benchmarks/runtime/workloads/typed-array-scan-workers.json @@ -0,0 +1,6 @@ +{ + "entry": "workloads/typed-array-scan-workers.ts", + "args": [ + "3" + ] +} diff --git a/benchmarks/runtime/workloads/typed-array-scan.json b/benchmarks/runtime/workloads/typed-array-scan.json new file mode 100644 index 000000000..0b2ba0592 --- /dev/null +++ b/benchmarks/runtime/workloads/typed-array-scan.json @@ -0,0 +1,6 @@ +{ + "entry": "workloads/typed-array-scan.ts", + "args": [ + "3" + ] +} diff --git a/benchmarks/runtime/workloads/unicode-lexer.json b/benchmarks/runtime/workloads/unicode-lexer.json new file mode 100644 index 000000000..91ccab087 --- /dev/null +++ b/benchmarks/runtime/workloads/unicode-lexer.json @@ -0,0 +1,6 @@ +{ + "entry": "workloads/unicode-lexer.ts", + "args": [ + "1" + ] +} diff --git a/benchmarks/runtime/workloads/unicode-lexer.ts b/benchmarks/runtime/workloads/unicode-lexer.ts new file mode 100644 index 000000000..21638bb86 --- /dev/null +++ b/benchmarks/runtime/workloads/unicode-lexer.ts @@ -0,0 +1,116 @@ +// A lexer over a large bundled module whose comments and strings are in +// several languages: line starts, tokens by kind and comment text, read +// with charCodeAt as hand-written scanners do. Each pass relexes the whole +// text, as a bundler or an editor does after a change. +const comments = [ + "// Berechnet die Größe des Puffers für übergroße Einträge", + "// Calcule la taille maximale avant la prochaine itération", + "// バッファのサイズを計算します", + "/* 计算缓冲区大小,然后返回结果 */", + "// Returns the cached value (fast path) — see the note above", + "/** Résumé: the état of the café's ñandú registry */", +]; +const strings = ['"Grüße"', '"naïve façade"', '"東京"', '"ok ✓"', '"plain ascii"', "'mañana'"]; +const names = ["buffer", "größe", "index", "resultat", "count", "value", "entry", "table"]; + +let seed = 11; +function random(): number { + seed = (Math.imul(seed, 1103515245) + 12345) >>> 0; + return seed / 4294967296; +} +function pick(list: readonly string[]): string { + return list[Math.floor(random() * list.length)]!; +} + +function makeSource(lines: number): string { + const out: string[] = []; + for (let i = 0; i < lines; i++) { + const r = random(); + if (r < 0.2) out.push(pick(comments)); + else if (r < 0.6) out.push(` const ${pick(names)}${i} = ${pick(names)}(${Math.floor(random() * 1000)}, ${pick(strings)});`); + else if (r < 0.85) out.push(` if (${pick(names)} >= ${i % 97}) return ${pick(names)}.${pick(names)} + 1;`); + else out.push(`function ${pick(names)}${i}(a, b) { return a * b - ${i}; }`); + } + return out.join("\n"); +} + +interface Counts { + lines: number; + identifiers: number; + numbers: number; + strings: number; + comments: number; + commentChars: number; + punctuation: number; +} + +function isIdentifierStart(ch: number): boolean { + return (ch >= 97 && ch <= 122) || (ch >= 65 && ch <= 90) || ch === 95 || ch === 36 || ch > 127; +} + +function isIdentifierPart(ch: number): boolean { + return isIdentifierStart(ch) || (ch >= 48 && ch <= 57); +} + +function lex(text: string): Counts { + const counts: Counts = { lines: 1, identifiers: 0, numbers: 0, strings: 0, comments: 0, commentChars: 0, punctuation: 0 }; + let pos = 0; + const end = text.length; + while (pos < end) { + const ch = text.charCodeAt(pos); + if (ch === 10) { + counts.lines++; + pos++; + } else if (ch === 32 || ch === 9 || ch === 13) { + pos++; + } else if (ch === 47 && text.charCodeAt(pos + 1) === 47) { + const start = pos; + while (pos < end && text.charCodeAt(pos) !== 10) pos++; + counts.comments++; + counts.commentChars += pos - start; + } else if (ch === 47 && text.charCodeAt(pos + 1) === 42) { + const start = pos; + pos += 2; + while (pos < end && !(text.charCodeAt(pos) === 42 && text.charCodeAt(pos + 1) === 47)) { + if (text.charCodeAt(pos) === 10) counts.lines++; + pos++; + } + pos += 2; + counts.comments++; + counts.commentChars += pos - start; + } else if (ch === 34 || ch === 39) { + pos++; + while (pos < end && text.charCodeAt(pos) !== ch) pos++; + pos++; + counts.strings++; + } else if (ch >= 48 && ch <= 57) { + while (pos < end && text.charCodeAt(pos) >= 48 && text.charCodeAt(pos) <= 57) pos++; + counts.numbers++; + } else if (isIdentifierStart(ch)) { + while (pos < end && isIdentifierPart(text.charCodeAt(pos))) pos++; + counts.identifiers++; + } else { + counts.punctuation++; + pos++; + } + } + return counts; +} + +function lineStarts(text: string): number[] { + const starts = [0]; + for (let pos = 0; pos < text.length; pos++) if (text.charCodeAt(pos) === 10) starts.push(pos + 1); + return starts; +} + +const scale = Number(process.argv[2] ?? "1"); +const source = makeSource(60000); +const passes = Math.round(6 * scale); +let last = lex(source); +let total = last.identifiers + lineStarts(source).length; +for (let i = 1; i < passes; i++) { + last = lex(source); + total += last.identifiers + lineStarts(source).length; +} +console.log("units", source.length, "passes", passes, "total", total); +console.log(JSON.stringify(last)); diff --git a/benchmarks/runtime/workloads/validate-errors.json b/benchmarks/runtime/workloads/validate-errors.json new file mode 100644 index 000000000..b8d457897 --- /dev/null +++ b/benchmarks/runtime/workloads/validate-errors.json @@ -0,0 +1,6 @@ +{ + "entry": "workloads/validate-errors.ts", + "args": [ + "1" + ] +} diff --git a/benchmarks/runtime/workloads/value-objects.json b/benchmarks/runtime/workloads/value-objects.json new file mode 100644 index 000000000..276fa4ab8 --- /dev/null +++ b/benchmarks/runtime/workloads/value-objects.json @@ -0,0 +1,6 @@ +{ + "entry": "workloads/value-objects.ts", + "args": [ + "2" + ] +} diff --git a/benchmarks/runtime/workloads/value-records.json b/benchmarks/runtime/workloads/value-records.json new file mode 100644 index 000000000..84c0e12dd --- /dev/null +++ b/benchmarks/runtime/workloads/value-records.json @@ -0,0 +1,6 @@ +{ + "entry": "workloads/value-records.ts", + "args": [ + "1" + ] +} diff --git a/benchmarks/runtime/workloads/word-graph.json b/benchmarks/runtime/workloads/word-graph.json new file mode 100644 index 000000000..95ffb27e5 --- /dev/null +++ b/benchmarks/runtime/workloads/word-graph.json @@ -0,0 +1,6 @@ +{ + "entry": "workloads/word-graph.ts", + "args": [ + "2" + ] +} diff --git a/benchmarks/scaling/strace-workloads.mjs b/benchmarks/scaling/strace-workloads.mjs index b4db5af19..f0644cf13 100644 --- a/benchmarks/scaling/strace-workloads.mjs +++ b/benchmarks/scaling/strace-workloads.mjs @@ -7,6 +7,7 @@ import { mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { parseArgs } from "node:util"; +import { readWorkloads } from "../runtime/workloads.mjs"; const here = dirname(fileURLToPath(import.meta.url)); const suite = resolve(here, "../runtime"); @@ -18,7 +19,7 @@ const { values } = parseArgs({ }, }); mkdirSync(values.out, { recursive: true }); -const { workloads } = JSON.parse(readFileSync(join(suite, "workloads.json"), "utf8")); +const workloads = readWorkloads(suite); const cli = join(values.scriptc, "packages/cli/dist/bootstrap.js"); // Same inputs as scripts/bench-runtime.mjs generates. function input(kind) { diff --git a/docs/content/docs/cli.mdx b/docs/content/docs/cli.mdx index 786cf2c63..df8274258 100644 --- a/docs/content/docs/cli.mdx +++ b/docs/content/docs/cli.mdx @@ -128,7 +128,7 @@ Verifies and loads the installed precompiled runtime objects and vendored librar
Coverage only. Map an exact bare module specifier to a local declaration file supplied by an embedder. Repeat the option for multiple modules. The declaration supplies checker types so application coverage can continue; runtime imports and values remain explicit SC1010 blockers. Relative paths resolve from the current working directory. Accepted files end in .d.ts, .d.mts, or .d.cts.
--tier <auto|static-only>
-
The tier policy for code that has no static lowering. auto is the default. static-only guarantees that the program contains no dynamic-tier demotions: every such construct stays a compile error in TypeScript and a deferred runtime error in JavaScript, which is how both modes behave today. Use it in CI to keep a program fully static. Library mode is always static-only. See Dynamic tier.
+
The tier policy for code that has no static lowering. auto is the default. static-only guarantees that the program contains no dynamic-tier demotions: every such construct stays a compile error in TypeScript and a deferred runtime error in JavaScript, which is how both modes behave today, and a checked cast whose value does not match its type throws instead of continuing with the value. Use it in CI to keep a program fully static. Library mode is always static-only. See Dynamic tier.
--explain-dynamic[=<name>]
Coverage only. After the report, list the functions that already operate on values typed only at runtime and the bindings, fields, functions, classes and modules that unsupported constructs would move to the dynamic tier, each with the diagnostic behind it. With a name such as x, Point, or Point.x, show only the matching units. See Dynamic tier.
diff --git a/docs/content/docs/coverage.mdx b/docs/content/docs/coverage.mdx index 1bb645bd9..b1f6e2c64 100644 --- a/docs/content/docs/coverage.mdx +++ b/docs/content/docs/coverage.mdx @@ -60,7 +60,7 @@ Code that operates on values whose types are known only at runtime, such as `any `--explain-dynamic` lists those functions and the operation that makes each one dynamic. It also lists the units an unsupported construct would move to the dynamic tier: the binding, field, class, function, or module that has no static representation for the construct, with the diagnostic behind each one. These are the constructs the dynamic tier is planned to compile; today they remain blockers in TypeScript and deferred runtime errors in JavaScript. Add a name, such as `--explain-dynamic=Point`, to show only the matching units. With `--print=diagnostics`, the document gains a `tiers` object with the same information. -`--tier=static-only` keeps every unsupported construct a blocker instead of moving it to the dynamic tier; code that already operates on runtime-typed values compiles as before. Today `auto`, the default, behaves the same way. +`--tier=static-only` keeps every unsupported construct a blocker instead of moving it to the dynamic tier; code that already operates on runtime-typed values compiles as before. It also makes a checked cast whose value does not match its type throw, where `auto`, the default, continues with the value in the dynamic tier (see [runtime type checks](/docs/limitations#runtime-type-checks)). Otherwise `auto` behaves the same way today. ## Diagnostic codes diff --git a/docs/content/docs/dependencies.mdx b/docs/content/docs/dependencies.mdx index b16c14e7d..85a232bb2 100644 --- a/docs/content/docs/dependencies.mdx +++ b/docs/content/docs/dependencies.mdx @@ -73,7 +73,58 @@ Some packages your program imports directly ship no declarations, so their types $ 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. +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=semver...`. `--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. + +`fs-extra` 8.x through 11.x is built in, and its declarations replace `@types/fs-extra`. The default import, a namespace import and named imports all read the same exports. fs's members are re-exported, and fs-extra's wrapped methods follow its rule: with a callback as the last argument they call back, otherwise they return a promise. The extra methods cover `copy`, `copySync`, `move`, `moveSync`, `remove`, `removeSync`, `emptyDir`, `ensureDir`, `mkdirs`, `mkdirp`, `ensureFile`, `pathExists`, `outputFile`, `readJson`, `readJsonSync`, `writeJson`, `writeJsonSync`, `outputJson` and their aliases, with fs-extra's error messages. Known differences: + +- `ensureLink`, `ensureSymlink`, `createLink`, `createSymlink`, `gracefulify`, the promise forms of the descriptor-based methods, and the callback forms of `stat`, `lstat`, `readdir`, `mkdtemp`, `realpath` and `readJson` aren't declared, so using one is a type error at build time. Paths are strings, text is UTF-8, and write flags, string modes and `copyFile` mode flags aren't accepted. +- graceful-fs's retry queue, which fs-extra uses under Node.js, isn't part of the implementation, so a program that runs out of file descriptors fails at the first `EMFILE` error instead of retrying. +- An ES module that imports fs-extra can use any of its exports by name. Node.js only exposes the names its CommonJS lexer finds in fs-extra's source, so a name like `readFile` throws a `SyntaxError` there. + +Small polyfill packages whose module is exactly a Node.js built-in on Node.js 24 compile as that built-in, at the `require()` that loads them, from your program's CommonJS files, from compiled packages, and from esbuild bundles whose module paths name the package and its version (as pnpm installs do). The package's own files aren't compiled. + + + + + + + + + + + + + +
PackageVersionsCompiles as
isarray0.x, 1.x, 2.xArray.isArray
safe-buffer5.xrequire("buffer")
process-nextick-args2.xprocess
inherits2.xrequire("util").inherits
util-deprecate1.xrequire("util").deprecate
readable-stream2.x, 3.x, 4.xrequire("stream") and its classes
+ +`readable-stream` compiles as Node.js's built-in streams, which is its own `READABLE_STREAM=disable` mode for 2.x and 4.x. That can differ from its default behavior under Node.js; see [limitations](/docs/limitations#readable-stream). + +## 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 diff --git a/docs/content/docs/index.mdx b/docs/content/docs/index.mdx index de6c5bd24..f771ec78c 100644 --- a/docs/content/docs/index.mdx +++ b/docs/content/docs/index.mdx @@ -68,25 +68,24 @@ Programs are checked against the `es2025` standard library, scriptc's own declar ## Runtime type checks -Compiled code relies on types, so scriptc checks a value whose type is only known at runtime before using it as a specific type. For example, parsed JSON must match the type you cast it to: +Compiled code relies on types, so scriptc checks a value whose type is only known at runtime before using it as a specific type. Parsed JSON that matches the type you cast it to uses the type's native layout. A value that does not match continues as the value it is, as under Node.js, which does not check the `as Config` assertion: ```ts:cast.ts type Config = { port: number }; -try { - const config = JSON.parse('{"port": "eighty"}') as Config; - console.log(config.port); -} catch (error) { - if (error instanceof Error) console.log(error.message); +for (const text of ['{"port": 8080}', '{"port": "eighty"}']) { + const config = JSON.parse(text) as Config; + console.log(config.port, typeof config.port); } ``` ```console $ scriptc run cast.ts -expected number at $.port, got string +8080 number +eighty string ``` -Under Node.js, the same `as Config` assertion performs no check. See [runtime type checks](/docs/limitations#runtime-type-checks) for details. +See [runtime type checks](/docs/limitations#runtime-type-checks) for where a mismatch still throws. ## Build-time evaluation diff --git a/docs/content/docs/limitations.mdx b/docs/content/docs/limitations.mdx index 823b7f028..05f9d329a 100644 --- a/docs/content/docs/limitations.mdx +++ b/docs/content/docs/limitations.mdx @@ -178,6 +178,10 @@ title: "Limitations" ../limitations/buffer-construction.mdx +### readable-stream + +../limitations/readable-stream.mdx + ### HTTP headers ../limitations/http-headers.mdx diff --git a/docs/content/limitations/readable-stream.mdx b/docs/content/limitations/readable-stream.mdx new file mode 100644 index 000000000..80ab72f01 --- /dev/null +++ b/docs/content/limitations/readable-stream.mdx @@ -0,0 +1 @@ +`readable-stream` 2.x, 3.x and 4.x compile as Node.js's built-in `stream` module. For 2.x and 4.x, that is the package's own behavior when the `READABLE_STREAM` environment variable is `disable`. Without that variable, Node.js runs the package's JavaScript copy of the stream classes, so a native build can differ from Node.js wherever a program depends on that difference: `instanceof` checks against the built-in classes, class identity, error messages, and timing details. The package's `lib/_stream_*.js` files also compile as the matching built-in classes, although Node.js loads the package's own implementations through them in every environment. `readable-stream` 3.x is a copy of Node.js 10's streams. Its own `disable` mode throws on Node.js 24, so no setting makes Node.js run it against the built-in classes; a native build maps the members its entry exports (the stream classes, `Stream`, `finished` and `pipeline`) to the built-in ones. diff --git a/docs/content/limitations/runtime-type-checks.mdx b/docs/content/limitations/runtime-type-checks.mdx index f6ead482b..bf325b713 100644 --- a/docs/content/limitations/runtime-type-checks.mdx +++ b/docs/content/limitations/runtime-type-checks.mdx @@ -1,5 +1,12 @@ Casts from values whose type is only known at runtime validate the value. -For example, `JSON.parse(s) as Config` throws a catchable error if the data does not match `Config`, identifying the offending path, such as `expected number at $.port, got string`. +A value that matches the type uses the type's native layout. +When the cast initializes a `const` or `let` binding declared in a function or block, and the binding is only read by the code after the cast, a value that does not match continues as the value it is, as under Node.js: with `JSON.parse(s) as Config` and `{"port": "eighty"}`, `config.port` is the string `"eighty"`, missing properties read `undefined`, and extra properties stay visible to `Object.keys`, `JSON.stringify`, and `util.inspect`. +That code then runs on dynamic values, which is slower than native records. +A statement of it that has no dynamic lowering throws a `TypeError` naming the construct and its location when such a value reaches it. + +In the other cases a mismatch throws a catchable `TypeError` identifying the offending path, such as `expected number at $.port, got string`: casts that initialize module-level bindings or appear in other positions, and a mismatched value that leaves the code after its cast through a typed parameter, field, return value, or outer binding, where it is checked against that type. +A value that matches there continues, copied if it is an object. +`--tier=static-only` makes every mismatch throw at the cast. A TypeScript assertion alone does not perform this validation when run under Node.js. Casts between program classes are checked too: casting an instance to a class it does not extend throws a `TypeError`, where Node.js reads the members structurally. diff --git a/docs/src/generated/node-v24-compatibility-meta.json b/docs/src/generated/node-v24-compatibility-meta.json deleted file mode 100644 index 167256bca..000000000 --- a/docs/src/generated/node-v24-compatibility-meta.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "schemaVersion": 3, - "nodeVersion": "24.22.0", - "nodeCommit": "f14cd3aa7bf9ca8df2cc5b8bb14beb0f335b1d93", - "artifactVersion": "ee72820d45a79aa154f0", - "rowCount": 3880 -} diff --git a/internal/compatibility/README.md b/internal/compatibility/README.md index 0b4fdb957..51c7d72c5 100644 --- a/internal/compatibility/README.md +++ b/internal/compatibility/README.md @@ -13,3 +13,7 @@ Statuses are evidence-aware: `supported` and `partial` require test evidence; `r Run `pnpm node-compat:backlog` from the repository root for a summary. The filters `--tier`, `--action`, `--priority`, `--chapter`, and `--status` compose; use `--format=json` or `--format=tsv` for tooling. Run `pnpm node-compat` from the repository root after changing the Node pin or the support manifest. `pnpm node-compat:check` is the offline drift/evidence gate; `pnpm --filter @internal/compatibility check:upstream` additionally verifies the pinned upstream inputs over the network. + +## npm packages that compile as builtins + +The ledger counts Node APIs, so npm packages that compile as a Node builtin have no rows. `packages/compiler/src/frontend/builtin-package-aliases.ts` lists them, each with the checked major versions and the reason its module is that builtin on Node 24. One entry is a documented divergence rather than an identity: `readable-stream` 2.x, 3.x and 4.x compile as `node:stream`; for 2.x and 4.x that is the package's `READABLE_STREAM=disable` behavior. Without that variable Node runs the package's own JavaScript streams. The `tests/corpus/readable-stream-disable` program pins the disabled behavior. 3.x, a copy of Node 10's streams, cannot run disabled on Node 24 (its disable branch throws), so `tests/corpus/readable-stream-3` pins only behavior its own streams share with the builtin. diff --git a/internal/compatibility/src/generate.mjs b/internal/compatibility/src/generate.mjs index 8c7f136cf..d2b333bb2 100644 --- a/internal/compatibility/src/generate.mjs +++ b/internal/compatibility/src/generate.mjs @@ -17,13 +17,17 @@ */ import { createHash } from "node:crypto"; -import { readFileSync, writeFileSync } from "node:fs"; +import { existsSync, readFileSync, writeFileSync } from "node:fs"; import { fileURLToPath } from "node:url"; const packageRoot = fileURLToPath(new URL("../", import.meta.url)); const root = fileURLToPath(new URL("../../../", import.meta.url)); const internalOutputPath = `${packageRoot}generated/node-v24-internal.json`; const backlogOutputPath = `${packageRoot}generated/node-v24-backlog.json`; const publicOutputPath = `${root}docs/src/generated/node-v24-compatibility.json`; +// The docs build derives this file's values from the public artifact; a +// merge that keeps a branch's copy would bring back a file every +// compatibility change rewrites. +const retiredOutputPaths = [`${root}docs/src/generated/node-v24-compatibility-meta.json`]; const compilerManifestPath = `${root}packages/compiler/surface-manifest.json`; const compilerCompatPath = `${packageRoot}static-support.json`; const pinPath = `${packageRoot}node-v24.json`; @@ -1009,6 +1013,10 @@ function publicSnapshot(snapshot) { } function checkLocal() { + for (const path of retiredOutputPaths) { + if (existsSync(path)) + throw new Error(`${path.slice(root.length)} is no longer generated; delete it`); + } const snapshot = JSON.parse(readFileSync(internalOutputPath, "utf8")); if (snapshot.nodeVersion !== pin.version || snapshot.nodeCommit !== pin.commit) { throw new Error( diff --git a/packages/cli/test/native-link-info.test.ts b/packages/cli/test/native-link-info.test.ts index 39964898d..57c60b1bc 100644 --- a/packages/cli/test/native-link-info.test.ts +++ b/packages/cli/test/native-link-info.test.ts @@ -72,7 +72,7 @@ describe.runIf(supported)("macOS arm64 native link info", () => { minimum_os: "14.0", }, program: { object, entry_symbol: "main" }, - runtime_abi: { version: 22, marker: "scr_runtime_abi_v22" }, + runtime_abi: { version: 23, marker: "scr_runtime_abi_v23" }, runtime_pack: { kind: "precompiled", package: "@scriptc/runtime-darwin-arm64", @@ -165,6 +165,6 @@ describe.runIf(supported)("macOS arm64 native link info", () => { () => null, (failure: { stderr?: string }) => failure, ); - expect(error?.stderr).toContain("scr_runtime_abi_v22"); + expect(error?.stderr).toContain("scr_runtime_abi_v23"); }); }); diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts index 6dfcc90ef..7a1967572 100644 --- a/packages/compiler/ambient/scriptc-node-fallback.d.ts +++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts @@ -4940,15 +4940,17 @@ declare module "net" { host?: string, connectListener?: () => void, ): Socket; - /* The caller-resolver dial (@types/node's net.LookupFunction, narrowed - * to the lowered shape): the runtime invokes it as Node does and dials - * the answered addresses in order under autoSelectFamily: true. */ + /* The caller-resolver dial (@types/node's net.LookupFunction): the + * runtime invokes it as Node's autoSelectFamily dial does (options + * { hints, all: true }, so the callback answers an address list) and + * dials the answered addresses in order. */ export type LookupFunction = ( hostname: string, - options: unknown, + options: import("node:dns").LookupOptions, callback: ( err: NodeJS.ErrnoException | null, - addresses: { address: string; family: number }[], + address: string | import("node:dns").LookupAddress[], + family?: number, ) => void, ) => void; export function connect( diff --git a/packages/compiler/package.json b/packages/compiler/package.json index 0c058c467..70bf3b27a 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/fs-extra/core/api.ts b/packages/compiler/shims/fs-extra/core/api.ts new file mode 100644 index 000000000..7a59f17e2 --- /dev/null +++ b/packages/compiler/shims/fs-extra/core/api.ts @@ -0,0 +1,1192 @@ +// fs-extra's exports over node:fs. +// +// fs-extra re-exports every fs member and wraps the asynchronous ones with +// universalify: a call whose last argument is a function takes the callback +// form, any other call returns a promise. The wrapped methods below follow +// that rule over fs.promises (callback forms settle through the promise); +// fs's other members are fs's own. The extra methods (copy, remove, mkdirs, +// the JSON helpers, ...) restate fs-extra's behavior and error messages. +// graceful-fs's EMFILE retry queue, which fs-extra loads under Node, has no +// counterpart: on Node 24 it only changes behavior when the process runs +// out of file descriptors. +// +// What is not declared here is a compile-time error: ensureLink, +// ensureSymlink, createLink, createSymlink and their sync forms, +// gracefulify, the descriptor-based methods' promise forms (fstat, read, +// write, ...), the callback forms of the methods that answer stats, +// listings or paths (stat, lstat, readdir, mkdtemp, realpath, readJson), +// Buffer and URL paths, and option shapes fs.promises does not compile +// natively (copyFile mode flags, append options, write flags, string modes, +// numeric utimes times). A readFile callback that fails receives an empty +// value beside the error, where Node passes undefined. +import * as fs from "node:fs"; +import * as fsp from "node:fs/promises"; +import * as path from "node:path"; +import type { Stats } from "node:fs"; + +// fs's other members, as fs-extra re-exports them (fs-extra's own versions +// of the wrapped methods below take their place). +export { + appendFileSync, + accessSync, + chownSync, + chmodSync, + close, + closeSync, + copyFileSync, + cp, + cpSync, + createReadStream, + createWriteStream, + exists, + existsSync, + fchown, + fchownSync, + fchmod, + fchmodSync, + fdatasync, + fdatasyncSync, + fstat, + fstatSync, + fsync, + fsyncSync, + ftruncate, + ftruncateSync, + futimes, + futimesSync, + lchownSync, + linkSync, + lstatSync, + lutimes, + lutimesSync, + mkdirSync, + mkdtempSync, + open, + openSync, + readdirSync, + read, + readSync, + readv, + readvSync, + readFileSync, + readlinkSync, + realpathSync, + renameSync, + rmSync, + rmdirSync, + statfs, + statSync, + statfsSync, + symlinkSync, + truncateSync, + unwatchFile, + unlinkSync, + utimesSync, + watch, + watchFile, + writeFileSync, + write, + writeSync, + writev, + writevSync, + Dirent, + Stats, + ReadStream, + WriteStream, + Dir, + opendir, + opendirSync, + constants, + promises, +} from "node:fs"; + +/** A Node-style callback with a result. */ +export type Callback = (err: NodeJS.ErrnoException | null, value: T) => void; +/** A Node-style callback without a result. */ +export type VoidCallback = (err: NodeJS.ErrnoException | null) => void; + +export type SymlinkType = "dir" | "file" | "junction"; +export type Utf8 = "utf8" | "utf-8"; + +export interface WriteFileOptions { + encoding?: Utf8 | undefined; + mode?: number | undefined; +} + +export interface ReadOptions { + throws?: boolean | undefined; + encoding?: Utf8 | undefined; +} + +export interface WriteOptions { + encoding?: Utf8 | undefined; + mode?: number | undefined; + spaces?: number | undefined; + EOL?: string | undefined; + finalEOL?: boolean | undefined; +} + +/** A copy filter: fs-extra accepts a boolean or a promise of one. */ +export type CopyFilter = (src: string, dest: string) => boolean | Promise; + +export interface CopyOptions { + dereference?: boolean | undefined; + overwrite?: boolean | undefined; + preserveTimestamps?: boolean | undefined; + errorOnExist?: boolean | undefined; + filter?: CopyFilter | undefined; +} + +export interface CopyOptionsSync { + dereference?: boolean | undefined; + overwrite?: boolean | undefined; + preserveTimestamps?: boolean | undefined; + errorOnExist?: boolean | undefined; + filter?: ((src: string, dest: string) => boolean) | undefined; +} + +export interface MoveOptions { + overwrite?: boolean | undefined; +} + +export interface EnsureOptions { + mode?: number | undefined; +} + +export interface MakeDirectoryOptions { + recursive?: boolean | undefined; + mode?: number | undefined; +} + +export interface RmOptions { + force?: boolean | undefined; + recursive?: boolean | undefined; +} + +function settleVoid(promise: Promise, callback: VoidCallback | undefined): Promise { + if (callback === undefined) return promise; + // The callback form's promise settles only through the callback (it + // never rejects); fs-extra answers undefined there. + return promise.then( + () => callback(null), + (err: NodeJS.ErrnoException) => callback(err), + ); +} + +/** The result of a callback-form call that settles through fs's own + * callback: callers ignore it (fs-extra answers undefined). */ +function idle(): Promise { + return new Promise(() => undefined); +} + +function codeOf(err: unknown): string | undefined { + return err instanceof Error ? (err as NodeJS.ErrnoException).code : undefined; +} + +/* ── fs methods in universalify's wrapper ─────────────────────────────── */ + +export function access(path: string, mode?: number): Promise; +export function access(path: string, callback: VoidCallback): void; +export function access(path: string, mode: number, callback: VoidCallback): void; +export function access( + path: string, + mode?: number | VoidCallback, + callback?: VoidCallback, +): Promise { + if (typeof mode === "function") return settleVoid(fsp.access(path), mode); + return settleVoid(fsp.access(path, mode ?? fs.constants.F_OK), callback); +} + +export function appendFile(path: string, data: string | Uint8Array): Promise; +export function appendFile(path: string, data: string | Uint8Array, callback: VoidCallback): void; +export function appendFile( + path: string, + data: string | Uint8Array, + callback?: VoidCallback, +): Promise { + return settleVoid(fsp.appendFile(path, data), callback); +} + +/** fs.writeFileSync(file, bytes, { mode }) as Node runs it: open with + * O_WRONLY | O_CREAT | O_TRUNC and the mode, write every byte, close. */ +function writeBytesSync(file: string, data: Uint8Array, mode: number): void { + const fd = fs.openSync( + file, + fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_TRUNC, + mode, + ); + try { + let offset = 0; + while (offset < data.length) offset += fs.writeSync(fd, data, offset, data.length - offset); + } finally { + fs.closeSync(fd); + } +} + +async function writeData( + file: string, + data: string | Uint8Array, + options: WriteFileOptions | Utf8 | undefined, +): Promise { + if (typeof data !== "string") { + // fs.promises.writeFile compiles string data; bytes take the + // synchronous write after the same microtask boundary. + await Promise.resolve(); + if (options === undefined || typeof options === "string") fs.writeFileSync(file, data); + else writeBytesSync(file, data, options.mode ?? 0o666); + return; + } + if (options === undefined || typeof options === "string") + return fsp.writeFile(file, data, "utf8"); + return fsp.writeFile(file, data, { encoding: "utf8", mode: options.mode ?? 0o666 }); +} + +export function writeFile( + file: string, + data: string | Uint8Array, + options?: WriteFileOptions | Utf8, +): Promise; +export function writeFile(file: string, data: string | Uint8Array, callback: VoidCallback): void; +export function writeFile( + file: string, + data: string | Uint8Array, + options: WriteFileOptions | Utf8, + callback: VoidCallback, +): void; +export function writeFile( + file: string, + data: string | Uint8Array, + options?: WriteFileOptions | Utf8 | VoidCallback, + callback?: VoidCallback, +): Promise { + if (typeof options === "function") return settleVoid(writeData(file, data, undefined), options); + return settleVoid(writeData(file, data, options), callback); +} + +export function chmod(path: string, mode: number): Promise; +export function chmod(path: string, mode: number, callback: VoidCallback): void; +export function chmod(path: string, mode: number, callback?: VoidCallback): Promise { + return settleVoid(fsp.chmod(path, mode), callback); +} + +export function chown(path: string, uid: number, gid: number): Promise; +export function chown(path: string, uid: number, gid: number, callback: VoidCallback): void; +export function chown( + path: string, + uid: number, + gid: number, + callback?: VoidCallback, +): Promise { + return settleVoid(fsp.chown(path, uid, gid), callback); +} + +export function lchown(path: string, uid: number, gid: number): Promise; +export function lchown(path: string, uid: number, gid: number, callback: VoidCallback): void; +export function lchown( + path: string, + uid: number, + gid: number, + callback?: VoidCallback, +): Promise { + return settleVoid(fsp.lchown(path, uid, gid), callback); +} + +export function copyFile(src: string, dest: string): Promise; +export function copyFile(src: string, dest: string, callback: VoidCallback): void; +export function copyFile(src: string, dest: string, callback?: VoidCallback): Promise { + return settleVoid(fsp.copyFile(src, dest), callback); +} + +export function link(existingPath: string, newPath: string): Promise; +export function link(existingPath: string, newPath: string, callback: VoidCallback): void; +export function link( + existingPath: string, + newPath: string, + callback?: VoidCallback, +): Promise { + return settleVoid(fsp.link(existingPath, newPath), callback); +} + +export function lstat(path: string): Promise { + return fsp.lstat(path); +} + +export function stat(path: string): Promise { + return fsp.stat(path); +} + +async function makeDirectory( + path: string, + options: number | MakeDirectoryOptions | undefined, +): Promise { + if (options === undefined) return fsp.mkdir(path); + if (typeof options === "number") return fsp.mkdir(path, { mode: options }); + const mode = options.mode ?? 0o777; + if (options.recursive === true) return fsp.mkdir(path, { recursive: true, mode }); + return fsp.mkdir(path, { mode }); +} + +export function mkdir(path: string, options?: number | MakeDirectoryOptions): Promise; +export function mkdir(path: string, callback: VoidCallback): void; +export function mkdir( + path: string, + options: number | MakeDirectoryOptions, + callback: VoidCallback, +): void; +export function mkdir( + path: string, + options?: number | MakeDirectoryOptions | VoidCallback, + callback?: VoidCallback, +): Promise { + if (typeof options === "function") return settleVoid(makeDirectory(path, undefined), options); + return settleVoid(makeDirectory(path, options), callback); +} + +export function mkdtemp(prefix: string): Promise { + return fsp.mkdtemp(prefix); +} + +export function readdir(path: string): Promise { + return fsp.readdir(path); +} + +export type ReadFileOptions = Utf8 | { encoding: Utf8 }; + +export function readFile(file: string): Promise; +export function readFile(file: string, options: ReadFileOptions): Promise; +export function readFile(file: string, callback: Callback): void; +export function readFile(file: string, options: ReadFileOptions, callback: Callback): void; +export function readFile( + file: string, + options?: ReadFileOptions | Callback, + callback?: Callback, +): Promise { + if (typeof options === "function") { + fsp.readFile(file).then( + (data) => options(null, data), + (err: NodeJS.ErrnoException) => options(err, Buffer.alloc(0)), + ); + return idle(); + } + if (options === undefined) return fsp.readFile(file); + if (callback !== undefined) { + fsp.readFile(file, "utf8").then( + (text) => callback(null, text), + (err: NodeJS.ErrnoException) => callback(err, ""), + ); + return idle(); + } + return fsp.readFile(file, "utf8"); +} + +export function readlink(path: string): Promise; +export function readlink(path: string, callback: Callback): void; +export function readlink(path: string, callback?: Callback): Promise { + if (callback !== undefined) { + fs.readlink(path, (err, target) => callback(err, target)); + return idle(); + } + return fsp.readlink(path); +} + +export function realpath(path: string): Promise { + return fsp.realpath(path); +} + +export function rename(oldPath: string, newPath: string): Promise; +export function rename(oldPath: string, newPath: string, callback: VoidCallback): void; +export function rename(oldPath: string, newPath: string, callback?: VoidCallback): Promise { + return settleVoid(fsp.rename(oldPath, newPath), callback); +} + +/** Removes a file or directory tree; a missing path is not an error + * (fs-extra's rimraf and fs.rm's force). */ +async function removeTree(target: string): Promise { + let entry: Stats; + try { + entry = await fsp.lstat(target); + } catch (err) { + if (codeOf(err) === "ENOENT") return; + throw err; + } + if (entry.isDirectory()) { + for (const item of await fsp.readdir(target)) await removeTree(path.join(target, item)); + await fsp.rmdir(target); + return; + } + await fsp.unlink(target); +} + +async function rmPath(target: string, options: RmOptions | undefined): Promise { + const recursive = options === undefined ? false : (options.recursive ?? false); + const force = options === undefined ? false : (options.force ?? false); + if (recursive && force) return removeTree(target); + if (!recursive && !force) return fsp.rm(target); + // The mixed forms: fs.rm's own checks, then the walk. + if (force && !fs.existsSync(target)) return; + const entry = await fsp.lstat(target); + if (entry.isDirectory() && !recursive) return fsp.rm(target); + return removeTree(target); +} + +export function rm(path: string, options?: RmOptions): Promise; +export function rm(path: string, callback: VoidCallback): void; +export function rm(path: string, options: RmOptions, callback: VoidCallback): void; +export function rm( + path: string, + options?: RmOptions | VoidCallback, + callback?: VoidCallback, +): Promise { + if (typeof options === "function") return settleVoid(rmPath(path, undefined), options); + return settleVoid(rmPath(path, options), callback); +} + +export function rmdir(path: string): Promise; +export function rmdir(path: string, callback: VoidCallback): void; +export function rmdir(path: string, callback?: VoidCallback): Promise { + return settleVoid(fsp.rmdir(path), callback); +} + +export function symlink(target: string, path: string, type?: SymlinkType): Promise; +export function symlink(target: string, path: string, callback: VoidCallback): void; +export function symlink( + target: string, + path: string, + type: SymlinkType | undefined, + callback: VoidCallback, +): void; +export function symlink( + target: string, + path: string, + type?: SymlinkType | VoidCallback, + callback?: VoidCallback, +): Promise { + if (typeof type === "function") return settleVoid(fsp.symlink(target, path), type); + return settleVoid(fsp.symlink(target, path, type ?? null), callback); +} + +export function truncate(path: string, len?: number): Promise; +export function truncate(path: string, callback: VoidCallback): void; +export function truncate(path: string, len: number, callback: VoidCallback): void; +export function truncate( + path: string, + len?: number | VoidCallback, + callback?: VoidCallback, +): Promise { + if (typeof len === "function") return settleVoid(fsp.truncate(path, 0), len); + return settleVoid(fsp.truncate(path, len ?? 0), callback); +} + +export function unlink(path: string): Promise; +export function unlink(path: string, callback: VoidCallback): void; +export function unlink(path: string, callback?: VoidCallback): Promise { + return settleVoid(fsp.unlink(path), callback); +} + +export function utimes(path: string, atime: Date, mtime: Date): Promise; +export function utimes(path: string, atime: Date, mtime: Date, callback: VoidCallback): void; +export function utimes( + path: string, + atime: Date, + mtime: Date, + callback?: VoidCallback, +): Promise { + return settleVoid(fsp.utimes(path, atime, mtime), callback); +} + +/* ── mkdirs, ensureDir, mkdirp ────────────────────────────────────────── */ + +function dirMode(options: EnsureOptions | number | undefined): number { + if (options === undefined) return 0o777; + if (typeof options === "number") return options; + return options.mode ?? 0o777; +} + +function makeDir(dir: string, mode: number): Promise { + return fsp.mkdir(path.resolve(dir), { recursive: true, mode }); +} + +function makeDirSync(dir: string, mode: number): void { + fs.mkdirSync(path.resolve(dir), { recursive: true, mode }); +} + +export function ensureDir(dir: string, options?: EnsureOptions | number): Promise; +export function ensureDir(dir: string, callback: VoidCallback): void; +export function ensureDir( + dir: string, + options: EnsureOptions | number, + callback: VoidCallback, +): void; +export function ensureDir( + dir: string, + options?: EnsureOptions | number | VoidCallback, + callback?: VoidCallback, +): Promise { + if (typeof options === "function") return settleVoid(makeDir(dir, 0o777), options); + return settleVoid(makeDir(dir, dirMode(options)), callback); +} + +export function mkdirs(dir: string, options?: EnsureOptions | number): Promise; +export function mkdirs(dir: string, callback: VoidCallback): void; +export function mkdirs( + dir: string, + options?: EnsureOptions | number | VoidCallback, +): Promise { + if (typeof options === "function") return settleVoid(makeDir(dir, 0o777), options); + return makeDir(dir, dirMode(options)); +} + +export function mkdirp(dir: string, options?: EnsureOptions | number): Promise; +export function mkdirp(dir: string, callback: VoidCallback): void; +export function mkdirp( + dir: string, + options?: EnsureOptions | number | VoidCallback, +): Promise { + if (typeof options === "function") return settleVoid(makeDir(dir, 0o777), options); + return makeDir(dir, dirMode(options)); +} + +export function ensureDirSync(dir: string, options?: EnsureOptions | number): void { + makeDirSync(dir, dirMode(options)); +} + +export function mkdirsSync(dir: string, options?: EnsureOptions | number): void { + makeDirSync(dir, dirMode(options)); +} + +export function mkdirpSync(dir: string, options?: EnsureOptions | number): void { + makeDirSync(dir, dirMode(options)); +} + +/* ── remove, emptyDir ─────────────────────────────────────────────────── */ + +export function remove(dir: string): Promise; +export function remove(dir: string, callback: VoidCallback): void; +export function remove(dir: string, callback?: VoidCallback): Promise { + return settleVoid(removeTree(dir), callback); +} + +export function removeSync(dir: string): void { + fs.rmSync(dir, { recursive: true, force: true }); +} + +async function emptyDirAsync(dir: string): Promise { + let items: string[]; + try { + items = await fsp.readdir(dir); + } catch { + await makeDir(dir, 0o777); + return; + } + while (items.length > 0) { + const item = items.pop(); + if (item !== undefined) await removeTree(path.join(dir, item)); + } +} + +export function emptyDir(dir: string): Promise; +export function emptyDir(dir: string, callback: VoidCallback): void; +export function emptyDir(dir: string, callback?: VoidCallback): Promise { + return settleVoid(emptyDirAsync(dir), callback); +} + +export function emptyDirSync(dir: string): void { + let items: string[]; + try { + items = fs.readdirSync(dir); + } catch { + makeDirSync(dir, 0o777); + return; + } + for (const item of items) fs.rmSync(path.join(dir, item), { recursive: true, force: true }); +} + +/* ── pathExists, ensureFile, outputFile ───────────────────────────────── */ + +async function pathExistsAsync(file: string): Promise { + try { + await fsp.access(file); + return true; + } catch { + return false; + } +} + +export function pathExists(file: string): Promise; +export function pathExists(file: string, callback: Callback): void; +export function pathExists(file: string, callback?: Callback): Promise { + const found = pathExistsAsync(file); + if (callback === undefined) return found; + return found.then((exists) => { + callback(null, exists); + return exists; + }); +} + +export function pathExistsSync(file: string): boolean { + return fs.existsSync(file); +} + +async function createFileAsync(file: string): Promise { + try { + if ((await fsp.stat(file)).isFile()) return; + } catch { + // Missing: created below. + } + const dir = path.dirname(file); + let dirIsDirectory = true; + try { + dirIsDirectory = (await fsp.stat(dir)).isDirectory(); + } catch (err) { + if (codeOf(err) !== "ENOENT") throw err; + await makeDir(dir, 0o777); + } + // A parent that is not a directory fails with readdir's ENOTDIR. + if (!dirIsDirectory) await fsp.readdir(dir); + await fsp.writeFile(file, "", "utf8"); +} + +export function ensureFile(file: string): Promise; +export function ensureFile(file: string, callback: VoidCallback): void; +export function ensureFile(file: string, callback?: VoidCallback): Promise { + return settleVoid(createFileAsync(file), callback); +} + +export function ensureFileSync(file: string): void { + try { + if (fs.statSync(file).isFile()) return; + } catch { + // Missing: created below. + } + const dir = path.dirname(file); + if (!fs.existsSync(dir)) makeDirSync(dir, 0o777); + fs.writeFileSync(file, ""); +} + +async function outputFileAsync( + file: string, + data: string | Uint8Array, + options: WriteFileOptions | Utf8 | undefined, +): Promise { + const dir = path.dirname(file); + if (!(await pathExistsAsync(dir))) await makeDir(dir, 0o777); + await writeData(file, data, options); +} + +export function outputFile( + file: string, + data: string | Uint8Array, + options?: WriteFileOptions | Utf8, +): Promise; +export function outputFile(file: string, data: string | Uint8Array, callback: VoidCallback): void; +export function outputFile( + file: string, + data: string | Uint8Array, + options: WriteFileOptions | Utf8, + callback: VoidCallback, +): void; +export function outputFile( + file: string, + data: string | Uint8Array, + options?: WriteFileOptions | Utf8 | VoidCallback, + callback?: VoidCallback, +): Promise { + if (typeof options === "function") + return settleVoid(outputFileAsync(file, data, undefined), options); + return settleVoid(outputFileAsync(file, data, options), callback); +} + +export function outputFileSync(file: string, data: string | Uint8Array): void { + const dir = path.dirname(file); + if (!fs.existsSync(dir)) makeDirSync(dir, 0o777); + fs.writeFileSync(file, data); +} + +/* ── JSON (jsonfile) ──────────────────────────────────────────────────── */ + +function jsonText(value: unknown, options: WriteOptions | Utf8 | undefined): string { + const opts = options === undefined || typeof options === "string" ? undefined : options; + const eol = opts === undefined ? "\n" : (opts.EOL ?? "\n"); + const finalEol = opts === undefined ? true : (opts.finalEOL ?? true); + const spaces = opts === undefined ? 0 : (opts.spaces ?? 0); + const text = JSON.stringify(value, null, spaces) ?? "undefined"; + return text.replace(/\n/g, eol) + (finalEol ? eol : ""); +} + +function jsonWriteOptions(options: WriteOptions | Utf8 | undefined): WriteFileOptions | undefined { + if (options === undefined || typeof options === "string") return undefined; + return { mode: options.mode }; +} + +function jsonThrows(options: ReadOptions | Utf8 | undefined): boolean { + if (options === undefined || typeof options === "string") return true; + return options.throws ?? true; +} + +function prefixed(file: string, err: unknown): Error { + const error = err instanceof Error ? err : new Error(String(err)); + error.message = `${file}: ${error.message}`; + return error; +} + +function parseJson(file: string, text: string, throws: boolean): any { + try { + return JSON.parse(text.replace(/^\uFEFF/, "")); + } catch (err) { + if (!throws) return null; + throw prefixed(file, err); + } +} + +async function readJsonAsync(file: string, options: ReadOptions | Utf8 | undefined): Promise { + const text = await fsp.readFile(file, "utf8"); + return parseJson(file, text, jsonThrows(options)); +} + +// readJson's callback form is not declared. +export function readJson(file: string, options?: ReadOptions | Utf8): Promise { + return readJsonAsync(file, options); +} + +export function readJsonSync(file: string, options?: ReadOptions | Utf8): any { + const throws = jsonThrows(options); + let text: string; + try { + text = fs.readFileSync(file, "utf8"); + } catch (err) { + // jsonfile reads inside the same try as the parse. + if (!throws) return null; + throw prefixed(file, err); + } + return parseJson(file, text, throws); +} + +async function writeJsonAsync( + file: string, + value: unknown, + options: WriteOptions | Utf8 | undefined, +): Promise { + await writeData(file, jsonText(value, options), jsonWriteOptions(options)); +} + +export function writeJson(file: string, value: any, options?: WriteOptions | Utf8): Promise; +export function writeJson(file: string, value: any, callback: VoidCallback): void; +export function writeJson( + file: string, + value: any, + options: WriteOptions | Utf8, + callback: VoidCallback, +): void; +export function writeJson( + file: string, + value: any, + options?: WriteOptions | Utf8 | VoidCallback, + callback?: VoidCallback, +): Promise { + if (typeof options === "function") + return settleVoid(writeJsonAsync(file, value, undefined), options); + return settleVoid(writeJsonAsync(file, value, options), callback); +} + +export function writeJsonSync(file: string, value: any, options?: WriteOptions | Utf8): void { + const text = jsonText(value, options); + if (options === undefined || typeof options === "string") fs.writeFileSync(file, text, "utf8"); + else fs.writeFileSync(file, text, { encoding: "utf8", mode: options.mode ?? 0o666 }); +} + +async function outputJsonAsync( + file: string, + value: unknown, + options: WriteOptions | Utf8 | undefined, +): Promise { + const dir = path.dirname(file); + if (!(await pathExistsAsync(dir))) await makeDir(dir, 0o777); + await writeJsonAsync(file, value, options); +} + +export function outputJson(file: string, value: any, options?: WriteOptions | Utf8): Promise; +export function outputJson(file: string, value: any, callback: VoidCallback): void; +export function outputJson( + file: string, + value: any, + options: WriteOptions | Utf8, + callback: VoidCallback, +): void; +export function outputJson( + file: string, + value: any, + options?: WriteOptions | Utf8 | VoidCallback, + callback?: VoidCallback, +): Promise { + if (typeof options === "function") + return settleVoid(outputJsonAsync(file, value, undefined), options); + return settleVoid(outputJsonAsync(file, value, options), callback); +} + +export function outputJsonSync(file: string, value: any, options?: WriteOptions | Utf8): void { + const dir = path.dirname(file); + if (!fs.existsSync(dir)) makeDirSync(dir, 0o777); + writeJsonSync(file, value, options); +} + +/* ── copy, move ───────────────────────────────────────────────────────── */ + +function isSrcSubdir(src: string, dest: string): boolean { + const srcParts = path + .resolve(src) + .split(path.sep) + .filter((part) => part !== ""); + const destParts = path + .resolve(dest) + .split(path.sep) + .filter((part) => part !== ""); + for (let i = 0; i < srcParts.length; i++) if (destParts[i] !== srcParts[i]) return false; + return true; +} + +function subdirMessage(src: string, dest: string, verb: string): string { + return `Cannot ${verb} '${src}' to a subdirectory of itself, '${dest}'.`; +} + +function sameNode(a: Stats, b: Stats): boolean { + return b.ino !== 0 && b.dev !== 0 && a.ino === b.ino && a.dev === b.dev; +} + +async function statOrNull(file: string): Promise { + try { + return await fsp.stat(file); + } catch (err) { + if (codeOf(err) === "ENOENT") return null; + throw err; + } +} + +/** fs-extra's checkPaths and checkParentPaths: the source exists, is not + * the destination, and a directory never copies or moves into itself. + * Answers whether the destination exists. */ +async function checkPaths(src: string, dest: string, verb: string): Promise { + const srcStat = await fsp.stat(src); + const destStat = await statOrNull(dest); + if (destStat !== null && sameNode(srcStat, destStat)) + throw new Error("Source and destination must not be the same."); + if (srcStat.isDirectory() && isSrcSubdir(src, dest)) + throw new Error(subdirMessage(src, dest, verb)); + const srcParent = path.resolve(path.dirname(src)); + let destParent = path.resolve(path.dirname(dest)); + while (destParent !== srcParent && destParent !== path.parse(destParent).root) { + const parentStat = await statOrNull(destParent); + if (parentStat === null) break; + if (sameNode(srcStat, parentStat)) throw new Error(subdirMessage(src, dest, verb)); + destParent = path.resolve(path.dirname(destParent)); + } + return destStat !== null; +} + +function statOrNullSync(file: string): Stats | null { + try { + return fs.statSync(file); + } catch (err) { + if (codeOf(err) === "ENOENT") return null; + throw err; + } +} + +/** checkPaths for copySync and moveSync. Answers the destination's stats, + * or null when it does not exist. */ +function checkPathsSync(src: string, dest: string, verb: string): Stats | null { + const srcStat = fs.statSync(src); + const destStat = statOrNullSync(dest); + if (destStat !== null && sameNode(srcStat, destStat)) + throw new Error("Source and destination must not be the same."); + if (srcStat.isDirectory() && isSrcSubdir(src, dest)) + throw new Error(subdirMessage(src, dest, verb)); + const srcParent = path.resolve(path.dirname(src)); + let destParent = path.resolve(path.dirname(dest)); + while (destParent !== srcParent && destParent !== path.parse(destParent).root) { + const parentStat = statOrNullSync(destParent); + if (parentStat === null) break; + if (sameNode(srcStat, parentStat)) throw new Error(subdirMessage(src, dest, verb)); + destParent = path.resolve(path.dirname(destParent)); + } + return destStat; +} + +interface CopyState { + overwrite: boolean; + errorOnExist: boolean; + dereference: boolean; + preserveTimestamps: boolean; + filter: CopyFilter | undefined; +} + +async function included(state: CopyState, src: string, dest: string): Promise { + const filter = state.filter; + if (filter === undefined) return true; + return await filter(src, dest); +} + +/** fs-extra's onLink: a symlink source creates a symlink at `dest`. Only + * a destination that is itself a symlink is replaced; over a regular file + * or a directory, symlink fails with EEXIST, whatever `overwrite` says. */ +async function copyLink( + target: string, + dest: string, + destExists: boolean, + dereference: boolean, +): Promise { + if (!destExists) return fsp.symlink(target, dest); + let destLinked: string; + try { + destLinked = await fsp.readlink(dest); + } catch (err) { + const code = codeOf(err); + if (code === "EINVAL" || code === "UNKNOWN") return fsp.symlink(target, dest); + throw err; + } + const destTarget = dereference ? path.resolve(process.cwd(), destLinked) : destLinked; + checkLinkTargets(target, destTarget, (await fsp.stat(dest)).isDirectory()); + await fsp.unlink(dest); + await fsp.symlink(target, dest); +} + +function copyLinkSync( + target: string, + dest: string, + destExists: boolean, + dereference: boolean, +): void { + if (!destExists) { + fs.symlinkSync(target, dest); + return; + } + let destLinked: string; + try { + destLinked = fs.readlinkSync(dest); + } catch (err) { + const code = codeOf(err); + if (code !== "EINVAL" && code !== "UNKNOWN") throw err; + fs.symlinkSync(target, dest); + return; + } + const destTarget = dereference ? path.resolve(process.cwd(), destLinked) : destLinked; + checkLinkTargets(target, destTarget, fs.statSync(dest).isDirectory()); + fs.unlinkSync(dest); + fs.symlinkSync(target, dest); +} + +/** Replacing a destination symlink never points it into its own target, + * nor removes a directory link the source lives under. */ +function checkLinkTargets(target: string, destTarget: string, destIsDirectory: boolean): void { + if (isSrcSubdir(target, destTarget)) + throw new Error(`Cannot copy '${target}' to a subdirectory of itself, '${destTarget}'.`); + if (destIsDirectory && isSrcSubdir(destTarget, target)) + throw new Error(`Cannot overwrite '${destTarget}' with '${target}'.`); +} + +async function copyEntry( + state: CopyState, + src: string, + dest: string, + destExists: boolean, + filtered: boolean, +): Promise { + if (!filtered && !(await included(state, src, dest))) return; + const srcStat = state.dereference ? await fsp.stat(src) : await fsp.lstat(src); + if (srcStat.isDirectory()) { + if (destExists && !(await fsp.stat(dest)).isDirectory()) + throw new Error(`Cannot overwrite non-directory '${dest}' with directory '${src}'.`); + if (!destExists) await fsp.mkdir(dest); + const items = await fsp.readdir(src); + while (items.length > 0) { + const item = items.pop(); + if (item === undefined) break; + const srcItem = path.join(src, item); + const destItem = path.join(dest, item); + const destItemStat = await statOrNull(destItem); + if (destItemStat !== null && sameNode(await fsp.stat(srcItem), destItemStat)) + throw new Error("Source and destination must not be the same."); + await copyEntry(state, srcItem, destItem, destItemStat !== null, false); + } + if (!destExists) await fsp.chmod(dest, srcStat.mode); + return; + } + if (srcStat.isSymbolicLink()) { + const linked = await fsp.readlink(src); + await copyLink( + state.dereference ? path.resolve(process.cwd(), linked) : linked, + dest, + destExists, + state.dereference, + ); + return; + } + if (destExists) { + if (state.overwrite) await fsp.unlink(dest); + else if (state.errorOnExist) throw new Error(`'${dest}' already exists`); + else return; + } + await fsp.copyFile(src, dest); + if (state.preserveTimestamps) { + const updated = await fsp.stat(src); + if ((srcStat.mode & 0o200) === 0) await fsp.chmod(dest, srcStat.mode | 0o200); + await fsp.utimes(dest, updated.atime, updated.mtime); + } + await fsp.chmod(dest, srcStat.mode); +} + +async function copyAsync( + src: string, + dest: string, + options: CopyOptions | undefined, +): Promise { + const state: CopyState = { + overwrite: options === undefined ? true : (options.overwrite ?? true), + errorOnExist: options === undefined ? false : (options.errorOnExist ?? false), + dereference: options === undefined ? false : (options.dereference ?? false), + preserveTimestamps: options === undefined ? false : (options.preserveTimestamps ?? false), + filter: options === undefined ? undefined : options.filter, + }; + const destExists = await checkPaths(src, dest, "copy"); + if (!(await included(state, src, dest))) return; + const parent = path.dirname(dest); + if (!(await pathExistsAsync(parent))) await makeDir(parent, 0o777); + await copyEntry(state, src, dest, destExists, true); +} + +export function copy(src: string, dest: string, options?: CopyOptions): Promise; +export function copy(src: string, dest: string, callback: VoidCallback): void; +export function copy(src: string, dest: string, options: CopyOptions, callback: VoidCallback): void; +export function copy( + src: string, + dest: string, + options?: CopyOptions | VoidCallback, + callback?: VoidCallback, +): Promise { + if (typeof options === "function") return settleVoid(copyAsync(src, dest, undefined), options); + return settleVoid(copyAsync(src, dest, options), callback); +} + +function copyEntrySync( + state: CopyOptionsSync, + src: string, + dest: string, + destExists: boolean, + filtered: boolean, +): void { + const filter = state.filter; + if (!filtered && filter !== undefined && !filter(src, dest)) return; + const srcStat = state.dereference === true ? fs.statSync(src) : fs.lstatSync(src); + if (srcStat.isDirectory()) { + if (destExists && !fs.statSync(dest).isDirectory()) + throw new Error(`Cannot overwrite non-directory '${dest}' with directory '${src}'.`); + if (!destExists) fs.mkdirSync(dest); + for (const item of fs.readdirSync(src)) { + const srcItem = path.join(src, item); + const destItem = path.join(dest, item); + // fs-extra's copyDirItem checks every entry against its destination: + // a destination symlink back into the source tree must not let the + // overwrite below unlink the source's own file. + const destItemStat = statOrNullSync(destItem); + if (destItemStat !== null && sameNode(fs.statSync(srcItem), destItemStat)) + throw new Error("Source and destination must not be the same."); + copyEntrySync(state, srcItem, destItem, destItemStat !== null, false); + } + if (!destExists) fs.chmodSync(dest, srcStat.mode); + return; + } + if (srcStat.isSymbolicLink()) { + const dereference = state.dereference === true; + const linked = fs.readlinkSync(src); + copyLinkSync( + dereference ? path.resolve(process.cwd(), linked) : linked, + dest, + destExists, + dereference, + ); + return; + } + if (destExists) { + if (state.overwrite ?? true) fs.unlinkSync(dest); + else if (state.errorOnExist === true) throw new Error(`'${dest}' already exists`); + else return; + } + fs.copyFileSync(src, dest); + if (state.preserveTimestamps === true) { + const updated = fs.statSync(src); + if ((srcStat.mode & 0o200) === 0) fs.chmodSync(dest, srcStat.mode | 0o200); + fs.utimesSync(dest, updated.atime, updated.mtime); + } + fs.chmodSync(dest, srcStat.mode); +} + +export function copySync(src: string, dest: string, options?: CopyOptionsSync): void { + const opts: CopyOptionsSync = options ?? {}; + const destStat = checkPathsSync(src, dest, "copy"); + const filter = opts.filter; + if (filter !== undefined && !filter(src, dest)) return; + const parent = path.dirname(dest); + if (!fs.existsSync(parent)) makeDirSync(parent, 0o777); + copyEntrySync(opts, src, dest, destStat !== null, true); +} + +async function moveAsync( + src: string, + dest: string, + options: MoveOptions | undefined, +): Promise { + const overwrite = options === undefined ? false : (options.overwrite ?? false); + await checkPaths(src, dest, "move"); + await makeDir(path.dirname(dest), 0o777); + if (overwrite) await removeTree(dest); + else if (await pathExistsAsync(dest)) throw new Error("dest already exists."); + try { + await fsp.rename(src, dest); + } catch (err) { + if (codeOf(err) !== "EXDEV") throw err; + await copyAsync(src, dest, { overwrite, errorOnExist: true }); + await removeTree(src); + } +} + +export function move(src: string, dest: string, options?: MoveOptions): Promise; +export function move(src: string, dest: string, callback: VoidCallback): void; +export function move(src: string, dest: string, options: MoveOptions, callback: VoidCallback): void; +export function move( + src: string, + dest: string, + options?: MoveOptions | VoidCallback, + callback?: VoidCallback, +): Promise { + if (typeof options === "function") return settleVoid(moveAsync(src, dest, undefined), options); + return settleVoid(moveAsync(src, dest, options), callback); +} + +export function moveSync(src: string, dest: string, options?: MoveOptions): void { + const overwrite = options === undefined ? false : (options.overwrite ?? false); + checkPathsSync(src, dest, "move"); + makeDirSync(path.dirname(dest), 0o777); + if (overwrite) fs.rmSync(dest, { recursive: true, force: true }); + else if (fs.existsSync(dest)) throw new Error("dest already exists."); + try { + fs.renameSync(src, dest); + } catch (err) { + if (codeOf(err) !== "EXDEV") throw err; + copySync(src, dest, { overwrite, errorOnExist: true }); + fs.rmSync(src, { recursive: true, force: true }); + } +} + +/* ── fs-extra's aliases ───────────────────────────────────────────────── */ + +export { + emptyDir as emptydir, + emptyDirSync as emptydirSync, + ensureFile as createFile, + ensureFileSync as createFileSync, + readJson as readJSON, + readJsonSync as readJSONSync, + writeJson as writeJSON, + writeJsonSync as writeJSONSync, + outputJson as outputJSON, + outputJsonSync as outputJSONSync, +}; diff --git a/packages/compiler/shims/fs-extra/index.ts b/packages/compiler/shims/fs-extra/index.ts new file mode 100644 index 000000000..43debf2d4 --- /dev/null +++ b/packages/compiler/shims/fs-extra/index.ts @@ -0,0 +1,9 @@ +// scriptc's built-in implementation of the `fs-extra` package, 8.x-11.x API. +// +// Programs whose installed fs-extra is 8.x through 11.x compile this module +// in its place (see packages/compiler/src/frontend/package-shims.ts), and its +// declarations stand in for @types/fs-extra. The default import, the +// namespace import and named imports all read the same exports, as they read +// fs-extra's single CommonJS export object under Node. +export * from "./core/api.ts"; +export * as default from "./core/api.ts"; 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..fb3404aab --- /dev/null +++ b/packages/compiler/shims/undici/core/client.ts @@ -0,0 +1,1169 @@ +// 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"); + +/** The value of an ASCII hex digit, or -1. */ +function hexValue(c: number): number { + if (c >= 0x30 && c <= 0x39) return c - 0x30; + if (c >= 0x41 && c <= 0x46) return c - 0x37; + if (c >= 0x61 && c <= 0x66) return c - 0x57; + return -1; +} + +/** 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; + /** Unconsumed bytes of the current arrival, then the later arrivals in + * order. */ + buf: Buffer; + queue: 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; + /** The chunk-size line read so far: its value, its digit count, whether + * an extension or whitespace follows the digits, and whether its CR was + * read. Extensions are skipped as they arrive, so the line keeps no + * bytes. */ + chunkSize: number; + chunkDigits: number; + chunkExtension: boolean; + chunkCR: boolean; + + constructor(client: Client, socket: Socket) { + this.client = client; + this.socket = socket; + this.buf = Buffer.alloc(0); + this.queue = []; + 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; + this.chunkSize = 0; + this.chunkDigits = 0; + this.chunkExtension = false; + this.chunkCR = 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 { + if (this.buf.length === 0 && this.queue.length === 0) this.buf = data; + else this.queue.push(data); + if (!this.paused) this.run(); + } + + /** Moves the next queued arrival into `buf`, after the bytes left in it; + * false when nothing is queued. Only a partial status, header or trailer + * line (bounded by maxHeaderSize) or a partial chunk-data CRLF is copied. */ + more(): boolean { + const next = this.queue.shift(); + if (next === undefined) return false; + this.buf = this.buf.length === 0 ? next : Buffer.concat([this.buf, next]); + return true; + } + + 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 { + // undici 7.x wraps llhttp's reason and leaves the code unset (its + // createError indexes the error-name table with the error number); + // 5.x reports the reason and its HPE_ code. + const error = + this.client.__profile.major >= 7 + ? new HTTPParserError(`Response does not match the HTTP/1.1 protocol (${message})`) + : new HTTPParserError(message, code, ""); + this.client.__onParseError(this.socket, error); + 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 (this.more()) continue; + 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; + } + if (this.more()) continue; + 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) { + if (this.more()) continue; + 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: hex digits, then optional whitespace and + // extensions, then CRLF. It is read byte by byte as it arrives. + if (this.buf.length === 0) { + if (this.more()) continue; + return; + } + let i = 0; + let done = false; + for (; i < this.buf.length && !done; i++) { + const c = this.buf[i]!; + if (this.chunkCR) { + if (c !== 0x0a) { + this.fail("Expected LF after chunk size", "STRICT"); + return; + } + done = true; + } else if (c === 0x0d) { + if (this.chunkDigits === 0) { + this.fail("Invalid character in chunk size", "INVALID_CHUNK_SIZE"); + return; + } + this.chunkCR = true; + } else if (this.chunkExtension) { + continue; + } else if (hexValue(c) >= 0) { + this.chunkSize = this.chunkSize * 16 + hexValue(c); + this.chunkDigits++; + if (this.chunkSize > Number.MAX_SAFE_INTEGER) { + this.fail("Chunk size overflow", "INVALID_CHUNK_SIZE"); + return; + } + } else if (this.chunkDigits > 0 && (c === 0x3b || c === 0x20 || c === 0x09)) { + this.chunkExtension = true; + } else { + this.fail("Invalid character in chunk size", "INVALID_CHUNK_SIZE"); + return; + } + } + this.buf = this.buf.subarray(i); + if (!done) continue; + const size = this.chunkSize; + this.chunkSize = 0; + this.chunkDigits = 0; + this.chunkExtension = false; + this.chunkCR = false; + 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) { + if (this.more()) continue; + 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) { + if (this.more()) continue; + 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) { + if (this.more()) continue; + 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..7f312481b --- /dev/null +++ b/packages/compiler/shims/undici/core/request.ts @@ -0,0 +1,287 @@ +// 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") { + // The client writes the Host value into the request head as given. + if (headerCharRegex.test(val)) throw new InvalidArgumentError(`invalid ${key} header`); + 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; + } + // The client writes the one Content-Length it validated or computed + // from the body; a second field would frame the request twice. Both + // profiles reject it with undici 7's error (5.x sends it as given). + 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 0fcf86ada..a8b30d646 100644 --- a/packages/compiler/src/backend/executable-features.ts +++ b/packages/compiler/src/backend/executable-features.ts @@ -28,6 +28,8 @@ export function executableLinkFeatures(mod: IrModule): NativeLinkFeatures { ...(features.dynTier ? { dynTier: true } : {}), ...(features.streamDyn ? { streamDyn: true } : {}), ...(features.eventsOnceSignal ? { eventsOnceSignal: true } : {}), + ...(features.dynBufferNum ? { dynBufferNum: true } : {}), + ...(features.builtinValues ? { builtinValues: true } : {}), events: features.processEvents, emitter: features.emitter, symbol: features.symbol, diff --git a/packages/compiler/src/backend/llvm/array-search.ts b/packages/compiler/src/backend/llvm/array-search.ts index 8c9590bee..74d79f7be 100644 --- a/packages/compiler/src/backend/llvm/array-search.ts +++ b/packages/compiler/src/backend/llvm/array-search.ts @@ -63,7 +63,9 @@ export function emitArraySearchHelpers( `define internal double @${arraySearchHelperName(kind)}(ptr %a, ptr %v) ${FN_ATTRS} {`, `entry:`, ` %lenp = getelementptr inbounds %ScrArr, ptr %a, i32 0, i32 1`, - ` %len = load ${sz}, ptr %lenp${sz === "i64" ? ", !range !{i64 0, i64 4294967296}" : ""}`, + ...(sz === "i64" + ? [` %len32 = load i32, ptr %lenp`, ` %len = zext i32 %len32 to i64`] + : [` %len = load i32, ptr %lenp`]), ` %fp = getelementptr inbounds %ScrArr, ptr %a, i32 0, i32 6`, ` %f32 = load i32, ptr %fp`, widen, diff --git a/packages/compiler/src/backend/llvm/classes.test.ts b/packages/compiler/src/backend/llvm/classes.test.ts index be1c58941..8a2a4659a 100644 --- a/packages/compiler/src/backend/llvm/classes.test.ts +++ b/packages/compiler/src/backend/llvm/classes.test.ts @@ -75,17 +75,25 @@ test("releases split into an inlinable fast path and an out-of-line remainder", const llvm = emitLlvmModule(mod, { pointerBits: 64 }); const body = (name: string): string => new RegExp(`^define internal void @${name}(?:\\.i32)?\\([^]*?^}`, "m").exec(llvm)?.[0] ?? ""; - // A hierarchy rooted in the program: a surviving release decrements and - // takes the possible-root step without loading the vtable. + // A hierarchy rooted in the program: a surviving release decrements the + // count (the low half of the rc word, which keeps the class id in its high + // half) and takes the possible-root step without loading the vtable. const fast = body(mangleClassRelease("Node")); - expect(fast).toContain("%shared = icmp ult i64 %rcm2, -3"); + expect(fast).toContain("%shared = icmp ult i32 %rcm2, -3"); expect(fast).toContain("load i32, ptr %bufp"); expect(fast).toContain(`call void @${mangleClassRelease("Node")}_slow(ptr %o)`); expect(fast).not.toContain("%ScrVt"); // Only the last reference dispatches, from a noinline remainder. const slow = body(`${mangleClassRelease("Node")}_slow`); expect(slow).toMatch(/noinline/); + // The class id indexes the hierarchy's vtable array. + expect(slow).toContain("%vt.id = load i32, ptr %vt.idp"); + expect(slow).toContain("ptr @sc_vta_Node, i64 0, i64 %vt.ix64"); expect(slow).toContain("getelementptr inbounds %ScrVt, ptr %vt, i64 0, i32 2"); + // 32-bit targets keep the vtable word. + const wasm = emitLlvmModule(mod, { pointerBits: 32 }); + expect(wasm).toContain("; class Node (vt at 1)"); + expect(wasm).not.toContain("@sc_vta_Node"); }); test("ProgramLifetime hierarchies are never freed and take no reference counts", () => { @@ -121,9 +129,11 @@ test("ProgramLifetime hierarchies are never freed and take no reference counts", expect(body(mangleClassRetain(cls)), cls).toMatch(/entry:\n {2}ret ptr %o\n}/); expect(body(mangleClassRelease(cls)), cls).toMatch(/entry:\n {2}ret void\n}/); // new: no collector header (no descriptor, no scr_cyc_alloc) and the - // program-lifetime count 2^63, not SIZE_MAX (which means published). + // program-lifetime count 2^31 under the class id (Node's preorder number + // 0 + 1, Leaf's 1 + 1), not SIZE_MAX (which means published). const nw = body(mangleClassNew(cls)); - expect(nw, cls).toContain("store i64 -9223372036854775808, ptr %o"); + const id = cls === "Node" ? 1 : 2; + expect(nw, cls).toContain(`store i64 ${id * 2 ** 32 + 2 ** 31}, ptr %o.vt`); expect(nw, cls).not.toContain("store i64 -1, ptr %o"); expect(nw, cls).not.toContain("scr_cyc_alloc"); expect(llvm, cls).not.toContain(`@${mangleClassTrace(cls)}.cyctype`); @@ -206,13 +216,15 @@ test("published ProgramLifetime instances use the published mark every other obj // a refusal restores the program-lifetime count); untraced, so it never // leaves a collector buffer. expect(llvm).toContain("call void @scr_publish(ptr"); - expect(llvm).toMatch(/@scr_pub_mark\(ptr %ctx, ptr %o, i1 false, i32 4\)/); - // The frozen-object guard of the store tests the count against SIZE_MAX: - // an unpublished instance (2^63) stays writable. + // Kind 4 (an object) | 0x100 (the rc word keeps a class id: the mark + // writes only the count). + expect(llvm).toMatch(/@scr_pub_mark\(ptr %ctx, ptr %o, i1 false, i32 260\)/); + // The frozen-object guard of the store tests the count against UINT32_MAX: + // an unpublished instance (2^31) stays writable. const store = llvm.split(/^define /m).find((fn) => fn.includes("call void @scr_throw_published_field(")) ?? ""; - expect(store).toMatch(/icmp eq i64 %\S+, -1/); + expect(store).toMatch(/icmp eq i32 %\S+, -1/); expect(store).toContain("call void @scr_publish(ptr"); // 32-bit targets use 2^31. const llvm32 = emitLlvmModule(mod, { pointerBits: 32 }); diff --git a/packages/compiler/src/backend/llvm/classes.ts b/packages/compiler/src/backend/llvm/classes.ts index f5a882bbf..5ad29430d 100644 --- a/packages/compiler/src/backend/llvm/classes.ts +++ b/packages/compiler/src/backend/llvm/classes.ts @@ -26,6 +26,7 @@ import { mangleCtorThunk, mangleFunction, mangleGlobal, + mangleVtArray, mangleVtInstance, mangleVtStruct, } from "../mangle.js"; @@ -81,6 +82,12 @@ export class LlClassMeta { * member's struct carries the i32 slot word before its fields * (classSlotWordIndex). */ threadSlot = false; + /** A member of a program-rooted hierarchy on a 64-bit target: instead of + * a vtable word, the rc word holds the class id (classIdRcWord) in its + * high 32 bits (CLASS_ID_OFFSET) and the reference count in its low 32 + * bits, and the hierarchy's vtables are one array indexed by the id + * (mangleVtArray). */ + idInRc = false; constructor(readonly def: IrClassDef) { this.root = this; @@ -96,6 +103,7 @@ export class LlClassMeta { export function buildClassGraph( mod: IrModule, fnByName: Map, + sizeType: "i32" | "i64" = "i64", ): Map { const metaMap = new Map(); for (const cls of mod.classes ?? []) { @@ -122,6 +130,11 @@ export function buildClassGraph( if (meta.base === null) number(meta, meta); meta.hierarchy = meta.base !== null || meta.children.length > 0; } + // The runtime reads the vtable word of runtime-rooted hierarchies (Error, + // EventEmitter, EventTarget, Event and the streams) and 32-bit targets + // have no spare half in the rc word: those keep the vtable word. + for (const meta of metaMap.values()) + meta.idInRc = sizeType === "i64" && meta.hierarchy && meta.root.def.runtime !== true; const slotRoots = new Set(); for (const key of mod.threadSlotKeys ?? []) { const meta = metaMap.get(key); @@ -336,11 +349,12 @@ function runtimePrefixOf(meta: LlClassMeta): RuntimePrefix | null { } /** The GEP index after a class's header words: rc at 0, the vtable word - * at 1 on hierarchy members, then a runtime prefix (state + display name) + * at 1 on hierarchy members that do not keep their class id in the rc + * word (LlClassMeta.idInRc), then a runtime prefix (state + display name) * on classes rooted at a runtime base, then the stream-state slot on * stream-rooted ones. */ function headerEnd(meta: LlClassMeta): number { - if (!meta.hierarchy) return 1; + if (!meta.hierarchy || meta.idInRc) return 1; if (streamRooted(meta)) return 5; return runtimePrefixOf(meta) ? 4 : 2; } @@ -445,6 +459,69 @@ export function programLifetimeRc(sizeType: "i32" | "i64"): string { return sizeType === "i32" ? "-2147483648" : "-9223372036854775808"; } +/** Lines that load the preorder number of the dynamic class of `obj`, an + * instance of `meta`'s hierarchy whose struct is `struct`, into `pre` (a + * `sizeType`): from the high half of the rc word (LlClassMeta.idInRc), or + * from the vtable its vtable word points at. */ +export function classPreLines( + meta: LlClassMeta, + struct: string, + obj: string, + pre: string, + sizeType: "i32" | "i64", +): string[] { + if (meta.idInRc) + return [ + ` ${pre}.idp = getelementptr inbounds i8, ptr ${obj}, i64 ${CLASS_ID_OFFSET}`, + ` ${pre}.id = load i32, ptr ${pre}.idp`, + ` ${pre}.n = add i32 ${pre}.id, -1 ; the class id is the preorder number + 1`, + ` ${pre} = zext i32 ${pre}.n to ${sizeType}`, + ]; + return [ + ` ${pre}.vtp = getelementptr inbounds %${struct}, ptr ${obj}, i64 0, i32 1`, + ` ${pre}.vt = load ptr, ptr ${pre}.vtp`, + ` ${pre}.prep = getelementptr inbounds %ScrVt, ptr ${pre}.vt, i64 0, i32 0`, + ` ${pre} = load ${sizeType}, ptr ${pre}.prep`, + ]; +} + +/** Lines that load the class id of the LlClassMeta.idInRc instance `obj` + * (`group` adds !invariant.group to the load) and point `vt` at its class's + * element of the hierarchy's vtable array. */ +export function classIdVtLines( + meta: LlClassMeta, + obj: string, + vt: string, + group: string, +): string[] { + const t = `${vt}.`; + const root = meta.root; + const vtt = `%${mangleVtStruct(root.def.name)}`; + const n = root.post - root.pre + 1; + return [ + ` ${t}idp = getelementptr inbounds i8, ptr ${obj}, i64 ${CLASS_ID_OFFSET}`, + ` ${t}id = load i32, ptr ${t}idp${group}`, + ` ${t}ix = sub i32 ${t}id, ${root.pre + 1}`, + ` ${t}ix64 = zext i32 ${t}ix to i64`, + ` ${vt} = getelementptr inbounds [${n} x ${vtt}], ptr @${mangleVtArray(root.def.name)}, i64 0, i64 ${t}ix64`, + ]; +} + +/** The byte offset of the class id in the rc word of an instance that + * keeps it there (LlClassMeta.idInRc): the high half of the little-endian + * 64-bit word. The count is the i32 at offset 0. */ +export const CLASS_ID_OFFSET = 4; + +/** The rc word an LlClassMeta.idInRc instance is created with: the class + * id, its preorder number plus 1, in the high 32 bits, and in the low 32 bits + * a count of 1, or for a program-lifetime instance the midpoint of the 32-bit + * count (as programLifetimeRc is of the 32-bit word). The id is never 0, + * 0x80000000 or 0xffffffff, the high halves of other objects' rc words, so + * the runtime can tell an object with a class id (scr_rc.h scr_rc_class_id). */ +export function classIdRcWord(meta: LlClassMeta, lifetime: boolean): string { + return (BigInt(meta.pre + 1) * 2n ** 32n + (lifetime ? 2n ** 31n : 1n)).toString(); +} + function undefFieldInits( host: ClassHost, meta: LlClassMeta, @@ -524,17 +601,18 @@ export function emitClassShapes( // ScrEmitter* / ScrStream* / ScrEventTargetObj* / ScrEventObj* are the // usual pointer reinterprets. const runtimePrefix = runtimePrefixOf(meta); - const prefix = meta.hierarchy - ? streamRooted(meta) - ? ["ptr", "ptr", "ptr", "ptr"] - : runtimePrefix - ? ["ptr", "ptr", "ptr"] - : ["ptr"] - : []; + const prefix = + meta.hierarchy && !meta.idInRc + ? streamRooted(meta) + ? ["ptr", "ptr", "ptr", "ptr"] + : runtimePrefix + ? ["ptr", "ptr", "ptr"] + : ["ptr"] + : []; const members = [...prefix, ...(meta.threadSlot ? ["i32"] : []), ...fieldTys]; typeDefs.push( `%${mangleClassStruct(cls.name)} = type { ${host.sizeType}${members.length ? ", " + members.join(", ") : ""} } ` + - `; class ${cls.name}${meta.hierarchy ? " (vt at 1)" : ""}${streamRooted(meta) ? " (ScrStream prefix at 2)" : runtimePrefix ? ` (${runtimePrefix.struct} prefix at 2)` : ""} { ${cls.fields.map((f) => llvmCommentText(f.name)).join("; ")} }`, + `; class ${cls.name}${meta.idInRc ? " (class id in rc)" : meta.hierarchy ? " (vt at 1)" : ""}${streamRooted(meta) ? " (ScrStream prefix at 2)" : runtimePrefix ? ` (${runtimePrefix.struct} prefix at 2)` : ""} { ${cls.fields.map((f) => llvmCommentText(f.name)).join("; ")} }`, ); } @@ -559,18 +637,40 @@ export function emitClassShapes( // One constant vtable instance per emitted hierarchy class: interval, // direct release, and the class's dispatch entry for every slot — the // method functions THEMSELVES (no adapters; see the header comment). - for (const cls of emitted) { - const meta = metaMap.get(cls.name)!; - if (!meta.hierarchy) continue; + const vtInit = (meta: LlClassMeta): string => { const entries = vtEntriesFor(meta).map(({ slot, impl }) => impl === null ? `ptr null` // outside the declaring subtree / fully-abstract chain : `ptr @${host.virtualEntry?.(`%${impl.def.name}.${slot.method}`) ?? mangleFunction(`%${impl.def.name}.${slot.method}`)}`, ); - const head = `%ScrVt { ${host.sizeType} ${meta.pre}, ${host.sizeType} ${meta.post}, ptr @${mangleClassReleaseDirect(cls.name)} }`; + const head = `%ScrVt { ${host.sizeType} ${meta.pre}, ${host.sizeType} ${meta.post}, ptr @${mangleClassReleaseDirect(meta.def.name)} }`; + return `{ ${[head, ...entries].join(", ")} }`; + }; + for (const cls of emitted) { + const meta = metaMap.get(cls.name)!; + if (!meta.hierarchy || meta.idInRc) continue; defs.push( `@${mangleVtInstance(cls.name)} = internal constant %${mangleVtStruct(meta.root.def.name)} ` + - `{ ${[head, ...entries].join(", ")} } ; class ${cls.name}`, + `${vtInit(meta)} ; class ${cls.name}`, + ``, + ); + } + // A hierarchy whose instances keep their class id in the rc word has + // one array of its classes' vtables, in preorder: the vtable of an + // instance is the element at its number minus the root's. + for (const name of roots) { + const root = metaMap.get(name)!; + if (!root.idInRc) continue; + const members = [...metaMap.values()] + .filter((m) => m.root === root) + .sort((a, b) => a.pre - b.pre); + const vtt = `%${mangleVtStruct(root.def.name)}`; + defs.push( + `@${mangleVtArray(root.def.name)} = internal constant [${members.length} x ${vtt}] [`, + ...members.map( + (m, i) => ` ${vtt} ${vtInit(m)}${i + 1 < members.length ? "," : ""} ; class ${m.def.name}`, + ), + `]`, ``, ); } @@ -605,6 +705,7 @@ export function emitClassShapes( release: `@${release}`, destroy: `${teardown}_destroy`, traced: host.tracedShapes.has(`object:${cls.name}`), + ...(meta.idInRc ? { count: "i32" as const } : {}), }, ]); } @@ -658,7 +759,13 @@ export function emitClassShapes( // retain: NULL-tolerant, immortal-skip — layout-generic (rc at 0), so // hierarchy members need no dispatch. - defs.push(...retainBody(host, mangleClassRetain(cls.name), `retain ${cls.name}`, lifetime), ``); + // An instance that keeps its class id in the rc word counts in the + // low half (CLASS_ID_OFFSET). + const count = meta.idInRc ? "i32" : host.sizeType; + defs.push( + ...retainBody(host, mangleClassRetain(cls.name), `retain ${cls.name}`, lifetime, count), + ``, + ); // An out-of-line teardown tears typed children down directly // (directChildReleaseLines). It takes the chain length when it has such @@ -714,6 +821,7 @@ export function emitClassShapes( traced, `release ${cls.name}`, lifetime, + count, ), ``, ); @@ -723,12 +831,16 @@ export function emitClassShapes( ` %isnull = icmp eq ptr %o, null`, ` br i1 %isnull, label %done, label %check`, `check:`, - ` %rc = load ${host.sizeType}, ptr %o`, - ` %imm = icmp eq ${host.sizeType} %rc, -1`, + ` %rc = load ${count}, ptr %o`, + ` %imm = icmp eq ${count} %rc, -1`, ` br i1 %imm, label %done, label %disp`, `disp:`, - ` %vtp = getelementptr inbounds %${struct}, ptr %o, i64 0, i32 1`, - ` %vt = load ptr, ptr %vtp`, + ...(meta.idInRc + ? classIdVtLines(meta, "%o", "%vt", "") + : [ + ` %vtp = getelementptr inbounds %${struct}, ptr %o, i64 0, i32 1`, + ` %vt = load ptr, ptr %vtp`, + ]), ` %relp = getelementptr inbounds %ScrVt, ptr %vt, i64 0, i32 2`, ` %rel = load ptr, ptr %relp`, ` call void %rel(ptr %o) ; the DYNAMIC class's teardown`, @@ -743,10 +855,10 @@ export function emitClassShapes( const reld: string[] = [ `define internal void @${mangleClassReleaseDirect(cls.name)}(ptr %o) ${FN_ATTRS} { ; direct release ${cls.name}`, `entry:`, - ` %rc = load ${host.sizeType}, ptr %o`, - ` %n = sub ${host.sizeType} %rc, 1`, - ` store ${host.sizeType} %n, ptr %o`, - ` %dead = icmp eq ${host.sizeType} %n, 0`, + ` %rc = load ${count}, ptr %o`, + ` %n = sub ${count} %rc, 1`, + ` store ${count} %n, ptr %o`, + ` %dead = icmp eq ${count} %n, 0`, ` br i1 %dead, label %free, label %${traced ? "root" : "done"}`, `free:`, ]; @@ -815,10 +927,22 @@ export function emitClassShapes( host, sizeOf, traced && !lifetime ? { type: cycType } : null, - meta.hierarchy ? 16 : 8, + meta.hierarchy && !meta.idInRc ? 16 : 8, ), ); - nw.push(` store ${host.sizeType} ${lifetime ? programLifetimeRc(host.sizeType) : 1}, ptr %o`); + if (meta.idInRc) { + // The class id in the high half of the rc word, the count in the low + // half (classIdRcWord). It never changes while the object lives: + // retains, releases and the runtime write only the low half. + host.declare(`declare ptr @llvm.launder.invariant.group.p0(ptr)`); + nw.push( + ` %o.vt = call ptr @llvm.launder.invariant.group.p0(ptr %o)`, + ` store i64 ${classIdRcWord(meta, lifetime)}, ptr %o.vt ; class ${llvmCommentText(cls.name)} is ${meta.pre}`, + ); + } else + nw.push( + ` store ${host.sizeType} ${lifetime ? programLifetimeRc(host.sizeType) : 1}, ptr %o`, + ); if (lifetime && audit) { // --sanitize builds: the RC audit skips its leak report when the // program keeps program-lifetime objects (scr_console.c), and @@ -826,7 +950,7 @@ export function emitClassShapes( host.declare(`declare void @scr_obj_lifetime_note(ptr)`); nw.push(` call void @scr_obj_lifetime_note(ptr %o)`); } - if (meta.hierarchy) { + if (meta.hierarchy && !meta.idInRc) { // The vtable word is written once, here, and never changes while the // object lives: its store and the virtual-call loads carry // !invariant.group. A recycled block may have held an object of diff --git a/packages/compiler/src/backend/llvm/common.ts b/packages/compiler/src/backend/llvm/common.ts index 8a8778919..e6fc56f6f 100644 --- a/packages/compiler/src/backend/llvm/common.ts +++ b/packages/compiler/src/backend/llvm/common.ts @@ -10,7 +10,9 @@ import type { import type { LlvmEmitterContext } from "./expr-context.js"; /** Field numbers of %ScrArr, which mirrors scr_runtime.h's ScrArr field for - * field (its static assertions pin `cyc`, `filled` and `data`). */ + * field (its static assertions pin `len`, `cap`, `cyc`, `filled` and + * `data`). `len` and `cap` are i32 on every target; the dense states end + * the allocation after the `cap` value slots at `data` (scrArrStates). */ export const SCR_ARR_FIELD = { rc: 0, len: 1, @@ -21,10 +23,92 @@ export const SCR_ARR_FIELD = { filled: 6, ops: 7, data: 8, - present: 9, - ext: 10, + ext: 9, } as const; +/** Loads the i32 `len` or `cap` of a %ScrArr at `ptr` into `dest` as a + * size-type count. */ +export function loadArrCount( + host: Pick, + dest: string, + ptr: string, + attach = "", +): void { + const B = host.B; + if (host.sizeType === "i32") { + B.line(`${dest} = load i32, ptr ${ptr}${attach}`); + return; + } + const raw = B.tmp(); + B.line(`${raw} = load i32, ptr ${ptr}${attach}`); + B.line(`${dest} = zext i32 ${raw} to ${host.sizeType}`); +} + +/** Stores the size-type count `value` (at most 2^32 - 1) into the i32 `len` + * or `cap` of a %ScrArr at `ptr`. */ +export function storeArrCount( + host: Pick, + value: string, + ptr: string, + attach = "", +): void { + const B = host.B; + let narrow = value; + if (host.sizeType !== "i32") { + narrow = B.tmp(); + B.line(`${narrow} = trunc ${host.sizeType} ${value} to i32`); + } + B.line(`store i32 ${narrow}, ptr ${ptr}${attach}`); +} + +/** The end of the dense state bytes of a %ScrArr into `dest` + * (scr_arr_states_end): `cap` value slots of `width` bytes at `data`, then + * one state byte per slot in reverse slot order. `cap` is a size-type count. */ +export function scrArrStates( + host: Pick, + dest: string, + data: string, + cap: string, + width: number, +): void { + host.B.line( + `${dest} = getelementptr inbounds [${width + 1} x i8], ptr ${data}, ${host.sizeType} ${cap}`, + ); +} + +/** The state byte of dense slot `offset` into `dest`, given the end of the + * states (scrArrStates): `end - 1 - offset`. */ +export function scrArrStatePtr( + host: Pick, + dest: string, + end: string, + offset: string, +): void { + const B = host.B; + const back = B.tmp(); + B.line(`${back} = xor ${host.sizeType} ${offset}, -1`); + B.line(`${dest} = getelementptr inbounds i8, ptr ${end}, ${host.sizeType} ${back}`); +} + +/** The state byte of dense slot `offset` of a %ScrArr into `dest`, from its + * `data` and size-type `cap`: scrArrStates and scrArrStatePtr in one + * address computation, for paths that read a single state. */ +export function scrArrStateAt( + host: Pick, + dest: string, + data: string, + cap: string, + width: number, + offset: string, +): void { + const B = host.B; + const back = B.tmp(); + B.line(`${back} = xor ${host.sizeType} ${offset}, -1`); + B.line( + `${dest} = getelementptr inbounds [${width + 1} x i8], ptr ${data}, ${host.sizeType} ${cap}, ${host.sizeType} ${back}`, + ); +} + /** Lines that decide whether the double `index` is an integer in * `[0, length)`, where `length` is an unsigned `sizeType` count, and give * that integer as an i64. `%{ok}` is the answer; `%{wide}` is meaningful diff --git a/packages/compiler/src/backend/llvm/dense-array-access.test.ts b/packages/compiler/src/backend/llvm/dense-array-access.test.ts index 6da427841..1b84cf0a0 100644 --- a/packages/compiler/src/backend/llvm/dense-array-access.test.ts +++ b/packages/compiler/src/backend/llvm/dense-array-access.test.ts @@ -170,6 +170,10 @@ test.each([32, 64] as const)( expect(fn.match(new RegExp(`icmp ult ${size} `, "g"))).toHaveLength(3); expect(fn).toMatch(/store double 0x4004000000000000, ptr /); expect(fn).toMatch(/zext i1 .* to i8/); + // Per push: data + (cap - len), then the state byte `width * cap - 1` + // past it; plus the boolean value slot. + expect(fn.match(/getelementptr inbounds \[8 x i8\], ptr .* -1$/gm)).toHaveLength(2); + expect(fn.match(/getelementptr inbounds \[1 x i8\], ptr .* -1$/gm)).toHaveLength(1); expect(fn.match(/getelementptr inbounds i8, ptr /g)).toHaveLength(4); expect(fn.match(/store i8 1, ptr /g)).toHaveLength(3); expect(fn.match(new RegExp(`add nuw ${size} .*, 1$`, "gm"))).toHaveLength(3); diff --git a/packages/compiler/src/backend/llvm/dense-array-access.ts b/packages/compiler/src/backend/llvm/dense-array-access.ts index d02d7f7c3..4488f90cb 100644 --- a/packages/compiler/src/backend/llvm/dense-array-access.ts +++ b/packages/compiler/src/backend/llvm/dense-array-access.ts @@ -17,7 +17,15 @@ * whose writes extend the bound when they continue it. */ import { isRefCounted, type IrExpr, type IrType } from "../../ir/ir.js"; -import { f64Lit, SCR_ARR_FIELD, sizeToDouble } from "./common.js"; +import { + f64Lit, + loadArrCount, + SCR_ARR_FIELD, + scrArrStatePtr, + scrArrStates, + sizeToDouble, + storeArrCount, +} from "./common.js"; import type { LlValue, LlvmEmitterContext } from "./expr-context.js"; import { exactInteger, widenInteger } from "./integer-values.js"; @@ -36,7 +44,8 @@ interface DenseGuard { len: string; /** The hole-free prefix bound loaded by the guard (i32). */ filled: string; - /** Dense value and state storage base pointers. */ + /** The dense value storage base pointer, and the end of the states + * (scrArrStates). */ data: string; states: string; } @@ -48,7 +57,9 @@ interface DenseGuard { * below the hole-free prefix bound branches to `filledLabel` first; both * that block and the returned (state-checked) dense block may use the * guard's values. A `proven` index is known to lie below the bound (a dense - * array loop's fast copy, dense-array-loops.ts) and always takes it. */ + * array loop's fast copy, dense-array-loops.ts) and always takes it. With + * `prefixOnly`, every other index branches to `slow` and no block is left + * open: only `offset` and `data` are meaningful. */ function emitDenseGuard( host: LlvmEmitterContext, array: string, @@ -57,7 +68,9 @@ function emitDenseGuard( slow: string, requireBelowLen: boolean, filledLabel: string, + width: number, proven = false, + prefixOnly = false, ): DenseGuard { const B = host.B; const size = host.sizeType; @@ -89,7 +102,6 @@ function emitDenseGuard( cap = B.tmp(), dataPtr = B.tmp(), data = B.tmp(), - statesPtr = B.tmp(), states = B.tmp(); B.line( `${dataPtr} = getelementptr inbounds %ScrArr, ptr ${array}, i32 0, i32 ${SCR_ARR_FIELD.data}`, @@ -97,26 +109,23 @@ function emitDenseGuard( host.markMemoryPointer(dataPtr, "array:header"); B.line(`${data} = load ptr, ptr ${dataPtr}${host.fieldAliasAttachment(dataPtr)}`); const loadChecked = (): void => { - B.line(`${lenPtr} = getelementptr inbounds %ScrArr, ptr ${array}, i32 0, i32 1`); + B.line( + `${lenPtr} = getelementptr inbounds %ScrArr, ptr ${array}, i32 0, i32 ${SCR_ARR_FIELD.len}`, + ); host.markMemoryPointer(lenPtr, "array:header"); + loadArrCount(host, len, lenPtr, host.fieldAliasAttachment(lenPtr)); B.line( - `${len} = load ${size}, ptr ${lenPtr}${host.fieldAliasAttachment(lenPtr)}${host.arrayLengthRange}`, + `${capPtr} = getelementptr inbounds %ScrArr, ptr ${array}, i32 0, i32 ${SCR_ARR_FIELD.cap}`, ); - B.line(`${capPtr} = getelementptr inbounds %ScrArr, ptr ${array}, i32 0, i32 2`); host.markMemoryPointer(capPtr, "array:header"); - B.line(`${cap} = load ${size}, ptr ${capPtr}${host.fieldAliasAttachment(capPtr)}`); - B.line( - `${statesPtr} = getelementptr inbounds %ScrArr, ptr ${array}, i32 0, i32 ${SCR_ARR_FIELD.present}`, - ); - host.markMemoryPointer(statesPtr, "array:header"); - B.line(`${states} = load ptr, ptr ${statesPtr}${host.fieldAliasAttachment(statesPtr)}`); + loadArrCount(host, cap, capPtr, host.fieldAliasAttachment(capPtr)); }; // Reads load the storage bases before branching: the header of a live // array is always readable, and unconditional loads let LLVM hoist them // out of loops whose only calls are read-only fallbacks. Writes load the // fields that only the state-checked path needs inside it, which keeps // them off a hot loop of prefix stores (that path stores to the header). - const eagerHeader = requireBelowLen; + const eagerHeader = requireBelowLen && !prefixOnly; if (eagerHeader) loadChecked(); const filledPtr = B.tmp(), filled = B.tmp(), @@ -148,6 +157,10 @@ function emitDenseGuard( B.line(`${t} = and i1 ${integral}, ${inFilled}`); inFilled = t; } + if (prefixOnly) { + B.condBr(inFilled, filledLabel, slow); + return { offset, len, filled, data, states }; + } const check = B.newLabel("arr.check"); B.condBr(inFilled, filledLabel, check); B.startBlock(check); @@ -171,6 +184,10 @@ function emitDenseGuard( const dense = B.newLabel("arr.dense"); B.condBr(ok, dense, slow); B.startBlock(dense); + // Only the state-checked block needs the states: the address is pure + // arithmetic on loaded fields, so computing it here keeps it off the + // prefix path. + scrArrStates(host, states, data, cap, width); return { offset, len, filled, data, states }; } @@ -186,11 +203,8 @@ function statePointer( host: LlvmEmitterContext, guard: Pick, ): string { - const B = host.B; - const statePtr = B.tmp(); - B.line( - `${statePtr} = getelementptr inbounds i8, ptr ${guard.states}, ${host.sizeType} ${guard.offset}`, - ); + const statePtr = host.B.tmp(); + scrArrStatePtr(host, statePtr, guard.states, guard.offset); host.markMemoryPointer(statePtr, "array:present"); return statePtr; } @@ -201,6 +215,11 @@ function slotType(acc: ArrayAccess): "i8" | "i64" { return acc === "bool" ? "i8" : "i64"; } +/** Bytes per dense slot (scr_arr_slot_width). */ +export function slotWidth(acc: ArrayAccess): number { + return acc === "bool" ? 1 : 8; +} + function valuePointer( host: LlvmEmitterContext, guard: Pick, @@ -245,7 +264,7 @@ export function emitDenseSlotUnlink( const unlink = B.newLabel("arr.unlink"), swap = B.newLabel("arr.unlink.swap"), done = B.newLabel("arr.unlink.done"); - const guard = emitDenseGuard(host, array, index, indexExpr, done, true, unlink); + const guard = emitDenseGuard(host, array, index, indexExpr, done, true, unlink, 8); // The state-checked dense block: leave holes and the tail to the store. B.br(done); B.startBlock(unlink); @@ -290,11 +309,29 @@ export function emitDenseArrayGet( const slow = B.newLabel("arr.get.slow"), join = B.newLabel("arr.get.join"), valueLabel = B.newLabel("arr.get.value"); - const guard = emitDenseGuard(host, array, index, indexExpr, slow, true, valueLabel, proven); - const state = loadState(host, guard); - const present = B.tmp(); - B.line(`${present} = icmp eq i8 ${state}, ${SCR_ARR_VALUE}`); - B.condBr(present, valueLabel, slow); + // The state of slot i is at data + cap * (width + 1) - 1 - i: an inline + // state check indexed by a loop's induction variable makes LLVM keep + // -1 - i as a second induction variable, so above the prefix the runtime + // getter reads the slot. + const induction = indexExpr?.kind === "varRef" && host.integerLoopBindings.has(indexExpr.localId); + const guard = emitDenseGuard( + host, + array, + index, + indexExpr, + slow, + true, + valueLabel, + slotWidth(acc), + proven, + induction, + ); + if (!induction) { + const state = loadState(host, guard); + const present = B.tmp(); + B.line(`${present} = icmp eq i8 ${state}, ${SCR_ARR_VALUE}`); + B.condBr(present, valueLabel, slow); + } B.startBlock(valueLabel); const ptr = valuePointer(host, guard, acc); let fast: string; @@ -360,6 +397,7 @@ export function emitDenseArrayState( index: LlValue, indexExpr: IrExpr | undefined, kind: "state" | "has", + width: number, ): string { const B = host.B; const fn = kind === "state" ? "scr_arr_state" : "scr_arr_has"; @@ -368,7 +406,7 @@ export function emitDenseArrayState( const slow = B.newLabel("arr.state.slow"), join = B.newLabel("arr.state.join"), filledLabel = B.newLabel("arr.state.filled"); - const guard = emitDenseGuard(host, array, index, indexExpr, slow, true, filledLabel); + const guard = emitDenseGuard(host, array, index, indexExpr, slow, true, filledLabel, width); const state = loadState(host, guard); const fast = B.tmp(); if (kind === "state") B.line(`${fast} = uitofp i8 ${state} to double`); @@ -421,7 +459,17 @@ export function emitDenseArraySet( const slow = B.newLabel("arr.set.slow"), join = B.newLabel("arr.set.join"), filledLabel = B.newLabel("arr.set.filled"); - const guard = emitDenseGuard(host, array, index, indexExpr, slow, false, filledLabel, proven); + const guard = emitDenseGuard( + host, + array, + index, + indexExpr, + slow, + false, + filledLabel, + slotWidth(acc), + proven, + ); const { offset, len } = guard; const storeValue = (valuePtr: string): void => { if (acc === "f64") { @@ -482,9 +530,11 @@ export function emitDenseArraySet( const nextLen = B.tmp(), lenPtr = B.tmp(); B.line(`${nextLen} = add ${host.sizeType} ${offset}, 1`); - B.line(`${lenPtr} = getelementptr inbounds %ScrArr, ptr ${array}, i32 0, i32 1`); + B.line( + `${lenPtr} = getelementptr inbounds %ScrArr, ptr ${array}, i32 0, i32 ${SCR_ARR_FIELD.len}`, + ); host.markMemoryPointer(lenPtr, "array:header"); - B.line(`store ${host.sizeType} ${nextLen}, ptr ${lenPtr}${host.fieldAliasAttachment(lenPtr)}`); + storeArrCount(host, nextLen, lenPtr, host.fieldAliasAttachment(lenPtr)); B.br(storedLabel); B.startBlock(storedLabel); if (acc === "ref" && oldState && old) { @@ -542,7 +592,7 @@ export function emitDenseArrayPop( const lenPtr = header(SCR_ARR_FIELD.len), len = B.tmp(), none = B.tmp(); - B.line(`${len} = load ${size}, ptr ${lenPtr}${host.fieldAliasAttachment(lenPtr)}`); + loadArrCount(host, len, lenPtr, host.fieldAliasAttachment(lenPtr)); B.line(`${none} = icmp eq ${size} ${len}, 0`); const check = B.newLabel("arr.pop.check"), dense = B.newLabel("arr.pop.dense"); @@ -557,12 +607,15 @@ export function emitDenseArrayPop( B.startBlock(dense); const last = B.tmp(); B.line(`${last} = sub nuw ${size} ${len}, 1`); + // A packed array's last index is below its capacity (filled <= cap). const dataPtr = header(SCR_ARR_FIELD.data), data = B.tmp(), - statesPtr = header(SCR_ARR_FIELD.present), + capPtr = header(SCR_ARR_FIELD.cap), + cap = B.tmp(), states = B.tmp(); B.line(`${data} = load ptr, ptr ${dataPtr}${host.fieldAliasAttachment(dataPtr)}`); - B.line(`${states} = load ptr, ptr ${statesPtr}${host.fieldAliasAttachment(statesPtr)}`); + loadArrCount(host, cap, capPtr, host.fieldAliasAttachment(capPtr)); + scrArrStates(host, states, data, cap, slotWidth(acc)); const valuePtr = valuePointer(host, { offset: last, data }, acc); const statePtr = statePointer(host, { offset: last, states }); const slotTy = slotType(acc); @@ -581,7 +634,7 @@ export function emitDenseArrayPop( B.line(`${last32} = trunc i64 ${last} to i32`); } B.line(`store i32 ${last32}, ptr ${filledPtr}${host.fieldAliasAttachment(filledPtr)}`); - B.line(`store ${size} ${last}, ptr ${lenPtr}${host.fieldAliasAttachment(lenPtr)}`); + B.line(`store i32 ${last32}, ptr ${lenPtr}${host.fieldAliasAttachment(lenPtr)}`); return raw; } @@ -608,13 +661,13 @@ export function emitDenseArrayPush( host.markMemoryPointer(ptr, "array:header"); return ptr; }; - const lenPtr = header(1), + const lenPtr = header(SCR_ARR_FIELD.len), len = B.tmp(), cap = B.tmp(), room = B.tmp(); - B.line(`${len} = load ${size}, ptr ${lenPtr}${host.fieldAliasAttachment(lenPtr)}`); - const capPtr = header(2); - B.line(`${cap} = load ${size}, ptr ${capPtr}${host.fieldAliasAttachment(capPtr)}`); + loadArrCount(host, len, lenPtr, host.fieldAliasAttachment(lenPtr)); + const capPtr = header(SCR_ARR_FIELD.cap); + loadArrCount(host, cap, capPtr, host.fieldAliasAttachment(capPtr)); B.line(`${room} = icmp ult ${size} ${len}, ${cap}`); const fast = B.newLabel("arr.push.dense"), slow = B.newLabel("arr.push.slow"), @@ -623,11 +676,18 @@ export function emitDenseArrayPush( B.startBlock(fast); const dataPtr = header(SCR_ARR_FIELD.data), data = B.tmp(), - statesPtr = header(SCR_ARR_FIELD.present), - states = B.tmp(); + spare = B.tmp(), + near = B.tmp(), + statePtr = B.tmp(); B.line(`${data} = load ptr, ptr ${dataPtr}${host.fieldAliasAttachment(dataPtr)}`); - B.line(`${states} = load ptr, ptr ${statesPtr}${host.fieldAliasAttachment(statesPtr)}`); - const statePtr = statePointer(host, { offset: len, states }); + // The state of slot `len` is data + width * cap + (cap - len) - 1. With + // `cap - len` formed first, the store addresses it from one register sum. + B.line(`${spare} = sub nuw ${size} ${cap}, ${len}`); + B.line(`${near} = getelementptr inbounds i8, ptr ${data}, ${size} ${spare}`); + B.line( + `${statePtr} = getelementptr inbounds [${slotWidth(acc)} x i8], ptr ${near}, ${size} ${cap}, ${size} -1`, + ); + host.markMemoryPointer(statePtr, "array:present"); const valuePtr = valuePointer(host, { offset: len, data }, acc); if (acc === "f64") { B.line(`store double ${value}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`); @@ -642,7 +702,7 @@ export function emitDenseArrayPush( const next = B.tmp(), fastValue = B.tmp(); B.line(`${next} = add nuw ${size} ${len}, 1`); - B.line(`store ${size} ${next}, ptr ${lenPtr}${host.fieldAliasAttachment(lenPtr)}`); + storeArrCount(host, next, lenPtr, host.fieldAliasAttachment(lenPtr)); // An append to a hole-free array keeps it hole-free. const filledPtr = header(SCR_ARR_FIELD.filled), filled = B.tmp(), diff --git a/packages/compiler/src/backend/llvm/dense-array-loops.ts b/packages/compiler/src/backend/llvm/dense-array-loops.ts index 271a5ddf8..708825344 100644 --- a/packages/compiler/src/backend/llvm/dense-array-loops.ts +++ b/packages/compiler/src/backend/llvm/dense-array-loops.ts @@ -22,7 +22,7 @@ import type { IntegerRange } from "../../ir/integer-ranges.js"; import { preservesArrayLength } from "../../ir/array-length-stability.js"; import { everyExprChild, everyStmtChild } from "../../ir/traverse.js"; import type { LlvmEmitterContext } from "./expr-context.js"; -import { SCR_ARR_FIELD } from "./common.js"; +import { loadArrCount, SCR_ARR_FIELD } from "./common.js"; export const INT32_MAX = 2147483647; @@ -187,12 +187,10 @@ export function emitDenseLoopGuard(host: LlvmEmitterContext, loop: DenseArrayLoo return ptr; }; const receiver = host.emitStableReceiver(loop.length, []); - const lenPtr = header(receiver.name, 1), + const lenPtr = header(receiver.name, SCR_ARR_FIELD.len), len = B.tmp(), small = B.tmp(); - B.line( - `${len} = load ${size}, ptr ${lenPtr}${host.fieldAliasAttachment(lenPtr)}${host.arrayLengthRange}`, - ); + loadArrCount(host, len, lenPtr, host.fieldAliasAttachment(lenPtr)); B.line(`${small} = icmp ule ${size} ${len}, ${INT32_MAX}`); let valid = small; for (const [id, ref] of loop.arrays) { diff --git a/packages/compiler/src/backend/llvm/dyn.ts b/packages/compiler/src/backend/llvm/dyn.ts index 731385825..19c1a17ac 100644 --- a/packages/compiler/src/backend/llvm/dyn.ts +++ b/packages/compiler/src/backend/llvm/dyn.ts @@ -39,6 +39,7 @@ import { isDirentShape, isDynTypedRefType, isRefCounted, + isUnitType, typeKey, } from "../../ir/ir.js"; import { dynDesc, streamTypedRefEligible } from "../../ir/analysis.js"; @@ -697,6 +698,46 @@ export class LlDyn { /** Emit the standard in-helper pending check: on a pending exception * run `cleanup` and return `dummy`. */ + /** The failure block of a union dynCheck whose value matched no arm. A + * value of the one container kind exactly one arm can take (an object + * for the union's only record arm, an array for its only array arm, + * beside unit and scalar arms) runs that arm's builder, which throws the + * deepest failing path and the reason there (`expected string at + * $.order.owner.id, got undefined`) instead of the union's own outer + * path. Other values, and an arm builder that unexpectedly succeeds, + * fall through to the union's message. Only the failure path changes. */ + private unionArmFailure(B: BlockBuilder, def: IrUnionDef, kind: string): void { + const scalar = (arm: IrType): boolean => + isUnitType(arm) || + arm.kind === "f64" || + arm.kind === "bool" || + arm.kind === "string" || + arm.kind === "bigint"; + const containers = def.arms.filter((arm) => !scalar(arm)); + const arm = containers.length === 1 ? containers[0]! : null; + const container = + arm?.kind === "array" + ? DYN_KIND.ARR + : arm?.kind === "record" && !this.host.recordsById.get(arm.shapeId)?.tuple + ? DYN_KIND.OBJ + : null; + if (!arm || container === null) return; + const host = this.host; + const deep = B.newLabel("dcu.deep"); + const outer = B.newLabel("dcu.outer"); + const same = B.tmp(); + B.line(`${same} = icmp eq i32 ${kind}, ${container}`); + B.condBr(same, deep, outer); + B.startBlock(deep); + const built = B.tmp(); + B.line(`${built} = call ptr @${this.dynCheckHelper(arm)}(ptr %d, ptr %path)`); + this.pendingBail(B, "dcu.deep", () => {}, "ptr null"); + const rc = vAdapters(host, arm); + B.line(`call void ${rc.release}(ptr ${built})`); + B.br(outer); + B.startBlock(outer); + } + private pendingBail(B: BlockBuilder, hint: string, cleanup: () => void, dummy: string): void { const p = B.tmp(); for (const line of this.host.pendingTestLines(p)) B.line(line.trimStart()); @@ -1407,22 +1448,29 @@ export class LlDyn { * differently), runs the checked-dynamic route — scr_json_parse plus the * dynCheck builder — which reports the exact error. Returns +1, or null * with the pending flag set. */ - jsonParseHelper(t: IrType): string | null { + jsonParseHelper(t: IrType, fallback = false): string | null { if (t.kind !== "record" && t.kind !== "array") return null; const key = typeKey(t); - const existing = this.jsonParsers.get(key); + const cacheKey = fallback ? `fallback:${key}` : key; + const existing = this.jsonParsers.get(cacheKey); if (existing) return existing; if (!this.jsonSchemaSupported(t, new Set())) return null; const host = this.host; const name = `sc_jp_${this.jsonParsers.size}`; - this.jsonParsers.set(key, name); + this.jsonParsers.set(cacheKey, name); const schema = this.jsonSchema(t); const check = this.dynCheckHelper(t); host.declare(`declare ptr @scr_json_parse_schema(ptr, ptr)`); host.declare(`declare ptr @scr_json_parse(ptr)`); host.declare(`declare void @scr_dyn_release(ptr)`); + // A fallback site (dynCheck.fallback): a value that fails validation + // parks as the checked value (scr_dyn_check_recover) and the helper + // returns the dummy with no exception pending. Syntax errors still + // throw. + if (fallback) host.declare(`declare zeroext i1 @scr_dyn_check_recover(ptr)`); + const recover = fallback ? [` call zeroext i1 @scr_dyn_check_recover(ptr %d)`] : []; this.defs.push( - `define internal ptr @${name}(ptr %text) ${FN_ATTRS} { ; JSON.parse as ${llvmCommentText(key)}`, + `define internal ptr @${name}(ptr %text) ${FN_ATTRS} { ; JSON.parse as ${llvmCommentText(key)}${fallback ? " (fallback)" : ""}`, `entry:`, ` %fast = call ptr @scr_json_parse_schema(ptr %text, ptr @${schema})`, ` %hit = icmp ne ptr %fast, null`, @@ -1437,6 +1485,7 @@ export class LlDyn { ` ret ptr null`, `check:`, ` %v = call ptr @${check}(ptr %d, ptr null)`, + ...recover, ` call void @scr_dyn_release(ptr %d)`, ` ret ptr %v`, `}`, @@ -1631,10 +1680,10 @@ export class LlDyn { : []), ] : [ - { index: 1, type: host.sizeType, name: "length" }, - { index: 2, type: host.sizeType, name: "capacity" }, + // The dense states follow the data slots: they move with data and capacity. + { index: SCR_ARR_FIELD.len, type: "i32", name: "length" }, + { index: SCR_ARR_FIELD.cap, type: "i32", name: "capacity" }, { index: SCR_ARR_FIELD.data, type: "ptr" as const, name: "data" }, - { index: SCR_ARR_FIELD.present, type: "ptr" as const, name: "presence" }, { index: SCR_ARR_FIELD.filled, type: "i32" as const, name: "filled" }, ]; members.forEach((member) => { @@ -2291,6 +2340,7 @@ export class LlDyn { B.line(`call void @scr_dyn_release_v(ptr ${materialized})`); B.terminate(`ret ptr ${checked}`); B.startBlock(lFail); + this.unionArmFailure(B, def, kind); B.line(`call void @scr_dyn_check_fail(ptr %path, ptr ${want}, ptr %d)`); B.terminate(`ret ptr null`); break; @@ -2359,6 +2409,7 @@ export class LlDyn { ); B.terminate(`ret ptr ${checked}`); B.startBlock(lFail); + this.unionArmFailure(B, def, kind); B.line(`call void @scr_dyn_check_fail(ptr %path, ptr ${want}, ptr %d)`); B.terminate(`ret ptr null`); break; diff --git a/packages/compiler/src/backend/llvm/emit-module.ts b/packages/compiler/src/backend/llvm/emit-module.ts index 4e4f11828..6a3df30cd 100644 --- a/packages/compiler/src/backend/llvm/emit-module.ts +++ b/packages/compiler/src/backend/llvm/emit-module.ts @@ -436,14 +436,15 @@ export function emitModuleParts(emitter: LlEmitter): string[] { `%ScrClosure = type { ${emitter.sizeType}, ptr, ${emitter.sizeType}, ptr, i32, i32, ptr, ptr }`, `%ScrFfiTable = type { ptr, ${emitter.sizeType}, ${emitter.sizeType}, ptr, i8, ptr, ptr, ${emitter.sizeType}, ${emitter.sizeType}, ${emitter.sizeType}, ptr, ptr }`, `%ScrRegex = type { ${emitter.sizeType}, ptr, ptr, ptr, double, ptr, ptr, ptr }`, - // ScrArr mirrors scr_runtime.h field-for-field: { rc, len, cap, elem, - // cyc, spare, filled, ops, data, present, ext } (field numbers in - // SCR_ARR_FIELD), where filled bounds the hole-free prefix, ops points at the element type's constant RC descriptor + // ScrArr mirrors scr_runtime.h field-for-field: { rc, len (i32), cap + // (i32), elem, cyc, spare, filled, ops, data, ext } (field numbers in + // SCR_ARR_FIELD), where the dense states follow the cap value slots at + // data, filled bounds the hole-free prefix, ops points at the element type's constant RC descriptor // (%ScrArrOps, SCR_ELEM_REF only) and ext at the out-of-line sparse, // property and metadata storage (NULL until first used). Live dynamic // stream commits swap its dense, presence and out-of-line storage while // preserving the target object's identity. - `%ScrArr = type { ${emitter.sizeType}, ${emitter.sizeType}, ${emitter.sizeType}, i8, i8, i16, i32, ptr, ptr, ptr, ptr }`, + `%ScrArr = type { ${emitter.sizeType}, i32, i32, i8, i8, i16, i32, ptr, ptr, ptr }`, `%ScrArrOps = type { ptr, ptr, ptr }`, // The ScrMap prefix { rc, key_kind, val_kind, traced } — the inline // RC fast paths test the traced byte (shapes.ts). @@ -684,17 +685,23 @@ export function emitModuleParts(emitter: LlEmitter): string[] { if (emitter.regexInstances.size > 0) out.push(``); for (const code of regexMatchers.values()) out.push(...code.lines, ``); for (const [, inst] of emitter.templateStringsInstances) { - // One immortal ScrArr per tagged-template site: a [N x ptr] data - // global of interned cooked-string literals, and the ScrArr header - // over it (rc == SIZE_MAX, len == cap == filled, SCR_ELEM_STR = 2, - // no REF entry points). Every dense slot is present; reads retain - // immortal strings — a no-op. + // One immortal ScrArr per tagged-template site: a data global of + // interned cooked-string literals followed by their states (the + // runtime finds them before data + cap * 9), and the ScrArr header over + // it (rc == SIZE_MAX, len == cap == filled, SCR_ELEM_STR = 2, no REF + // entry points). Every dense slot is present; reads retain immortal + // strings — a no-op. Slots are 8 bytes on every target: on 32-bit + // targets each holds the pointer in its low half and zero in its high + // half, so the states start where the runtime looks for them. const n = inst.slots.length; const present = n === 0 ? "zeroinitializer" : `[ ${inst.slots.map(() => "i8 1").join(", ")} ]`; + const wide = emitter.sizeType === "i64"; + const slot = wide ? "ptr" : "<{ ptr, i32 }>"; + const slotValue = (s: string): string => + wide ? `ptr ${s}` : `<{ ptr, i32 }> <{ ptr ${s}, i32 0 }>`; out.push( - `@${inst.sym}_data = internal constant [${n} x ptr] [ ${inst.slots.map((s) => `ptr ${s}`).join(", ")} ]`, - `@${inst.sym}_present = internal constant [${n} x i8] ${present}`, - `@${inst.sym} = internal global %ScrArr { ${emitter.sizeType} -1, ${emitter.sizeType} ${n}, ${emitter.sizeType} ${n}, i8 2, i8 0, i16 0, i32 ${n}, ptr null, ptr @${inst.sym}_data, ptr @${inst.sym}_present, ptr null }`, + `@${inst.sym}_data = internal constant <{ [${n} x ${slot}], [${n} x i8] }> <{ [${n} x ${slot}] [ ${inst.slots.map(slotValue).join(", ")} ], [${n} x i8] ${present} }>, align 8`, + `@${inst.sym} = internal global %ScrArr { ${emitter.sizeType} -1, i32 ${n}, i32 ${n}, i8 2, i8 0, i16 0, i32 ${n}, ptr null, ptr @${inst.sym}_data, ptr null }`, ); } if (emitter.templateStringsInstances.size > 0) out.push(``); @@ -1333,14 +1340,17 @@ export function helperDefs(emitter: LlEmitter): string[] { // of the capacity word (SCR_STR_ASCII_BIT, the low half's sign bit on // little-endian targets) makes UTF-16 units bytes. Unproven strings and // anything but an in-range index take the runtime's UTF-16 mapping. - // A scan's receiver reads inline too where runtime state is ordinary - // global storage (ScrStrScan; SCR_TL makes it thread-local otherwise). - const scanInline = emitter.mod.workers !== true && emitter.mod.lib?.threadInstances !== true; + // A scan's receiver reads inline too (ScrStrScan): from ordinary global + // storage, or, in worker executables, from the thread's static TLS block, + // one thread-pointer-relative load (see executableTls). + const scanInline = + emitter.executableTls || + (emitter.mod.workers !== true && emitter.mod.lib?.threadInstances !== true); let scanDeclared = false; for (const helper of STRING_READ_HELPERS) { if (!emitter.decls.has(helper.decl)) continue; if (scanInline && !scanDeclared) { - defs.push(stringScanDecl(emitter.sizeType), ``); + defs.push(stringScanDecl(emitter.sizeType, emitter.executableTls), ``); scanDeclared = true; } defs.push(...helper.define(emitter.sizeType, FN_ATTRS, scanInline), ``); diff --git a/packages/compiler/src/backend/llvm/emit-statements.ts b/packages/compiler/src/backend/llvm/emit-statements.ts index a5da56f55..da2ffbcab 100644 --- a/packages/compiler/src/backend/llvm/emit-statements.ts +++ b/packages/compiler/src/backend/llvm/emit-statements.ts @@ -36,7 +36,7 @@ import { emitDiscardedSplice } from "./expr-containers.js"; import { InternalCompilerError } from "../../errors.js"; import { emitDenseArrayGet, emitDenseArrayPop, emitDenseArraySet } from "./dense-array-access.js"; import { emitBorrowedInput } from "./borrowed-inputs.js"; -import { f64Lit, llvmCommentText, sizeToDouble } from "./common.js"; +import { f64Lit, llvmCommentText, loadArrCount, sizeToDouble } from "./common.js"; import { RESULT_DST_ID, RESULT_ENTRY_PREFIX } from "./record-results.js"; import { matchIntegerArrayForLoop, @@ -440,9 +440,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { done = B.newLabel("arr.len.done"); B.line(`${lenPtr} = getelementptr inbounds %ScrArr, ptr ${arr.name}, i32 0, i32 1`); emitter.markMemoryPointer(lenPtr, "array:header"); - B.line( - `${len} = load ${emitter.sizeType}, ptr ${lenPtr}${emitter.fieldAliasAttachment(lenPtr)}${emitter.arrayLengthRange}`, - ); + loadArrCount(emitter, len, lenPtr, emitter.fieldAliasAttachment(lenPtr)); B.line( `${lenNumber} = ${sizeToDouble(emitter.sizeType)} ${emitter.sizeType} ${len} to double`, ); @@ -1092,12 +1090,26 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { B.line( `${lenPtr} = getelementptr inbounds %${integerArrayLoop ? "ScrArr" : "ScrBytes"}, ptr ${receiver.name}, i64 0, i32 1`, ); - emitter.markMemoryPointer(lenPtr, integerArrayLoop ? "array:header" : "bytes:header"); - B.line( - `${len} = load ${emitter.sizeType}, ptr ${lenPtr}${emitter.fieldAliasAttachment(lenPtr)}`, - ); B.line(`${index} = load ${emitter.sizeType}, ptr ${integerSlot}`); - B.line(`${inBounds} = icmp ult ${emitter.sizeType} ${index}, ${len}`); + if (integerArrayLoop) { + // The counter starts at most 2^32 - 1 and steps only while it is + // below the length, so it fits the i32 length field: compare in + // 32 bits, which lets the compare read the field directly. + emitter.markMemoryPointer(lenPtr, "array:header"); + B.line(`${len} = load i32, ptr ${lenPtr}${emitter.fieldAliasAttachment(lenPtr)}`); + let narrow = index; + if (emitter.sizeType === "i64") { + narrow = B.tmp(); + B.line(`${narrow} = trunc i64 ${index} to i32`); + } + B.line(`${inBounds} = icmp ult i32 ${narrow}, ${len}`); + } else { + emitter.markMemoryPointer(lenPtr, "bytes:header"); + B.line( + `${len} = load ${emitter.sizeType}, ptr ${lenPtr}${emitter.fieldAliasAttachment(lenPtr)}`, + ); + B.line(`${inBounds} = icmp ult ${emitter.sizeType} ${index}, ${len}`); + } B.condBr(inBounds, lb, exit); } else if (countedLoop && integerSlot && countedLimit) { const index = B.tmp(); @@ -1203,9 +1215,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { // The inline form of scr_arr_len, like the `length` intrinsic. B.line(`${lenPtr} = getelementptr inbounds %ScrArr, ptr ${arr!.name}, i32 0, i32 1`); emitter.markMemoryPointer(lenPtr, "array:header"); - B.line( - `${rawLen} = load ${emitter.sizeType}, ptr ${lenPtr}${emitter.fieldAliasAttachment(lenPtr)}${emitter.arrayLengthRange}`, - ); + loadArrCount(emitter, rawLen, lenPtr, emitter.fieldAliasAttachment(lenPtr)); B.line( `${len} = ${sizeToDouble(emitter.sizeType)} ${emitter.sizeType} ${rawLen} to double`, ); diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 3c0ab2378..8a4b16fc5 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -266,6 +266,7 @@ import { buildClassGraph, classMembershipIntervals, classFieldIndex, + classPreLines, classStructSym, programLifetimeClass, type LlClassMeta, @@ -621,13 +622,6 @@ export class LlEmitter { return `, !tbaa !${tag}`; } - /** The `!range` attachment of an array length load: a length is at most - * 2^32 - 1 (the runtime's SCR_ARR_MAX_LENGTH), so conversions to double - * and back fold. 32-bit targets load the full range anyway. */ - get arrayLengthRange(): string { - return this.sizeType === "i64" ? ", !range !{i64 0, i64 4294967296}" : ""; - } - /** Whether an element access at `index` of `arr` lies in the hole-free * prefix: the counter of a dense array loop's fast copy indexing one of * the arrays its entry test covered. */ @@ -1471,7 +1465,7 @@ export class LlEmitter { // The class graph: base/children links, hierarchy membership, the // whole-program preorder numbering for instanceof over runtime and // compiled error objects, and the per-hierarchy virtual slot lists. - this.classMeta = buildClassGraph(mod, this.fnByName); + this.classMeta = buildClassGraph(mod, this.fnByName, this.sizeType); if (mod.lifetimeRegions === true) { this.regions = new LifetimeRegions( this.shapeHost, @@ -2822,10 +2816,16 @@ export class LlEmitter { } else if (t.kind === "string") { this.declare(`declare void @scr_throw_str(ptr)`); B.line(`call void @scr_throw_str(ptr ${v.name})`); - } else if (t.kind === "object" && this.classMeta.get(t.className)?.hierarchy === true) { + } else if ( + t.kind === "object" && + this.classMeta.get(t.className)?.hierarchy === true && + this.classMeta.get(t.className)?.idInRc !== true + ) { // Hierarchy instances carry a vtable word: the OBJ kind keeps the // dynamic class inspectable (catch-binding instanceof, the uncaught - // printer's "name: message" for Error instances). + // printer's "name: message" for Error instances). Instances that keep + // their class id in the rc word are never Errors and are thrown as + // REF payloads (narrow-guards.ts emitCaughtClassIdInstanceof). const rc = vAdapters(this.shapeHost, t); this.declare(`declare void @scr_throw_obj(ptr, ptr, ptr, ptr)`); B.line( @@ -3158,20 +3158,20 @@ export class LlEmitter { return { ptr: p, type, inline: this.inlineUnions.classField(className, field) }; } - /** `o->vt->pre` — the dynamic class's preorder number (borrowed object; - * the static class names which struct spelling carries the vt word). */ + /** The dynamic class's preorder number (borrowed object; the static class + * names the struct spelling and where the number is, classPreLines). */ loadVtPre(objName: string, staticClassName: string): string { const B = this.B; - const vtp = B.tmp(); - const vt = B.tmp(); - const prep = B.tmp(); const pre = B.tmp(); - B.line( - `${vtp} = getelementptr inbounds %${classStructSym(staticClassName)}, ptr ${objName}, i64 0, i32 1`, - ); - B.line(`${vt} = load ptr, ptr ${vtp}`); - B.line(`${prep} = getelementptr inbounds %ScrVt, ptr ${vt}, i64 0, i32 0`); - B.line(`${pre} = load ${this.sizeType}, ptr ${prep}`); + const meta = this.classMetaOf(staticClassName); + for (const line of classPreLines( + meta, + classStructSym(staticClassName), + objName, + pre, + this.sizeType, + )) + B.line(line.trimStart()); return pre; } @@ -5103,6 +5103,7 @@ export class LlEmitter { pre: meta.pre, post: meta.post, runtimeRooted: meta.root.def.runtime === true, + idInRc: meta.idInRc, // Inline scalar-union fields are plain bits: nothing to push. fields: meta.def.fields.flatMap((f) => this.inlineUnions.classField(className, f.name) @@ -5141,15 +5142,17 @@ export class LlEmitter { return this.own({ name: value.name, type: e.type }); } - /** Branches on "`obj` is published" (rc == SIZE_MAX); leaves the builder - * in the cold block and answers the continuation label. */ - private emitPublishedTest(obj: string): string { + /** Branches on "`obj` is published" (rc == SIZE_MAX, or a count of + * UINT32_MAX in the low half for `count` i32: classes.ts + * LlClassMeta.idInRc); leaves the builder in the cold block and answers + * the continuation label. */ + private emitPublishedTest(obj: string, count: "i32" | "i64" = this.sizeType): string { const B = this.B; const rc = B.tmp(), frozen = B.tmp(), expected = B.tmp(); - B.line(`${rc} = load ${this.sizeType}, ptr ${obj}`); - B.line(`${frozen} = icmp eq ${this.sizeType} ${rc}, -1`); + B.line(`${rc} = load ${count}, ptr ${obj}`); + B.line(`${frozen} = icmp eq ${count} ${rc}, -1`); this.declare(`declare i1 @llvm.expect.i1(i1, i1)`); B.line(`${expected} = call i1 @llvm.expect.i1(i1 ${frozen}, i1 false)`); const cold = B.newLabel("pub.frozen"), @@ -5189,22 +5192,17 @@ export class LlEmitter { if (published === null) return; if (className !== null && !published.classGuarded(className)) return; const B = this.B; - const ok = this.emitPublishedTest(obj); - let owner: string; const meta = className === null ? undefined : this.classMeta.get(className); + const ok = this.emitPublishedTest(obj, meta?.idInRc === true ? "i32" : this.sizeType); + let owner: string; if (meta === undefined) owner = this.cstr("Object"); else if (!meta.hierarchy) owner = this.cstr(meta.def.jsName ?? meta.def.name); else { // The DYNAMIC class names the object, as in Node. this.publishedNamesSym ??= "@sc_pub_class_names"; - const vtp = B.tmp(), - vt = B.tmp(), - pre = B.tmp(), + const pre = this.loadVtPre(obj, meta.def.name), slot = B.tmp(), name = B.tmp(); - B.line(`${vtp} = getelementptr inbounds ptr, ptr ${obj}, i64 1`); - B.line(`${vt} = load ptr, ptr ${vtp}`); - B.line(`${pre} = load ${this.sizeType}, ptr ${vt}`); B.line( `${slot} = getelementptr inbounds ptr, ptr ${this.publishedNamesSym}, ${this.sizeType} ${pre}`, ); diff --git a/packages/compiler/src/backend/llvm/expr-async.ts b/packages/compiler/src/backend/llvm/expr-async.ts index 6da63977d..901a8749e 100644 --- a/packages/compiler/src/backend/llvm/expr-async.ts +++ b/packages/compiler/src/backend/llvm/expr-async.ts @@ -374,7 +374,7 @@ export function emitSerializationExpr( // falls back to exactly this route whenever the fast path declines. const parsed = e.value; if (!e.preserveRefs && parsed.kind === "libCall" && parsed.fn === "json.parse") { - const fused = host.dyn.jsonParseHelper(e.type); + const fused = host.dyn.jsonParseHelper(e.type, e.fallback === true); if (fused) { const text = emitBorrowedInput(host, parsed.args[0]!); const r = B.tmp(); @@ -400,6 +400,12 @@ export function emitSerializationExpr( B.line( `${t} = call ${ty === "i1" ? "zeroext i1" : ty} @${helper}(ptr ${dynV.name}, ptr null)`, ); + // A fallback site: a value that fails validation parks as the + // checked value instead of throwing (dynCheck.fallback). + if (e.fallback) { + host.declare(`declare zeroext i1 @scr_dyn_check_recover(ptr)`); + B.line(`call zeroext i1 @scr_dyn_check_recover(ptr ${dynV.name})`); + } const out = host.own({ name: t, type: e.type }); host.emitPendingCheck(); return out; diff --git a/packages/compiler/src/backend/llvm/expr-calls.ts b/packages/compiler/src/backend/llvm/expr-calls.ts index b49c8895e..43dfce459 100644 --- a/packages/compiler/src/backend/llvm/expr-calls.ts +++ b/packages/compiler/src/backend/llvm/expr-calls.ts @@ -34,6 +34,7 @@ import { } from "../mangle.js"; import { classEnvironmentIndex, + classIdVtLines, classStructSym, vtEntriesFor, type LlClassMeta, @@ -1193,15 +1194,20 @@ export function emitCallExpr( if (!borrowed.has(index)) host.toFrame(a); }); const recv = args[0]!.name; - const vtp = B.tmp(); const vt = B.tmp(); const fnp = B.tmp(); const fn = B.tmp(); - B.line( - `${vtp} = getelementptr inbounds %${classStructSym(e.className)}, ptr ${recv}, i64 0, i32 1`, - ); // Set once at allocation (classes.ts launders each new object). - B.line(`${vt} = load ptr, ptr ${vtp}, !invariant.group !{}`); + if (meta.idInRc) + for (const line of classIdVtLines(meta, recv, vt, ", !invariant.group !{}")) + B.line(line.trimStart()); + else { + const vtp = B.tmp(); + B.line( + `${vtp} = getelementptr inbounds %${classStructSym(e.className)}, ptr ${recv}, i64 0, i32 1`, + ); + B.line(`${vt} = load ptr, ptr ${vtp}, !invariant.group !{}`); + } B.line( `${fnp} = getelementptr inbounds %${mangleVtStruct(meta.root.def.name)}, ptr ${vt}, i64 0, i32 ${slotIdx + 1}`, ); diff --git a/packages/compiler/src/backend/llvm/expr-containers.ts b/packages/compiler/src/backend/llvm/expr-containers.ts index 0ca76e2d5..afa69998d 100644 --- a/packages/compiler/src/backend/llvm/expr-containers.ts +++ b/packages/compiler/src/backend/llvm/expr-containers.ts @@ -17,7 +17,7 @@ import { vAdapters, } from "./shapes.js"; import type { LlvmEmitterContext, LlValue } from "./expr-context.js"; -import { F64_INF, f64Lit, sizeToDouble } from "./common.js"; +import { F64_INF, f64Lit, loadArrCount, sizeToDouble } from "./common.js"; import { MUTATING_COLLECTION_METHODS } from "../publication.js"; import { borrowsStringInputs, emitStringInputs } from "./string-lifetimes.js"; import { @@ -564,9 +564,7 @@ export function emitArrIntrinsic( t = B.tmp(); B.line(`${p} = getelementptr inbounds %ScrArr, ptr ${r.name}, i32 0, i32 1`); host.markMemoryPointer(p, "array:header"); - B.line( - `${len} = load ${host.sizeType}, ptr ${p}${host.fieldAliasAttachment(p)}${host.arrayLengthRange}`, - ); + loadArrCount(host, len, p, host.fieldAliasAttachment(p)); B.line(`${t} = ${sizeToDouble(host.sizeType)} ${host.sizeType} ${len} to double`); return { name: t, type: e.type }; } diff --git a/packages/compiler/src/backend/llvm/expr-dynamic.ts b/packages/compiler/src/backend/llvm/expr-dynamic.ts index bc4f1128a..da3e1bfa4 100644 --- a/packages/compiler/src/backend/llvm/expr-dynamic.ts +++ b/packages/compiler/src/backend/llvm/expr-dynamic.ts @@ -28,6 +28,7 @@ import { elemAccess, vAdapters } from "./shapes.js"; import { LlvmUnsupportedError } from "./unsupported.js"; import type { LlvmEmitterContext, ExprOf, LlValue } from "./expr-context.js"; import { emitUnionWiden } from "./expr-records.js"; +import { DYN_BUFFER_NUM_METHODS } from "../../ir/runtime-features.js"; export function emitDynamicExpr( host: LlvmEmitterContext, @@ -226,9 +227,12 @@ export function emitDynamicExpr( }); argsPtr = arr; } - host.declare( - `declare ptr @scr_dyn_invoke_prepared(ptr, ptr, ptr, ptr, ${host.sizeType}, ptr)`, - ); + // Fixed-width Buffer method names take scr_bytes_dyn.c's entry, which + // runs them on Buffer receivers and dispatches any other receiver. + const invoke = DYN_BUFFER_NUM_METHODS.has(e.method) + ? "scr_dyn_invoke_buffer_num" + : "scr_dyn_invoke_prepared"; + host.declare(`declare ptr @${invoke}(ptr, ptr, ptr, ptr, ${host.sizeType}, ptr)`); let name = host.cstr(e.calleeName); if (e.calleeNameValue !== undefined) { const value = emitBorrowedInput(host, e.calleeNameValue); @@ -237,7 +241,7 @@ export function emitDynamicExpr( } const t = B.tmp(); B.line( - `${t} = call ptr @scr_dyn_invoke_prepared(ptr ${recv.name}, ptr ${prepared}, ptr ${host.cstr(e.method)}, ptr ${argsPtr}, ${host.sizeType} ${args.length}, ptr ${name})`, + `${t} = call ptr @${invoke}(ptr ${recv.name}, ptr ${prepared}, ptr ${host.cstr(e.method)}, ptr ${argsPtr}, ${host.sizeType} ${args.length}, ptr ${name})`, ); const out = host.own({ name: t, type: e.type }); host.emitPendingCheck(); @@ -1058,6 +1062,42 @@ export function emitDynamicExpr( host.declare( `declare ptr @scr_caught_check_obj(ptr, ${host.sizeType}, ${host.sizeType}, ptr)`, ); + if (target.idInRc) { + // A REF payload (emitCaughtInstanceof): a match retains it through + // its adapter; anything else gets the runtime's TypeError from the + // check against an empty interval. + const ok = emitCaughtInstanceof(host, c.name, e.className); + const yes = B.newLabel("caught.cast"), + no = B.newLabel("caught.cast.fail"), + join = B.newLabel("caught.cast.done"); + B.condBr(ok, yes, no); + B.startBlock(yes); + const payloadSlot = B.tmp(), + payload = B.tmp(), + retainSlot = B.tmp(), + retain = B.tmp(), + retained = B.tmp(); + B.line(`${payloadSlot} = getelementptr inbounds %ScrCaught, ptr ${c.name}, i64 0, i32 4`); + B.line(`${payload} = load ptr, ptr ${payloadSlot}`); + B.line(`${retainSlot} = getelementptr inbounds %ScrCaught, ptr ${c.name}, i64 0, i32 5`); + B.line(`${retain} = load ptr, ptr ${retainSlot}`); + B.line(`${retained} = call ptr ${retain}(ptr ${payload})`); + const fromYes = B.currentLabel(); + B.br(join); + B.startBlock(no); + const failed = B.tmp(); + B.line( + `${failed} = call ptr @scr_caught_check_obj(ptr ${c.name}, ${host.sizeType} 1, ${host.sizeType} 0, ptr ${host.cstr(display)})`, + ); + const fromNo = B.currentLabel(); + B.br(join); + B.startBlock(join); + const t = B.tmp(); + B.line(`${t} = phi ptr [ ${retained}, %${fromYes} ], [ ${failed}, %${fromNo} ]`); + const out = host.own({ name: t, type: e.type }); + host.emitPendingCheck(); + return out; + } let pre = String(target.pre); let post = String(target.post); const intervals = classMembershipIntervals(host.classMeta, target.def.name); diff --git a/packages/compiler/src/backend/llvm/expr-primitives.test.ts b/packages/compiler/src/backend/llvm/expr-primitives.test.ts index 1790854ef..41ecd189e 100644 --- a/packages/compiler/src/backend/llvm/expr-primitives.test.ts +++ b/packages/compiler/src/backend/llvm/expr-primitives.test.ts @@ -306,3 +306,69 @@ test("sums that only feed a bitwise conversion of int32 operands add in 32 bits" } expect(body("number_add")).not.toMatch(/ = add i32 |fcmp ord/); }); + +test.each([32, 64] as const)( + "tagged-template string arrays keep 8-byte slots before their states on %i-bit targets", + (bits) => { + const strings: IrExpr = { + kind: "templateStrings", + key: "site", + cooked: ["a", "b", "c"], + type: { kind: "array", elem: STRING }, + loc, + }; + const mod = fixture(); + mod.functions[0]!.body = [{ kind: "exprStmt", expr: strings, loc }]; + const llvm = emitLlvmModule(mod, { pointerBits: bits }); + const data = /^@sc_tsa_0_data = internal constant .*$/m.exec(llvm)![0]; + // The runtime reads each slot as 8 bytes and finds the states after + // cap * 8 bytes of slots (scr_arr_states_end). + if (bits === 64) expect(data).toContain("<{ [3 x ptr], [3 x i8] }>"); + else { + expect(data).toContain("<{ [3 x <{ ptr, i32 }>], [3 x i8] }>"); + expect(data.match(/<\{ ptr @\S+, i32 0 \}>/g)).toHaveLength(3); + } + expect(data).toContain("[3 x i8] [ i8 1, i8 1, i8 1 ]"); + }, +); + +test("string reads of a scan receiver load it inline where emitted code can address it", () => { + const receiver: IrExpr = { kind: "varRef", localId: "text", type: STRING, loc }; + const mod = fixture(); + mod.functions = [ + { name: "__main", params: [], returnType: VOID, locals: [], body: [], loc }, + { + name: "units", + params: [{ name: "text", localId: "text", type: STRING }], + returnType: F64, + locals: [{ id: "text", name: "text", type: STRING, mutable: false }], + loc, + body: [ + { + kind: "return", + loc, + value: { kind: "strIntrinsic", method: "length", receiver, args: [], type: F64, loc }, + }, + ], + }, + ]; + const helper = (llvm: string): string => + /^define internal double @sc_str_utf16_len\([^]*?^}/m.exec(llvm)![0]; + const plain = emitLlvmModule(mod); + expect(plain).toContain("@scr_str_scan = external global"); + expect(helper(plain)).toContain("%scan.same"); + // ELF worker executables keep the receiver in the thread's static TLS block. + const elf = emitLlvmModule( + { ...mod, workers: true }, + { targetTriple: "x86_64-unknown-linux-gnu" }, + ); + expect(elf).toContain("@scr_str_scan = external thread_local(localexec) global"); + expect(helper(elf)).toContain("%scan.same"); + // Darwin worker executables leave every non-ASCII read to the runtime. + const darwin = emitLlvmModule( + { ...mod, workers: true }, + { targetTriple: "arm64-apple-macosx14.0.0" }, + ); + expect(darwin).not.toContain("@scr_str_scan"); + expect(helper(darwin)).not.toContain("%scan.same"); +}); diff --git a/packages/compiler/src/backend/llvm/expr-primitives.ts b/packages/compiler/src/backend/llvm/expr-primitives.ts index 0d2cd90e8..69f4def2e 100644 --- a/packages/compiler/src/backend/llvm/expr-primitives.ts +++ b/packages/compiler/src/backend/llvm/expr-primitives.ts @@ -32,7 +32,7 @@ import { emitPowerOfTwoRemainder, emitSignedIntegerRemainder, } from "./integer-remainder.js"; -import { emitDenseArrayGet, emitDenseArrayState } from "./dense-array-access.js"; +import { emitDenseArrayGet, emitDenseArrayState, slotWidth } from "./dense-array-access.js"; import { emitWrappingSum, markWrappingOperands } from "./wrapping-sums.js"; export function emitLiteralExpr( @@ -936,7 +936,14 @@ export function emitContainerExpr( const idx = host.emitExpr(e.index); if (e.arr.type.kind !== "array") throw new InternalCompilerError("llvm emitter bug: arrayHas on non-array"); - const t = emitDenseArrayState(host, arr.name, idx, e.index, "has"); + const t = emitDenseArrayState( + host, + arr.name, + idx, + e.index, + "has", + slotWidth(elemAccess(e.arr.type.elem)), + ); return { name: t, type: e.type }; } case "arrayState": { @@ -944,7 +951,14 @@ export function emitContainerExpr( const idx = host.emitExpr(e.index); if (e.arr.type.kind !== "array") throw new InternalCompilerError("llvm emitter bug: arrayState on non-array"); - const t = emitDenseArrayState(host, arr.name, idx, e.index, "state"); + const t = emitDenseArrayState( + host, + arr.name, + idx, + e.index, + "state", + slotWidth(elemAccess(e.arr.type.elem)), + ); return { name: t, type: e.type }; } case "arrIntrinsic": diff --git a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts index bbd0226fb..6b51f9afa 100644 --- a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts +++ b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts @@ -20,7 +20,7 @@ import { } from "../../ir/ir.js"; import { mangleFunction, mangleGlobal, mangleRecordStruct } from "../mangle.js"; import { BlockBuilder } from "./blocks.js"; -import { type LlClassMeta, classFieldIndex, classStructSym } from "./classes.js"; +import { type LlClassMeta, classFieldIndex, classPreLines, classStructSym } from "./classes.js"; import { SCR_ARR_FIELD, llvmCommentText } from "./common.js"; import { FN_ATTRS, @@ -188,10 +188,10 @@ export function streamTypedRefCommitAdapter( ` call void @scr_arr_outline(ptr %next)`, ]; const storageMembers = [ - { name: "len", index: 1, type: host.sizeType }, - { name: "cap", index: 2, type: host.sizeType }, + // The dense states follow the data slots, so they move with data and cap. + { name: "len", index: SCR_ARR_FIELD.len, type: "i32" }, + { name: "cap", index: SCR_ARR_FIELD.cap, type: "i32" }, { name: "data", index: SCR_ARR_FIELD.data, type: "ptr" }, - { name: "present", index: SCR_ARR_FIELD.present, type: "ptr" }, { name: "filled", index: SCR_ARR_FIELD.filled, type: "i32" }, { name: "ext", index: SCR_ARR_FIELD.ext, type: "ptr" }, ]; @@ -232,10 +232,13 @@ export function streamTypedRefCommitAdapter( ]; if (meta.hierarchy && meta.children.length) { lines.push( - ` %derived_vt_slot = getelementptr inbounds %${classStructSym(t.className)}, ptr %target, i64 0, i32 1`, - ` %derived_vt = load ptr, ptr %derived_vt_slot`, - ` %derived_pre_slot = getelementptr inbounds %ScrVt, ptr %derived_vt, i64 0, i32 0`, - ` %derived_pre = load ${host.sizeType}, ptr %derived_pre_slot`, + ...classPreLines( + meta, + classStructSym(t.className), + "%target", + "%derived_pre", + host.sizeType, + ), ); const descendants = (current: typeof meta): (typeof meta)[] => current.children.flatMap((child) => [child, ...descendants(child)]); @@ -728,16 +731,9 @@ export function streamTypedRefMaterializeAdapter( } if (adapter.classOps !== undefined) liveClassOps(host, snapshot, meta); if (meta.hierarchy && meta.children.length) { - const vtSlot = B.tmp(), - vt = B.tmp(), - preSlot = B.tmp(), - pre = B.tmp(); - B.line( - `${vtSlot} = getelementptr inbounds %${classStructSym(t.className)}, ptr %p, i64 0, i32 1`, - ); - B.line(`${vt} = load ptr, ptr ${vtSlot}`); - B.line(`${preSlot} = getelementptr inbounds %ScrVt, ptr ${vt}, i64 0, i32 0`); - B.line(`${pre} = load ${host.sizeType}, ptr ${preSlot}`); + const pre = B.tmp(); + for (const line of classPreLines(meta, classStructSym(t.className), "%p", pre, host.sizeType)) + B.line(line.trimStart()); const descendants = (current: typeof meta): (typeof meta)[] => current.children.flatMap((child) => [child, ...descendants(child)]); for (const child of descendants(meta).reverse()) { @@ -1364,16 +1360,9 @@ function liveClassOps(host: LlvmEmitterContext, snapshot: string, meta: LlClassM if (subclasses.length > 0) { exact = `@${snapshot}_cexact`; const B = new BlockBuilder(); - const vtSlot = B.tmp(), - vt = B.tmp(), - preSlot = B.tmp(), - pre = B.tmp(); - B.line( - `${vtSlot} = getelementptr inbounds %${classStructSym(meta.def.name)}, ptr %p, i64 0, i32 1`, - ); - B.line(`${vt} = load ptr, ptr ${vtSlot}`); - B.line(`${preSlot} = getelementptr inbounds %ScrVt, ptr ${vt}, i64 0, i32 0`); - B.line(`${pre} = load ${host.sizeType}, ptr ${preSlot}`); + const pre = B.tmp(); + for (const line of classPreLines(meta, classStructSym(meta.def.name), "%p", pre, host.sizeType)) + B.line(line.trimStart()); for (const child of subclasses.reverse()) { const childAdapter = host.liveDynRefAdapter({ kind: "object", className: child.def.name }); const match = B.tmp(), diff --git a/packages/compiler/src/backend/llvm/integer-clones.ts b/packages/compiler/src/backend/llvm/integer-clones.ts index f3efa1259..314eae5ea 100644 --- a/packages/compiler/src/backend/llvm/integer-clones.ts +++ b/packages/compiler/src/backend/llvm/integer-clones.ts @@ -1009,10 +1009,10 @@ class CloneEmitter { B.line( `${field} = getelementptr inbounds %ScrArr, ptr ${array}, i32 0, i32 ${SCR_ARR_FIELD.len}`, ); - B.line(`${len} = load i64, ptr ${field}`); - B.line(`${fits} = icmp ule i64 ${len}, 2147483647`); + B.line(`${len} = load i32, ptr ${field}`); + B.line(`${fits} = icmp ule i32 ${len}, 2147483647`); this.require(fits); - B.line(`${value} = trunc i64 ${len} to i32`); + B.line(`${value} = add i32 ${len}, 0`); return value; } diff --git a/packages/compiler/src/backend/llvm/lib-dispatch.ts b/packages/compiler/src/backend/llvm/lib-dispatch.ts index 924027ee2..283c5b38d 100644 --- a/packages/compiler/src/backend/llvm/lib-dispatch.ts +++ b/packages/compiler/src/backend/llvm/lib-dispatch.ts @@ -865,6 +865,7 @@ export function emitLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue { case "blob": case "wasm": case "record": + case "checkedCast": case "disposable": case "punycode": case "constants": diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 8cd40240f..b8335fe04 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -374,6 +374,7 @@ export const LIB_FN_SYMS: Record = { "fsp.globStrings": "scr_fsp_glob_strings", "fsp.globError": "scr_fsp_glob_error", "fs.realpathSync": "scr_fs_realpath", + "fs.realpathValue": "scr_fs_realpath_value", "fs.realpathNativeSync": "scr_fs_realpath_promise", "fs.unlinkSync": "scr_fs_unlink", "fs.chmodSync": "scr_fs_chmod", @@ -437,6 +438,7 @@ export const LIB_FN_SYMS: Record = { "buffer.concatDyn": "scr_buffer_concat_dyn", "buffer.concatLenDyn": "scr_buffer_concat_len_dyn", "buffer.alloc": "scr_buffer_alloc", + "buffer.allocValue": "scr_buffer_alloc_value", "buffer.allocUnsafe": "scr_buffer_alloc_unsafe", "buffer.allocUnsafeSlow": "scr_buffer_alloc_unsafe_slow", "buffer.poolFromStr": "scr_buffer_from_str", @@ -444,9 +446,11 @@ export const LIB_FN_SYMS: Record = { "buffer.poolFromBytes": "scr_buffer_from_bytes", "buffer.poolFromArray": "scr_buffer_from_arr", "buffer.poolFromDyn": "scr_buffer_from_dyn_pooled", + "buffer.poolFromDynRef": "scr_buffer_from_dyn_ref_pooled", "buffer.poolSize": "scr_buffer_pool_size", "buffer.setPoolSize": "scr_buffer_set_pool_size", "buffer.byteLenDyn": "scr_buffer_byte_length_dyn", + "buffer.byteLenValue": "scr_buffer_byte_length_value", "buffer.byteLenStr": "scr_bytes_byte_length_str", "buffer.isEncoding": "scr_bytes_is_encoding", // The checked-dynamic compare/equals validators (scr_bytes_io.c): @@ -472,6 +476,7 @@ export const LIB_FN_SYMS: Record = { "fs.existsChk": "scr_fs_exists_async", "fs.mkdtempSyncChk": "scr_fs_mkdtemp_sync_chk", "net.connectAttempt": "scr_net_connect_attempt", + "net.connectLookupDyn": "scr_net_connect_lookup_dyn", "fs.lchmodSyncChk": "scr_fs_lchmod_sync_chk", "fsp.lchmodChk": "scr_fsp_lchmod_chk", "fs.readFileSyncBuf": "scr_fs_read_file_bytes", @@ -626,6 +631,9 @@ export const LIB_FN_SYMS: Record = { "insp.regex": "scr_insp_regex", "insp.date": "scr_insp_date", "insp.error": "scr_insp_error", + "insp.errorHasCause": "scr_insp_error_has_cause", + "insp.errorBagHasProperties": "scr_insp_error_bag_has_properties", + "insp.errorCause": "scr_insp_error_cause", "insp.begin": "scr_insp_begin", "insp.layout": "scr_insp_layout", "insp.entry": "scr_insp_entry", @@ -643,6 +651,9 @@ export const LIB_FN_SYMS: Record = { "insp.circular": "scr_insp_circular", // Undeclared source properties kept beside a checked record (scr_json.c). "record.sourceHas": "scr_record_source_has", + // Checked-cast fallback reads (scr_json.c); take answers +1. + "checkedCast.fallbackTaken": "scr_dyn_check_fallback_taken", + "checkedCast.fallbackTake": "scr_dyn_check_fallback_take", // WHATWG URL + URLSearchParams (scr_url.c / scr_url_params.c): // constructions +1; url.new, the fileURLToPath pair, the win32 // pathToFileURL flavor, and sp.fromPairs throw catchably (may-throw). @@ -1688,6 +1699,7 @@ export const USES_TIMERS_LIB_FNS = new Set([ "net.connect", "net.connectCb", "net.connectLookup", + "net.connectLookupDyn", "net.connectAttempt", "fs.existsChk", "fs.callbackValue", diff --git a/packages/compiler/src/backend/llvm/local-array-reads.ts b/packages/compiler/src/backend/llvm/local-array-reads.ts index 6951896d2..b9fd0f54b 100644 --- a/packages/compiler/src/backend/llvm/local-array-reads.ts +++ b/packages/compiler/src/backend/llvm/local-array-reads.ts @@ -12,7 +12,7 @@ import { everyStmtList } from "../../ir/traverse.js"; import { analyzeCallLifetimes, type CallLifetimes } from "./call-lifetimes.js"; import type { LlValue, LlvmEmitterContext } from "./expr-context.js"; import { ReferenceEffects } from "./reference-effects.js"; -import { SCR_ARR_FIELD, exactIndexLines } from "./common.js"; +import { SCR_ARR_FIELD, exactIndexLines, loadArrCount, scrArrStateAt } from "./common.js"; import { exactInteger, widenInteger } from "./integer-values.js"; import { READ_FALLBACK } from "./dense-array-access.js"; @@ -457,12 +457,14 @@ export function emitProjectedArrayRead(host: LlvmEmitterContext, read: LocalArra /** Share the checked dense lookup between optional stack boxes and strict * borrowed reads. Capacity, length, and presence guards precede every load; - * sparse and noncanonical indices keep their runtime lookup semantics. */ + * sparse and noncanonical indices keep their runtime lookup semantics. + * `induction` says `integerIndex` is a loop's induction variable. */ function emitDenseReferenceArrayRead( host: LlvmEmitterContext, array: LlValue, index: LlValue, integerIndex: string | null, + induction: boolean, present: (value: string) => void, no: string, slow: string, @@ -476,9 +478,11 @@ function emitDenseReferenceArrayRead( // noncanonical numeric properties retain the runtime lookup semantics. const capPtr = B.tmp(), cap = B.tmp(); - B.line(`${capPtr} = getelementptr inbounds %ScrArr, ptr ${array.name}, i32 0, i32 2`); + B.line( + `${capPtr} = getelementptr inbounds %ScrArr, ptr ${array.name}, i32 0, i32 ${SCR_ARR_FIELD.cap}`, + ); host.markMemoryPointer(capPtr, "array:header"); - B.line(`${cap} = load ${host.sizeType}, ptr ${capPtr}${host.fieldAliasAttachment(capPtr)}`); + loadArrCount(host, cap, capPtr, host.fieldAliasAttachment(capPtr)); let offset: string, inRange: string; if (integerIndex) { offset = integerIndex; @@ -499,6 +503,15 @@ function emitDenseReferenceArrayRead( B.startBlock(range); B.br(dense); B.startBlock(dense); + // The state and value paths below both index from `data`: one load here + // serves both. + const dataPtr = B.tmp(), + data = B.tmp(); + B.line( + `${dataPtr} = getelementptr inbounds %ScrArr, ptr ${array.name}, i32 0, i32 ${SCR_ARR_FIELD.data}`, + ); + host.markMemoryPointer(dataPtr, "array:header"); + B.line(`${data} = load ptr, ptr ${dataPtr}${host.fieldAliasAttachment(dataPtr)}`); // Below the hole-free prefix bound every state is a present value. const filledPtr = B.tmp(), filled = B.tmp(), @@ -516,45 +529,38 @@ function emitDenseReferenceArrayRead( } B.line(`${belowFilled} = icmp ult ${host.sizeType} ${offset}, ${filledSized}`); const valueLabel = B.newLabel("local.array.value"); - B.condBr(belowFilled, valueLabel, lenLabel); - B.startBlock(lenLabel); - const lenPtr = B.tmp(), - len = B.tmp(), - belowLen = B.tmp(); - B.line(`${lenPtr} = getelementptr inbounds %ScrArr, ptr ${array.name}, i32 0, i32 1`); - host.markMemoryPointer(lenPtr, "array:header"); - B.line( - `${len} = load ${host.sizeType}, ptr ${lenPtr}${host.fieldAliasAttachment(lenPtr)}${host.arrayLengthRange}`, - ); - B.line(`${belowLen} = icmp ult ${host.sizeType} ${offset}, ${len}`); - const stateLabel = B.newLabel("local.array.state"); - B.condBr(belowLen, stateLabel, no); - B.startBlock(stateLabel); - const statesPtr = B.tmp(), - states = B.tmp(), - statePtr = B.tmp(), - denseState = B.tmp(), - densePresent = B.tmp(); - B.line( - `${statesPtr} = getelementptr inbounds %ScrArr, ptr ${array.name}, i32 0, i32 ${SCR_ARR_FIELD.present}`, - ); - host.markMemoryPointer(statesPtr, "array:header"); - B.line(`${states} = load ptr, ptr ${statesPtr}${host.fieldAliasAttachment(statesPtr)}`); - B.line(`${statePtr} = getelementptr inbounds i8, ptr ${states}, ${host.sizeType} ${offset}`); - host.markMemoryPointer(statePtr, "array:present"); - B.line(`${denseState} = load i8, ptr ${statePtr}${host.fieldAliasAttachment(statePtr)}`); - B.line(`${densePresent} = icmp eq i8 ${denseState}, 1`); - B.condBr(densePresent, valueLabel, no); + if (induction) { + // The state of slot i is at data + cap * (width + 1) - 1 - i, and an + // inline state check makes LLVM keep -1 - i as a second induction + // variable in the loop; above the prefix the runtime reads the slot. + B.condBr(belowFilled, valueLabel, slow); + } else { + B.condBr(belowFilled, valueLabel, lenLabel); + B.startBlock(lenLabel); + const lenPtr = B.tmp(), + len = B.tmp(), + belowLen = B.tmp(); + B.line( + `${lenPtr} = getelementptr inbounds %ScrArr, ptr ${array.name}, i32 0, i32 ${SCR_ARR_FIELD.len}`, + ); + host.markMemoryPointer(lenPtr, "array:header"); + loadArrCount(host, len, lenPtr, host.fieldAliasAttachment(lenPtr)); + B.line(`${belowLen} = icmp ult ${host.sizeType} ${offset}, ${len}`); + const stateLabel = B.newLabel("local.array.state"); + B.condBr(belowLen, stateLabel, no); + B.startBlock(stateLabel); + const statePtr = B.tmp(), + denseState = B.tmp(), + densePresent = B.tmp(); + scrArrStateAt(host, statePtr, data, cap, loadType === "i8" ? 1 : 8, offset); + host.markMemoryPointer(statePtr, "array:present"); + B.line(`${denseState} = load i8, ptr ${statePtr}${host.fieldAliasAttachment(statePtr)}`); + B.line(`${densePresent} = icmp eq i8 ${denseState}, 1`); + B.condBr(densePresent, valueLabel, no); + } B.startBlock(valueLabel); - const dataPtr = B.tmp(), - data = B.tmp(), - valuePtr = B.tmp(), + const valuePtr = B.tmp(), raw = B.tmp(); - B.line( - `${dataPtr} = getelementptr inbounds %ScrArr, ptr ${array.name}, i32 0, i32 ${SCR_ARR_FIELD.data}`, - ); - host.markMemoryPointer(dataPtr, "array:header"); - B.line(`${data} = load ptr, ptr ${dataPtr}${host.fieldAliasAttachment(dataPtr)}`); // Boolean arrays store one byte per element; every other kind an 8-byte slot. const slotType = loadType === "i8" ? "i8" : "i64"; B.line( @@ -590,6 +596,7 @@ export function emitBorrowedArrayRead( array, index, integerIndex, + loopIndex !== null, (value) => B.line(`store ptr ${value}, ptr ${slot}`), slow, slow, @@ -632,7 +639,18 @@ export function emitLocalArrayRead( B.line(`store ptr ${read.borrow ? value : host.retainFrame(value, owned)}, ptr ${localSlot}`); }; if (inline) - emitDenseReferenceArrayRead(host, array, index, integerIndex, store, no, slow, join, "ptr"); + emitDenseReferenceArrayRead( + host, + array, + index, + integerIndex, + integerIndex !== null, + store, + no, + slow, + join, + "ptr", + ); else B.br(slow); B.startBlock(slow); const present = B.newLabel("local.array.slow.value"); @@ -687,6 +705,7 @@ export function emitLocalArrayRead( array, index, integerIndex, + integerIndex !== null, storeValue, no, slow, diff --git a/packages/compiler/src/backend/llvm/narrow-guards.ts b/packages/compiler/src/backend/llvm/narrow-guards.ts index 910af6f79..4744bed7c 100644 --- a/packages/compiler/src/backend/llvm/narrow-guards.ts +++ b/packages/compiler/src/backend/llvm/narrow-guards.ts @@ -14,7 +14,7 @@ * throw. The guards are cheap: one load and compare per site, which LLVM * folds where an identical test already dominates. */ import { InternalCompilerError } from "../../errors.js"; -import { classMembershipIntervals } from "./classes.js"; +import { classMembershipIntervals, type LlClassMeta } from "./classes.js"; import type { LlvmEmitterContext } from "./expr-context.js"; import { vAdapters } from "./shapes.js"; @@ -124,6 +124,7 @@ export function emitCaughtInstanceof( const B = host.B; const target = host.classMetaOf(className); const intervals = classMembershipIntervals(host.classMeta, target.def.name); + if (target.idInRc) return emitCaughtClassIdInstanceof(host, caught, target, intervals); if (!target.hierarchy) { const adapterSlot = B.tmp(); const adapter = B.tmp(); @@ -169,6 +170,80 @@ export function emitCaughtInstanceof( return result; } +/** `e instanceof C` for a class whose instances keep their class id in + * the rc word (classes.ts LlClassMeta.idInRc). They are thrown as REF + * payloads, like standalone class instances: the retain adapter names the + * hierarchy (the adapter of whichever static class threw), and the payload's + * class id is tested against the intervals. */ +function emitCaughtClassIdInstanceof( + host: LlvmEmitterContext, + caught: string, + target: LlClassMeta, + intervals: { pre: number; post: number }[], +): string { + const B = host.B; + const kp = B.tmp(), + kind = B.tmp(), + isRef = B.tmp(), + adapterSlot = B.tmp(), + adapter = B.tmp(); + B.line(`${kp} = getelementptr inbounds %ScrCaught, ptr ${caught}, i64 0, i32 1`); + B.line(`${kind} = load i32, ptr ${kp}`); + B.line(`${isRef} = icmp eq i32 ${kind}, ${CAUGHT_REF_KIND}`); + B.line(`${adapterSlot} = getelementptr inbounds %ScrCaught, ptr ${caught}, i64 0, i32 5`); + B.line(`${adapter} = load ptr, ptr ${adapterSlot}`); + let anyAdapter: string | undefined; + for (const m of host.classMeta.values()) { + if (m.root !== target.root) continue; + const rc = vAdapters(host.shapeHost, { kind: "object", className: m.def.name }); + const matches = B.tmp(); + B.line(`${matches} = icmp eq ptr ${adapter}, ${rc.retain}`); + if (anyAdapter === undefined) anyAdapter = matches; + else { + const joined = B.tmp(); + B.line(`${joined} = or i1 ${anyAdapter}, ${matches}`); + anyAdapter = joined; + } + } + const inHierarchy = B.tmp(); + B.line(`${inHierarchy} = and i1 ${isRef}, ${anyAdapter!}`); + const test = B.newLabel("caught.class"), + join = B.newLabel("caught.class.done"), + from = B.currentLabel(); + B.condBr(inHierarchy, test, join); + B.startBlock(test); + const payloadSlot = B.tmp(), + payload = B.tmp(); + B.line(`${payloadSlot} = getelementptr inbounds %ScrCaught, ptr ${caught}, i64 0, i32 4`); + B.line(`${payload} = load ptr, ptr ${payloadSlot}`); + const pre = host.loadVtPre(payload, target.def.name); + let result: string | undefined; + for (const interval of intervals) { + const ge = B.tmp(), + le = B.tmp(), + t = B.tmp(); + B.line(`${ge} = icmp uge ${host.sizeType} ${pre}, ${interval.pre}`); + B.line(`${le} = icmp ule ${host.sizeType} ${pre}, ${interval.post}`); + B.line(`${t} = and i1 ${ge}, ${le}`); + if (result === undefined) result = t; + else { + const combined = B.tmp(); + B.line(`${combined} = or i1 ${result}, ${t}`); + result = combined; + } + } + if (result === undefined) throw new InternalCompilerError("empty class membership"); + const tested = B.currentLabel(); + B.br(join); + B.startBlock(join); + const out = B.tmp(); + B.line(`${out} = phi i1 [ false, %${from} ], [ ${result}, %${tested} ]`); + return out; +} + +/** SCR_EXC_REF (scr_exception.h). */ +const CAUGHT_REF_KIND = 4; + /** SCR_EXC_F64 = 1, SCR_EXC_BOOL = 2, SCR_EXC_STR = 3 (scr_exception.h). */ export const CAUGHT_SCALAR_KINDS = { f64: 1, bool: 2, string: 3 } as const; diff --git a/packages/compiler/src/backend/llvm/publish.ts b/packages/compiler/src/backend/llvm/publish.ts index 4f807424b..15221bbc3 100644 --- a/packages/compiler/src/backend/llvm/publish.ts +++ b/packages/compiler/src/backend/llvm/publish.ts @@ -63,6 +63,9 @@ export interface PublishClassInfo { post: number; /** A runtime-provided root class (errors, emitters, streams). */ runtimeRooted: boolean; + /** Instances keep their class id in the rc word (classes.ts + * LlClassMeta.idInRc). */ + idInRc: boolean; /** The class's own and inherited fields: GEP index and storage type. */ fields: { name: string; index: number; type: IrType }[]; } @@ -70,6 +73,12 @@ export interface PublishClassInfo { /** scr_pub_mark's object kinds (SCR_PUB_KIND_* in scr_runtime.h), for the RC audit. */ const PUB_KIND_OBJECT = 4; const PUB_KIND_UNION = 5; +/** SCR_PUB_CLASS_ID, or'ed into a kind: the object keeps its class id + * in the high half of its rc word (classes.ts LlClassMeta.idInRc). */ +const PUB_CLASS_ID = 0x100; +/** SCR_PUB_LOG_COUNT (scr_publish.c), in a log entry's `buf`: `rc` is the + * count of the low half. */ +const PUB_LOG_COUNT = "-2147483648"; const RUNTIME_WALKERS = { string: "@scr_pub_str_fn", @@ -307,9 +316,18 @@ export class PublishWalkers { const lines = [ head, `entry:`, - ` %vtp = getelementptr inbounds ptr, ptr %o, i64 1`, - ` %vt = load ptr, ptr %vtp`, - ` %pre = load ${this.host.sizeType}, ptr %vt`, + ...(info.idInRc + ? [ + ` %idp = getelementptr inbounds i8, ptr %o, i64 4 ; the class id (classes.ts classIdRcWord)`, + ` %id = load i32, ptr %idp`, + ` %idpre = add i32 %id, -1`, + ` %pre = zext i32 %idpre to ${this.host.sizeType}`, + ] + : [ + ` %vtp = getelementptr inbounds ptr, ptr %o, i64 1`, + ` %vt = load ptr, ptr %vtp`, + ` %pre = load ${this.host.sizeType}, ptr %vt`, + ]), ` switch ${this.host.sizeType} %pre, label %other [ ${cases.map((c) => `${this.host.sizeType} ${c.pre}, label %c${c.pre}`).join(" ")} ]`, ]; for (const c of cases) @@ -344,6 +362,7 @@ export class PublishWalkers { info.fields, path, loc, + info.idInRc, ); } @@ -355,20 +374,23 @@ export class PublishWalkers { * targets), and an object with a collector header leaves its candidate * buffer through scr_cyc_pub_take. Anything else calls scr_pub_mark, as * does every mark of an RC-audit build. */ - private markLines(cyc: boolean, kind: number): string[] { + private markLines(cyc: boolean, kind: number, classId = false): string[] { const call = (name: string): string[] => [ - ` %${name} = call zeroext i1 @scr_pub_mark(ptr %ctx, ptr %o, i1 ${cyc ? "true" : "false"}, i32 ${kind})`, + ` %${name} = call zeroext i1 @scr_pub_mark(ptr %ctx, ptr %o, i1 ${cyc ? "true" : "false"}, i32 ${classId ? kind | PUB_CLASS_ID : kind})`, ` br i1 %${name}, label %walk, label %done`, ]; if (this.host.rcAudit === true) return call("m"); const S = this.host.sizeType; + // An object that keeps its class id in the rc word is marked and + // logged through its count, the low half. + const C = classId ? "i32" : S; const field = (name: string, slot: number): string[] => [ ` %${name}p = getelementptr inbounds ptr, ptr %ctx, i32 ${slot}`, ` %${name} = load ${S === "i64" ? "i64" : "i32"}, ptr %${name}p`, ]; const lines = [ - ` %mrc = load ${S}, ptr %o`, - ` %mimm = icmp eq ${S} %mrc, -1`, + ` %mrc = load ${C}, ptr %o`, + ` %mimm = icmp eq ${C} %mrc, -1`, ` br i1 %mimm, label %done, label %mtest`, `mtest:`, ` %mrefp = getelementptr inbounds ptr, ptr %ctx, i32 6`, @@ -380,13 +402,13 @@ export class PublishWalkers { ` %mok1 = and i1 %mok0, %mroom`, ]; let ok = "%mok1"; - if (S === "i64") { + if (C === "i64") { // 0xfffffffe and up are the log's escapes (SCR_PUB_RC_LIFETIME, _BIG). lines.push(` %msmall = icmp ult i64 %mrc, 4294967294`, ` %mok2 = and i1 %mok1, %msmall`); ok = "%mok2"; } lines.push(` br i1 ${ok}, label %mfast, label %mslow`, `mslow:`, ...call("ms"), `mfast:`); - let buf = "0"; + let buf = classId ? String(PUB_LOG_COUNT) : "0"; if (cyc) { // ScrCycHdr.buf, the word before the object: its top bit while the // object is a buffered candidate. @@ -402,9 +424,10 @@ export class PublishWalkers { ...field("mtaken", 7), ` %mtaken1 = add ${S} %mtaken, 1`, ` store ${S} %mtaken1, ptr %mtakenp`, + ...(classId ? [` %mix2 = or i32 %mix1, ${PUB_LOG_COUNT}`] : []), ` br label %mput`, `mput:`, - ` %mbuf = phi i32 [ 0, %mfast ], [ %mix1, %mtake ]`, + ` %mbuf = phi i32 [ ${buf}, %mfast ], [ %mix${classId ? 2 : 1}, %mtake ]`, ); buf = "%mbuf"; } @@ -414,13 +437,13 @@ export class PublishWalkers { ` %me = getelementptr inbounds { ptr, i32, i32 }, ptr %mlog, ${S} %mlogged`, ` store ptr %o, ptr %me`, ` %merc = getelementptr inbounds { ptr, i32, i32 }, ptr %me, i32 0, i32 1`, - S === "i64" ? ` %mrc32 = trunc i64 %mrc to i32` : ` %mrc32 = add i32 %mrc, 0`, + C === "i64" ? ` %mrc32 = trunc i64 %mrc to i32` : ` %mrc32 = add i32 %mrc, 0`, ` store i32 %mrc32, ptr %merc`, ` %mebuf = getelementptr inbounds { ptr, i32, i32 }, ptr %me, i32 0, i32 2`, ` store i32 ${buf}, ptr %mebuf`, ` %mlogged1 = add ${S} %mlogged, 1`, ` store ${S} %mlogged1, ptr %mloggedp`, - ` store ${S} -1, ptr %o`, + ` store ${C} -1, ptr %o`, ` br label %walk`, ); return lines; @@ -431,13 +454,14 @@ export class PublishWalkers { * before) are the ones scr_pub_push skips: they are tested here, without * the call. The lines end in a fresh block labelled `pushed${id}`. */ private push(v: string, fn: string, id: string): string[] { - const size = this.host.sizeType; return [ ` %null${id} = icmp eq ptr ${v}, null`, ` br i1 %null${id}, label %pushed${id}, label %live${id}`, `live${id}:`, - ` %rc${id} = load ${size}, ptr ${v}`, - ` %immortal${id} = icmp eq ${size} %rc${id}, -1`, + // The count (the low half of a 64-bit rc word) is UINT32_MAX for an + // immortal object of every kind (scr_rc.h SCR_RC_COUNT). + ` %rc${id} = load i32, ptr ${v}`, + ` %immortal${id} = icmp eq i32 %rc${id}, -1`, ` br i1 %immortal${id}, label %pushed${id}, label %push${id}`, `push${id}:`, ...this.pushLines(v, fn, id), @@ -480,8 +504,9 @@ export class PublishWalkers { members: { name: string; index: number; type: IrType }[], path: string, loc: SrcLoc, + classId = false, ): string[] { - const lines = [head, `entry:`, ...this.markLines(traced, PUB_KIND_OBJECT), `walk:`]; + const lines = [head, `entry:`, ...this.markLines(traced, PUB_KIND_OBJECT, classId), `walk:`]; let n = 0; for (const member of members) { const fn = this.walker(member.type, `${path}.${member.name}`, loc); diff --git a/packages/compiler/src/backend/llvm/regions.test.ts b/packages/compiler/src/backend/llvm/regions.test.ts index 51ceb21c8..f2fed6b8e 100644 --- a/packages/compiler/src/backend/llvm/regions.test.ts +++ b/packages/compiler/src/backend/llvm/regions.test.ts @@ -117,7 +117,13 @@ test("ProgramLifetime instances live in the current region and count in their ch ); // The first reference is implied: allocation leaves the chunk count alone. expect(nw, cls).not.toContain("2097151"); - expect(nw, cls).toContain("store i64 -9223372036854775808, ptr %o"); + // The rc word: the class id in the high half (the hierarchy keeps it + // there instead of a vtable word) over the program-lifetime count, the + // midpoint of the 32-bit count in the low half. + const word = /store i64 (\d+), ptr %o\.vt/.exec(nw)?.[1]; + expect(word, cls).toBeDefined(); + expect(BigInt(word!) % 2n ** 32n, cls).toBe(2n ** 31n); + expect(BigInt(word!) / 2n ** 32n > 0n, cls).toBe(true); expect(nw, cls).not.toContain("scr_rt_calloc"); // retain/release: the chunk count, NULL-tolerant, once a region // exists, and only in chunks that have an owner (freeable regions). diff --git a/packages/compiler/src/backend/llvm/regions.ts b/packages/compiler/src/backend/llvm/regions.ts index 1ec46a5b1..e7ad39f7c 100644 --- a/packages/compiler/src/backend/llvm/regions.ts +++ b/packages/compiler/src/backend/llvm/regions.ts @@ -36,7 +36,7 @@ import { mangleRecordStruct, } from "../mangle.js"; import { threadHotAddressLines } from "./alloc.js"; -import { classFieldBase, type LlClassMeta } from "./classes.js"; +import { classFieldBase, classPreLines, type LlClassMeta } from "./classes.js"; import { llvmCommentText } from "./common.js"; import type { NullableRefFields } from "./nullable-fields.js"; import { FN_ATTRS, rcMembers, releaseSym, vAdapters, type ShapeHost } from "./shapes.js"; @@ -389,9 +389,7 @@ export class LifetimeRegions { out.push( `define internal void ${walk}(ptr %o, ptr %w) ${FN_ATTRS} { ; region walk ${llvmCommentText(cls.name)} and its subclasses`, `entry:`, - ` %vtp = getelementptr inbounds ptr, ptr %o, i64 1`, - ` %vt = load ptr, ptr %vtp`, - ` %pre = load ${S}, ptr %vt`, + ...classPreLines(meta, mangleClassStruct(cls.name), "%o", "%pre", S), ` switch ${S} %pre, label %done [ ${members.map((m) => `${S} ${m.pre}, label %c${m.pre}`).join(" ")} ]`, ...members.flatMap((m) => [ `c${m.pre}: ; ${llvmCommentText(m.def.name)}`, diff --git a/packages/compiler/src/backend/llvm/shapes.ts b/packages/compiler/src/backend/llvm/shapes.ts index 9cceaa443..8844921c5 100644 --- a/packages/compiler/src/backend/llvm/shapes.ts +++ b/packages/compiler/src/backend/llvm/shapes.ts @@ -1054,6 +1054,7 @@ export function arrNewCall( elem.kind === "promise" || // promise entries (Promise.all inputs): full REF story elem.kind === "child" || // spawned child handles: scr_child_* adapters, no trace elem.kind === "netServer" || + elem.kind === "stats" || // fs.Stats snapshots: scr_stats_* adapters, no trace elem.kind === "dyn" || // native collection seeds/drains: traced REF elem.kind === "regex" || // RegExp values: scr_regex_* adapters, no trace (no refs inside) (elem.kind === "array" && traceAdapter(host, elem) !== null); @@ -1249,6 +1250,11 @@ export function rcMembers( ]; } +/** The integer type a release or retain loads and stores the count as: + * the rc word, or its low half (an i32 at offset 0) for class instances that + * keep their class number in the high half (classes.ts LlClassMeta.idInRc). */ +export type RcCount = "i32" | "i64"; + /** The NULL- and immortal-skipping retain body shared by every shape. A * retain never touches the cycle header: a buffered candidate that is * retained again stays a candidate (scr_cycle.c, markRoots). */ @@ -1257,8 +1263,9 @@ export function retainBody( fnName: string, comment = "", immortal = false, + count: RcCount = host.sizeType, ): string[] { - const S = host.sizeType; + const S = count; if (immortal) return immortalRcBody(host, fnName, "ptr", comment); return [ `define internal ptr @${fnName}(ptr %o) ${FN_ATTRS} {${comment ? ` ; ${comment}` : ""}`, @@ -1325,6 +1332,8 @@ export interface DirectDestroy { readonly destroy: string; /** The child carries a collector header. */ readonly traced: boolean; + /** The child's count type, when it is not the rc word (RcCount). */ + readonly count?: RcCount; } /** The release of child `value` (a loaded field, possibly NULL) inside a @@ -1339,19 +1348,20 @@ export function directChildReleaseLines( comment: string, ): string[] { const S = host.sizeType; + const C = target.count ?? S; host.declare(`declare void @scr_rc_destroy(ptr, ptr)`); const lines = [ ` %${tag}.nn = icmp ne ptr ${value}, null`, ` br i1 %${tag}.nn, label %${tag}.chk, label %${tag}.next`, `${tag}.chk:`, - ` %${tag}.rc = load ${S}, ptr ${value}`, - ` %${tag}.last = icmp eq ${S} %${tag}.rc, 1`, + ` %${tag}.rc = load ${C}, ptr ${value}`, + ` %${tag}.last = icmp eq ${C} %${tag}.rc, 1`, ` br i1 %${tag}.last, label %${tag}.dead, label %${tag}.shared`, `${tag}.shared:`, ` call void ${target.release}(ptr ${value}) ; ${comment}`, ` br label %${tag}.next`, `${tag}.dead: ; ${comment}: the last reference`, - ` store ${S} 0, ptr ${value}`, + ` store ${C} 0, ptr ${value}`, ]; if (target.traced) { host.declare(`declare void @scr_cyc_on_dead(ptr)`); @@ -1514,8 +1524,9 @@ export function releaseFastPath( traced: boolean, comment = "", immortal = false, + count: RcCount = host.sizeType, ): string[] { - const S = host.sizeType; + const S = count; if (immortal) return immortalRcBody(host, fnName, "void", comment); return [ `define internal void @${fnName}(ptr %o) ${FN_ATTRS} {${comment ? ` ; ${comment} (fast path)` : ""}`, diff --git a/packages/compiler/src/backend/llvm/string-reads.ts b/packages/compiler/src/backend/llvm/string-reads.ts index ef0702361..d7bbed272 100644 --- a/packages/compiler/src/backend/llvm/string-reads.ts +++ b/packages/compiler/src/backend/llvm/string-reads.ts @@ -13,8 +13,8 @@ * With `scan`, a string that is not proven ASCII is first compared with * the runtime's published scan receiver (ScrStrScan, scr_runtime.h): the * string an indexed scan converted to UTF-16 units, whose length and units - * then read inline. Only targets whose runtime state is not thread-local - * publish it to emitted code. + * then read inline. Where the runtime keeps it per thread, the declaration + * is thread-local. */ import { exactIndexLines, sizeToDouble } from "./common.js"; @@ -53,8 +53,8 @@ export function stringScanType(sz: string): string { return `{ ptr, ptr, ${sz} }`; } -export function stringScanDecl(sz: string): string { - return `@scr_str_scan = external global ${stringScanType(sz)}`; +export function stringScanDecl(sz: string, threadLocal: boolean): string { + return `@scr_str_scan = external ${threadLocal ? "thread_local " : ""}global ${stringScanType(sz)}`; } /** The scan receiver test of `%s`: branches to `hit` with `%scan.n` (its diff --git a/packages/compiler/src/backend/llvm/thread-slots.test.ts b/packages/compiler/src/backend/llvm/thread-slots.test.ts index dba4453b3..b8d8dccf9 100644 --- a/packages/compiler/src/backend/llvm/thread-slots.test.ts +++ b/packages/compiler/src/backend/llvm/thread-slots.test.ts @@ -85,12 +85,11 @@ test("slots.get reads the slot word, the page and the entry inline", () => { ]; expect(validateModule(module(calls))).toEqual([]); const llvm = emitLlvmModule(module(calls), { pointerBits: 64 }); - // The slot word sits after the header words of every hierarchy member. + // The slot word sits after the rc word of every hierarchy member, which + // keeps its class id in the rc word instead of a vtable word. + expect(llvm).toMatch(new RegExp(`%${mangleClassStruct("Node")} = type \\{ i64, i32, ptr \\}`)); expect(llvm).toMatch( - new RegExp(`%${mangleClassStruct("Node")} = type \\{ i64, ptr, i32, ptr \\}`), - ); - expect(llvm).toMatch( - new RegExp(`%${mangleClassStruct("Leaf")} = type \\{ i64, ptr, i32, ptr, (i32|double) \\}`), + new RegExp(`%${mangleClassStruct("Leaf")} = type \\{ i64, i32, ptr, (i32|double) \\}`), ); expect(llvm).toMatch(new RegExp(`%${mangleClassStruct("Plain")} = type \\{ i64, ptr \\}`)); // One block per family name. @@ -102,7 +101,7 @@ test("slots.get reads the slot word, the page and the entry inline", () => { ); const main = body(llvm, "sc_f_main"); expect(main).toMatch( - /getelementptr inbounds %\S+, ptr %\S+, i64 0, i32 2\n {2}(%\S+) = load atomic i32, ptr %\S+ monotonic, align 4\n {2}%\S+ = lshr i32 \1, 10/, + /getelementptr inbounds %\S+, ptr %\S+, i64 0, i32 1\n {2}(%\S+) = load atomic i32, ptr %\S+ monotonic, align 4\n {2}%\S+ = lshr i32 \1, 10/, ); expect(main).not.toMatch(/call ptr @scr_slots_get/); expect(main).toMatch( diff --git a/packages/compiler/src/backend/mangle.ts b/packages/compiler/src/backend/mangle.ts index 43c9cea71..da6cbe0f5 100644 --- a/packages/compiler/src/backend/mangle.ts +++ b/packages/compiler/src/backend/mangle.ts @@ -109,6 +109,13 @@ export function mangleVtStruct(rootClassName: string): string { export function mangleVtInstance(className: string): string { return `sc_vtable_${sanitize(className)}`; } +/** The per-HIERARCHY array of the vtables of its classes in preorder, for + * hierarchies whose instances keep their class id in the rc word + * (classes.ts LlClassMeta.idInRc). `sc_vta_` differs from `sc_vtable_` and + * `sc_vtt_` before any user-controlled text. */ +export function mangleVtArray(rootClassName: string): string { + return `sc_vta_${sanitize(rootClassName)}`; +} /** A virtual slot's member name inside the vtable struct. Sibling * branches of one hierarchy can each declare a slot for the SAME method * name (root-most in their own subtree, overridden below — mixin layers diff --git a/packages/compiler/src/backend/native-link-info-core.ts b/packages/compiler/src/backend/native-link-info-core.ts index 84f448e93..c139b4a65 100644 --- a/packages/compiler/src/backend/native-link-info-core.ts +++ b/packages/compiler/src/backend/native-link-info-core.ts @@ -49,6 +49,12 @@ export interface NativeLinkFeatures { /** events.once with options: scr_events_once_signal.c. Absent (not * false) for programs without it. */ eventsOnceSignal?: boolean; + /** Fixed-width Buffer methods called on checked-dynamic values link + * scr_bytes_dyn.c; absent (false) for programs without them. */ + dynBufferNum?: boolean; + /** Builtin functions read as values (Buffer.byteLength, Buffer.alloc, + * fs.realpathSync) link scr_builtin_values.c; absent (false) otherwise. */ + builtinValues?: boolean; events: boolean; emitter: boolean; symbol: boolean; diff --git a/packages/compiler/src/backend/native/contracts.ts b/packages/compiler/src/backend/native/contracts.ts index a9c93d3df..7f8456484 100644 --- a/packages/compiler/src/backend/native/contracts.ts +++ b/packages/compiler/src/backend/native/contracts.ts @@ -119,6 +119,11 @@ export interface CcOptions { /** events.once with options (events.onceSignal): compiles * scr_events_once_signal.c. */ eventsOnceSignal?: boolean; + /** Fixed-width Buffer methods called on checked-dynamic values: + * compiles scr_bytes_dyn.c. */ + dynBufferNum?: boolean; + /** Builtin functions read as values: compiles scr_builtin_values.c. */ + builtinValues?: boolean; /** The program constructs EventTarget/Event/CustomEvent values or * calls events.addAbortListener (the event.* libCalls), or uses the * native fetch AbortSignal built on the same engine: compiles diff --git a/packages/compiler/src/backend/native/executable.ts b/packages/compiler/src/backend/native/executable.ts index 949f03839..a1b018aef 100644 --- a/packages/compiler/src/backend/native/executable.ts +++ b/packages/compiler/src/backend/native/executable.ts @@ -179,6 +179,10 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [ unit((u) => u.opts.assert || u.regex || u.opts.symbol, "scr_assert.c"), unit((u) => u.opts.inspect, "scr_inspect.c", "scr_console_native.c"), unit((u) => u.opts.dynInvoke || u.opts.workers || u.nativeFetch, "scr_dyn_invoke.c"), + // Fixed-width Buffer methods called on checked-dynamic values. + unit((u) => u.opts.dynBufferNum === true && u.opts.dynInvoke, "scr_bytes_dyn.c"), + // Builtin functions read as values in JavaScript dependencies. + unit((u) => u.opts.builtinValues === true, "scr_builtin_values.c"), unit((u) => u.opts.dc, "scr_dc.c"), unit((u) => u.opts.dynTier, "scr_js_value.c"), unit((u) => u.opts.eventTarget || u.nativeFetch, "scr_event_target.c"), diff --git a/packages/compiler/src/backend/runtime-abi.ts b/packages/compiler/src/backend/runtime-abi.ts index 21edd235f..67be86571 100644 --- a/packages/compiler/src/backend/runtime-abi.ts +++ b/packages/compiler/src/backend/runtime-abi.ts @@ -2,8 +2,8 @@ * of the link contract: a program object keeps this symbol undefined and the * matching runtime defines it, so incompatible manual links fail before the * program can start. */ -export const RUNTIME_ABI_VERSION = 22 as const; -export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v22" as const; +export const RUNTIME_ABI_VERSION = 23 as const; +export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v23" as const; /** Object consumption is useful today, but the complete scr_* surface may * still change before 1.0. The versioned marker prevents accidental mixing; diff --git a/packages/compiler/src/backend/runtime-pack-core.ts b/packages/compiler/src/backend/runtime-pack-core.ts index 5c9d417c5..69aade6ce 100644 --- a/packages/compiler/src/backend/runtime-pack-core.ts +++ b/packages/compiler/src/backend/runtime-pack-core.ts @@ -86,6 +86,8 @@ export interface RuntimeFeatureSet extends NativeLinkFeatures { dynTier: boolean; streamDyn: boolean; eventsOnceSignal: boolean; + dynBufferNum: boolean; + builtinValues: boolean; warningReplace: boolean; stdioExtended: boolean; nativeFetch: boolean; @@ -279,6 +281,8 @@ export function effectiveRuntimeFeatures( dynTier: features.dynTier === true, streamDyn: features.streamDyn === true, eventsOnceSignal: features.eventsOnceSignal === true, + dynBufferNum: features.dynBufferNum === true, + builtinValues: features.builtinValues === true, warningReplace: features.warningReplace === true, stdioExtended: features.stdioExtended === true, nativeFetch, diff --git a/packages/compiler/src/backend/runtime-pack-targets.test.ts b/packages/compiler/src/backend/runtime-pack-targets.test.ts index 96503e2db..e80d48e28 100644 --- a/packages/compiler/src/backend/runtime-pack-targets.test.ts +++ b/packages/compiler/src/backend/runtime-pack-targets.test.ts @@ -52,7 +52,7 @@ async function fixture(target: NativeTargetSpec, workers = false) { object_format: target.objectFormat, minimum_os: target.minimumOs, }, - runtime_abi: { version: 22, marker: "scr_runtime_abi_v22" }, + runtime_abi: { version: 23, marker: "scr_runtime_abi_v23" }, compiler: { command: "fixture", identity: "fixture", target: target.llvmTriple }, macros: { executable: [], excluded: ["SCR_LIB"], sanitizer: "external-toolchain-required" }, flavors: { diff --git a/packages/compiler/src/backend/runtime-pack.test.ts b/packages/compiler/src/backend/runtime-pack.test.ts index cd342f85b..7a24facd6 100644 --- a/packages/compiler/src/backend/runtime-pack.test.ts +++ b/packages/compiler/src/backend/runtime-pack.test.ts @@ -122,7 +122,7 @@ async function fixture(target: NativeTargetSpec = MACOS_ARM64_TARGET) { object_format: target.objectFormat, minimum_os: target.minimumOs, }, - runtime_abi: { version: 22, marker: "scr_runtime_abi_v22" }, + runtime_abi: { version: 23, marker: "scr_runtime_abi_v23" }, compiler: { command: "clang", identity: "fixture clang", diff --git a/packages/compiler/src/cli/usage.ts b/packages/compiler/src/cli/usage.ts index 26b439167..69d30b76e 100644 --- a/packages/compiler/src/cli/usage.ts +++ b/packages/compiler/src/cli/usage.ts @@ -73,8 +73,9 @@ Options: --tier tier policy (default: auto). static-only keeps every construct without a static lowering a compile error, so - the program has no dynamic-tier demotions. Both modes - currently compile identically + the program has no dynamic-tier demotions, and makes a + checked cast whose value does not fit its type throw + instead of continuing with the value --explain-dynamic[=] coverage only: list the functions that operate on dynamic values and the bindings, fields, functions, classes and diff --git a/packages/compiler/src/executable/executable-cache.ts b/packages/compiler/src/executable/executable-cache.ts index 064b1f867..185ba3ff4 100644 --- a/packages/compiler/src/executable/executable-cache.ts +++ b/packages/compiler/src/executable/executable-cache.ts @@ -68,6 +68,8 @@ export interface EarlyExecutableNativeFeatures { dynTier?: boolean; streamDyn?: boolean; eventsOnceSignal?: boolean; + dynBufferNum?: boolean; + builtinValues?: boolean; events: boolean; emitter: boolean; symbol: boolean; diff --git a/packages/compiler/src/frontend/builtin-package-aliases.test.ts b/packages/compiler/src/frontend/builtin-package-aliases.test.ts new file mode 100644 index 000000000..2d1001bc2 --- /dev/null +++ b/packages/compiler/src/frontend/builtin-package-aliases.test.ts @@ -0,0 +1,70 @@ +import { expect, test } from "vitest"; +import { builtinAliasOf, versionedPackageOfKey } from "./builtin-package-aliases.js"; + +test("aliases name the builtin each checked major's module is on Node 24", () => { + expect(builtinAliasOf("isarray", "1.0.0", "")).toEqual({ whole: "Array.isArray" }); + expect(builtinAliasOf("isarray", "0.0.1", "index.js")).toEqual({ whole: "Array.isArray" }); + expect(builtinAliasOf("isarray", "3.0.0", "")).toBeNull(); + expect(builtinAliasOf("safe-buffer", "5.1.2", "")).toEqual({ whole: 'require("buffer")' }); + expect(builtinAliasOf("process-nextick-args", "2.0.1", "")).toEqual({ whole: "process" }); + expect(builtinAliasOf("process-nextick-args", "1.0.7", "")).toBeNull(); + expect(builtinAliasOf("inherits", "2.0.4", "inherits.js")).toEqual({ + whole: 'require("util").inherits', + }); + // The browser file is the package's own implementation. + expect(builtinAliasOf("inherits", "2.0.4", "inherits_browser.js")).toBeNull(); + expect(builtinAliasOf("util-deprecate", "1.0.2", "node.js")).toEqual({ + whole: 'require("util").deprecate', + }); + expect(builtinAliasOf("util-deprecate", "1.0.2", "browser.js")).toBeNull(); +}); + +test("readable-stream aliases follow each major's layout", () => { + const rs2 = builtinAliasOf("readable-stream", "2.3.8", "readable.js"); + expect(rs2?.whole).toBe('require("stream").Readable'); + expect([...(rs2?.members?.names ?? [])].sort()).toEqual( + ["Duplex", "PassThrough", "Readable", "Stream", "Transform", "Writable"].sort(), + ); + expect(builtinAliasOf("readable-stream", "2.3.8", "writable.js")?.whole).toBe( + 'require("stream").Writable', + ); + expect(builtinAliasOf("readable-stream", "2.3.8", "lib/_stream_duplex.js")?.whole).toBe( + 'require("stream").Duplex', + ); + expect(builtinAliasOf("readable-stream", "2.3.8", "lib/internal/streams/destroy.js")).toBeNull(); + // 3.x has no root writable.js; its entry also carries finished and pipeline. + const rs3 = builtinAliasOf("readable-stream", "3.6.2", ""); + expect(rs3?.whole).toBe('require("stream").Readable'); + expect([...(rs3?.members?.names ?? [])]).toContain("pipeline"); + expect(builtinAliasOf("readable-stream", "3.6.2", "lib/_stream_writable.js")?.whole).toBe( + 'require("stream").Writable', + ); + expect(builtinAliasOf("readable-stream", "3.6.2", "writable.js")).toBeNull(); + expect(builtinAliasOf("readable-stream", "3.6.2", "lib/internal/streams/pipeline.js")).toBeNull(); + const rs4 = builtinAliasOf("readable-stream", "4.7.0", "lib/ours/index.js"); + expect(rs4?.whole).toBeNull(); + expect(rs4?.members?.names.has("pipeline")).toBe(true); + expect(rs4?.members?.names.has("promises")).toBe(false); + expect(builtinAliasOf("readable-stream", "4.7.0", "transform.js")).toBeNull(); + expect(builtinAliasOf("readable-stream", "4.7.0", "lib/_stream_transform.js")?.whole).toBe( + 'require("stream").Transform', + ); +}); + +test("pnpm module keys name the package, version and file", () => { + expect( + versionedPackageOfKey("../node_modules/.pnpm/isarray@1.0.0/node_modules/isarray/index.js"), + ).toEqual({ name: "isarray", version: "1.0.0", subpath: "index.js" }); + expect( + versionedPackageOfKey( + "node_modules/.pnpm/@scope+pkg@2.1.0_peer@1.0.0/node_modules/@scope/pkg/lib/a.js", + ), + ).toEqual({ name: "@scope/pkg", version: "2.1.0", subpath: "lib/a.js" }); + // A store entry's own dependency links name other packages. + expect( + versionedPackageOfKey( + "node_modules/.pnpm/from2@2.3.0/node_modules/readable-stream/readable.js", + ), + ).toBeNull(); + expect(versionedPackageOfKey("node_modules/isarray/index.js")).toBeNull(); +}); diff --git a/packages/compiler/src/frontend/builtin-package-aliases.ts b/packages/compiler/src/frontend/builtin-package-aliases.ts new file mode 100644 index 000000000..ccb8dc5a3 --- /dev/null +++ b/packages/compiler/src/frontend/builtin-package-aliases.ts @@ -0,0 +1,150 @@ +/* Polyfill packages whose behavior on Node 24 IS a Node builtin. The + * package-shim registry (package-shims.ts) consults this table: a require + * of such a package, from an installed copy's importer or through a bundled + * __commonJS chunk whose path names the package, compiles as the builtin + * expression its module evaluates to on Node 24, at the require's position + * (loading a builtin runs no code, and none of these packages has an + * observable side effect at load). + * + * Each entry names the major versions whose entry modules were checked: + * + * - isarray 0.x, 1.x, 2.x: `module.exports = Array.isArray || function …`; + * Node always has Array.isArray, so the export is Array.isArray itself. + * - safe-buffer 5.x: `module.exports = buffer` when Buffer.from, alloc, + * allocUnsafe and allocUnsafeSlow exist (Node 6 and later); the rest of + * the module only builds its unexported SafeBuffer fallback. + * - process-nextick-args 2.x: `module.exports = process` unless + * process.version is v0.x or a pre-1.8 v1.x. + * - inherits 2.x: inherits.js exports require("util").inherits (2.0.1 + * directly, 2.0.3 and later whenever it is a function, always on Node). + * - util-deprecate 1.x: node.js (its Node entry) is + * `module.exports = require("util").deprecate`. + * - readable-stream 2.x, 3.x, 4.x: a documented divergence. The package + * ships a JavaScript copy of Node's streams; its READABLE_STREAM=disable + * behavior hands out Node's own stream classes instead, and that is what + * compiles: + * - 2.x readable.js exports stream.Readable carrying Readable, Writable, + * Duplex, Transform, PassThrough and Stream; writable.js exports + * stream.Writable and duplex/transform/passthrough.js read the entry; + * - 3.x (a copy of Node 10's streams) readable.js exports the same + * members plus finished and pipeline. Its own disable branch throws on + * Node 24 (it Object.assigns every stream export onto stream.Readable, + * which inherits the getter-only `promises` accessor), so 3.x maps the + * members its default branch exports onto the builtin's; + * - 4.x lib/ours/index.js exports a plain object of named stream exports. + * The lib/_stream_*.js implementation files map to the matching builtin + * classes as well (Node itself loads the package's JavaScript + * implementations through them, whatever the environment). Without the + * variable Node runs the package's own JavaScript streams. */ + +export interface BuiltinAlias { + /** The expression the module's export evaluates to, or null when only + * its member reads have a builtin spelling. */ + whole: string | null; + /** For an export that carries another object's members (readable-stream's + * entry): member reads of these names read `object`'s members. */ + members?: { object: string; names: ReadonlySet }; +} + +const STREAM_CLASSES = ["Readable", "Writable", "Duplex", "Transform", "PassThrough"]; + +/** The members readable-stream 4.x's entry copies from `stream` under + * READABLE_STREAM=disable. */ +const RS4_ENTRY_EXPORTS = [ + "_uint8ArrayToBuffer", + "_isUint8Array", + "isDisturbed", + "isErrored", + "isReadable", + ...STREAM_CLASSES, + "addAbortSignal", + "finished", + "destroy", + "pipeline", + "compose", + "Stream", +]; + +function majorOf(version: string): number { + return Number(/^(\d+)\./.exec(version)?.[1] ?? Number.NaN); +} + +function stripJs(subpath: string): string { + return subpath.replace(/^\.\//, "").replace(/\.js$/, ""); +} + +/** The builtin a package module is on Node 24, or null. `subpath` is the + * file inside the package ("" for its main entry). */ +export function builtinAliasOf(pkg: string, version: string, subpath: string): BuiltinAlias | null { + const major = majorOf(version); + const file = stripJs(subpath); + switch (pkg) { + case "isarray": + return (major === 0 || major === 1 || major === 2) && (file === "" || file === "index") + ? { whole: "Array.isArray" } + : null; + case "safe-buffer": + return major === 5 && (file === "" || file === "index") + ? { whole: 'require("buffer")' } + : null; + case "process-nextick-args": + return major === 2 && (file === "" || file === "index") ? { whole: "process" } : null; + case "inherits": + return major === 2 && (file === "" || file === "inherits") + ? { whole: 'require("util").inherits' } + : null; + case "util-deprecate": + return major === 1 && (file === "" || file === "node") + ? { whole: 'require("util").deprecate' } + : null; + case "readable-stream": { + if (major !== 2 && major !== 3 && major !== 4) return null; + const stream = 'require("stream")'; + const entry = major === 4 ? file === "lib/ours/index" : file === "readable"; + if (file === "" || entry) { + if (major === 4) + return { whole: null, members: { object: stream, names: new Set(RS4_ENTRY_EXPORTS) } }; + const names = new Set([ + ...STREAM_CLASSES, + "Stream", + ...(major === 3 ? ["finished", "pipeline"] : []), + ]); + return { whole: `${stream}.Readable`, members: { object: stream, names } }; + } + for (const cls of STREAM_CLASSES) { + const lower = cls.toLowerCase(); + if (file === `lib/_stream_${lower}` || (major === 2 && file === lower)) + return { whole: `${stream}.${cls}` }; + } + return null; + } + default: + return null; + } +} + +/** Package names the table can alias (a cheap pre-filter). */ +export const BUILTIN_ALIAS_PACKAGES: ReadonlySet = new Set([ + "isarray", + "safe-buffer", + "process-nextick-args", + "inherits", + "util-deprecate", + "readable-stream", +]); + +/** The package, version and in-package file a bundler's module key names + * when it carries a version (pnpm's `node_modules/.pnpm/@/ + * node_modules//`), or null. */ +export function versionedPackageOfKey( + key: string, +): { name: string; version: string; subpath: string } | null { + const m = + /(?:^|\/)node_modules\/\.pnpm\/((?:@[^/+]+\+)?[^/@]+)@([^/_]+)[^/]*\/node_modules\/((?:@[^/]+\/)?[^/]+)\/(.+)$/.exec( + key, + ); + if (m === null) return null; + const [, storeName, version, name, subpath] = m; + if (storeName!.replace("+", "/") !== name) return null; + return { name: name!, version: version!, subpath: subpath! }; +} 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/lowering/builtins/json.ts b/packages/compiler/src/frontend/lowering/builtins/json.ts index 54bcae4b5..7eb8aa9b2 100644 --- a/packages/compiler/src/frontend/lowering/builtins/json.ts +++ b/packages/compiler/src/frontend/lowering/builtins/json.ts @@ -650,5 +650,57 @@ export function lowerJsonProperty( loc, }; } + if (member === "parse") { + // JSON.parse as a value: ToString of the text, then the reviver form + // (a reviver that is not a function is ignored, as in Node). + const loc = locOf(expr); + const name = "%builtin.JSON.parse"; + if (!lowerer.builtinCallableValueFns.has(name)) { + lowerer.builtinCallableValueFns.set(name, name); + const params = ["text", "reviver"].map((name) => ({ localId: name, name, type: DYN })); + lowerer.liftedFns.push({ + name, + params, + returnType: DYN, + locals: params.map((p) => ({ id: p.localId, name: p.name, type: DYN, mutable: false })), + loc, + body: [ + { + kind: "return", + value: { + kind: "libCall", + fn: "json.parseReviver", + args: [ + { + kind: "libCall", + fn: "dyn.toStringCoerce", + args: [varRef("text", DYN, loc)], + type: STRING, + loc, + }, + varRef("reviver", DYN, loc), + ], + type: DYN, + loc, + }, + loc, + }, + ], + }); + } + return { + kind: "dynFrom", + value: { + kind: "closure", + fnName: name, + captures: [], + type: { kind: "func", params: [DYN, DYN], ret: DYN }, + loc, + }, + fnName: "parse", + type: DYN, + loc, + }; + } lowerer.unsupported("SC1090", expr, `JSON methods as values (call '${member}' directly)`); } diff --git a/packages/compiler/src/frontend/lowering/builtins/process.ts b/packages/compiler/src/frontend/lowering/builtins/process.ts index 78dc6ef14..25653f541 100644 --- a/packages/compiler/src/frontend/lowering/builtins/process.ts +++ b/packages/compiler/src/frontend/lowering/builtins/process.ts @@ -266,6 +266,53 @@ export function isOptionalProcessStreamProperty( return lowerer.mapTypeOf(lowerer.typeOf(expr.expression))?.kind === "procStream"; } +/** The process.versions entries @types/node declares as plain strings + * that the runtime's versions object (node and openssl only) lacks. */ +const ABSENT_DECLARED_VERSIONS: ReadonlySet = new Set([ + "http_parser", + "v8", + "ares", + "uv", + "zlib", + "modules", +]); + +/** A process.versions member read whose declaration promises a string the + * runtime does not have. @types/node declares the entries above as plain + * strings, so a checked string read would throw on the absent entry and + * equality with undefined would fold to false. These reads keep the + * undefined arm, like the optional stream properties. The shipped fallback + * types declare other entries `string | undefined` and never reach this. + * An element read with a computed key qualifies when the checker types it + * as a plain string. */ +export function isOptionalProcessVersionsRead(lowerer: Lowerer, node: ts.Node): boolean { + let key: string | null; + if (ts.isPropertyAccessExpression(node)) key = node.name.text; + else if (ts.isElementAccessExpression(node)) + key = ts.isStringLiteralLike(node.argumentExpression) ? node.argumentExpression.text : null; + else return false; + if (key !== null && !ABSENT_DECLARED_VERSIONS.has(key)) return false; + if (!programMentionsVersions(lowerer)) return false; + const symbol = lowerer.typeOf(node.expression).getSymbol(); + return ( + symbol !== undefined && + ts.symbolName(symbol) === "ProcessVersions" && + symbol.declarations.some( + (d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(d.getSourceFile()), + ) && + lowerer.mapTypeOf(lowerer.typeOf(node))?.kind === "string" + ); +} + +/** Whether any implementation source spells `versions`. Programs that + * never do skip the per-read checker queries above entirely. */ +function programMentionsVersions(lowerer: Lowerer): boolean { + lowerer.mentionsProcessVersions ??= lowerer.program + .getImplementationSourceFiles() + .some((sf) => !sf.isDeclarationFile && sf.text.includes("versions")); + return lowerer.mentionsProcessVersions; +} + export function lowerProcessStreamProperty( lowerer: Lowerer, expr: ts.PropertyAccessExpression, diff --git a/packages/compiler/src/frontend/lowering/builtins/timers.ts b/packages/compiler/src/frontend/lowering/builtins/timers.ts index 7230d18cd..2d531663f 100644 --- a/packages/compiler/src/frontend/lowering/builtins/timers.ts +++ b/packages/compiler/src/frontend/lowering/builtins/timers.ts @@ -156,7 +156,11 @@ function isTimerHandleTyped( const sym = t.getAliasSymbol() ?? t.getSymbol(); if (sym === undefined || ts.symbolName(sym) !== name) return false; return sym.declarations.some( - (d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(ts.getSourceFileOfNode(d)), + (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(ts.getSourceFileOfNode(d)), ); } function isTimeoutTyped(lowerer: Lowerer, node: ts.Expression): boolean { diff --git a/packages/compiler/src/frontend/lowering/call-signatures.ts b/packages/compiler/src/frontend/lowering/call-signatures.ts index 47cdd834f..6e4d3aba1 100644 --- a/packages/compiler/src/frontend/lowering/call-signatures.ts +++ b/packages/compiler/src/frontend/lowering/call-signatures.ts @@ -956,6 +956,28 @@ export function lambdaSignature( return { shapes, funcType: funcTypeFromParamShapes(shapes, DYN) }; } } + // A callback whose arguments arrive as the runtime produced them, wider + // than its contextual overload says (dns.lookup's callback when the + // options are only known at run time: the address is a string or an + // address list): every parameter binds as a checked-dynamic value. + // Rest parameters keep their packing and optional or defaulted ones stay + // omittable (the prologue applies a default to an undefined argument). + // The slot discards the result, so a sync callback returns void and an + // async one keeps its promise. + if (lowerer.dynParamLambdas.has(node)) { + const shapes: ParamShape[] = node.parameters.nodes + .filter((param) => !isThisParameter(param)) + .map((param) => + param.dotDotDotToken + ? { type: DYN, mode: "dynRest" } + : param.initializer + ? { type: DYN, mode: "omittable", bodyType: DYN } + : { type: DYN, mode: param.questionToken ? "omittable" : "required" }, + ); + const isAsync = node.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword); + const ret = isAsync ? lowerer.declaredReturnType(node, node) : VOID; + return { shapes, funcType: funcTypeFromParamShapes(shapes, ret) }; + } if (node.typeParameters?.nodes && !lowerer.erasingLambdas.has(node)) { // Generic function-like forms monomorphize only where a static home // exists: top-level generic function declarations, generic methods diff --git a/packages/compiler/src/frontend/lowering/checked-fallback.ts b/packages/compiler/src/frontend/lowering/checked-fallback.ts new file mode 100644 index 000000000..b1d746c1d --- /dev/null +++ b/packages/compiler/src/frontend/lowering/checked-fallback.ts @@ -0,0 +1,861 @@ +/* Checked-cast fallback: a value that fails a checked cast keeps running. + * + * `JSON.parse(text) as T` (and `const x: T = anyValue`, `await res.json() + * as T`) is a checked conversion: the static code after it reads `T`'s + * native layout, so a value that does not fit `T` used to throw a + * TypeError at the cast. Node never checks: the program continues with the + * value it has. This module makes the failing case continue too, without + * changing the code that runs when the value fits. + * + * The unit is a FALLBACK SITE: a function-local binding whose only write + * is the checked cast (the ROOT), declared with `const`/`let` in a function + * body. The code that can observe the root is the statements after the + * cast in each enclosing statement list up to the root's declaration (the + * TAILS; every other reference disqualifies the site). Each tail is lowered + * twice: + * + * - the static lowering, exactly as before, used while the cast held; + * - the dynamic lowering, used after a failure: the root reads its checked + * dynamic value (a shadow local), and every expression derived from it is + * typed `any` (fallbackTainted), so the region lowers through scriptc's + * ordinary `any` path, which has Node's semantics: missing members read + * undefined, values keep their real types, extra members stay visible, + * and objects keep their identity. + * + * `if (flag) dynamic else static` selects the lowering after each tail's + * first statement; the cast sets the flag. A derived value that flows into + * a typed slot outside the region (a typed parameter, a field, an outer + * binding) passes the same checked conversion as any `any` value does + * there: it continues when it fits and throws that conversion's TypeError + * at that use when it does not. A statement of the dynamic lowering that + * has no `any` lowering compiles to a runtime fence that throws at that + * statement only when a failed value reaches it. + * + * Code without a fallback site is unchanged; code with one runs its static + * tail unchanged behind one flag test per tail, and the cast reads one + * thread-local word after the check (checkedCast.fallbackTaken). + * `--tier=static-only` (and library mode) opens no sites: casts throw. */ +import * as ts from "../ts7/adapter.js"; +import { + BOOL, + DYN, + type IrExpr, + type IrLocal, + type IrStmt, + type IrType, + type SrcLoc, +} from "../../ir/ir.js"; +import { isJsSourceFile, locOf } from "../program.js"; +import { PoisonError, type Lowerer } from "./lowerer.js"; + +export interface FallbackSite { + root: ts.Symbol; + /** The root's declared name (a module-level root is a module binding). */ + name: ts.Identifier; + /** The statement holding the checked cast (the declaration itself, or + * the `root = value as T` expression statement). */ + boundaryStmt: ts.Statement; + /** Statements after which a tail starts, innermost first; removed as + * their lists split. */ + anchors: Set; + flag: IrLocal; + shadow: IrLocal; + armed: boolean; + /** An initializer-less root's flag and shadow declarations, and the + * statement list they were emitted into. */ + decls: { stmts: IrStmt[]; out: IrStmt[] } | null; +} + +export interface FallbackTaint { + symbols: Set; + memo: Map; +} + +/** Tails larger than this keep the throwing cast: every node is lowered + * twice. */ +const MAX_TAIL_NODES = 20000; + +/** A site arms only while fewer than this many splits enclose its tail + * or share its split point: each one lowers the tail again, so a tail is + * lowered at most 2^n times. */ +const MAX_NESTED_SPLITS = 2; + +type StatementList = { statements: { nodes: readonly ts.Statement[] } }; + +function isStatementList(node: ts.Node | undefined): boolean { + return ( + node !== undefined && (ts.isBlock(node) || ts.isModuleBlock(node) || ts.isSourceFile(node)) + ); +} + +function listOf(node: ts.Node): readonly ts.Statement[] { + return (node as unknown as StatementList).statements.nodes; +} + +function strip(e: ts.Expression): ts.Expression { + let v = e; + while (ts.isParenthesizedExpression(v)) v = v.expression; + return v; +} + +function enclosingFunction(node: ts.Node): ts.Node | undefined { + let n: ts.Node | undefined = node.parent; + while (n !== undefined && !ts.isFunctionLike(n) && !ts.isSourceFile(n)) n = n.parent; + return n; +} + +/** `value as T` / `value` over an `any` or `unknown` value, or a + * dynamic value flowing into an annotated declaration. */ +function isCheckedCast(lowerer: Lowerer, e: ts.Expression, declared: ts.Type | null): boolean { + const dynamicFlags = ts.TypeFlags.Any | ts.TypeFlags.Unknown; + const dynamic = (x: ts.Expression): boolean => + (lowerer.checker.getTypeAtLocation(x).flags & dynamicFlags) !== 0; + const v = strip(e); + if (ts.isAsExpression(v) || ts.isTypeAssertion(v)) { + const target = lowerer.checker.getTypeFromTypeNode(v.type); + if (target.flags & dynamicFlags) return false; + return dynamic(strip(v.expression)); + } + return declared !== null && (declared.flags & dynamicFlags) === 0 && dynamic(v); +} + +function writesSymbol(lowerer: Lowerer, n: ts.Node, sym: ts.Symbol): ts.Node | null { + const names = (e: ts.Node): boolean => + ts.isIdentifier(e) && lowerer.resolveValueSymbol(e) === sym; + const targetNames = (t: ts.Expression): boolean => { + const e = strip(t); + if (names(e)) return true; + if (ts.isArrayLiteralExpression(e) || ts.isObjectLiteralExpression(e)) { + let found = false; + const walk = (x: ts.Node): void => { + if (names(x)) found = true; + else ts.forEachChild(x, walk); + }; + walk(e); + return found; + } + return false; + }; + if (ts.isBinaryExpression(n)) { + const k = n.operatorToken.kind; + if ( + k >= ts.SyntaxKind.FirstAssignment && + k <= ts.SyntaxKind.LastAssignment && + targetNames(n.left) + ) + return n; + } else if ( + (ts.isPrefixUnaryExpression(n) || ts.isPostfixUnaryExpression(n)) && + (n.operator === ts.SyntaxKind.PlusPlusToken || n.operator === ts.SyntaxKind.MinusMinusToken) && + targetNames(n.operand as ts.Expression) + ) { + return n; + } else if ( + (ts.isForOfStatement(n) || ts.isForInStatement(n)) && + !ts.isVariableDeclarationList(n.initializer) && + targetNames(n.initializer as ts.Expression) + ) { + return n; + } + return null; +} + +/** Hoisted or registered-once declarations, and `super()` calls (which + * initialize the instance once), that a second lowering would duplicate. */ +function tailIsRelowerable(stmts: readonly ts.Statement[]): boolean { + let ok = true; + let count = 0; + const visit = (n: ts.Node): void => { + if (!ok) return; + if (++count > MAX_TAIL_NODES) { + ok = false; + return; + } + if ( + ts.isFunctionDeclaration(n) || + ts.isClassDeclaration(n) || + ts.isClassExpression(n) || + ts.isModuleDeclaration(n) || + ts.isEnumDeclaration(n) || + (ts.isCallExpression(n) && n.expression.kind === ts.SyntaxKind.SuperKeyword) || + (ts.isVariableDeclarationList(n) && + (n.flags & (ts.NodeFlags.Let | ts.NodeFlags.Const | ts.NodeFlags.Using)) === 0) + ) { + ok = false; + return; + } + ts.forEachChild(n, visit); + }; + for (const s of stmts) visit(s); + return ok; +} + +interface Candidate { + root: ts.Symbol; + name: ts.Identifier; + boundaryStmt: ts.Statement; + anchors: ts.Statement[]; +} + +/** The fallback site a declaration statement starts, or null. */ +function fallbackCandidate(lowerer: Lowerer, stmt: ts.VariableStatement): Candidate | null { + const list = stmt.declarationList; + if ((list.flags & (ts.NodeFlags.Let | ts.NodeFlags.Const)) === 0) return null; + if ((list.flags & ts.NodeFlags.Using) !== 0) return null; + if (list.declarations.nodes.length !== 1) return null; + if (ts.canHaveModifiers(stmt) && (ts.getModifiers(stmt)?.length ?? 0) > 0) return null; + const decl = list.declarations.nodes[0]!; + if (!ts.isIdentifier(decl.name)) return null; + if (isJsSourceFile(ts.getSourceFileOfNode(stmt))) return null; + // Cheap gates before any scan: a checked-cast initializer, or a typed + // declaration without one. + const declared = decl.type !== undefined ? lowerer.checker.getTypeFromTypeNode(decl.type) : null; + if ( + decl.initializer !== undefined + ? !isCheckedCast(lowerer, decl.initializer, declared) + : declared === null + ) + return null; + const rootList = stmt.parent; + // Module-level bindings are module state (and module statements lower + // one at a time): roots live in blocks. + if (rootList === undefined || !ts.isBlock(rootList)) return null; + const fn = enclosingFunction(stmt); + if (fn === undefined) return null; + const root = lowerer.checker.getSymbolAtLocation(decl.name); + if (root === undefined) return null; + // A root in a module-level block is scanned across its file. + const body = ts.isSourceFile(fn) ? fn : (fn as ts.FunctionLikeDeclaration).body; + if (body === undefined) return null; + + // The only write: the initializer, or one `root = value as T` statement. + let boundaryStmt: ts.Statement | null = null; + let boundaryWrite: ts.Node | null = null; + const writes: ts.Node[] = []; + const findWrites = (n: ts.Node): void => { + const w = writesSymbol(lowerer, n, root); + if (w) writes.push(w); + ts.forEachChild(n, findWrites); + }; + findWrites(body); + if (decl.initializer !== undefined) { + if (writes.length > 0) return null; + boundaryStmt = stmt; + } else { + if (writes.length !== 1) return null; + const w = writes[0]!; + if ( + !ts.isBinaryExpression(w) || + w.operatorToken.kind !== ts.SyntaxKind.EqualsToken || + !ts.isIdentifier(strip(w.left)) || + !ts.isExpressionStatement(w.parent) || + !isCheckedCast(lowerer, w.right, declared) + ) + return null; + if (enclosingFunction(w) !== fn) return null; + boundaryStmt = w.parent; + boundaryWrite = w; + } + + // The split points: the statement holding the cast in each enclosing + // statement list, up to the root's list. + const anchors: ts.Statement[] = []; + let cur: ts.Node = boundaryStmt; + for (;;) { + const parent: ts.Node | undefined = cur.parent; + if (parent === undefined || ts.isFunctionLike(parent)) return null; + if (ts.isCaseClause(parent) || ts.isDefaultClause(parent)) return null; + if (isStatementList(parent)) { + anchors.push(cur as ts.Statement); + if (parent === rootList) break; + if (ts.isSourceFile(parent)) return null; + } + cur = parent; + } + const anchorIndex = new Map(); + for (const a of anchors) + anchorIndex.set(a, { list: a.parent!, index: listOf(a.parent!).indexOf(a) }); + const tails: ts.Statement[] = []; + for (const a of anchors) { + const { list, index } = anchorIndex.get(a)!; + tails.push(...listOf(list).slice(index + 1)); + } + if (!tailIsRelowerable(tails)) return null; + + // Every other reference sits in a tail. + const inTail = (ref: ts.Node): boolean => { + let n: ts.Node = ref; + while (n.parent !== undefined && n !== body) { + const parent = n.parent; + if (isStatementList(parent)) { + for (const a of anchors) { + if (a.parent !== parent) continue; + const at = anchorIndex.get(a)!.index; + const mine = listOf(parent).indexOf(n as ts.Statement); + if (mine > at) return true; + if (mine < at) return false; + } + } + n = parent; + } + return false; + }; + let referencesOk = true; + const scanRefs = (n: ts.Node): void => { + if (!referencesOk) return; + if (ts.isIdentifier(n) && n !== decl.name && lowerer.resolveValueSymbol(n) === root) { + const isBoundaryTarget = + boundaryWrite !== null && + ts.isBinaryExpression(boundaryWrite) && + strip(boundaryWrite.left) === n; + if (!isBoundaryTarget && !inTail(n)) referencesOk = false; + return; + } + ts.forEachChild(n, scanRefs); + }; + scanRefs(body); + if (!referencesOk) return null; + return { root, name: decl.name, boundaryStmt, anchors }; +} + +function declareSiteLocals( + lowerer: Lowerer, + candidate: Candidate, + loc: SrcLoc, +): { site: FallbackSite; decls: IrStmt[] } { + const name = ts.symbolName(candidate.root); + const flag = lowerer.declareHiddenLocal(`%fb.${name}`, BOOL); + flag.mutable = true; + const shadow = lowerer.declareHiddenLocal(`%fbv.${name}`, DYN); + shadow.mutable = true; + const decls: IrStmt[] = [ + { + kind: "varDecl", + localId: flag.id, + init: { kind: "boolLit", value: false, type: BOOL, loc }, + loc, + }, + { kind: "varDecl", localId: shadow.id, init: null, loc }, + ]; + const site: FallbackSite = { + root: candidate.root, + name: candidate.name, + boundaryStmt: candidate.boundaryStmt, + anchors: new Set(candidate.anchors), + flag, + shadow, + armed: false, + decls: null, + }; + return { site, decls }; +} + +/** Before an initializer-less typed declaration lowers: open the fallback + * site its later `root = value as T` statement starts, declaring the flag + * (false) and the shadow that holds a failed value where the root is + * declared. Declarations with an initializer open when they arm. */ +export function openFallbackSite(lowerer: Lowerer, stmt: ts.Statement, out: IrStmt[]): void { + if (!ts.isVariableStatement(stmt) || lowerer.fnStack.length === 0) return; + if (lowerer.tierMode === "static-only") return; + const decls = stmt.declarationList.declarations.nodes; + if (decls.length !== 1 || decls[0]!.initializer !== undefined || decls[0]!.type === undefined) + return; + const candidate = fallbackCandidate(lowerer, stmt); + if (candidate === null) return; + const opened = declareSiteLocals(lowerer, candidate, locOf(stmt)); + out.push(...opened.decls); + opened.site.decls = { stmts: opened.decls, out }; + lowerer.fallbackSites.set(candidate.root, opened.site); +} + +/** A site that will not arm: drop its declarations again, so the program + * lowers exactly as it did without the site. */ +function closeUnarmed(lowerer: Lowerer, site: FallbackSite): void { + const decls = site.decls; + if (decls !== null) { + for (const d of decls.stmts) { + const at = decls.out.indexOf(d); + if (at >= 0) decls.out.splice(at, 1); + } + } + const locals = lowerer.ctx.locals; + for (const local of [site.flag, site.shadow]) { + const at = locals.indexOf(local); + if (at >= 0) locals.splice(at, 1); + } +} + +const FALLBACK_TARGETS = new Set(["record", "array", "f64", "string", "bool"]); + +/** A plain check of a record, array or scalar target that holds no class + * instance: casts to program classes keep their checked semantics (a + * documented scriptc boundary: a class cast checks the instance's class). */ +function fallbackCheck(lowerer: Lowerer, check: IrExpr & { kind: "dynCheck" }): boolean { + if (check.preserveRefs || check.exact || !FALLBACK_TARGETS.has(check.type.kind)) return false; + return !holdsClassInstance(lowerer, check.type, new Set()); +} + +function holdsClassInstance(lowerer: Lowerer, t: IrType, seen: Set): boolean { + switch (t.kind) { + case "object": + case "classval": + return true; + case "array": + return holdsClassInstance(lowerer, t.elem, seen); + case "record": { + if (seen.has(t.shapeId)) return false; + seen.add(t.shapeId); + const shape = lowerer.shapes.get(t.shapeId); + if (shape === undefined) return true; + if (shape.indexValue !== undefined && holdsClassInstance(lowerer, shape.indexValue, seen)) + return true; + return shape.fields.some((f) => holdsClassInstance(lowerer, f.type, seen)); + } + case "union": { + const def = lowerer.unions.get(t.unionId); + return def === undefined || def.arms.some((arm) => holdsClassInstance(lowerer, arm, seen)); + } + default: + return false; + } +} + +/** After a statement lowers: when it holds a fallback site's cast, mark the + * check as a fallback check and read its outcome into the flag and the + * shadow. A cast whose lowering is not a plain check keeps throwing. */ +export function armFallbackSite(lowerer: Lowerer, stmt: ts.Statement, lowered: IrStmt[]): void { + // `const root = value as T`: the check is the declaration's initializer. + if ( + ts.isVariableStatement(stmt) && + lowerer.fnStack.length > 0 && + lowerer.tierMode !== "static-only" && + lowered.some((s) => s.kind === "varDecl" && s.init?.kind === "dynCheck") + ) { + const decls = stmt.declarationList.declarations.nodes; + const decl = decls.length === 1 ? decls[0]! : undefined; + if (decl?.initializer !== undefined && ts.isIdentifier(decl.name)) { + const symbol = lowerer.checker.getSymbolAtLocation(decl.name); + const local = symbol ? lowerer.bindingIn(lowerer.ctx, symbol) : null; + const at = + local === null + ? -1 + : lowered.findIndex( + (s) => s.kind === "varDecl" && s.localId === local.id && s.init?.kind === "dynCheck", + ); + const write = at >= 0 ? lowered[at]! : null; + const init = write?.kind === "varDecl" ? write.init : null; + if (init?.kind === "dynCheck" && fallbackCheck(lowerer, init)) { + const candidate = fallbackCandidate(lowerer, stmt); + if (candidate !== null) { + const opened = declareSiteLocals(lowerer, candidate, locOf(stmt)); + if (armSite(lowerer, opened.site, lowered, local!.id)) + lowered.splice(at, 0, ...opened.decls); + else closeUnarmed(lowerer, opened.site); + } + } + } + } + if (lowerer.fallbackSites.size === 0) return; + for (const site of [...lowerer.fallbackSites.values()]) { + if (site.boundaryStmt !== stmt || site.armed) continue; + lowerer.fallbackSites.delete(site.root); + const id = lowerer.bindingIn(lowerer.ctx, site.root)?.id; + if (id === undefined || !armSite(lowerer, site, lowered, id)) closeUnarmed(lowerer, site); + } +} + +function armSite(lowerer: Lowerer, site: FallbackSite, lowered: IrStmt[], id: string): boolean { + const at = lowered.findIndex( + (s) => + (s.kind === "varDecl" && s.localId === id && s.init?.kind === "dynCheck") || + (s.kind === "assign" && s.localId === id && s.value.kind === "dynCheck"), + ); + if (at < 0) return false; + const write = lowered[at]!; + let check: IrExpr | null = null; + if (write.kind === "varDecl") check = write.init; + else if (write.kind === "assign") check = write.value; + if (check === null || check.kind !== "dynCheck" || !fallbackCheck(lowerer, check)) return false; + // Every tail must split: each split point's list is lowering whole. + const whole = [...site.anchors].every((anchor) => { + const real = listOf(anchor.parent!); + const index = real.indexOf(anchor); + return lowerer.activeStmtLists.some( + (entry) => + entry.stmts[entry.index] === anchor && + entry.stmts.length - entry.index === real.length - index, + ); + }); + if (!whole) return false; + // Splits around this site (sequential casts each split the code after + // them) and sites sharing a split point multiply the lowerings. + let nested = lowerer.fallbackSplitDepth; + for (const other of lowerer.fallbackArmed) { + for (const a of other.anchors) if (site.anchors.has(a)) nested++; + } + if (nested >= MAX_NESTED_SPLITS) return false; + check.fallback = true; + const loc = check.loc; + lowered.splice( + at + 1, + 0, + { + kind: "assign", + localId: site.flag.id, + value: { kind: "libCall", fn: "checkedCast.fallbackTaken", args: [], type: BOOL, loc }, + loc, + }, + { + kind: "if", + cond: { kind: "varRef", localId: site.flag.id, type: BOOL, loc }, + then: [ + { + kind: "assign", + localId: site.shadow.id, + value: { kind: "libCall", fn: "checkedCast.fallbackTake", args: [], type: DYN, loc }, + loc, + }, + ], + else_: null, + loc, + }, + ); + site.armed = true; + lowerer.fallbackArmed.push(site); + return true; +} + +/** Armed sites whose tail starts after `stmt`, consumed. */ +export function fallbackSplitsAt(lowerer: Lowerer, stmt: ts.Statement): FallbackSite[] { + if (lowerer.fallbackArmed.length === 0) return []; + const here: FallbackSite[] = []; + for (const site of lowerer.fallbackArmed) { + if (site.anchors.delete(stmt)) here.push(site); + } + if (here.length > 0) + lowerer.fallbackArmed = lowerer.fallbackArmed.filter((s) => s.anchors.size > 0); + return here; +} + +/** The tail after a split point: `if (flag) else ` per + * site. The static lowering runs first and is exactly the lowering the + * tail had without a site. */ +export function lowerFallbackTail( + lowerer: Lowerer, + rest: readonly ts.Statement[], + sites: readonly FallbackSite[], +): IrStmt[] { + if (sites.length === 0) return lowerer.lowerStmts(rest); + // An empty tail (the cast ends its block) needs no split. + if (rest.length === 0) return []; + const [site, ...others] = sites as [FallbackSite, ...FallbackSite[]]; + const at = locOf(rest.length > 0 ? rest[0]! : site.boundaryStmt); + let statics: IrStmt[]; + let dynamic: IrStmt[]; + lowerer.fallbackSplitDepth++; + try { + statics = scoped(lowerer, () => lowerFallbackTail(lowerer, rest, others)); + dynamic = lowerDynamicTail(lowerer, rest, site, others); + } finally { + lowerer.fallbackSplitDepth--; + } + return [ + { + kind: "if", + cond: { kind: "varRef", localId: site.flag.id, type: BOOL, loc: at }, + then: dynamic, + else_: statics, + loc: at, + }, + ]; +} + +function scoped(lowerer: Lowerer, lower: () => T, frame = new Map()): T { + lowerer.scopes.push(frame); + try { + return lower(); + } finally { + lowerer.scopes.pop(); + } +} + +function lowerDynamicTail( + lowerer: Lowerer, + rest: readonly ts.Statement[], + site: FallbackSite, + others: readonly FallbackSite[], +): IrStmt[] { + const saved = { + taint: lowerer.fallbackTaint, + sink: lowerer.diagSink, + stats: lowerer.suppressStats, + sites: new Map(lowerer.fallbackSites), + armed: lowerer.fallbackArmed, + scopes: lowerer.scopes.length, + }; + lowerer.fallbackTaint = fallbackTaintOf(lowerer, saved.taint, site.root, rest); + lowerer.diagSink = []; + lowerer.suppressStats = true; + lowerer.fallbackArmed = []; + try { + return scoped( + lowerer, + () => lowerFallbackTail(lowerer, rest, others), + new Map([[site.root, site.shadow]]), + ); + } catch (error) { + if (!(error instanceof PoisonError) && process.env.SCRIPTC_FALLBACK_DEBUG) throw error; + while (lowerer.scopes.length > saved.scopes) lowerer.scopes.pop(); + const loc = rest.length > 0 ? locOf(rest[0]!) : locOf(site.boundaryStmt); + return [ + { + kind: "runtimeFence", + code: "SC1090", + message: `a value that failed its checked cast reached code with no dynamic lowering`, + loc, + }, + ]; + } finally { + lowerer.fallbackTaint = saved.taint; + lowerer.diagSink = saved.sink; + lowerer.suppressStats = saved.stats; + lowerer.fallbackSites = saved.sites; + lowerer.fallbackArmed = saved.armed; + } +} + +/* ── taint: which expressions of a dynamic tail read a failed value ── */ + +/** True when `node`'s value derives from a failed checked value in the + * dynamic lowering being built: it is typed `any` there. */ +export function fallbackTainted(lowerer: Lowerer, node: ts.Node): boolean { + const taint = lowerer.fallbackTaint; + if (taint === null) return false; + const known = taint.memo.get(node); + if (known !== undefined) return known; + taint.memo.set(node, false); + const result = computeTainted(lowerer, taint, node); + taint.memo.set(node, result); + return result; +} + +function computeTainted(lowerer: Lowerer, taint: FallbackTaint, node: ts.Node): boolean { + const t = (n: ts.Node): boolean => fallbackTainted(lowerer, n); + if (ts.isIdentifier(node)) { + const sym = lowerer.resolveValueSymbol(node); + return sym !== null && taint.symbols.has(sym); + } + if ( + ts.isParenthesizedExpression(node) || + ts.isNonNullExpression(node) || + ts.isAsExpression(node) || + ts.isTypeAssertion(node) || + ts.isSatisfiesExpression(node) || + ts.isAwaitExpression(node) + ) + return t(node.expression); + if (ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node)) + return t(node.expression); + if (ts.isCallExpression(node)) return t(node.expression); + if (ts.isConditionalExpression(node)) return t(node.whenTrue) || t(node.whenFalse); + if (ts.isBinaryExpression(node)) { + const k = node.operatorToken.kind; + switch (k) { + case ts.SyntaxKind.QuestionQuestionToken: + case ts.SyntaxKind.BarBarToken: + case ts.SyntaxKind.AmpersandAmpersandToken: + case ts.SyntaxKind.PlusToken: + return t(node.left) || t(node.right); + case ts.SyntaxKind.CommaToken: + case ts.SyntaxKind.EqualsToken: + return t(node.right); + case ts.SyntaxKind.PlusEqualsToken: + case ts.SyntaxKind.QuestionQuestionEqualsToken: + case ts.SyntaxKind.BarBarEqualsToken: + case ts.SyntaxKind.AmpersandAmpersandEqualsToken: + return t(node.left) || t(node.right); + default: + return false; + } + } + // A literal beside a failed value (`value ?? {}`, `ok ? value : []`) + // is built dynamically too: its contextual type is the static one. + if (ts.isObjectLiteralExpression(node) || ts.isArrayLiteralExpression(node)) { + // A literal spreading a failed value is built dynamically, and so is + // one holding a failed value that initializes a binding (the binding + // turns dynamic) or sits in such a literal. A literal flowing straight + // into a typed slot keeps its layout: each failed member is checked + // into its field, so members that fit continue. + let holds = false; + if (ts.isObjectLiteralExpression(node)) { + for (const p of node.properties.nodes) { + if (ts.isSpreadAssignment(p) && t(p.expression)) return true; + if ( + (ts.isPropertyAssignment(p) && t(p.initializer)) || + (ts.isShorthandPropertyAssignment(p) && t(p.name)) + ) + holds = true; + } + } else { + for (const e of node.elements.nodes) + if (t(ts.isSpreadElement(e) ? e.expression : e)) holds = true; + } + if (holds) { + // The outermost literal of a nest decides for all of it. + let top: ts.Node = node; + for (;;) { + const host: ts.Node | undefined = top.parent; + if (host !== undefined && ts.isPropertyAssignment(host) && host.parent !== undefined) { + top = host.parent; + continue; + } + if (host !== undefined && ts.isArrayLiteralExpression(host)) { + top = host; + continue; + } + break; + } + // An untyped destination (`console.log`, `JSON.stringify`) takes it + // dynamically. + const contextual = lowerer.checker.getContextualType(top); + if ( + contextual !== undefined && + (contextual.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0 + ) + return true; + const host = top.parent; + if (host !== undefined && ts.isVariableDeclaration(host) && host.initializer === top) + return true; + } + let child: ts.Node = node; + let parent = node.parent; + while (parent !== undefined && ts.isParenthesizedExpression(parent)) { + child = parent; + parent = parent.parent; + } + if (parent === undefined) return false; + if (ts.isBinaryExpression(parent)) { + const k = parent.operatorToken.kind; + if ( + k === ts.SyntaxKind.QuestionQuestionToken || + k === ts.SyntaxKind.BarBarToken || + k === ts.SyntaxKind.AmpersandAmpersandToken + ) + return t(parent.left === child ? parent.right : parent.left); + } + if (ts.isConditionalExpression(parent) && parent.condition !== child) + return t(parent.whenTrue === child ? parent.whenFalse : parent.whenTrue); + return false; + } + if (ts.isPrefixUnaryExpression(node) || ts.isPostfixUnaryExpression(node)) { + if ( + node.operator === ts.SyntaxKind.PlusPlusToken || + node.operator === ts.SyntaxKind.MinusMinusToken + ) + return t(node.operand); + return false; + } + return false; +} + +/** The taint of a dynamic tail: the root, and every binding declared in the + * tail that a tainted value is written to (a fixpoint over the tail, + * nested functions included). Parameters of callbacks passed to a tainted + * callee are tainted too: the dynamic call passes them dynamic values. */ +function fallbackTaintOf( + lowerer: Lowerer, + outer: FallbackTaint | null, + root: ts.Symbol, + region: readonly ts.Statement[], +): FallbackTaint { + const symbols = new Set(outer?.symbols ?? []); + symbols.add(root); + const taint: FallbackTaint = { symbols, memo: new Map() }; + const saved = lowerer.fallbackTaint; + lowerer.fallbackTaint = taint; + try { + growTaint(lowerer, taint, region); + } finally { + lowerer.fallbackTaint = saved; + } + return taint; +} + +function growTaint(lowerer: Lowerer, taint: FallbackTaint, region: readonly ts.Statement[]): void { + const symbols = taint.symbols; + const declared = new Set(); + const collectDeclared = (n: ts.Node): void => { + if ( + (ts.isVariableDeclaration(n) || ts.isParameter(n) || ts.isBindingElement(n)) && + ts.isIdentifier(n.name) + ) { + const s = lowerer.checker.getSymbolAtLocation(n.name); + if (s) declared.add(s); + } + ts.forEachChild(n, collectDeclared); + }; + for (const s of region) collectDeclared(s); + const bindNames = (name: ts.BindingName): boolean => { + let changed = false; + const visit = (n: ts.Node): void => { + if (ts.isIdentifier(n)) { + const s = lowerer.checker.getSymbolAtLocation(n); + if (s && !symbols.has(s)) { + symbols.add(s); + changed = true; + } + return; + } + if (ts.isBindingElement(n)) { + if (n.name !== undefined) visit(n.name); + return; + } + ts.forEachChild(n, visit); + }; + visit(name); + return changed; + }; + for (let round = 0; round < 32; round++) { + let changed = false; + taint.memo.clear(); + const visit = (n: ts.Node): void => { + if ( + ts.isVariableDeclaration(n) && + n.initializer !== undefined && + fallbackTainted(lowerer, n.initializer) + ) { + if (bindNames(n.name)) changed = true; + } else if (ts.isBinaryExpression(n)) { + const k = n.operatorToken.kind; + if ( + k >= ts.SyntaxKind.FirstAssignment && + k <= ts.SyntaxKind.LastAssignment && + ts.isIdentifier(strip(n.left)) + ) { + const s = lowerer.resolveValueSymbol(strip(n.left) as ts.Identifier); + if (s && declared.has(s) && !symbols.has(s) && fallbackTainted(lowerer, n.right)) { + symbols.add(s); + changed = true; + } + } + } else if ( + ts.isForOfStatement(n) && + ts.isVariableDeclarationList(n.initializer) && + fallbackTainted(lowerer, n.expression) + ) { + for (const d of n.initializer.declarations.nodes) if (bindNames(d.name)) changed = true; + } else if (ts.isCallExpression(n) && fallbackTainted(lowerer, n.expression)) { + for (const arg of n.arguments.nodes) { + const a = strip(arg); + if (ts.isArrowFunction(a) || ts.isFunctionExpression(a)) { + for (const p of a.parameters.nodes) if (bindNames(p.name)) changed = true; + } + } + } + ts.forEachChild(n, visit); + }; + for (const s of region) visit(s); + if (!changed) break; + } + taint.memo.clear(); +} diff --git a/packages/compiler/src/frontend/lowering/cjs-exports-object.ts b/packages/compiler/src/frontend/lowering/cjs-exports-object.ts index 30030b18f..ae8cce4ca 100644 --- a/packages/compiler/src/frontend/lowering/cjs-exports-object.ts +++ b/packages/compiler/src/frontend/lowering/cjs-exports-object.ts @@ -25,6 +25,7 @@ import { cjsExportDiscardReason, cjsExportTargetLiteral, isCjsJsFile, + isJsSourceFile, isModuleExportsAccess, isRequireStatement, locOf, @@ -367,6 +368,50 @@ function symbolIsThisFreeFunction(lowerer: Lowerer, sym: ts.Symbol | undefined): ); } +/** The ES-module half (a TypeScript module required from JavaScript, such + * as a package shim): require() answers its namespace, so a call through + * it binds `this` to the namespace. True when the export is a function + * that never reads `this` (every declaration of an overload set), a + * builtin module's member re-exported from its declarations, or reached + * through an `export *` of a module where that holds. */ +function esModuleExportIgnoresThis( + lowerer: Lowerer, + sf: ts.SourceFile, + name: string, + depth: number, +): boolean { + const moduleSym = lowerer.checker.getSymbolAtLocation(sf); + const exported = moduleSym?.getExports().get(name as ts.__String); + if (exported !== undefined) { + const target = + exported.flags & ts.SymbolFlags.Alias ? lowerer.checker.getAliasedSymbol(exported) : exported; + const decls = target.declarations; + if (decls.length === 0) return false; + return decls.every( + (d) => + (ts.isFunctionDeclaration(d) && !functionUsesThis(d)) || + (ts.getSourceFileOfNode(d).isDeclarationFile && ts.isFunctionDeclaration(d)), + ); + } + for (const stmt of sf.statements.nodes) { + if ( + !ts.isExportDeclaration(stmt) || + stmt.exportClause !== undefined || + stmt.moduleSpecifier === undefined || + !ts.isStringLiteral(stmt.moduleSpecifier) + ) + continue; + const dep = resolveImport(lowerer.program, sf, stmt.moduleSpecifier.text); + if ( + dep !== null && + !dep.isDeclarationFile && + esModuleExportIgnoresThis(lowerer, dep, name, depth + 1) + ) + return true; + } + return false; +} + /** True when every value the module assigns to its export `name` is a * function that never reads `this`. A call through the exports object * (`exports.f(x)`) binds `this` to that object, which the static export @@ -378,6 +423,7 @@ export function cjsExportCallIgnoresThis( depth = 0, ): boolean { if (depth > 16) return false; + if (!isJsSourceFile(sf)) return esModuleExportIgnoresThis(lowerer, sf, name, depth); const values: ts.Expression[] = []; ts.walkPreorder(sf, (node) => { if ( diff --git a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts index de84e3fed..cb5c28444 100644 --- a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts +++ b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts @@ -5245,7 +5245,15 @@ export class ClassDynamicDispatch { if (methodInfo) { owner = methodInfo.declarer; params = methodInfo.sig.params; - result = methodInfo.sig.ret; + // An untyped async method's `dyn` result is its promise's payload; the + // call answers the promise. An async generator method answers its + // generator object, which its signature already returns. + result = + methodInfo.sig.async === true && + !methodInfo.sig.gen && + methodInfo.sig.ret.kind !== "promise" + ? { kind: "promise", inner: methodInfo.sig.ret } + : methodInfo.sig.ret; callee = `%${owner.def.name}.${method}`; virtual = lowerer.overrideBelow(owner, method) || methodInfo.sig.abstract === true; } else if ( diff --git a/packages/compiler/src/frontend/lowering/class-protocols.ts b/packages/compiler/src/frontend/lowering/class-protocols.ts index 4de7f03ad..172224053 100644 --- a/packages/compiler/src/frontend/lowering/class-protocols.ts +++ b/packages/compiler/src/frontend/lowering/class-protocols.ts @@ -6,6 +6,8 @@ export class ClassProtocols { private implemented: Set | null = null; private readonly methods = new Map(); private readonly answers = new Map(); + /** The program classes each Pick or Omit view accepts (classesAcceptedBy). */ + private readonly accepted = new Map(); /** What markIdentityFlows learned about the program, while it runs. */ private facts: FlowFacts | null = null; @@ -414,6 +416,14 @@ export class ClassProtocols { for (const part of ts.constituentTypes(type)) add(part); return; } + // A Pick or Omit view's values are instances of the class it views, + // or of any other program class that has the view's members. + if (this.pickOfProgramClass(type)) { + add(type.getAliasTypeArguments()[0]!); + for (const other of this.classesAcceptedBy(type)) + for (const related of this.classFamily(other, facts)) out.add(related); + return; + } const symbol = type.getSymbol(); if (symbol === undefined) return; if (this.isProgramClassInstance(type)) { @@ -429,6 +439,30 @@ export class ClassProtocols { return out; } + /** The program classes with methods whose instances are assignable to + * `type`. */ + private classesAcceptedBy(type: ts.Type): ts.Symbol[] { + const known = this.accepted.get(type); + if (known !== undefined) return known; + this.index(); + const found = new Set(); + for (const entries of this.methods.values()) + for (const declaration of entries) { + const symbol = declaration.name + ? this.checker.getSymbolAtLocation(declaration.name) + : undefined; + if ( + symbol !== undefined && + !found.has(symbol) && + this.checker.isTypeAssignableTo(this.checker.getDeclaredTypeOfSymbol(symbol), type) + ) + found.add(symbol); + } + const result = [...found]; + this.accepted.set(type, result); + return result; + } + /** A class symbol with its base classes and the program classes that * extend it. */ private classFamily(symbol: ts.Symbol, facts: FlowFacts): ts.Symbol[] { @@ -570,6 +604,20 @@ export class ClassProtocols { } } + /** `Pick` or `Omit` over a program class instance type: a + * view of the class's own members, spelled inline instead of as an + * interface. Like an interface the class matches, a view that exposes a + * method needs a live receiver (see protocol). */ + private pickOfProgramClass(type: ts.Type): boolean { + const alias = type.getAliasSymbol(); + if (alias === undefined || (ts.symbolName(alias) !== "Pick" && ts.symbolName(alias) !== "Omit")) + return false; + if (!alias.declarations.every((declaration) => declaration.getSourceFile().isDeclarationFile)) + return false; + const source = type.getAliasTypeArguments()[0]; + return source !== undefined && this.isProgramClassInstance(source); + } + private isProgramClassInstance(type: ts.Type): boolean { const symbol = type.getSymbol(); return ( @@ -592,6 +640,8 @@ export class ClassProtocols { return element !== undefined && this.carriesIdentity(element, marked); } if (this.isProgramClassInstance(type)) return true; + // A live Pick or Omit view of a program class holds the instance itself. + if (this.pickOfProgramClass(type) && this.usesCheckedIdentity(type)) return true; const symbol = type.getSymbol(); if (symbol === undefined) return false; if (marked.has(symbol)) return true; @@ -704,12 +754,13 @@ export class ClassProtocols { const properties = this.checker.getPropertiesOfType(type); if (symbol && protocols.has(symbol)) return true; if ( - !symbol || - !symbol.declarations.some( - (declaration) => - ts.isInterfaceDeclaration(declaration) && - !ts.getSourceFileOfNode(declaration).isDeclarationFile, - ) + !this.pickOfProgramClass(type) && + (!symbol || + !symbol.declarations.some( + (declaration) => + ts.isInterfaceDeclaration(declaration) && + !ts.getSourceFileOfNode(declaration).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 6c6c7811d..7b4b58ae3 100644 --- a/packages/compiler/src/frontend/lowering/class-prototypes.ts +++ b/packages/compiler/src/frontend/lowering/class-prototypes.ts @@ -189,12 +189,16 @@ export function classPrototypeData( continue; const compiled = classMethodValue(lowerer, access, info, method, locOf(access)); if (!compiled) continue; + // A method value without a checked-dynamic form (an async method's + // promise-returning signature) gets no data slot. + const boxedValue = lowerer.coerceToExpected(compiled, DYN); + if (boxedValue.type.kind !== "dyn") continue; const descriptor: IrExpr = { kind: "dynObjLit", fields: [ { key: { kind: "strLit", value: "value", type: STRING, loc }, - value: lowerer.coerceToExpected(compiled, DYN), + value: boxedValue, }, { key: { kind: "strLit", value: "writable", type: STRING, loc }, 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/coercions/fresh-spread.ts b/packages/compiler/src/frontend/lowering/coercions/fresh-spread.ts new file mode 100644 index 000000000..656ac3ee1 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/coercions/fresh-spread.ts @@ -0,0 +1,97 @@ +/* Array spreads whose elements nothing else can reach. */ +import * as ts from "../../ts7/adapter.js"; +import type { IrExpr, IrType, SrcLoc } from "../../../ir/ir.js"; +import type { Lowerer } from "../lowerer.js"; + +/** Whether the elements a spread inserts are reachable only through it: an + * array literal (`...(cond ? [{ ... }] : [])`), or an unexported const + * initialized by one that this spread is the only reference to, and that + * the spread reads at most once per evaluation of the initializer. Converting such elements + * into the destination's element layout copies them, which nothing can + * observe. */ +export function freshSpreadSource(lowerer: Lowerer, node: ts.Expression): boolean { + let e = node; + while (ts.isParenthesizedExpression(e)) e = e.expression; + if (ts.isArrayLiteralExpression(e)) + return e.elements.nodes.every( + (x) => !ts.isSpreadElement(x) && (ts.isObjectLiteralExpression(x) || !isReference(x)), + ); + if (ts.isConditionalExpression(e)) + return freshSpreadSource(lowerer, e.whenTrue) && freshSpreadSource(lowerer, e.whenFalse); + if (!ts.isIdentifier(e)) return false; + const symbol = lowerer.resolveValueSymbol(e); + const decl = symbol?.valueDeclaration; + if (!symbol || !decl || !ts.isVariableDeclaration(decl) || !decl.initializer) return false; + const list = decl.parent; + if (!ts.isVariableDeclarationList(list) || (list.flags & ts.NodeFlags.Const) === 0) return false; + // Other modules can import an exported binding and reach its elements. + const statement = list.parent; + if ( + ts.isVariableStatement(statement) && + statement.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) + ) + return false; + if (!freshSpreadSource(lowerer, decl.initializer)) return false; + // The binding's only use is this spread; an export specifier naming it + // (`export { name }`) is another. + let scope: ts.Node = list; + while (scope.parent && !ts.isBlock(scope) && !ts.isSourceFile(scope)) scope = scope.parent; + let uses = 0; + const visit = (n: ts.Node): void => { + if (uses > 1) return; + if (ts.isExportSpecifier(n) && (n.propertyName ?? n.name).text === e.text) { + uses = 2; + return; + } + if (ts.isIdentifier(n) && n !== decl.name && n.text === e.text) { + if (lowerer.resolveValueSymbol(n) === symbol) uses++; + } + ts.forEachChild(n, visit); + }; + visit(scope); + return uses === 1 && runsOnceInScope(node, scope); +} + +/** Whether `node` runs at most once each time `scope` runs: no loop body, + * loop test or update, function, or class field initializer lies between + * them. A spread that runs again inserts the same elements again, and + * copies would no longer be the same objects. */ +function runsOnceInScope(node: ts.Node, scope: ts.Node): boolean { + let child = node; + for (let n = node.parent; n && n !== scope; child = n, n = n.parent) { + if (ts.isFunctionLike(n) || ts.isPropertyDeclaration(n)) return false; + if (ts.isForOfStatement(n) || ts.isForInStatement(n)) { + if (child !== n.expression) return false; + } else if (ts.isForStatement(n)) { + if (child !== n.initializer) return false; + } else if (ts.isWhileStatement(n) || ts.isDoStatement(n)) { + return false; + } + } + return true; +} + +function isReference(e: ts.Expression): boolean { + return !( + ts.isStringLiteral(e) || + ts.isNumericLiteral(e) || + e.kind === ts.SyntaxKind.TrueKeyword || + e.kind === ts.SyntaxKind.FalseKeyword || + e.kind === ts.SyntaxKind.NullKeyword + ); +} + +/** A fresh array spread converted into the destination's array layout by + * the structural width lift, or null when the layouts do not lift. */ +export function liftedFreshSpread( + lowerer: Lowerer, + node: ts.Expression, + source: IrExpr, + arrType: IrType, + loc: SrcLoc, +): IrExpr | null { + if (source.type.kind !== "array" || arrType.kind !== "array") return null; + if (!freshSpreadSource(lowerer, node)) return null; + const lift = lowerer.widthLiftPlan(source.type, arrType); + return lift ? lowerer.applyWidthLift(lift, source, arrType, loc) : null; +} diff --git a/packages/compiler/src/frontend/lowering/containers/array-construction.ts b/packages/compiler/src/frontend/lowering/containers/array-construction.ts index 53d46d00d..83a184400 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-construction.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-construction.ts @@ -1,5 +1,6 @@ import { generatorDrain } from "../iterator-adapters.js"; import { identityPreservingWidening } from "../coercions/identity.js"; +import { liftedFreshSpread } from "../coercions/fresh-spread.js"; import { InternalCompilerError } from "../../../errors.js"; import { dynUndefinedExpr, @@ -142,6 +143,16 @@ export function lowerArraySpreadItems( if (source.type.kind === "record" && lowerer.shapes.get(source.type.shapeId)?.tuple) { source = lowerer.widthCoerce(source, arrType) ?? source; } + if ( + !typeEquals(source.type, arrType) && + !( + source.type.kind === "array" && + identityPreservingWidening(lowerer, source.type.elem, elem) + ) + ) { + // Fresh elements (an array literal) convert into the element layout. + source = liftedFreshSpread(lowerer, node.expression, source, arrType, loc) ?? source; + } if (!typeEquals(source.type, arrType)) { if ( source.type.kind !== "array" || diff --git a/packages/compiler/src/frontend/lowering/containers/array-methods.ts b/packages/compiler/src/frontend/lowering/containers/array-methods.ts index 12078720e..e9a130a2d 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-methods.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-methods.ts @@ -1,3 +1,4 @@ +import { liftedFreshSpread } from "../coercions/fresh-spread.js"; import { lowerArrayIteratorValue } from "../iterator-adapters.js"; import { identityPreservingWidening } from "../coercions/identity.js"; import { arrayConcatHelper } from "./array-concat.js"; @@ -793,6 +794,10 @@ export function lowerArrayMethodCall( loc, }; } + if (!typeEquals(src.type, receiverIr)) { + // Fresh elements (an array literal) convert into the element layout. + src = liftedFreshSpread(lowerer, spreadArg.expression, src, receiverIr, loc) ?? src; + } if (!typeEquals(src.type, receiverIr)) { lowerer.unsupported( "SC1090", diff --git a/packages/compiler/src/frontend/lowering/containers/bytes.ts b/packages/compiler/src/frontend/lowering/containers/bytes.ts index f8e49a516..49292c260 100644 --- a/packages/compiler/src/frontend/lowering/containers/bytes.ts +++ b/packages/compiler/src/frontend/lowering/containers/bytes.ts @@ -24,6 +24,7 @@ import { typeEquals, } from "../../../ir/ir.js"; import { isJsSourceFile, locOf } from "../../program.js"; +import { stdlibGlobalNameOf } from "../surfaces.js"; import type { Lowerer } from "../lowerer.js"; import { newFnCtx, own } from "../lowerer.js"; import { buildBytesSortFn, closureKeyOrder } from "../lower-array-sort.js"; @@ -1306,6 +1307,185 @@ function knownBufferProducer( return type?.kind === "cryptoHash" || type?.kind === "cryptoHmac"; } +/** Buffer.from over a checked-dynamic source and encoding, both already in + * repeatable locals: Node's fromString takes utf8 for an absent, non-string + * or empty encoding and throws ERR_UNKNOWN_ENCODING for an unknown name; + * other sources ignore the encoding. */ +function dynBufferFromParts(source: IrExpr, encoding: IrExpr, loc: SrcLoc): IrExpr { + const utf8: IrExpr = { kind: "strLit", value: "utf8", type: STRING, loc }; + const isString = (x: IrExpr): IrExpr => ({ + kind: "dynTest", + test: "string", + value: x, + type: BOOL, + loc, + }); + const name: IrExpr = { + kind: "ternary", + cond: { + kind: "logical", + op: "&&", + left: isString(encoding), + right: { + kind: "strEq", + negated: true, + left: { kind: "dynCheck", value: encoding, type: STRING, loc }, + right: { kind: "strLit", value: "", type: STRING, loc }, + type: BOOL, + loc, + }, + type: BOOL, + loc, + }, + then: { kind: "dynCheck", value: encoding, type: STRING, loc }, + else_: utf8, + type: STRING, + loc, + }; + return { + kind: "ternary", + cond: isString(source), + then: { + kind: "libCall", + fn: "buffer.poolFromStrChecked", + args: [{ kind: "dynCheck", value: source, type: STRING, loc }, name], + type: BYTES_U8, + loc, + }, + else_: { + kind: "libCall", + fn: "buffer.poolFromDynRef", + args: [source, utf8], + type: BYTES_U8, + loc, + }, + type: BYTES_U8, + loc, + }; +} + +/** Lowers `source` and `encNode` (or undefined) into hidden dyn locals, in + * source order, and builds the result from their references. */ +function withDynBufferArguments( + lowerer: Lowerer, + source: IrExpr, + encNode: ts.Expression | undefined, + loc: SrcLoc, + build: (source: IrExpr, encoding: IrExpr) => IrExpr, +): IrExpr { + const v = lowerer.declareHiddenLocal("%bufferValue", DYN); + const e = lowerer.declareHiddenLocal("%bufferEncoding", DYN); + const result = build(varRef(v.id, DYN, loc), varRef(e.id, DYN, loc)); + return { + kind: "seqExpr", + stmts: [ + { kind: "varDecl", localId: v.id, init: lowerer.coerceToExpected(source, DYN), loc }, + { + kind: "varDecl", + localId: e.id, + init: + encNode === undefined + ? dynUndefinedExpr(loc) + : lowerer.coerceToExpected(lowerer.lowerExpr(encNode), DYN), + loc, + }, + ], + result, + type: result.type, + loc, + }; +} + +/** Buffer.from(value, encoding) over a checked-dynamic value whose encoding + * is not a literal: yazl forwards its `encoding` parameter, often undefined. + * Null when the encoding is absent or a literal. */ +function dynBufferFromRuntimeEncoding( + lowerer: Lowerer, + value: IrExpr, + encNode: ts.Expression | undefined, + loc: SrcLoc, +): IrExpr | null { + if (encNode === undefined || lowerer.typeOf(encNode).isStringLiteralType()) return null; + return withDynBufferArguments(lowerer, value, encNode, loc, (source, encoding) => + dynBufferFromParts(source, encoding, loc), + ); +} + +/** The deprecated `new Buffer(arg[, encoding])` in a dependency's + * JavaScript, through the global or an alias of it (buffer-crc32 and yazl + * keep it as the fallback for runtimes without Buffer.alloc/Buffer.from). + * Node never emits DEP0005 for calls from node_modules, so the construction + * is exactly Node's: a number allocates zero-filled bytes (and throws + * ERR_INVALID_ARG_TYPE beside a string second argument), anything else + * converts as Buffer.from does. Null elsewhere: other callers owe the + * deprecation warning. */ +export function lowerNodeModulesBufferConstruct( + lowerer: Lowerer, + expr: ts.NewExpression, +): IrExpr | null { + const sf = expr.getSourceFile(); + if (!isJsSourceFile(sf) || !/[\\/]node_modules[\\/]/.test(sf.fileName)) return null; + const args = expr.arguments?.nodes ?? []; + if (args.length < 1 || args.length > 2 || args.some(ts.isSpreadElement)) return null; + if (stdlibGlobalNameOf(lowerer, expr.expression) !== "Buffer") return null; + const loc = locOf(expr); + const value = withDynBufferArguments( + lowerer, + lowerer.lowerExpr(args[0]!), + args[1], + loc, + (source, encoding) => ({ + kind: "ternary", + cond: { kind: "dynTest", test: "number", value: source, type: BOOL, loc }, + then: { + kind: "ternary", + cond: { kind: "dynTest", test: "string", value: encoding, type: BOOL, loc }, + then: { kind: "libCall", fn: "buffer.newStringFail", args: [source], type: BYTES_U8, loc }, + else_: { + kind: "libCall", + fn: "buffer.alloc", + args: [{ kind: "dynCheck", value: source, type: F64, loc }], + type: BYTES_U8, + loc, + }, + type: BYTES_U8, + loc, + }, + else_: dynBufferFromParts(source, encoding, loc), + type: BYTES_U8, + loc, + }), + ); + return { kind: "libCall", fn: "buffer.brand", args: [value], type: BYTES_U8, loc }; +} + +/** Buffer.from(array or bytes, extra): Node copies an array-like source and + * ignores the encoding/offset argument (yazl passes its `encoding` through + * unchanged), but JavaScript still evaluates it after the source. */ +function withIgnoredBufferArgument( + lowerer: Lowerer, + extraNode: ts.Expression | undefined, + source: IrExpr, + build: (source: IrExpr) => IrExpr, +): IrExpr { + if (extraNode === undefined) return build(source); + const extra = lowerer.lowerExpr(extraNode); + if (isSafeToDiscard(extra)) return build(source); + const loc = source.loc; + const tmp = lowerer.declareHiddenLocal("%bufferSource", source.type); + const result = build(varRef(tmp.id, source.type, loc)); + return { + kind: "seqExpr", + stmts: [ + { kind: "varDecl", localId: tmp.id, init: source, loc }, + { kind: "exprStmt", expr: extra, loc }, + ], + result, + type: result.type, + loc, + }; +} + export function lowerBufferStaticCall( lowerer: Lowerer, call: ts.CallExpression, @@ -1378,17 +1558,23 @@ function lowerBufferStaticValue( ) { // A number[] literal (contextually typed by the lib's readonly // number[] slot — build it element-wise, the Set-seed pattern). - if (args.length === 1) { - const elems = argNode.elements.nodes.map((el) => lowerer.lowerExprExpecting(el, F64)); - const seed: IrExpr = { kind: "arrayLit", elems, type: arrayOf(F64), loc }; - return { kind: "libCall", fn: "buffer.poolFromArray", args: [seed], type: BYTES_U8, loc }; - } + const elems = argNode.elements.nodes.map((el) => lowerer.lowerExprExpecting(el, F64)); + const seed: IrExpr = { kind: "arrayLit", elems, type: arrayOf(F64), loc }; + return withIgnoredBufferArgument(lowerer, args[1], seed, (array) => ({ + kind: "libCall", + fn: "buffer.poolFromArray", + args: [array], + type: BYTES_U8, + loc, + })); } else { const srcIr = lowerer.mapTypeOf(lowerer.typeOf(argNode)); const value = lowerer.lowerExpr(argNode); // JS callback storage can be native DYN even when the checker // reports any (or a narrowed Buffer). Dispatch on its actual ABI. if (value.type.kind === "dyn") { + const runtimeEncoding = dynBufferFromRuntimeEncoding(lowerer, value, args[1], loc); + if (runtimeEncoding) return runtimeEncoding; const encName = args[1] ? bufEncoding(lowerer, "Buffer.from", args[1]) : "utf8"; const enc: IrExpr = { kind: "strLit", value: encName, type: STRING, loc }; return { @@ -1432,26 +1618,25 @@ function lowerBufferStaticValue( // { 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")) + (srcIr?.kind === "bytes" && srcIr.elem === "u8") || + (srcIr === null && value.type.kind === "bytes" && value.type.elem === "u8") ) { - return { + return withIgnoredBufferArgument(lowerer, args[1], value, (bytes) => ({ kind: "libCall", fn: "buffer.poolFromBytes", - args: [value], + args: [bytes], type: BYTES_U8, loc, - }; + })); } - if (args.length === 1 && srcIr?.kind === "array" && srcIr.elem.kind === "f64") { - return { + if (srcIr?.kind === "array" && srcIr.elem.kind === "f64") { + return withIgnoredBufferArgument(lowerer, args[1], value, (array) => ({ kind: "libCall", fn: "buffer.poolFromArray", - args: [value], + args: [array], type: BYTES_U8, loc, - }; + })); } // A union of strings, u8 views and undefined/null (a JS-inferred // npm result such as pako's deflate): the checked form dispatches diff --git a/packages/compiler/src/frontend/lowering/containers/collection-methods.ts b/packages/compiler/src/frontend/lowering/containers/collection-methods.ts index 6aa6d6faf..370974c85 100644 --- a/packages/compiler/src/frontend/lowering/containers/collection-methods.ts +++ b/packages/compiler/src/frontend/lowering/containers/collection-methods.ts @@ -526,6 +526,55 @@ export function lowerMapForEachCall( return { kind: "call", callee: helper, args: [receiver, fnArg], type: VOID, loc }; } +/** True when an arrow's expression body is lowered as a statement: the + * function being lowered returns void (`xs.forEach((x) => s.add(x))`) and + * the arrow's contextual signature returns void. An arrow that returns the + * Set (`(n) => s.add(n)`, a `map` callback) or a body inlined into a + * caller that reads its value is not. */ +function setAddArrowResultVoid(lowerer: Lowerer, arrow: ts.ArrowFunction): boolean { + if (lowerer.ctx.returnType.kind !== "void") return false; + const contextual = lowerer.checker.getContextualType(arrow); + if (contextual === undefined) return false; + const signatures = lowerer.checker.getCallSignatures( + lowerer.checker.getNonNullableType(contextual), + ); + return ( + signatures.length === 1 && + (lowerer.checker.getReturnTypeOfSignature(signatures[0]!).flags & ts.TypeFlags.Void) !== 0 + ); +} + +/** True when nothing reads an add() call's result: an expression + * statement, the expression body of an arrow whose result is void, a `void` + * operand, a for header's initializer or incrementor, a comma's left + * operand, or a conditional or comma result that is itself discarded. */ +function setAddResultDiscarded(lowerer: Lowerer, call: ts.CallExpression): boolean { + let node: ts.Node = call; + for (;;) { + let parent = node.parent; + while (parent !== undefined && ts.isParenthesizedExpression(parent)) { + node = parent; + parent = parent.parent; + } + if (parent === undefined) return false; + if (ts.isExpressionStatement(parent) || ts.isVoidExpression(parent)) return true; + if (ts.isArrowFunction(parent)) + return parent.body === node && setAddArrowResultVoid(lowerer, parent); + if (ts.isForStatement(parent)) + return parent.initializer === node || parent.incrementor === node; + if (ts.isBinaryExpression(parent) && parent.operatorToken.kind === ts.SyntaxKind.CommaToken) { + if (parent.left === node) return true; + node = parent; + continue; + } + if (ts.isConditionalExpression(parent) && parent.condition !== node) { + node = parent; + continue; + } + return false; + } +} + /** Ambient Set method calls — Map's lowering with the value slot gone. * `add`/`has`/`delete`/`clear` lower to setIntrinsic; `forEach` desugars * like Map's over the shared iteration primitives. Null when this isn't @@ -560,8 +609,8 @@ export function lowerSetMethodCall( const value = lowerer.lowerExpr(access.expression); const receiver = lowerer.runtimeOptionalPropertyReceiver(access.expression, value, receiverIr, name) ?? value; - // The lib's `add` returns the Set (chaining typechecks); the lowered - // add is a void statement — fence chained receivers like Map's set. + // A used add() result lowers to its receiver (below), so a chained + // receiver is a Set; anything else lowering to a non-Set stays fenced. if (receiver.type.kind !== "set") { lowerer.noLowering( "chained Set method calls", @@ -584,7 +633,28 @@ export function lowerSetMethodCall( } if (name === "add") { const v = lowerer.lowerCollectionKey(call.arguments.nodes[0]!, receiverIr.elem); - return { kind: "setIntrinsic", method: "add", receiver, args: [v], type: VOID, loc }; + if (setAddResultDiscarded(lowerer, call)) { + return { kind: "setIntrinsic", method: "add", receiver, args: [v], type: VOID, loc }; + } + // add() returns its Set: a used result (`s.add(a).add(b)`, + // `s.add(x) === s`) reads the receiver once into a slot, adds, and + // answers the slot, as Map's set() does. + const slot = lowerer.declareHiddenLocal("%setAddReceiver", receiver.type); + const ref = varRef(slot.id, receiver.type, loc); + return { + kind: "seqExpr", + stmts: [ + { kind: "varDecl", localId: slot.id, init: receiver, loc }, + { + kind: "exprStmt", + expr: { kind: "setIntrinsic", method: "add", receiver: ref, args: [v], type: VOID, loc }, + loc, + }, + ], + result: ref, + type: receiver.type, + loc, + }; } if (name === "has" || name === "delete") { const v = lowerer.lowerCollectionKey(call.arguments.nodes[0]!, receiverIr.elem); diff --git a/packages/compiler/src/frontend/lowering/cycle-initialization.ts b/packages/compiler/src/frontend/lowering/cycle-initialization.ts index fb62fb998..092588a6e 100644 --- a/packages/compiler/src/frontend/lowering/cycle-initialization.ts +++ b/packages/compiler/src/frontend/lowering/cycle-initialization.ts @@ -37,6 +37,8 @@ export interface CycleInitFlag { /** The declaring module. */ module: ts.SourceFile; settled: ReadonlySet; + /** Local names other files import the binding under, by file name. */ + localNames?: ReadonlyMap; /** True for a binding of an import cycle. A binding flagged only for its * own module's early calls is checked opportunistically: whatever cannot * be checked keeps the unchecked behavior instead of refusing. */ @@ -45,23 +47,40 @@ export interface CycleInitFlag { * 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). */ + /** A class binding: its references are the class's own IR forms and the + * static members it reaches, its own and its bases' (`global` is the flag + * itself). */ className?: string; + /** The class and its base classes, whose `%C.static:` functions a static + * member access through the class can name. */ + staticOwners?: readonly string[]; staticGlobals?: ReadonlySet; - /** The source references to the class outside its own body. Only IR - * built for one of them is checked: compiler-generated dispatch (error - * property helpers, prototype data, reflection) also names the class - * and must keep working before the class statement ran. */ - references?: readonly SrcLoc[]; + /** The source references to the class outside its own body, with the + * local name each one spells (an import can rename the class; Node's + * ReferenceError names that binding). Only IR built for one of them is + * checked: compiler-generated dispatch (error property helpers, + * prototype data, reflection) also names the class and must keep + * working before the class statement ran. */ + references?: readonly ClassReference[]; } +/** A source reference to a flagged class and the local name it spells. */ +interface ClassReference { + loc: SrcLoc; + name: string; +} + +/** The static field storage of a flag without a class. */ +const NO_STATIC_GLOBALS: ReadonlySet = new Set(); + /** Registers the initialization flags after global collection. Bindings * whose early access has no exact representation are refused: a `var` * (Node reads `undefined` there, but module storage only starts when the - * declaring module's body starts), a class (its references are static), a - * binding without module storage, or one with a use the lowering resolves - * from the binding's type (a property key, a `typeof` operand). */ + * declaring module's body starts), a binding without module storage, a + * class that another module's `extends` clause can reach early (the + * subclass is static), or one with a use the lowering resolves from the + * binding's type (a property key, a `typeof` operand). Other classes get a + * flag like their module's early-reached classes. */ export function markCycleEarlyBindings(lowerer: Lowerer): void { const early = cycleEarlyBindings( lowerer.program, @@ -80,7 +99,14 @@ export function markCycleEarlyBindings(lowerer: Lowerer): void { continue; } if (binding.kind === "class") { - refuse(binding.reference, `the class ${where}`); + // The class's references from code that can run before its + // statement check its initialization flag: Node's ReferenceError + // only when the class is actually reached early. + const info = lowerer.classBySymbol.get(binding.symbol); + const heritage = earlyHeritageReference(lowerer, binding); + if (info !== undefined && !info.generic && heritage === null) + registerClassInitFlag(lowerer, binding, info, true); + else refuse(heritage ?? binding.reference, `the class ${where}`); continue; } const global = lowerer.globalsBySymbol.get(binding.symbol); @@ -103,7 +129,7 @@ export function markCycleEarlyBindings(lowerer: Lowerer): void { for (const binding of moduleEarlyBindings(lowerer.program, lowerer.moduleOrder)) { if (binding.kind === "class") { const info = lowerer.classBySymbol.get(binding.symbol); - if (info !== undefined) registerClassInitFlag(lowerer, binding, info); + if (info !== undefined) registerClassInitFlag(lowerer, binding, info, false); continue; } const global = lowerer.globalsBySymbol.get(binding.symbol); @@ -146,51 +172,149 @@ function registerClassInitFlag( lowerer: Lowerer, binding: CycleEarlyBinding, info: ClassInfo, + cycle: boolean, ): void { const className = info.def.name; if (lowerer.classInitFlags.has(className)) return; const flagId = `%g.class.${className}%initialized`; const global: IrGlobal = { id: flagId, name: "%initialized", type: BOOL, mutable: true }; lowerer.globalsList.push(global); + const staticOwners: string[] = []; + const staticGlobals = new Set(); + for (let c: ClassInfo | null = info; c; c = c.base) { + staticOwners.push(c.def.name); + for (const field of c.staticFields) staticGlobals.add(field.globalId); + } lowerer.classInitFlags.set(className, { global, flagId, name: binding.name.text, module: binding.module, settled: binding.settled, - cycle: false, + cycle, className, - staticGlobals: new Set(info.staticFields.map((f) => f.globalId)), - references: classReferences(lowerer, binding.symbol, binding.name), + staticOwners, + staticGlobals, + references: classReferences(lowerer, binding.symbol, binding.name, cycle), }); } -/** The source spans of the identifiers naming the class outside its own - * declaration, in every program file. */ -function classReferences(lowerer: Lowerer, symbol: ts.Symbol, name: ts.Identifier): SrcLoc[] { - const declaration = name.parent; - const out: SrcLoc[] = []; +/** A reference to a cycle class in another class's `extends` clause, in a + * module that can run before the class statement: Node evaluates the clause + * when that module's class statement runs, but the compiled subclass is + * static and has no access to check. Null when there is none. */ +function earlyHeritageReference(lowerer: Lowerer, binding: CycleEarlyBinding): ts.Node | null { + const declaration = binding.name.parent; for (const sf of lowerer.program.getImplementationSourceFiles()) { - if (sf.isDeclarationFile || !sf.text.includes(name.text)) continue; + if (sf.isDeclarationFile || sf === binding.module || binding.settled.has(sf)) continue; + const names = classLocalNames(lowerer, sf, binding.symbol, binding.name.text, true); + if (names.size === 0) continue; + let found: ts.Node | null = null; + ts.walkPreorder(sf, (node) => { + if (found !== null || node === declaration) return "skip"; + if (!ts.isHeritageClause(node)) return undefined; + // Node evaluates the whole clause when the class statement runs: + // any reference inside it (parenthesized, a factory argument) + // reaches the class. + ts.walkPreorder(node, (inner) => { + if (found !== null) return "skip"; + if ( + ts.isIdentifier(inner) && + names.has(inner.text) && + resolvesTo(lowerer, inner, binding.symbol, true) + ) + found = inner; + return undefined; + }); + return "skip"; + }); + if (found !== null) return found; + } + return null; +} + +/** Whether `node` names `symbol`, through import aliases when `aliases`. */ +function resolvesTo( + lowerer: Lowerer, + node: ts.Identifier, + symbol: ts.Symbol, + aliases: boolean, +): boolean { + let referenced = lowerer.checker.getSymbolAtLocation(node); + if (aliases && referenced !== undefined && referenced.flags & ts.SymbolFlags.Alias) + referenced = lowerer.checker.getAliasedSymbol(referenced); + return referenced === symbol; +} + +/** The local names under which `sf` can spell the class: its declared name + * (when the file's text contains it) and, with `aliases`, the local names + * of the file's imports that resolve to it (`import { Sink as Alias }`, a + * default import, a re-exported name). */ +function classLocalNames( + lowerer: Lowerer, + sf: ts.SourceFile, + symbol: ts.Symbol, + declared: string, + aliases: boolean, +): Set { + const names = new Set(); + if (sf.text.includes(declared)) names.add(declared); + if (!aliases) return names; + for (const statement of sf.statements.nodes) { + if (!ts.isImportDeclaration(statement)) continue; + const clause = statement.importClause; + if (clause === undefined) continue; + const locals: ts.Identifier[] = []; + if (clause.name !== undefined) locals.push(clause.name); + const bindings = clause.namedBindings; + if (bindings !== undefined && ts.isNamedImports(bindings)) + for (const element of bindings.elements.nodes) locals.push(element.name); + for (const local of locals) + if (!names.has(local.text) && resolvesTo(lowerer, local, symbol, true)) names.add(local.text); + } + return names; +} + +/** The source spans of the identifiers naming the class outside its own + * declaration, in every program file. For a class of an import cycle that + * includes other modules' import bindings; another module's references to a + * class flagged only for its own module's early calls always run after the + * class statement. */ +function classReferences( + lowerer: Lowerer, + symbol: ts.Symbol, + name: ts.Identifier, + cycle: boolean, +): ClassReference[] { + const declaration = name.parent; + const out: ClassReference[] = []; + for (const sf of lowerer.program.getImplementationSourceFiles()) { + if (sf.isDeclarationFile) continue; + // Another module names the class through its import bindings, under + // the class's name or a local one. + const names = classLocalNames(lowerer, sf, symbol, name.text, cycle); + if (names.size === 0) continue; ts.walkPreorder(sf, (node) => { if (node === declaration) return "skip"; - if ( - ts.isIdentifier(node) && - node.text === name.text && - lowerer.checker.getSymbolAtLocation(node) === symbol - ) - out.push(locOf(node)); + if (ts.isIdentifier(node) && names.has(node.text) && resolvesTo(lowerer, node, symbol, cycle)) + out.push({ loc: locOf(node), name: node.text }); return undefined; }); } return out; } +/** The source reference to the flagged class that `loc`'s IR was built + * for, or undefined. */ +function coveredReference(flag: CycleInitFlag, loc: SrcLoc): ClassReference | undefined { + return (flag.references ?? []).find( + (r) => r.loc.file === loc.file && loc.start <= r.loc.start && r.loc.end <= loc.end, + ); +} + /** Whether `loc` is the IR of a source reference to the flagged class. */ function coversReference(flag: CycleInitFlag, loc: SrcLoc): boolean { - return (flag.references ?? []).some( - (r) => r.file === loc.file && loc.start <= r.start && r.end <= loc.end, - ); + return coveredReference(flag, loc) !== undefined; } /** The class statement's static initialization, followed by the store of @@ -218,6 +342,7 @@ function registerInitFlag( name: binding.name.text, module: binding.module, settled: binding.settled, + ...(binding.localNames ? { localNames: binding.localNames } : {}), cycle, }; lowerer.cycleInitFlags.set(global.id, flag); @@ -361,16 +486,23 @@ function rewriteFunction( for (const [id, flag] of lowerer.cycleInitFlags) { if (runsEarly(flag)) active.set(id, flag); } - // Classes, by name; their static field storage joins `active`. A class's - // own members run only once the class exists, or inside its own - // definition, where the inner binding is initialized. + // Classes, by name, and the static field storage each reaches (a base + // class's statics through a subclass too). A class's own members run only + // once the class exists, or inside its own definition, where the inner + // binding is initialized. const classes = new Map(); + const statics = new Map(); for (const [className, flag] of lowerer.classInitFlags) { if (!runsEarly(flag) || fn.name.startsWith(`%${className}.`)) continue; classes.set(className, flag); - if (flag.staticGlobals) for (const id of flag.staticGlobals) active.set(id, flag); + for (const id of flag.staticGlobals ?? NO_STATIC_GLOBALS) + statics.set(id, [...(statics.get(id) ?? []), flag]); } if (active.size === 0 && classes.size === 0) return; + const flagOf = (localId: string, loc: SrcLoc): CycleInitFlag | undefined => + active.get(localId) ?? statics.get(localId)?.find((flag) => coversReference(flag, loc)); + const namesStatic = (flag: CycleInitFlag, fnName: string): boolean => + (flag.staticOwners ?? []).some((owner) => fnName.startsWith(`%${owner}.static:`)); const classFlagOf = (e: IrExpr): CycleInitFlag | undefined => { if (classes.size === 0) return undefined; switch (e.kind) { @@ -384,25 +516,34 @@ function rewriteFunction( case "call": for (const [className, flag] of classes) { if ( - (e.callee.startsWith(`%${className}.static:`) || - e.callee === `%dyn.class.instanceof:${className}`) && + (namesStatic(flag, e.callee) || e.callee === `%dyn.class.instanceof:${className}`) && coversReference(flag, e.loc) ) return flag; } return undefined; + case "closure": + // A static method read as a value (`const f = K.m`) reads K. + for (const flag of classes.values()) + if (namesStatic(flag, e.fnName) && coversReference(flag, e.loc)) return flag; + return undefined; default: return undefined; } }; let temps = 0; const check = (flag: CycleInitFlag, loc: SrcLoc): IrStmt => { + // Node's ReferenceError names the binding the reference spells. + const name = + flag.className !== undefined + ? (coveredReference(flag, loc)?.name ?? flag.name) + : (flag.localNames?.get(loc.file) ?? flag.name); const expr: IrExpr = { kind: "intrinsic", name: "module.tdzCheck", args: [ { kind: "varRef", localId: flag.flagId, type: BOOL, loc }, - { kind: "strLit", value: flag.name, type: STRING, loc }, + { kind: "strLit", value: name, type: STRING, loc }, ], type: VOID, loc, @@ -457,9 +598,8 @@ function rewriteFunction( }; } if (e.kind !== "varRef" && e.kind !== "incDec" && e.kind !== "assignExpr") return e; - const flag = active.get(e.localId); + const flag = flagOf(e.localId, e.loc); if (flag === undefined) return e; - if (flag.className !== undefined && !coversReference(flag, e.loc)) return e; // `K.x = v` reads K before it evaluates v. if (e.kind === "assignExpr" && flag.className === undefined) { const t = temp(e.value.type); @@ -477,9 +617,8 @@ function rewriteFunction( }, stmt: (s: IrStmt): IrStmt => { if (s.kind !== "assign" || s.initializes) return s; - const flag = active.get(s.localId); + const flag = flagOf(s.localId, s.loc); if (flag === undefined) return s; - if (flag.className !== undefined && !coversReference(flag, s.loc)) return s; // `K.x = v` reads K before it evaluates v. if (flag.className !== undefined) return { kind: "block", body: [check(flag, s.loc), s], loc: s.loc }; @@ -525,7 +664,11 @@ function rewriteFunction( for (const [className, flag] of [...classes]) { if (flag.module !== initOf || !setsFlag(s, flag)) continue; classes.delete(className); - if (flag.staticGlobals) for (const id of flag.staticGlobals) active.delete(id); + for (const id of flag.staticGlobals ?? NO_STATIC_GLOBALS) { + const rest = statics.get(id)!.filter((other) => other !== flag); + if (rest.length > 0) statics.set(id, rest); + else statics.delete(id); + } } const out = rewriteStmt(s); for (const [id, flag] of [...active]) { diff --git a/packages/compiler/src/frontend/lowering/expressions/array-literals.ts b/packages/compiler/src/frontend/lowering/expressions/array-literals.ts index a80367dd0..56b6e280e 100644 --- a/packages/compiler/src/frontend/lowering/expressions/array-literals.ts +++ b/packages/compiler/src/frontend/lowering/expressions/array-literals.ts @@ -462,7 +462,14 @@ export function lowerArrayLiteral( // A same-family array whose ELEMENT lifts (string[] into a // (string | symbol)[] literal — per-element wrap/width copy): // the interned width helper reshapes before the spread copies. - if (src.type.kind === "array" && !typeEquals(src.type, type)) { + // A fixed TUPLE (`[...FILES]` with FILES typed `[string, string, + // string]`) spreads its positions in order: the width helper copies + // the tuple's fields into an array of the literal's element type. + if ( + (src.type.kind === "array" || + (src.type.kind === "record" && lowerer.shapes.get(src.type.shapeId)?.tuple === true)) && + !typeEquals(src.type, type) + ) { const w = lowerer.widthCoerce(src, type); if (w) src = w; } diff --git a/packages/compiler/src/frontend/lowering/expressions/binary.ts b/packages/compiler/src/frontend/lowering/expressions/binary.ts index 07d983cc0..ccf9dc8c0 100644 --- a/packages/compiler/src/frontend/lowering/expressions/binary.ts +++ b/packages/compiler/src/frontend/lowering/expressions/binary.ts @@ -1981,8 +1981,60 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr case ts.SyntaxKind.LessThanLessThanToken: case ts.SyntaxKind.GreaterThanGreaterThanToken: case ts.SyntaxKind.GreaterThanGreaterThanGreaterThanToken: { - if (!bothNum) + // Booleans take part as ToNumber converts them, true to 1 and false + // to 0: JavaScript code writes `isFn(a) ^ isFn(b)` and `flags | ok`. + // A hoisted JavaScript `var` can also hold undefined, and ToInt32 + // maps ToNumber(undefined) (NaN) and ToNumber(null) to 0. + const optionalBoolTag = (e: IrExpr): number => { + if (e.type.kind !== "union") return -1; + const arms = lowerer.unions.get(e.type.unionId)?.arms ?? []; + const ok = arms.every( + (a) => a.kind === "bool" || a.kind === "undefinedT" || a.kind === "nullT", + ); + return ok ? arms.findIndex((a) => a.kind === "bool") : -1; + }; + const numeric = (e: IrExpr): boolean => + e.type.kind === "f64" || e.type.kind === "bool" || optionalBoolTag(e) >= 0; + const boolBits = !bothNum && numeric(arithmeticLeft) && numeric(arithmeticRight); + if (!bothNum && !boolBits) operatorUnsupported(lowerer, expr, ts.tokenToString(op) ?? "?", [left.type, right.type]); + const bitPrefix: IrStmt[] = []; + const oneOrZero = (cond: IrExpr): IrExpr => ({ + kind: "ternary", + cond, + then: numLit(1, loc), + else_: numLit(0, loc), + type: F64, + loc, + }); + // The tag test reads an operand twice: when either side needs it, + // both sides evaluate once into temporaries, in source order. A left + // operand that is safe to repeat still takes a temporary when the + // right one does: the right operand runs first in the prefix, and it + // can assign what the left operand reads. + const stash = optionalBoolTag(arithmeticLeft) >= 0 || optionalBoolTag(arithmeticRight) >= 0; + const rightTemp = stash && !isSafeToRepeat(arithmeticRight); + const stable = (e: IrExpr, beforeTemp: boolean): IrExpr => { + if (!stash || (isSafeToRepeat(e) && !beforeTemp)) return e; + const tmp = lowerer.declareHiddenLocal("%bits", e.type); + bitPrefix.push({ kind: "varDecl", localId: tmp.id, init: e, loc }); + return varRef(tmp.id, e.type, loc); + }; + const bitOperand = (operand: IrExpr, beforeTemp: boolean): IrExpr => { + const e = stable(operand, beforeTemp); + if (e.type.kind === "bool") return oneOrZero(e); + const tag = optionalBoolTag(e); + if (tag < 0 || e.type.kind !== "union") return e; + const unionId = e.type.unionId; + return { + kind: "ternary", + cond: { kind: "unionIsTag", unionId, tag, negated: false, value: e, type: BOOL, loc }, + then: oneOrZero({ kind: "unionNarrow", unionId, tag, value: e, type: BOOL, loc }), + else_: numLit(0, loc), + type: F64, + loc, + }; + }; const bitOp = op === ts.SyntaxKind.AmpersandToken ? "&" @@ -1995,14 +2047,17 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr : op === ts.SyntaxKind.GreaterThanGreaterThanToken ? ">>" : ">>>"; - return { + const bits: IrExpr = { kind: "bin", op: bitOp, - left: arithmeticLeft, - right: arithmeticRight, + left: bitOperand(arithmeticLeft, rightTemp), + right: bitOperand(arithmeticRight, false), type: F64, loc, }; + return bitPrefix.length === 0 + ? bits + : { kind: "seqExpr", stmts: bitPrefix, result: bits, type: F64, loc }; } case ts.SyntaxKind.EqualsEqualsEqualsToken: case ts.SyntaxKind.ExclamationEqualsEqualsToken: diff --git a/packages/compiler/src/frontend/lowering/expressions/element-access.ts b/packages/compiler/src/frontend/lowering/expressions/element-access.ts index 7ef4b9f25..bede93072 100644 --- a/packages/compiler/src/frontend/lowering/expressions/element-access.ts +++ b/packages/compiler/src/frontend/lowering/expressions/element-access.ts @@ -235,7 +235,7 @@ export function lowerElementAccess(lowerer: Lowerer, expr: ts.ElementAccessExpre const receiver = tryLowerExpression(lowerer, expr.expression); if (receiver?.type.kind === "dyn") { const key = lowerer.lowerExprExpecting(expr.argumentExpression, DYN); - const optional = hasOptionalChainGuard(expr.expression); + const optional = hasOptionalChainGuard(expr.expression, lowerer); return lowerer.maybeNarrow( { kind: "dynKeyGet", @@ -378,7 +378,7 @@ export function lowerElementAccess(lowerer: Lowerer, expr: ts.ElementAccessExpre lowerer.lowerExpr(expr.argumentExpression), expr.argumentExpression, ); - const optional = hasOptionalChainGuard(expr.expression); + const optional = hasOptionalChainGuard(expr.expression, lowerer); return lowerer.maybeNarrow( { kind: "dynKeyGet", @@ -488,7 +488,7 @@ export function lowerElementAccess(lowerer: Lowerer, expr: ts.ElementAccessExpre ? lowerer.coerceToExpected(rawKey, DYN) : lowerRecordPropertyKey(lowerer, rawKey, expr.argumentExpression); if (key.type.kind === "string" || key.type.kind === "dyn") { - const opt = hasOptionalChainGuard(expr.expression); + const opt = hasOptionalChainGuard(expr.expression, lowerer); return lowerer.maybeNarrow( { kind: "dynKeyGet", diff --git a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts index b5f99d682..a7596cc6d 100644 --- a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts +++ b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts @@ -211,6 +211,11 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc: if (arithmetic && !runtimeOptionalUseOf(expr)) { return { kind: "varRef", localId: local.id, type: local.type, loc }; } + // A failed checked value in a fallback tail reads its dynamic value + // plainly: `undefined` is a value it can hold (checked-fallback.ts). + if ((active || captured) && lowerer.fallbackTaint !== null && local.type.kind === "dyn") { + return { kind: "varRef", localId: local.id, type: local.type, loc }; + } if (active || captured) { const use = runtimeOptionalUseOf(expr); if (use?.complex) { diff --git a/packages/compiler/src/frontend/lowering/expressions/keyed-field-targets.ts b/packages/compiler/src/frontend/lowering/expressions/keyed-field-targets.ts index e53d27aa9..932e2e4b6 100644 --- a/packages/compiler/src/frontend/lowering/expressions/keyed-field-targets.ts +++ b/packages/compiler/src/frontend/lowering/expressions/keyed-field-targets.ts @@ -1,7 +1,8 @@ -/* Read-modify-write targets for computed record keys: `r[k] op= v`, `r[k]++` and `r[k] ??= v`. */ +/* Computed record keys: read-modify-write targets (`r[k] op= v`, `r[k]++`, `r[k] ??= v`) and runtime-keyed destructuring reads. */ import * as ts from "../../ts7/adapter.js"; import { BOOL, + DYN, F64, STRING, typeEquals, @@ -153,19 +154,30 @@ function keyIs(key: IrExpr, field: string, loc: SrcLoc): IrExpr { } /** A key outside its literal type (smuggled through a cast) names no - * declared field: a fixed-shape record cannot add the property, so the - * update throws the TypeError of a keyed write miss. */ -function keyMiss(key: IrExpr, target: KeyedFieldTarget, loc: SrcLoc): IrExpr { + * declared field: a fixed-shape record cannot add the property, so an + * update throws the TypeError of a keyed write miss, and a plain read the + * keyed read miss's (no undefined is representable in the field type). */ +function keyMiss(key: IrExpr, target: KeyedFieldTarget, loc: SrcLoc, read = false): IrExpr { const message: IrExpr = { kind: "strConcat", left: { kind: "strConcat", - left: { kind: "strLit", value: "Cannot add property '", type: STRING, loc }, + left: { + kind: "strLit", + value: read ? "record has no key '" : "Cannot add property '", + type: STRING, + loc, + }, right: key, type: STRING, loc, }, - right: { kind: "strLit", value: "' to a fixed-shape object", type: STRING, loc }, + right: { + kind: "strLit", + value: read ? "' (typed slot — no undefined is representable)" : "' to a fixed-shape object", + type: STRING, + loc, + }, type: STRING, loc, }; @@ -182,8 +194,15 @@ function keyMiss(key: IrExpr, target: KeyedFieldTarget, loc: SrcLoc): IrExpr { }; } -/** The current value of a keyed target. */ -export function keyedFieldRead(lowerer: Lowerer, target: KeyedFieldTarget, loc: SrcLoc): IrExpr { +/** The current value of a keyed target. `read` marks a read that no + * write follows (a destructuring element), which words a key miss as a + * read. */ +export function keyedFieldRead( + lowerer: Lowerer, + target: KeyedFieldTarget, + loc: SrcLoc, + read = false, +): IrExpr { const { obj, shapeId, key, fieldType } = target; if (!target.keys) { const type = target.missNaN @@ -220,7 +239,7 @@ export function keyedFieldRead(lowerer: Lowerer, target: KeyedFieldTarget, loc: type: fieldType, loc, }), - keyMiss(key, target, loc), + keyMiss(key, target, loc, read), ); } @@ -269,3 +288,76 @@ export function saveKeyedFieldKey( ): void { if (target.container === "recordKeyed" && target.key) target.key = save(target.key, "%keyedKey"); } + +/** The value a destructuring element with a runtime computed key binds + * (`const { [name]: v } = source`), or null when the source's + * representation has no keyed read for that key. The key evaluates here, + * once, into `out` (element order), and converts with ToPropertyKey: + * - a checked-dynamic source (or a JavaScript record, through its checked + * view) reads the live property, undefined when absent; + * - a pure index-signature record reads its entry, undefined when absent; + * - a record whose declared fields include every name the key's literal + * type allows, all stored with one type, reads the field the key names. */ +export function runtimeKeyPatternRead( + lowerer: Lowerer, + keyNode: ts.Expression, + source: IrExpr, + out: IrStmt[], + loc: SrcLoc, +): IrExpr | null { + const save = (value: IrExpr, name: string): IrExpr => { + const local = lowerer.declareHiddenLocal(name, value.type); + out.push({ kind: "varDecl", localId: local.id, init: value, loc }); + return { kind: "varRef", localId: local.id, type: value.type, loc }; + }; + const jsRecord = + source.type.kind === "record" && + isJsSourceFile(keyNode.getSourceFile()) && + lowerer.dynConvertible(source.type); + if (source.type.kind === "dyn" || jsRecord) { + const raw = lowerer.lowerExpr(keyNode); + if (!lowerer.dynConvertible(raw.type)) return null; + const key = save( + { + kind: "libCall", + fn: "dyn.propertyKey", + args: [lowerer.coerceInto(keyNode, raw, DYN)], + type: DYN, + loc, + }, + "%patternKey", + ); + return { kind: "dynKeyGet", value: lowerer.coerceToExpected(source, DYN), key, type: DYN, loc }; + } + if (source.type.kind !== "record") return null; + const shapeId = source.type.shapeId; + const shape = lowerer.shapes.get(shapeId); + if (!shape || shape.tuple) return null; + if (shape.fields.length === 0 && shape.indexValue) { + const raw = lowerer.lowerExpr(keyNode); + if (raw.type.kind !== "string") return null; + const type = + shape.indexValue.kind === "dyn" + ? shape.indexValue + : lowerer.withUndefinedArmOf(shape.indexValue); + if (!type) return null; + const key = save(raw, "%patternKey"); + return { kind: "recordKeyGet", obj: source, shapeId, key, type, loc }; + } + const keys = literalFieldKeys(lowerer, keyNode); + const fields = keys?.map((k) => shape.fields.find((f) => f.name === k)); + const first = fields?.[0]; + if (!keys || !first || fields!.some((f) => !f || !typeEquals(f.type, first.type))) return null; + const raw = lowerer.lowerExpr(keyNode); + if (raw.type.kind !== "string") return null; + const target: KeyedFieldTarget = { + container: "recordKeyed", + obj: source, + shapeId, + field: keys[0]!, + keys, + key: save(raw, "%patternKey"), + fieldType: first.type, + }; + return keyedFieldRead(lowerer, target, loc, true); +} diff --git a/packages/compiler/src/frontend/lowering/expressions/narrowing.ts b/packages/compiler/src/frontend/lowering/expressions/narrowing.ts index dec06fdd9..a70f0dfa9 100644 --- a/packages/compiler/src/frontend/lowering/expressions/narrowing.ts +++ b/packages/compiler/src/frontend/lowering/expressions/narrowing.ts @@ -10,6 +10,7 @@ import { type IrLocal, type IrType, NULL_T, + STRING, type SrcLoc, UNDEFINED_T, arrayOf, @@ -25,7 +26,10 @@ import { hasRuntimeStatics } from "../class-runtime-statics.js"; import { jsBindingHasOpenWrites } from "../lower-stmts.js"; import { narrowClassUnion, narrowStoredClassValue, structuralNarrowing } from "../class-unions.js"; import { checkedClassAssertion, checkedClassUnionAssertion } from "../class-assertions.js"; -import { isOptionalProcessStreamProperty } from "../builtins/process.js"; +import { + isOptionalProcessStreamProperty, + isOptionalProcessVersionsRead, +} from "../builtins/process.js"; import { requestGeneralStreamSurface } from "../general-streams.js"; import { literalValues } from "../../literal-values.js"; import { literalUnionArm } from "../../union-discriminants.js"; @@ -413,6 +417,9 @@ export function maybeNarrow(lowerer: Lowerer, expr: IrExpr, node: ts.Node): IrEx // instead of misreading the payload (the dyn boundary's usual stance). // Object/array narrowings stay dyn-typed and keep their fences. if (expr.type.kind === "dyn") { + // A process.versions entry declared as a string can be absent here. + if (isOptionalProcessVersionsRead(lowerer, node)) + return lowerer.coerceToExpected(expr, lowerer.withUndefinedArm(STRING)); // An inferred JS member's storage may be deliberately wider than its // declaration (prototype-selected buffers, null-initialized scratch // objects). Extract at the consuming operation, not at the read. @@ -591,6 +598,7 @@ export function maybeNarrow(lowerer: Lowerer, expr: IrExpr, node: ts.Node): IrEx if (expr.type.kind !== "union") return expr; if (ts.isPropertyAccessExpression(node) && isOptionalProcessStreamProperty(lowerer, node)) return expr; + if (isOptionalProcessVersionsRead(lowerer, node)) return expr; // Represented fields and index values can include undefined even when // checker inference or lib.d.ts declares strings (RegExp captures). diff --git a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts index 303b6ed55..e276326c9 100644 --- a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts +++ b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts @@ -3193,6 +3193,22 @@ export function lowerShorthandValue( `generic functions as values (call '${propName.text}' directly)`, ); } + // A program class name (`{ parse, TomlError }`, an imported class + // included): the class object, exactly the long form `{ C: C }`. + // Generic classes keep the identifier path's contextual pinning. + const classInfo = lowerer.classBySymbol.get(resolved); + if (classInfo && !classInfo.generic) { + const decoratedGlobal = classInfo.classDecorators?.valueGlobalId; + if (decoratedGlobal !== undefined) { + return { + kind: "varRef", + localId: decoratedGlobal, + type: { kind: "classval", className: classInfo.def.name }, + loc, + }; + } + return lowerer.classValueRef(classInfo, propName); + } } lowerer.rejectUnresolvedSymbol( symbol diff --git a/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts b/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts index 5dc512a6c..fbcecdc32 100644 --- a/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts +++ b/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts @@ -98,7 +98,11 @@ export function isRequireMainFilename(lowerer: Lowerer, expr: ts.Expression): bo export function isOptionalChainTail(lowerer: Lowerer, expr: ts.Expression): boolean { const tail = chainTailDot(lowerer, expr); if (!tail) return false; - const recvT = lowerer.mapTypeOf(lowerer.typeOf(tail.expression)); + const recvTs = lowerer.typeOf(tail.expression); + // An `any` receiver short-circuits the whole tail too: `a?.b.c` reads + // `.c` normally once `a` is present. + if (recvTs.flags & ts.TypeFlags.Any) return true; + const recvT = lowerer.mapTypeOf(recvTs); if (recvT?.kind === "dyn") return true; if (recvT?.kind === "union") { const def = lowerer.unions.get(recvT.unionId); @@ -119,7 +123,7 @@ export function isOptionalChainTail(lowerer: Lowerer, expr: ts.Expression): bool * throwing. Walks the receiver spine only; the argument of the access * itself is not part of the guard. Parentheses end the chain * (`(a?.b).c` reads `.c` from undefined); `!` does not. */ -export function hasOptionalChainGuard(expr: ts.Expression): boolean { +export function hasOptionalChainGuard(expr: ts.Expression, lowerer?: Lowerer): boolean { let cur: ts.Expression = expr; for (;;) { if (ts.isNonNullExpression(cur)) { @@ -131,7 +135,11 @@ export function hasOptionalChainGuard(expr: ts.Expression): boolean { ts.isElementAccessExpression(cur) || ts.isCallExpression(cur) ) { - if (cur.questionDotToken) return true; + // Inside an optional chain's guarded body (dynamic reads pass the + // lowerer), the enclosing optChain already short-circuits on the + // consumed token, so the steps above it read normally: `a?.b.c` + // throws when `a.b` is undefined. + if (cur.questionDotToken) return lowerer === undefined || !lowerer.chainHandled.has(cur); cur = cur.expression; continue; } diff --git a/packages/compiler/src/frontend/lowering/expressions/property-access.ts b/packages/compiler/src/frontend/lowering/expressions/property-access.ts index ed24b72fb..b1fa41424 100644 --- a/packages/compiler/src/frontend/lowering/expressions/property-access.ts +++ b/packages/compiler/src/frontend/lowering/expressions/property-access.ts @@ -1,4 +1,5 @@ /* Property reads (`a.b`): lowerExprInner's property-access branch, intrinsic and union properties, field reads and targets, and match-group reads. */ +import { staticallySelected } from "../static-typeof.js"; import * as ts from "../../ts7/adapter.js"; import { lowerHistogramProperty } from "../builtins/performance.js"; import { staticFunctionNameRead } from "../static-function-names.js"; @@ -27,6 +28,7 @@ import { import { BOOL, BYTES_ELEMENT_NAME, + BYTES_U8, DYN, F64, type IrExpr, @@ -479,7 +481,10 @@ export function lowerPropertyAccessExpr( // undefined value so stored probes compose with optional calls. if ( stdlibGlobalNameOf(lowerer, expr.expression) === "globalThis" && - !lowerer.checker.getPropertyOfType(lowerer.typeOf(expr.expression), expr.name.text) + !lowerer.checker.getPropertyOfType( + lowerer.typeOf(staticallySelected(lowerer, expr.expression)), + expr.name.text, + ) ) { return dynUndefinedExpr(loc); } @@ -874,7 +879,7 @@ export function lowerPropertyAccessExpr( type: STRING, loc: locOf(expr.name), }; - const opt = hasOptionalChainGuard(expr.expression); + const opt = hasOptionalChainGuard(expr.expression, lowerer); return lowerer.maybeNarrow( { kind: "dynKeyGet", @@ -983,7 +988,7 @@ export function lowerPropertyAccessExpr( type: STRING, loc: locOf(expr.name), }; - const opt = hasOptionalChainGuard(expr.expression); + const opt = hasOptionalChainGuard(expr.expression, lowerer); return lowerer.maybeNarrow( { kind: "dynKeyGet", @@ -1115,7 +1120,7 @@ export function lowerPropertyAccessExpr( type: STRING, loc: locOf(expr.name), }; - const opt = hasOptionalChainGuard(expr.expression); + const opt = hasOptionalChainGuard(expr.expression, lowerer); const read: IrExpr = { kind: "dynKeyGet", key, @@ -1210,7 +1215,7 @@ export function lowerPropertyAccessExpr( type: STRING, loc: locOf(expr.name), }; - const opt = hasOptionalChainGuard(expr.expression); + const opt = hasOptionalChainGuard(expr.expression, lowerer); return lowerer.maybeNarrow( { kind: "dynKeyGet", @@ -1629,6 +1634,38 @@ export function lowerIntrinsicProperty( if (netAddressStatic) return netAddressStatic; if (lowerer.stdlibGlobalMember(expr, "Buffer") === "isBuffer") return lowerCheckedPredicateValue(lowerer, "Buffer.isBuffer", "buffer", locOf(expr)); + // Buffer statics as function values (`Buffer.byteLength.bind(Buffer)` in + // truncate-utf8-bytes): the checked forms take any argument as Node does. + if (lowerer.stdlibGlobalMember(expr, "Buffer") === "byteLength") + return lowerer.lowerNativeCallableValue( + { + fn: "buffer.byteLenValue", + params: [DYN, DYN], + result: F64, + valueParams: [ + { mode: "required", type: DYN }, + { mode: "optional", type: DYN, defaultValue: undefined }, + ], + }, + "Buffer.byteLength", + locOf(expr), + ); + if (lowerer.stdlibGlobalMember(expr, "Buffer") === "alloc") + return bufferAllocValue(lowerer, locOf(expr)); + if (lowerer.stdlibGlobalMember(expr, "Buffer") === "compare") + return lowerer.lowerNativeCallableValue( + { + fn: "buffer.compareChk", + params: [DYN, DYN], + result: F64, + valueParams: [ + { mode: "required", type: DYN }, + { mode: "required", type: DYN }, + ], + }, + "Buffer.compare", + locOf(expr), + ); if (lowerer.stdlibGlobalMember(expr, "Date") === "now") return lowerer.lowerNativeCallableValue( { @@ -2210,11 +2247,80 @@ export function lowerIntrinsicProperty( own(STDLIB_SURFACE.string, name) !== undefined : ARRAY_METHODS.has(name) ) { + // A JavaScript string method read as a value (smol-toml's + // `!!(''.isWellFormed)` feature probe): String.prototype's method, the + // runtime's interned function that runs it on its receiver. + if (kind === "string" && isJsSourceFile(expr.getSourceFile()) && !expr.questionDotToken) { + const receiver = lowerer.lowerExpr(expr.expression); + const method: IrExpr = { + kind: "libCall", + fn: "dyn.builtinMethod", + args: [ + { kind: "strLit", value: "String", type: STRING, loc: locOf(expr) }, + { kind: "strLit", value: name, type: STRING, loc: locOf(expr.name) }, + ], + type: DYN, + loc: locOf(expr), + }; + return isSafeToDiscard(receiver) + ? method + : { + kind: "seqExpr", + stmts: [{ kind: "exprStmt", expr: receiver, loc: locOf(expr.expression) }], + result: method, + type: DYN, + loc: locOf(expr), + }; + } lowerer.unsupported("SC1090", expr, `${kind} methods as values (call '${name}' directly)`); } return null; } +/** Buffer.alloc as a function value (`var alloc = Buffer.alloc` in + * tar-stream): the runtime checks the size, allocates and fills with + * `fill` and `encoding` as Node's alloc does. */ +function bufferAllocValue(lowerer: Lowerer, loc: SrcLoc): IrExpr { + const name = "%builtin.Buffer.alloc"; + if (!lowerer.builtinCallableValueFns.has(name)) { + lowerer.builtinCallableValueFns.set(name, name); + const params = ["size", "fill", "encoding"].map((id) => ({ localId: id, name: id, type: DYN })); + lowerer.liftedFns.push({ + name, + params, + returnType: BYTES_U8, + locals: params.map((p) => ({ id: p.localId, name: p.name, type: DYN, mutable: false })), + loc, + body: [ + { + kind: "return", + value: { + kind: "libCall", + fn: "buffer.allocValue", + args: params.map((p) => varRef(p.localId, DYN, loc)), + type: BYTES_U8, + loc, + }, + loc, + }, + ], + }); + } + return { + kind: "dynFrom", + value: { + kind: "closure", + fnName: name, + captures: [], + type: { kind: "func", params: [DYN, DYN, DYN], ret: BYTES_U8 }, + loc, + }, + fnName: "alloc", + type: DYN, + loc, + }; +} + /** A `T | undefined` class field read that the checker narrowed to `T` * where the field was assigned a value that may be missing (an unchecked * indexed read). TypeScript's assignment narrowing is not a runtime fact @@ -2728,7 +2834,7 @@ export function lowerFieldRead(lowerer: Lowerer, expr: ts.PropertyAccessExpressi kind: "dynKeyGet", value, key: { kind: "strLit", value: expr.name.text, type: STRING, loc: locOf(expr.name) }, - ...(expr.questionDotToken || hasOptionalChainGuard(expr.expression) + ...(expr.questionDotToken || hasOptionalChainGuard(expr.expression, lowerer) ? { optional: true as const } : {}), type: DYN, @@ -3084,7 +3190,7 @@ export function lowerUnionProperty( kind: "dynKeyGet", value: lowerer.coerceToExpected(value, DYN), key, - ...(hasOptionalChainGuard(expr.expression) ? { optional: true as const } : {}), + ...(hasOptionalChainGuard(expr.expression, lowerer) ? { optional: true as const } : {}), type: DYN, loc: locOf(expr), }; diff --git a/packages/compiler/src/frontend/lowering/expressions/relational.ts b/packages/compiler/src/frontend/lowering/expressions/relational.ts index f8b8c7604..5b9d05414 100644 --- a/packages/compiler/src/frontend/lowering/expressions/relational.ts +++ b/packages/compiler/src/frontend/lowering/expressions/relational.ts @@ -1380,6 +1380,24 @@ export function lowerInExpression( if (key === "code" && isExecExceptionType(lowerer, lowerer.typeOf(expr.right))) { refuseExecExceptionCode(lowerer, expr); } + // A literal key that names no declared member of an index-signature + // record (`"id" in match.groups`): the same keyed presence probe a + // runtime key takes — declared fields by name, then the overflow keys. + { + const recvT = lowerer.mapTypeOf(lowerer.typeOf(expr.right)); + const shape = recvT?.kind === "record" ? lowerer.shapes.get(recvT.shapeId) : undefined; + if ( + shape?.indexValue && + !shape.tuple && + !OBJECT_PROTOTYPE_KEYS.has(key) && + !shape.fields.some( + (f) => f.name === key || f.name === `%get:${key}` || f.name === `%set:${key}`, + ) + ) { + const probed = lowerRuntimeKeyIn(lowerer, expr, loc); + if (probed) return probed; + } + } let recv = lowerer.lowerExpr(expr.right); const classInfo = recv.type.kind === "classval" ? lowerer.classes.get(recv.type.className) : undefined; diff --git a/packages/compiler/src/frontend/lowering/expressions/typeof.ts b/packages/compiler/src/frontend/lowering/expressions/typeof.ts index 46005e228..cc3ac06fb 100644 --- a/packages/compiler/src/frontend/lowering/expressions/typeof.ts +++ b/packages/compiler/src/frontend/lowering/expressions/typeof.ts @@ -171,6 +171,21 @@ export function lowerTypeofExpr(lowerer: Lowerer, expr: ts.TypeOfExpression, loc } if (constructorGlobalTypeof(lowerer, tested) !== null) return { kind: "strLit", value: "function", type: STRING, loc }; + // `typeof global.setTimeout` / `typeof globalThis.queueMicrotask`: a + // callable standard-library global read through the global object + // answers like its bare spelling (JavaScript sources represent the + // value as an identity token, so the operand cannot answer). + if (ts.isPropertyAccessExpression(tested) && !tested.questionDotToken) { + const name = stdlibGlobalNameOf(lowerer, tested); + if (name !== null && name !== "globalThis") { + const t = lowerer.typeOf(tested); + if ( + lowerer.checker.getCallSignatures(t).length > 0 || + lowerer.checker.getConstructSignatures(t).length > 0 + ) + 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 diff --git a/packages/compiler/src/frontend/lowering/generic-functions.ts b/packages/compiler/src/frontend/lowering/generic-functions.ts index 880cccc8b..ac5949656 100644 --- a/packages/compiler/src/frontend/lowering/generic-functions.ts +++ b/packages/compiler/src/frontend/lowering/generic-functions.ts @@ -275,10 +275,13 @@ export function genericCallInstance( { const rdecl = rsig ? rsig.declaration : undefined; if ( + rsig && rdecl && (ts.isFunctionDeclaration(rdecl) || ts.isMethodDeclaration(rdecl)) && !rdecl.body ) { + const overload = overloadCallInstance(lowerer, expr, info, rsig, rdecl); + if (overload) return overload; lowerer.unsupported( "SC1090", expr, @@ -313,14 +316,7 @@ export function genericCallInstance( fenceGenericSignatureResult(lowerer, expr, retTs); lowerer.badType(expr, retTs); } - // A body that can return an indexed read that misses returns undefined - // there, as in JavaScript, whatever the declared result says. An async - // body's promise is always present and settles with undefined there. - const returnType = lowerer.runtimeOptionalGenericReturns.has(info.decl) - ? mappedReturn.kind === "promise" - ? { kind: "promise" as const, inner: lowerer.runtimeOptionalType(mappedReturn.inner) } - : lowerer.runtimeOptionalType(mappedReturn) - : mappedReturn; + const returnType = runtimeReturnType(lowerer, info, mappedReturn); // keyof-constrained type parameters (`K extends keyof T`): the bound // LITERAL is semantic — the instance body reads the named field — so @@ -331,23 +327,11 @@ export function genericCallInstance( // share one runtime-keyed instance per IR signature, exactly the // widened discipline. const named = staticNameBindings(lowerer, expr, info); - const keyofTps = keyofConstrainedTypeParams(info); - if (keyofTps.size > 0) { + if (keyofConstrainedTypeParams(info).size > 0) { const tsBindings = new Map(); const bindings = lowerer.inferTypeParamBindings(expr, info, rsig, tsBindings); - const litKey = info.typeParams - .filter((tp) => keyofTps.has(tp)) - .map((tp) => { - const bound = tsBindings.get(tp); - return bound?.isStringLiteralType() - ? JSON.stringify(bound.value) - : bound?.isNumberLiteralType() - ? String(bound.value) - : "*"; - }) - .join(","); return internGenericInstance(lowerer, expr, info, params, returnType, () => bindings, { - extraKey: `@${litKey}${named?.key ?? ""}`, + extraKey: `${keyofLiteralKey(info, tsBindings)}${named?.key ?? ""}`, tsBindings, ...(named ? { staticFnNames: named.names } : {}), }); @@ -363,6 +347,110 @@ export function genericCallInstance( ); } +/** A call that resolved to an OVERLOAD signature of a generic function + * or method with an implementation (`request(p: WithHandler): + * Promise; request(p: Plain): Promise; request(p: Params) + * {...}`): the implementation monomorphizes under the bindings the + * overload's resolved signature proves for its type parameters (the + * structural unification every generic call uses, run against the + * overload; unbound parameters take their declared defaults). The + * instance's parameters and result are the IMPLEMENTATION's under those + * bindings — the body was checked against them — and the call's + * arguments and result coerce across, exactly as a non-generic overload + * call reconciles with its implementation (parameters the overload omits + * must be optional in the implementation). Null when the overload does + * not belong to this implementation or the arities do not line up. */ +function overloadCallInstance( + lowerer: Lowerer, + expr: ts.CallExpression, + info: GenericFnInfo, + rsig: ts.Signature, + rdecl: ts.FunctionDeclaration | ts.MethodDeclaration, +): GenericInstance | null { + const impl = info.decl; + if ( + rdecl.parent !== impl.parent || + rdecl.name === undefined || + impl.name === undefined || + rdecl.name.getText() !== impl.name.getText() || + rdecl.parameters.nodes.length > impl.parameters.nodes.length || + impl.parameters.nodes + .slice(rdecl.parameters.nodes.length) + .some((p) => p.questionToken === undefined && p.initializer === undefined) || + rdecl.parameters.nodes.some((p) => p.dotDotDotToken !== undefined) + ) + return null; + const declSig = lowerer.checker.getSignatureFromDeclaration(impl); + if (!declSig) return null; + const tsBindings = new Map(); + const bindings = new Map(); + unifySignatureBindings(lowerer, info, rsig, bindings, tsBindings); + // Explicit type arguments name the OVERLOAD's type parameters, which + // need not match the implementation's in order: the resolved overload + // already carries them, so unification binds first, and an explicit + // argument binds only a still-unbound implementation parameter of the + // same name. + const overloadTps = rdecl.typeParameters?.nodes ?? []; + expr.typeArguments?.nodes.forEach((ta, i) => { + const name = overloadTps[i]?.name.text; + const tp = info.typeParams.find((p) => ts.symbolName(p) === name); + if (!tp || bindings.has(tp)) return; + const taT = lowerer.checker.getTypeFromTypeNode(ta); + const mapped = lowerer.mapTypeOf(taT); + if (mapped) { + bindings.set(tp, mapped); + tsBindings.set(tp, taT); + } + }); + bindDefaultTypeParams(lowerer, info.typeParams, impl.typeParameters?.nodes, bindings, tsBindings); + // A type parameter the overload never mentions (a handler the overload + // omits) is `unknown`, the checker's own answer for an uninferred one. + for (const tp of info.typeParams) { + if (bindings.has(tp)) continue; + bindings.set(tp, DYN); + tsBindings.set(tp, lowerer.checker.getUnknownType()); + } + const previous = lowerer.typeParamBindings; + const previousTs = lowerer.typeParamTsBindings; + lowerer.typeParamBindings = bindings; + lowerer.typeParamTsBindings = tsBindings; + let params: ParamShape[]; + let returnType: IrType | null; + try { + params = paramShapes( + lowerer, + impl.parameters.nodes, + declSig, + (_param, i) => expr.arguments.nodes[i] ?? expr, + ); + returnType = lowerer.mapTypeOf(lowerer.checker.getReturnTypeOfSignature(declSig)); + } finally { + lowerer.typeParamBindings = previous; + lowerer.typeParamTsBindings = previousTs; + } + if (!returnType) return null; + // keyof-constrained parameters key their instance on the bound literal, + // as on the implementation-signature path. + const runtimeReturn = runtimeReturnType(lowerer, info, returnType); + return internGenericInstance(lowerer, expr, info, params, runtimeReturn, () => bindings, { + ...(keyofConstrainedTypeParams(info).size > 0 + ? { extraKey: keyofLiteralKey(info, tsBindings) } + : {}), + tsBindings, + }); +} + +/** An instance's result type: a body that can return an indexed read + * that misses returns undefined there, as in JavaScript, whatever the + * declared result says. An async body's promise is always present and + * settles with undefined there. */ +function runtimeReturnType(lowerer: Lowerer, info: GenericFnInfo, mapped: IrType): IrType { + if (!lowerer.runtimeOptionalGenericReturns.has(info.decl)) return mapped; + return mapped.kind === "promise" + ? { kind: "promise", inner: lowerer.runtimeOptionalType(mapped.inner) } + : lowerer.runtimeOptionalType(mapped); +} + /** A generic factory whose result is one of its own local classes * (`function base(v: T) { return class { value: T = v }; }`). Each * instance has its own class, so the instance is keyed on its bindings and @@ -596,6 +684,25 @@ function keyofConstrainedTypeParams(info: GenericFnInfo): Set { return out; } +/** The instance-key suffix of the literals bound to `info`'s + * keyof-constrained type parameters (`@"a"`; `*` for a non-literal + * binding): two literals can map to one IR signature while their bodies + * read different fields. */ +function keyofLiteralKey(info: GenericFnInfo, tsBindings: Map): string { + const keyofTps = keyofConstrainedTypeParams(info); + const literals = info.typeParams + .filter((tp) => keyofTps.has(tp)) + .map((tp) => { + const bound = tsBindings.get(tp); + return bound?.isStringLiteralType() + ? JSON.stringify(bound.value) + : bound?.isNumberLiteralType() + ? String(bound.value) + : "*"; + }); + return `@${literals.join(",")}`; +} + /** Type-parameter symbol → concrete IR type for one instantiation. * Explicit type arguments bind directly; the rest come from structurally * matching each DECLARED param/return type (which mentions the type @@ -736,6 +843,27 @@ function unifySignatureBindings( // so match it against the whole remaining union. Removing null // too is safe only when no undeclared null arm remains. unify(dParts[0]!, lowerer.checker.getNonNullableType(inst), depth + 1); + } else if (dParts.length > 1 && iParts.length === dParts.length) { + // `T | Promise` against `Res | Promise`: each declared + // part that names a generic target (Promise, an interface, an + // alias) pairs with the one instantiated part of the same target; + // a single remaining bare type parameter takes the single + // remaining instantiated part. + const remaining = [...iParts]; + const bare: ts.Type[] = []; + for (const d of dParts) { + if (d.flags & ts.TypeFlags.TypeParameter) { + bare.push(d); + continue; + } + const target = d.getSymbol() ?? d.getAliasSymbol(); + if (target === undefined) return; + const matches = remaining.filter((i) => (i.getSymbol() ?? i.getAliasSymbol()) === target); + if (matches.length !== 1) return; + remaining.splice(remaining.indexOf(matches[0]!), 1); + unify(d, matches[0]!, depth + 1); + } + if (bare.length === 1 && remaining.length === 1) unify(bare[0]!, remaining[0]!, depth + 1); } return; } diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index b1b4086cd..301026253 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -2180,6 +2180,57 @@ function lowerStringMethodCallWithOptionalArgs( return changed ? { ...lowered, args } : lowered; } +/** `.then(String)` / `.then(Number)` / `.then(Boolean)` over a promise of + * a number, string or boolean: the ambient constructor converts its first + * argument (the settled value), so the handler is an interned closure over + * the settled type applying that conversion. The constructors' own value + * form takes a string, which a number or boolean settled value is not. + * Null for every other handler. */ +function primitiveConversionHandler( + lowerer: Lowerer, + node: ts.Expression, + inner: IrType, +): IrExpr | null { + if (!ts.isIdentifier(node)) return null; + const name = node.text; + if (name !== "String" && name !== "Number" && name !== "Boolean") return null; + // A string settled value already fits the constructors' value form. + if (inner.kind !== "f64" && inner.kind !== "bool") return null; + if (!lowerer.isStdlibSymbol(lowerer.resolveValueSymbol(node) ?? undefined)) return null; + const loc = locOf(node); + const ret: IrType = name === "String" ? STRING : name === "Number" ? F64 : BOOL; + const fnT = funcOf([inner], ret); + const key = `then.${name}:${typeKey(inner)}`; + let fnName = lowerer.arrHofHelpers.get(key); + if (fnName === undefined) { + fnName = `%then.${name}.${lowerer.arrHofHelpers.size}`; + lowerer.arrHofHelpers.set(key, fnName); + const value = varRef("v.0", inner, loc); + let body: IrExpr; + if (name === "Boolean") body = { kind: "toBool", operand: value, type: BOOL, loc }; + else if (name === "String") body = { kind: "toString", operand: value, type: STRING, loc }; + else if (inner.kind === "f64") body = value; + else + body = { + kind: "ternary", + cond: value, + then: { kind: "numLit", value: 1, type: F64, loc }, + else_: { kind: "numLit", value: 0, type: F64, loc }, + type: F64, + loc, + }; + lowerer.liftedFns.push({ + name: fnName, + params: [{ localId: "v.0", name: "v", type: inner }], + returnType: ret, + locals: [{ id: "v.0", name: "v", type: inner, mutable: false }], + body: [{ kind: "return", value: body, loc }], + loc, + }); + } + return { kind: "closure", fnName, captures: [], type: fnT, loc }; +} + function lowerNumberConstructorValue( lowerer: Lowerer, argNode: ts.Expression, @@ -3367,6 +3418,8 @@ function lowerDynReceiverMethodCall( ? lowerer.checker.getNonNullableType(rawRecvTs) : rawRecvTs; if (lowerer.classImplementedProtocol(recvTs)) { + const viewed = pickViewGenericMethodCall(lowerer, call, access, recvTs); + if (viewed) return viewed; const property = lowerer.checker.getPropertyOfType(recvTs, access.name.text); if ( property && @@ -4327,8 +4380,13 @@ function lowerArrayIsArrayCall( if (lowerer.stdlibGlobalMember(access, "Array") !== "isArray") return null; if (call.arguments.nodes.length !== 1) return null; // the stdlib chokepoint fences const argNode = call.arguments.nodes[0]!; - const arg = lowerer.lowerExpr(argNode); + let arg = lowerer.lowerExpr(argNode); const loc = locOf(call); + // A checker-narrowed dyn read arrives as a validated extraction (the + // dynCheck bridge). The runtime value may be wider than the flow type + // claims (an overload narrower than what arrives), so test the dyn value + // itself instead of folding on the extracted type (typeof's rule). + if (arg.kind === "dynCheck" && arg.value.type.kind === "dyn") arg = arg.value; if (arg.type.kind === "dyn") { return { kind: "dynTest", test: "array", value: arg, type: BOOL, loc }; } @@ -5781,7 +5839,9 @@ export function lowerPromiseMethodCall( if (inner.kind === "moduleNs") markModuleNsHandlerParams(lowerer, call.arguments.nodes[0]!, inner); - let cb = lowerer.lowerExpr(call.arguments.nodes[0]!); + let cb = + primitiveConversionHandler(lowerer, call.arguments.nodes[0]!, inner) ?? + lowerer.lowerExpr(call.arguments.nodes[0]!); // A TYPED handler on a DYN-settling promise (the tracePromise // result's `.then((value) => ...)` — the checker's generic // instantiation typed the parameter, but the settled value is a @@ -9338,15 +9398,18 @@ function lowerObjLitGenericMethodCall( lowerer.checker.getCallSignatures(recvT).length > 0 ) { const value = tryLowerExpression(lowerer, access.expression); + // Builtin function values held as checked-dynamic functions + // (String.fromCharCode, JSON.stringify) bind the same way. if ( - value?.type.kind === "func" && - lowerer.dynConvertible(value.type) && + value && + (value.type.kind === "dyn" || + (value.type.kind === "func" && lowerer.dynConvertible(value.type))) && !call.arguments.nodes.some(ts.isSpreadElement) ) { const loc = locOf(call); return { kind: "dynInvoke", - recv: { kind: "dynFrom", value, type: DYN, loc }, + recv: value.type.kind === "dyn" ? value : { kind: "dynFrom", value, type: DYN, loc }, method: name, calleeName: ts.getTextOfNode(access), args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), @@ -10411,6 +10474,53 @@ function maybePromiseHandlerResult( return stmt; } +/** `view.m(args)` where `view` is a `Pick` / `Omit` view of a + * program class C and `m` is one of C's GENERIC methods (its overloads + * included): generic methods have no dispatchable method value, so the + * view's live receiver is extracted as the C instance it holds (a checked + * extraction — a value of another class throws Node-style TypeError + * text naming C) and the call monomorphizes statically against C, the + * same lowering a C-typed receiver gets. Null for other receivers. */ +function pickViewGenericMethodCall( + lowerer: Lowerer, + call: ts.CallExpression, + access: ts.PropertyAccessExpression, + recvTs: ts.Type, +): IrExpr | null { + const alias = recvTs.getAliasSymbol(); + if (alias === undefined || (ts.symbolName(alias) !== "Pick" && ts.symbolName(alias) !== "Omit")) + return null; + if (!alias.declarations.every((d) => ts.getSourceFileOfNode(d).isDeclarationFile)) return null; + const source = recvTs.getAliasTypeArguments()[0]; + const symbol = source?.getSymbol(); + const info = symbol ? lowerer.classBySymbol.get(symbol) : undefined; + if (!info || call.questionDotToken || access.questionDotToken) return null; + const found = findGenericMethodOn(lowerer, info, access.name.text); + if (!found) return null; + const loc = locOf(access.expression); + const lowered = lowerer.lowerExpr(access.expression); + const instance: IrType = { kind: "object", className: info.def.name }; + // A view that statically holds an instance of another class (a + // structurally compatible one) calls that class's method in Node, not + // C's: the call is refused rather than extracted as a C instance. + if ( + lowered.type.kind === "object" && + lowered.type.className !== info.def.name && + !lowerer.isSubclassOf(lowered.type.className, info.def.name) + ) { + lowerer.unsupported( + "SC1090", + access, + `calling the generic method '${access.name.text}' through a '${ts.symbolName(alias)}' view of '${info.def.name}' that holds a '${lowerer.fmt(lowered.type)}'`, + ); + } + const receiver: IrExpr = + lowered.type.kind === "object" + ? lowered + : { kind: "dynCheck", value: lowerer.coerceToExpected(lowered, DYN), type: instance, loc }; + return lowerClassGenericMethodCall(lowerer, call, access, info, found, receiver); +} + /** `Object.assign(err, ...sources)` in TypeScript onto an error-hierarchy * target (`Object.assign(new Error(message), details)`): the sources' * own members land on the error — the runtime copy over its diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index 49e7ad9fc..7bcb52aaa 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -8,6 +8,7 @@ import { } from "./constructor-returns.js"; import { checkedIterableSpread } from "./checked-iterable-spread.js"; import { lowerReadlineUiNew } from "./builtins/readline.js"; +import { builtinMemberRequireDecl } from "./builtins/module-bindings.js"; import { checkedClassUnionAssertion } from "./class-assertions.js"; import { collectClassMethodMutations } from "./class-method-mutations.js"; import { InternalCompilerError } from "../../errors.js"; @@ -100,7 +101,7 @@ import { isAssertClassNew } from "./assert-instances.js"; import { lowerArrayConstructor } from "./containers/array-construction.js"; import { lowerMapSeedNew, lowerSetSeedNew } from "./containers/collection-methods.js"; import { lowerObjectConstructor } from "./containers/indexed-objects.js"; -import { knownBufEncoding } from "./containers/bytes.js"; +import { knownBufEncoding, lowerNodeModulesBufferConstruct } from "./containers/bytes.js"; import { isSafeToRepeat } from "./expressions/evaluation-safety.js"; import { lowerSearchParamsNew } from "./builtins/url.js"; import { lowerTextCodecNew } from "./builtins/text-codecs.js"; @@ -1036,11 +1037,16 @@ export function builtinStreamInfoOf( // member to the stdlib class symbol. The declaration itself is // alias plumbing (streamClassAliasDecl — both declaration walks // skip it). + // A `var`/`let` binding of the inline require member + // (`var Transform = require("stream").Transform`) is the same alias + // plumbing as builtinMemberRequireDecl's: its declaration owns no + // storage either way. const decl = symbol.valueDeclaration; if ( decl && ts.isVariableDeclaration(decl) && - (ts.getCombinedNodeFlags(decl) & ts.NodeFlags.Const) !== 0 && + ((ts.getCombinedNodeFlags(decl) & ts.NodeFlags.Const) !== 0 || + builtinMemberRequireDecl(decl.name, decl.initializer)) && decl.initializer !== undefined && ts.isPropertyAccessExpression(decl.initializer) && !decl.initializer.questionDotToken && @@ -1399,7 +1405,8 @@ export function builtinEmitterInfoOf( if ( decl !== undefined && ts.isVariableDeclaration(decl) && - (ts.getCombinedNodeFlags(decl) & ts.NodeFlags.Const) !== 0 && + ((ts.getCombinedNodeFlags(decl) & ts.NodeFlags.Const) !== 0 || + builtinMemberRequireDecl(decl.name, decl.initializer)) && decl.initializer !== undefined && ts.isPropertyAccessExpression(decl.initializer) && !decl.initializer.questionDotToken && @@ -5549,8 +5556,18 @@ export function lowerStaticFieldRead( } // `C.name` — the JS-observable class name, a compile-time constant on // the direct spelling (tsc rejects user statics named `name`, so the - // chain above can never shadow it in TypeScript sources). + // chain above can never shadow it in TypeScript sources). A class of an + // import cycle reads it from its class object instead, whose reference + // checks the class's initialization (checkCycleBindingAccesses). if (expr.name.text === "name" && info.def.jsName !== undefined) { + if (lowerer.classInitFlags.get(info.def.name)?.cycle === true) + return { + kind: "libCall", + fn: "class.name", + args: [classValueRef(lowerer, info, expr.expression)], + type: STRING, + loc, + }; return { kind: "strLit", value: info.def.jsName, type: STRING, loc }; } if (info.callableBase) @@ -6770,14 +6787,22 @@ function thunkAsyncSlot( ): void { if (overridden.sig.async !== true) return; const declarer = overridden.declarer; + // The slot answers the body's promise: an untyped async method's `dyn` + // result is the promise's payload, so the slot's ABI is that promise. An + // async generator method's slot keeps its generator object. + const ret: IrType = + overridden.sig.gen || overridden.sig.ret.kind === "promise" + ? overridden.sig.ret + : { kind: "promise", inner: overridden.sig.ret }; declarer.returnAdapters ??= new Map(); if (!declarer.returnAdapters.has(mName)) { declarer.returnAdapters.set(mName, { params: [...overridden.sig.params], - ret: overridden.sig.ret, + ret, async: true, }); } + overridden.sig.ret = ret; delete overridden.sig.async; } @@ -9204,6 +9229,11 @@ function isCollectionOrErrorClass(ref: BuiltinClassRef | null): boolean { export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { const loc = locOf(expr); + // A dependency's deprecated `new Buffer(...)` fallback (containers/bytes.ts). + { + const buffer = lowerNodeModulesBufferConstruct(lowerer, expr); + if (buffer) return buffer; + } // new Function(...): Node's EvalError under // --disallow-code-generation-from-strings (code-generation.ts). if (codeGenerationCalleeOf(lowerer, expr.expression) === "Function") diff --git a/packages/compiler/src/frontend/lowering/lower-dgram.ts b/packages/compiler/src/frontend/lowering/lower-dgram.ts index 4cb238352..b8b905718 100644 --- a/packages/compiler/src/frontend/lowering/lower-dgram.ts +++ b/packages/compiler/src/frontend/lowering/lower-dgram.ts @@ -441,8 +441,31 @@ function lowerDnsModuleCall( const hostname = dyn(args[0]!); const options = args.length === 3 ? dyn(args[1]!) : dynUndefinedExpr(loc); const last = args[args.length - 1]!; - const typed = typedResult(last); - const cb = lowerDnsCallback(lowerer, last, "lookup callbacks"); + // Options known only at run time (a checked-dynamic value, or one cast + // to an overload's option type) decide between Node's two callback + // shapes there: the address is a string, or an address list with + // `all: true`. An inline callback then binds its parameters as + // checked-dynamic values, whatever the overload the checker picked + // says, and its body tests what actually arrived. + let runtimeOptions = false; + if (args.length === 3) { + let inner: ts.Expression = args[1]!; + while (ts.isParenthesizedExpression(inner) || ts.isAsExpression(inner)) + inner = inner.expression; + runtimeOptions = lowerer.mapTypeOf(lowerer.typeOf(inner))?.kind === "dyn"; + } + let callbackNode: ts.Expression = last; + while (ts.isParenthesizedExpression(callbackNode)) callbackNode = callbackNode.expression; + const dynCallback = + runtimeOptions && (ts.isArrowFunction(callbackNode) || ts.isFunctionExpression(callbackNode)); + const typed = dynCallback ? boolLit(false, loc) : typedResult(last); + if (dynCallback) lowerer.dynParamLambdas.add(callbackNode); + let cb: IrExpr; + try { + cb = lowerDnsCallback(lowerer, last, "lookup callbacks"); + } finally { + if (dynCallback) lowerer.dynParamLambdas.delete(callbackNode); + } return { kind: "libCall", fn: "dns.lookupDyn", diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index 76c043f4f..e1096f2fe 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -1,3 +1,5 @@ +import { lowerDynObjectLiteral } from "./expressions/object-literals.js"; +import { fallbackTainted } from "./checked-fallback.js"; import { lowerWebAssemblyValue, refuseWebAssemblyUse } from "./builtins/webassembly.js"; import { dynUndefinedExpr, nodeThrowExpr, numLit, strLit, varRef } from "../../ir/build.js"; import { InternalCompilerError } from "../../errors.js"; @@ -9,6 +11,7 @@ import { InternalCompilerError } from "../../errors.js"; * (FieldTarget). */ import * as ts from "../ts7/adapter.js"; import { checkedPrivateReceiver, localPrivateBrandClass } from "./private-brands.js"; +import { staticCondition } from "./static-typeof.js"; import { constituentTypes } from "../ts7/checker.js"; import type { Lowerer } from "./lowerer.js"; import { lowerMaybePromiseAwait, maybePromiseUnionOf } from "./maybe-promise.js"; @@ -270,6 +273,25 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { const chainRecv = lowerer.chainRecvByNode.get(expr); if (chainRecv) return { ...chainRecv, loc }; + // A failed checked value in a fallback tail: assertions check nothing + // (checked-fallback.ts). + if ( + lowerer.fallbackTaint !== null && + (ts.isAsExpression(expr) || + ts.isTypeAssertion(expr) || + ts.isNonNullExpression(expr) || + ts.isSatisfiesExpression(expr)) && + fallbackTainted(lowerer, expr.expression) + ) + return lowerer.lowerExpr(expr.expression); + if (lowerer.fallbackTaint !== null && fallbackTainted(lowerer, expr)) { + if (ts.isObjectLiteralExpression(expr)) return lowerDynObjectLiteral(lowerer, expr); + if (ts.isArrayLiteralExpression(expr) && !expr.elements.nodes.some(ts.isSpreadElement)) { + const elems = expr.elements.nodes.map((el) => lowerer.lowerExprExpecting(el, DYN)); + return { kind: "dynArrLit", elems, type: DYN, loc }; + } + } + // --external-types supplies CHECKER truth only. A value rooted in one // of those declarations has no runtime implementation in scriptc, so // every use fences at its expression instead of accidentally lowering @@ -1078,6 +1100,11 @@ export function lowerCondition(lowerer: Lowerer, expr: ts.Expression): IrExpr { }); if (presence !== null) return presence; } + // Global detection (`typeof window < "u"`, `typeof self !== + // "undefined" && …`): a condition whose typeof answers are static + // folds, so the untaken arm never lowers (static-typeof.ts). + const folded = staticCondition(lowerer, e); + if (folded !== null) return { kind: "boolLit", value: folded, type: BOOL, loc: locOf(e) }; if (ts.isBinaryExpression(e)) { const op = e.operatorToken.kind; if (op === ts.SyntaxKind.AmpersandAmpersandToken || op === ts.SyntaxKind.BarBarToken) { diff --git a/packages/compiler/src/frontend/lowering/lower-functions.ts b/packages/compiler/src/frontend/lowering/lower-functions.ts index bec8528c4..0ddfa0ac4 100644 --- a/packages/compiler/src/frontend/lowering/lower-functions.ts +++ b/packages/compiler/src/frontend/lowering/lower-functions.ts @@ -2,6 +2,7 @@ import { declareTypedArguments } from "./typed-arguments.js"; import { varRef } from "../../ir/build.js"; import { InternalCompilerError } from "../../errors.js"; import * as ts from "../ts7/adapter.js"; +import { isRebindableFunctionSymbol } from "./rebindable-functions.js"; import { bodyReadsArguments } from "../arguments-usage.js"; import type { Lowerer } from "./lowerer.js"; import { @@ -59,6 +60,8 @@ function declareCallReceiver( * same collection lists top-level declarations ride. */ export function isTopLevelFnSymbol(lowerer: Lowerer, ident: ts.Identifier): boolean { const symbol = lowerer.resolveValueSymbol(ident); + // A reassigned JavaScript declaration is read through its module global. + if (isRebindableFunctionSymbol(lowerer, symbol ?? undefined)) return false; if (symbol && lowerer.staticCallables.get(symbol)?.kind === "declared-function") return true; const decl = symbol ? symbol.declarations[0] : undefined; return ( diff --git a/packages/compiler/src/frontend/lowering/lower-inspect.ts b/packages/compiler/src/frontend/lowering/lower-inspect.ts index a8657f978..4be5d0b7e 100644 --- a/packages/compiler/src/frontend/lowering/lower-inspect.ts +++ b/packages/compiler/src/frontend/lowering/lower-inspect.ts @@ -1,4 +1,12 @@ -import { dynUndefinedExpr, boolLit, helperLoc, numLit, strLit, varRef } from "../../ir/build.js"; +import { + dynUndefinedExpr, + boolLit, + helperLoc, + nodeThrowExpr, + numLit, + strLit, + varRef, +} from "../../ir/build.js"; import { builtinClassDynValueOf, builtinClassKindOf } from "./builtins/builtin-class-dyn.js"; import { InternalCompilerError } from "../../errors.js"; @@ -13,6 +21,7 @@ import { type IrExpr, type IrFunction, type IrStmt, + type IrStrIntrinsicMethod, type IrType, RUNTIME_ERROR_CLASSES, STRING, @@ -111,13 +120,589 @@ function inspectKey(name: string): string { function errorRenderable(info: ClassInfo): boolean { // The builtin classes' name/message/code live in runtime slots (the // ScrError prefix insp.error reads), not IR fields; only USER-declared - // fields are own properties the runtime rendering would miss. #private - // fields are NOT own properties — Node never prints them — so they - // don't disqualify. - if (!info.builtinError && info.def.fields.some((f) => !f.name.startsWith("#"))) return false; + // fields are own properties the runtime rendering would miss. A user + // class's layout repeats its builtin base's prefix fields, which are + // those same slots. #private fields are NOT own properties — Node never + // prints them — so they don't disqualify. + if (!info.builtinError) { + const inherited = new Set(); + for (let c = info.base; c; c = c.base) { + if (c.builtinError || c.def.runtime) for (const f of c.def.fields) inherited.add(f.name); + } + if (info.def.fields.some((f) => !f.name.startsWith("#") && !inherited.has(f.name))) + return false; + } return info.subclasses.every((s) => errorRenderable(s)); } +/** The accessors that change what Node reads for an error's header. The + * runtime renders the instance's own name and message slots. */ +const ERROR_HEADER_ACCESSORS = [ + "get:name", + "set:name", + "get:message", + "set:message", + "get:stack", + "set:stack", +]; + +/** Whether Node's header for an instance of `info`'s hierarchy can differ + * from the runtime's slots: a program class among its ancestors or + * descendants declares a `name`, `message` or `stack` accessor, or the + * program writes a prototype's `name` or `message`. */ +function errorHeaderOverridden(lowerer: Lowerer, info: ClassInfo): boolean { + if (lowerer.prototypeMethodAccesses.has("name") || lowerer.prototypeMethodAccesses.has("message")) + return true; + const declares = (c: ClassInfo): boolean => + !c.builtinError && !c.def.runtime && ERROR_HEADER_ACCESSORS.some((m) => c.methods.has(m)); + for (let c = info.base; c; c = c.base) if (declares(c)) return true; + const below = (c: ClassInfo): boolean => declares(c) || c.subclasses.some(below); + return below(info); +} + +/** Error keys Node's error inspector folds into the base or hides. */ +const ERROR_SPECIAL_KEYS = new Set(["name", "message", "stack", "cause", "errors"]); + +/** The declared own (enumerable) fields of a user error class, in layout + * order (the base chain first). */ +function ownErrorFields(info: ClassInfo): { name: string; type: IrType }[] { + // The builtin bases' layout fields (name, message) are runtime slots the + // error base already renders, not own enumerable properties. + const builtin = new Set(); + for (let c = info.base; c; c = c.base) { + if (c.builtinError || c.def.runtime) for (const f of c.def.fields) builtin.add(f.name); + } + return info.def.fields.filter( + (f) => isClassOwnEnumerableFieldName(f.name) && !builtin.has(f.name), + ); +} + +/** `info` and its descendants that declare fields, most-derived first (a + * runtime instance tests against its own class before an ancestor's). */ +function fieldedErrorClasses(info: ClassInfo): ClassInfo[] { + const out: ClassInfo[] = []; + const visit = (c: ClassInfo): void => { + for (const s of c.subclasses) visit(s); + if (!c.builtinError && !c.def.runtime && ownErrorFields(c).length > 0) out.push(c); + }; + visit(info); + return out; +} + +/** Why a fielded error class's own properties have no static rendering, + * or null when every field renders. `visiting` and `out` are the caller's + * inspectSupport walk: a field typed as the class itself (`next?: Chain`) + * is a recursive shape, not a new walk. */ +function fieldedErrorReason( + lowerer: Lowerer, + info: ClassInfo, + visiting: Set = new Set(), + out: { recursive: boolean } = { recursive: false }, +): string | null { + const key = `error:${info.def.name}`; + if (visiting.has(key)) { + out.recursive = true; + return null; + } + visiting.add(key); + try { + for (let c: ClassInfo | null = info; c && !c.builtinError && !c.def.runtime; c = c.base) { + // Node prints the own properties in creation order, which puts a + // declared `cause` among the fields; the cause renders after them. + if (c.fieldOrder.some((f) => f.errorCause)) + return "its field 'cause' is one Node's error inspector treats specially"; + // A JavaScript class's fields exist once its constructor creates them, + // which can be conditional; the layout reads every field. + if (c.decl && isJsSourceFile(c.decl.getSourceFile()) && ownErrorFields(c).length > 0) + return "its fields are JavaScript properties, which its constructor may not create"; + } + for (const f of ownErrorFields(info)) { + if (ERROR_SPECIAL_KEYS.has(f.name)) + return `its field '${f.name}' is one Node's error inspector treats specially`; + const why = inspectSupport(lowerer, f.type, visiting, out); + if (why !== null) return `its field '${f.name}': ${why}`; + } + return null; + } finally { + visiting.delete(key); + } +} + +/** True when an instance of `c` can contain itself: its fields reach `c` + * or one of its bases, whose slots can hold a `c` (`next: Chain` in a + * subclass of Chain, a field typed as Error). */ +function errorInstanceReachesItself(lowerer: Lowerer, c: ClassInfo): boolean { + const holders = new Set(); + for (let a: ClassInfo | null = c; a; a = a.base) holders.add(a.def.name); + const seen = new Set(); + const walk = (u: IrType): boolean => { + for (const x of typeConstituents(lowerer, u)) { + if (x.kind === "object" && holders.has(x.className)) return true; + const k = typeKey(x); + if (!seen.has(k)) { + seen.add(k); + if (walk(x)) return true; + } + } + return false; + }; + return walk({ kind: "object", className: c.def.name }); +} + +/** An error value whose runtime class may be a program-declared subclass, + * which may declare fields (`class Bailable extends Error { bail = false }`) + * or carry a `cause`: Node prints the error base (`[Bailable [Error]: m]`, + * the stackless form compiled errors use) followed by the own properties, + * `[Bailable [Error]: m] { bail: true }`, with the usual single-line/ + * multi-line choice counting the base. Each program class (most-derived + * first) is an instanceof arm; other values keep the runtime's error + * rendering. Beyond the depth budget an instance with own properties + * prints `[Name]`, as Node does for any object with keys; one without + * prints its header. */ +function errorInspectHelper( + lowerer: Lowerer, + t: IrType & { kind: "object" }, + info: ClassInfo, + loc: SrcLoc, +): string { + const key = `insp.userError:${typeKey(t)}`; + const existing = lowerer.inspectHelpers.get(key); + if (existing) return existing; + const name = `%util.insp.${lowerer.inspectHelpers.size}`; + lowerer.inspectHelpers.set(key, name); + const v = (): IrExpr => varRef("v.0", t, loc); + const r = (): IrExpr => varRef("r.0", F64, loc); + const d = (): IrExpr => varRef("d.0", F64, loc); + const rPlus1 = (): IrExpr => ({ + kind: "bin", + op: "+", + left: r(), + right: numLit(1, loc), + type: F64, + loc, + }); + const base = (): IrExpr => ({ + kind: "libCall", + fn: "insp.error", + args: [v(), r(), d()], + type: STRING, + loc, + }); + const error = (): IrExpr => lowerer.upcastTo(v(), "%Error"); + const hasCause = (): IrExpr => ({ + kind: "libCall", + fn: "insp.errorHasCause", + args: [error()], + type: BOOL, + loc, + }); + const entry = (text: IrExpr): IrStmt => ({ + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "insp.entry", + args: [text, boolLit(false, loc)], + type: VOID, + loc, + }, + loc, + }); + /** The own properties of an instance of `c`: its fields, then its + * `cause` (an assignment in the constructor or later, or the + * constructor's options, which Node lists last as `[cause]`). */ + const propertiesArm = ( + c: ClassInfo, + display: string, + fields: { name: string; type: IrType }[], + ): IrStmt[] => { + const cls: IrType & { kind: "object" } = { kind: "object", className: c.def.name }; + const instance: IrExpr = + c.def.name === t.className ? v() : { kind: "downcast", value: v(), type: cls, loc }; + // A class whose fields can hold the instance itself (`next: Chain`) + // runs Node's circular machinery, as other cycle-capable objects do: + // an instance already on the traversal stack renders [Circular *N], + // before the depth answer, and the frame's text gets the + // prefix when the walk found it circular. + const onCycle = errorInstanceReachesItself(lowerer, c); + const circular = (): IrExpr => varRef("cc.0", F64, loc); + const arm: IrStmt[] = [ + ...(onCycle + ? ([ + { + kind: "varDecl", + localId: "cc.0", + init: { kind: "libCall", fn: "insp.circCheck", args: [v()], type: F64, loc }, + loc, + }, + { + kind: "if", + cond: { + kind: "bin", + op: ">", + left: circular(), + right: numLit(0, loc), + type: BOOL, + loc, + }, + then: [ + { + kind: "return", + value: { + kind: "libCall", + fn: "insp.circular", + args: [circular()], + type: STRING, + loc, + }, + loc, + }, + ], + else_: null, + loc, + }, + ] satisfies IrStmt[]) + : []), + { + kind: "if", + cond: { kind: "bin", op: ">", left: r(), right: d(), type: BOOL, loc }, + then: [{ kind: "return", value: strLit(`[${display}]`, loc), loc }], + else_: null, + loc, + }, + { + kind: "varDecl", + localId: "base.0", + init: improvedErrorBase(lowerer, v(), base(), display, loc), + loc, + }, + { + kind: "exprStmt", + expr: { kind: "libCall", fn: "insp.begin", args: [rPlus1()], type: VOID, loc }, + loc, + }, + ...(onCycle + ? ([ + { + kind: "exprStmt", + expr: { kind: "libCall", fn: "insp.seenPush", args: [v()], type: VOID, loc }, + loc, + }, + ] satisfies IrStmt[]) + : []), + ]; + for (const f of fields) { + const read: IrExpr = { + kind: "fieldGet", + obj: instance, + className: c.def.name, + field: f.name, + type: f.type, + loc, + }; + arm.push( + entry( + concatAll( + [ + strLit(`${inspectKey(f.name)}: `, loc), + inspectExpr(lowerer, f.type, read, rPlus1(), d(), loc), + ], + loc, + ), + ), + ); + } + arm.push({ + kind: "if", + cond: hasCause(), + then: [ + entry({ + kind: "libCall", + fn: "insp.errorCause", + args: [error(), rPlus1(), d()], + type: STRING, + loc, + }), + ], + else_: null, + loc, + }); + const ended: IrExpr = { + kind: "libCall", + fn: "insp.end", + args: [ + varRef("base.0", STRING, loc), + strLit("{", loc), + strLit("}", loc), + rPlus1(), + boolLit(false, loc), + boolLit(false, loc), + ], + type: STRING, + loc, + }; + arm.push({ + kind: "return", + value: onCycle + ? { kind: "libCall", fn: "insp.refWrap", args: [v(), ended], type: STRING, loc } + : ended, + loc, + }); + return arm; + }; + const body: IrStmt[] = []; + for (const c of userErrorClasses(info)) { + const display = c.def.jsName ?? c.decl?.name?.text ?? c.def.name.replace(/^%/, ""); + const fields = ownErrorFields(c); + const why = fields.length > 0 ? fieldedErrorReason(lowerer, c) : null; + let arm: IrStmt[]; + if (why !== null) { + // An instance of a subclass whose own properties have no rendering + // throws a named refusal instead of printing a partial object. + arm = [ + { + kind: "return", + value: nodeThrowExpr( + 0, + "SC2020", + `inspecting a '${display}' error (${why}) is not supported yet`, + STRING, + loc, + ), + loc, + }, + ]; + } else if (fields.length === 0) { + // A field-less program class: without a `cause` it has no own + // properties, and Node prints the header alone at any depth. + arm = [ + { + kind: "if", + cond: { kind: "unary", op: "!", operand: hasCause(), type: BOOL, loc }, + then: [ + { kind: "return", value: improvedErrorBase(lowerer, v(), base(), display, loc), loc }, + ], + else_: null, + loc, + }, + ...propertiesArm(c, display, fields), + ]; + } else { + arm = propertiesArm(c, display, fields); + } + if (c.def.name === t.className) { + body.push({ kind: "block", body: arm, loc }); + break; + } + body.push({ + kind: "if", + cond: { kind: "instanceOf", value: v(), className: c.def.name, type: BOOL, loc }, + then: arm, + else_: null, + loc, + }); + } + if (body.at(-1)?.kind !== "block") body.push({ kind: "return", value: base(), loc }); + const fn: IrFunction = { + name, + params: [ + { localId: "v.0", name: "v", type: t }, + { localId: "r.0", name: "r", type: F64 }, + { localId: "d.0", name: "d", type: F64 }, + ], + returnType: STRING, + locals: [ + { id: "v.0", name: "v", type: t, mutable: false }, + { id: "r.0", name: "r", type: F64, mutable: false }, + { id: "d.0", name: "d", type: F64, mutable: false }, + { id: "base.0", name: "base", type: STRING, mutable: false }, + { id: "cc.0", name: "cc", type: F64, mutable: false }, + { id: "name.0", name: "name", type: STRING, mutable: true }, + { id: "raw.0", name: "raw", type: STRING, mutable: true }, + ], + body, + loc, + }; + // Dynamic stores (a JavaScript class's `this.x = ...` outside its + // declared fields, or a write through an untyped reference) add a + // property bag to a class after this helper was built. The rendering + // reads only a `cause` from it, so an instance whose bag holds another + // property throws a named refusal instead of printing a partial object. + lowerer.shapeOrderHelperFinalizers.push(() => { + const bagged = [info, ...userErrorClasses(info)].filter( + (c) => + !c.builtinError && + !c.def.runtime && + c.def.fields.some((f) => f.name === DYN_CLASS_PROPERTIES), + ); + if (bagged.length === 0) return; + fn.body = [ + ...bagged.map((c): IrStmt => ({ + kind: "if", + cond: { + kind: "logical", + op: "&&", + left: { kind: "instanceOf", value: v(), className: c.def.name, type: BOOL, loc }, + right: { + kind: "libCall", + fn: "insp.errorBagHasProperties", + args: [ + { + kind: "fieldGet", + obj: + c.def.name === t.className + ? v() + : { + kind: "downcast", + value: v(), + type: { kind: "object", className: c.def.name }, + loc, + }, + className: c.def.name, + field: DYN_CLASS_PROPERTIES, + type: DYN, + loc, + }, + ], + type: BOOL, + loc, + }, + type: BOOL, + loc, + }, + then: [ + { + kind: "return", + value: nodeThrowExpr( + 0, + "SC2020", + `inspecting a '${c.def.jsName ?? c.def.name.replace(/^%/, "")}' error with properties created at run time is not supported yet`, + STRING, + loc, + ), + loc, + }, + ], + else_: null, + loc, + })), + ...body, + ]; + }); + lowerer.liftedFns.push(fn); + return name; +} + +/** The program-declared classes among `info` and its descendants, most + * derived first (an instance tests against its own class before an + * ancestor's). Runtime-provided error classes are not among them. */ +function userErrorClasses(info: ClassInfo): ClassInfo[] { + const out: ClassInfo[] = []; + const visit = (c: ClassInfo): void => { + for (const s of c.subclasses) visit(s); + if (!c.builtinError && !c.def.runtime) out.push(c); + }; + visit(info); + return out; +} + +/** Node's improveStack over the stackless base (`[Error: m]`): when the + * instance's constructor name differs from its `name` and the name ends in + * "Error", the header names both — `[Bailable [Error]: m]`, or + * `[FetchError: m]` when the constructor name already contains it. */ +function improvedErrorBase( + lowerer: Lowerer, + value: IrExpr, + base: IrExpr, + display: string, + loc: SrcLoc, +): IrExpr { + const str = ( + method: IrStrIntrinsicMethod, + receiver: IrExpr, + args: IrExpr[], + type: IrType, + ): IrExpr => ({ + kind: "strIntrinsic", + method, + receiver, + args, + type, + loc, + }); + const and = (left: IrExpr, right: IrExpr): IrExpr => ({ + kind: "logical", + op: "&&", + left, + right, + type: BOOL, + loc, + }); + const eq = (left: IrExpr, right: IrExpr, negated = false): IrExpr => ({ + kind: "strEq", + negated, + left, + right, + type: BOOL, + loc, + }); + const nameRef = (): IrExpr => varRef("name.0", STRING, loc); + const baseRef = (): IrExpr => varRef("raw.0", STRING, loc); + const after = (): IrExpr => ({ + kind: "bin", + op: "+", + left: str("length", nameRef(), [], F64), + right: numLit(1, loc), + type: F64, + loc, + }); + const next = (): IrExpr => str("charAt", baseRef(), [after()], STRING); + const cond = and( + and( + eq(nameRef(), strLit(display, loc), true), + str("endsWith", nameRef(), [strLit("Error", loc)], BOOL), + ), + and(str("startsWith", baseRef(), [concatAll([strLit("[", loc), nameRef()], loc)], BOOL), { + kind: "logical", + op: "||", + left: eq(next(), strLit(":", loc)), + right: eq(next(), strLit("]", loc)), + type: BOOL, + loc, + }), + ); + const rest = (): IrExpr => str("slice", baseRef(), [after()], STRING); + const improved: IrExpr = { + kind: "ternary", + cond: str("includes", strLit(display, loc), [nameRef()], BOOL), + then: concatAll([strLit(`[${display}`, loc), rest()], loc), + else_: concatAll([strLit(`[${display} [`, loc), nameRef(), strLit("]", loc), rest()], loc), + type: STRING, + loc, + }; + return { + kind: "seqExpr", + stmts: [ + { + kind: "varDecl", + localId: "name.0", + init: { + kind: "fieldGet", + obj: lowerer.upcastTo(value, "%Error"), + className: "%Error", + field: "name", + type: STRING, + loc, + }, + loc, + }, + { kind: "varDecl", localId: "raw.0", init: base, loc }, + ], + result: { kind: "ternary", cond, then: improved, else_: baseRef(), type: STRING, loc }, + type: STRING, + loc, + }; +} + function isErrorClass(lowerer: Lowerer, className: string): boolean { if (RUNTIME_ERROR_CLASSES.has(className)) return true; return lowerer.isSubclassOf(className, "%Error"); @@ -222,9 +807,18 @@ function inspectSupport( // for those classes (the runtime's capsule view). if (eventTargetRootOf(info) !== null) return null; if (isErrorClass(lowerer, t.className)) { - return errorRenderable(info) - ? null - : "error subclasses with declared fields have no inspect lowering yet (the extra own properties are runtime-dynamic)"; + if (userErrorClasses(info).length > 0 && errorHeaderOverridden(lowerer, info)) + return "error subclasses whose name, message or stack is an accessor or a prototype property have no inspect lowering yet"; + if (errorRenderable(info)) return null; + // Subclasses with declared fields render the error base followed + // by their own properties. The static class's own fields must + // render; a subclass whose fields cannot is a runtime refusal on + // its own instances only (see errorInspectHelper). + const own = fieldedErrorClasses(info).find((c) => c === info); + const ownWhy = own ? fieldedErrorReason(lowerer, own, visiting, out) : null; + if (ownWhy !== null) + return `'${t.className.replace(/^%(m\d+\.)?/, "")}' error values (${ownWhy})`; + return null; } // A class with subclasses, or a JavaScript class, renders through its // checked-dynamic view, which names the instance's own class and holds @@ -373,6 +967,16 @@ export function inspectExpr( }; } if (isErrorClass(lowerer, t.className)) { + const info = lowerer.classes.get(t.className); + if (info && userErrorClasses(info).length > 0) { + return { + kind: "call", + callee: errorInspectHelper(lowerer, t, info, loc), + args: [value, recurse, depth], + type: STRING, + loc, + }; + } return { kind: "libCall", fn: "insp.error", @@ -475,8 +1079,17 @@ function typeConstituents(lowerer: Lowerer, u: IrType): IrType[] { return [u.key, u.value]; case "union": return lowerer.unions.get(u.unionId)?.arms.slice() ?? []; - case "object": - return lowerer.classes.get(u.className)?.def.fields.map((f) => f.type) ?? []; + case "object": { + const info = lowerer.classes.get(u.className); + if (!info) return []; + const types = info.def.fields.map((f) => f.type); + // An error value can be an instance of a program-declared subclass, + // whose own fields its inspection renders (errorInspectHelper). + if (isErrorClass(lowerer, u.className)) + for (const c of userErrorClasses(info)) + for (const f of ownErrorFields(c)) types.push(f.type); + return types; + } default: return []; } diff --git a/packages/compiler/src/frontend/lowering/lower-modules.ts b/packages/compiler/src/frontend/lowering/lower-modules.ts index 6e40ad080..c7bbf958d 100644 --- a/packages/compiler/src/frontend/lowering/lower-modules.ts +++ b/packages/compiler/src/frontend/lowering/lower-modules.ts @@ -8,6 +8,7 @@ import { InternalCompilerError } from "../../errors.js"; * seeds), npm/JSON import collection, per-file %init functions, %main, and * the module artifacts (globals, embedded npm tables) the IR module carries. */ import * as ts from "../ts7/adapter.js"; +import { collectRebindableFunctions, isRebindableFunction } from "./rebindable-functions.js"; import { bindingSource } from "../binding-source.js"; import type { Lowerer } from "./lowerer.js"; import { dirname as dirnamePath, resolve as resolvePath } from "node:path"; @@ -157,6 +158,49 @@ export interface FileParts { topStmts: ts.Statement[]; } +/** Preflight's module graph (LoadResult.moduleEdges), for the walks that + * append modules outside the startup order, and the set collecting the + * inline require() calls that close a cycle there. */ +export interface AppendGraph { + edges?: ReadonlyMap | undefined; + inlineCycleCalls?: Set; +} + +/** An appended module's edges: its import and export declarations, then + * the require() edges preflight resolved — a CommonJS file's only edges + * (the compiled CommonJS package a workspace module loaded on demand + * imports, and the sibling packages and files IT requires). */ +function appendedEdgesOf( + program: ts.Program, + graph: AppendGraph | undefined, +): (sf: ts.SourceFile) => CycleEdge[] { + const cache = new Map(); + return (sf) => { + let out = cache.get(sf); + if (out !== undefined) return out; + out = orderedImportsOf(program, sf).flatMap(({ stmt, dep }) => + dep !== null && dep !== sf + ? [{ dep, stmt: stmt as ts.ImportDeclaration | ts.ExportDeclaration }] + : [], + ); + for (const e of graph?.edges?.get(sf) ?? []) + if (e.stmt === undefined && e.dep !== sf) out.push(e); + cache.set(sf, out); + return out; + }; +} + +/** Lazy inline requires run after their module's top level: their modules + * join the order afterwards, as in preflight's walk. */ +function visitLazyEdges( + sf: ts.SourceFile, + edgesOf: (sf: ts.SourceFile) => CycleEdge[], + state: ReadonlyMap, + visit: (sf: ts.SourceFile) => void, +): void { + for (const e of edgesOf(sf)) if (e.lazy === true && state.get(e.dep) === undefined) visit(e.dep); +} + /** Program modules selected by statically resolved child_process.fork() * calls join the compiled graph without becoming startup dependencies of * the parent entry. Their guarded init functions are selected by %main only @@ -167,8 +211,9 @@ export function appendForkModules( order: ts.SourceFile[], targets: ts.SourceFile[], onCycle: (cycle: string, reason: string) => void, + graph?: AppendGraph, ): void { - appendEntryModules(program, order, targets, forkTargetPaths(program, order), onCycle); + appendEntryModules(program, order, targets, forkTargetPaths(program, order), onCycle, graph); } export function appendWorkerModules( @@ -176,8 +221,9 @@ export function appendWorkerModules( order: ts.SourceFile[], targets: ts.SourceFile[], onCycle: (cycle: string, reason: string) => void, + graph?: AppendGraph, ): void { - appendEntryModules(program, order, targets, workerTargetPaths(program, order), onCycle); + appendEntryModules(program, order, targets, workerTargetPaths(program, order), onCycle, graph); } function appendEntryModules( @@ -186,6 +232,7 @@ function appendEntryModules( targets: ts.SourceFile[], paths: readonly string[], onCycle: (cycle: string, reason: string) => void, + graph?: AppendGraph, ): void { if (order.length === 0) return; const byPath = new Map( @@ -195,22 +242,24 @@ function appendEntryModules( for (const sf of order) state.set(sf, "done"); const added: ts.SourceFile[] = []; const stack: string[] = []; - const staticEdgesOf = (sf: ts.SourceFile): CycleEdge[] => - orderedImportsOf(program, sf).flatMap(({ stmt, dep }) => - dep !== null && dep !== sf - ? [{ dep, stmt: stmt as ts.ImportDeclaration | ts.ExportDeclaration }] - : [], - ); - const cycleAdmissionReason = makeCycleAdmission(program, staticEdgesOf); + const staticEdgesOf = appendedEdgesOf(program, graph); + const cycleAdmissionReason = makeCycleAdmission(program, (sf) => + staticEdgesOf(sf).filter((e) => e.call === undefined), + ); const visit = (sf: ts.SourceFile): void => { const prior = state.get(sf); if (prior === "done") return; state.set(sf, "visiting"); stack.push(sf.fileName); for (const edge of staticEdgesOf(sf)) { + if (edge.lazy === true) continue; // visited after this module completes const edgeState = state.get(edge.dep); if (edgeState === "done") continue; if (edgeState === "visiting") { + if (edge.call !== undefined) { + graph?.inlineCycleCalls?.add(edge.call); + continue; + } const reason = cycleAdmissionReason(sf, edge); if (reason !== null) { const cycleStart = stack.indexOf(edge.dep.fileName); @@ -223,6 +272,7 @@ function appendEntryModules( stack.pop(); state.set(sf, "done"); added.push(sf); + visitLazyEdges(sf, staticEdgesOf, state, visit); }; const knownTargets = new Set(targets); @@ -247,8 +297,12 @@ export function splitFiles(lowerer: Lowerer): FileParts[] { const files = lowerer.moduleOrder.length > 0 ? lowerer.moduleOrder : [lowerer.entry]; return files.map((sf) => { const fp: FileParts = { sf, fnDecls: [], classDecls: [], topStmts: [] }; + const rebindable = collectRebindableFunctions(lowerer, sf); for (const stmt of sf.statements.nodes) { - if (ts.isFunctionDeclaration(stmt)) fp.fnDecls.push(stmt); + // A reassigned JavaScript declaration lives in a module global that + // the init body assigns first (rebindable-functions.ts). + if (ts.isFunctionDeclaration(stmt) && rebindable.has(stmt)) fp.topStmts.push(stmt); + else if (ts.isFunctionDeclaration(stmt)) fp.fnDecls.push(stmt); else if (ts.isClassDeclaration(stmt)) fp.classDecls.push(stmt); // Namespaces: ambient/type-only ones are zero-runtime and skip; // instantiated bodies FLATTEN into this file's parts (functions/ @@ -314,27 +368,30 @@ export function appendDynamicImportModules( program: ts.Program, order: ts.SourceFile[], onCycle: (cycle: string, reason: string) => void, + graph?: AppendGraph, ): void { if (order.length === 0) return; // no preflight order — sites keep their fences const state = new Map(); for (const sf of order) state.set(sf, "done"); const added: ts.SourceFile[] = []; const stack: string[] = []; - const staticEdgesOf = (sf: ts.SourceFile): CycleEdge[] => - orderedImportsOf(program, sf).flatMap(({ stmt, dep }) => - dep !== null && dep !== sf - ? [{ dep, stmt: stmt as ts.ImportDeclaration | ts.ExportDeclaration }] - : [], - ); - const cycleAdmissionReason = makeCycleAdmission(program, staticEdgesOf); + const staticEdgesOf = appendedEdgesOf(program, graph); + const cycleAdmissionReason = makeCycleAdmission(program, (sf) => + staticEdgesOf(sf).filter((e) => e.call === undefined), + ); const visit = (sf: ts.SourceFile): void => { if (state.get(sf) !== undefined) return; state.set(sf, "visiting"); stack.push(sf.fileName); for (const e of staticEdgesOf(sf)) { + if (e.lazy === true) continue; // visited after this module completes const s = state.get(e.dep); if (s === "done") continue; if (s === "visiting") { + if (e.call !== undefined) { + graph?.inlineCycleCalls?.add(e.call); + continue; + } // A back edge: Node answers it from the cache. Same admission // question as preflight's static walk, same fence when refused. const reason = cycleAdmissionReason(sf, e); @@ -349,6 +406,7 @@ export function appendDynamicImportModules( stack.pop(); state.set(sf, "done"); added.push(sf); + visitLazyEdges(sf, staticEdgesOf, state, visit); }; const scanned = new Set(); for (;;) { @@ -1230,6 +1288,36 @@ function jsDynHoldableInitializer(lowerer: Lowerer, init: ts.Expression | undefi return false; } +/** A JavaScript default-export object literal holding a class the program + * could not lower (`export default { parse, TomlDate }` where TomlDate + * extends an unsupported base): the literal narrows that member away, its + * diagnostics deferred to the reads that reach it, so the binding's type + * drops it too. Otherwise the module's own initialization would fail the + * width check against members no build can supply. */ +function withoutRejectedClassMembers( + lowerer: Lowerer, + expression: ts.Expression, + type: IrType, +): IrType { + let literal = expression; + while (ts.isParenthesizedExpression(literal)) literal = literal.expression; + if (!ts.isObjectLiteralExpression(literal) || type.kind !== "record") return type; + const shape = lowerer.shapes.get(type.shapeId); + if (!shape || shape.tuple) return type; + const rejected = (t: IrType): boolean => + t.kind === "classval" && + !lowerer.classes.has(t.className) && + lowerer.deferredClassByName.has(t.className); + if (!shape.fields.some((f) => rejected(f.type))) return type; + const kept = shape.fields.filter((f) => !rejected(f.type)); + const shapeId = lowerer.shapes.intern( + kept.map((f) => ({ name: f.name, type: f.type })), + false, + shape.indexValue, + ); + return { kind: "record", shapeId }; +} + export function collectGlobals( lowerer: Lowerer, sf: ts.SourceFile, @@ -1245,6 +1333,21 @@ export function collectGlobals( // collected earlier in this pass must resolve them (lower-expando.ts). collectExpandoMembers(lowerer, sf); for (const stmt of topStmts) { + if (ts.isFunctionDeclaration(stmt) && stmt.name && isRebindableFunction(lowerer, stmt)) { + const symbol = lowerer.checker.getSymbolAtLocation(stmt.name); + if (symbol && !lowerer.globalsBySymbol.has(symbol)) { + const g: IrGlobal = { + id: `%g.${tag}${stmt.name.text}`, + name: stmt.name.text, + type: DYN, + mutable: true, + }; + lowerer.globalsBySymbol.set(symbol, g); + lowerer.globalsByDeclNode.set(stmt, g); + lowerer.globalsList.push(g); + } + continue; + } // `export default `: the module's `default` binding is a const // module global (registered under the checker's default-export // symbol — the symbol a default IMPORT's alias chain terminates at, @@ -1332,6 +1435,7 @@ export function collectGlobals( type = unitOnlyUnion(lowerer.unions); } if (type.kind === "void") lowerer.badType(stmt.expression, tsType); + if (isJsSourceFile(sf)) type = withoutRejectedClassMembers(lowerer, stmt.expression, type); const g: IrGlobal = { id: `%g.${tag}default`, name: "default", type, mutable: false }; lowerer.globalsBySymbol.set(symbol, g); lowerer.globalsList.push(g); @@ -1369,8 +1473,17 @@ export function collectGlobals( // common/tls's `exports.check = function (certs) {...}` — // keep func-ness with dyn pieces; everything else is dyn). const strict = lowerer.checker.getTypeOfSymbol(symbol); - const t = lowerer.mapTypeOf(strict) ?? dynFallbackType(lowerer, nameNode, strict); + let t = lowerer.mapTypeOf(strict) ?? dynFallbackType(lowerer, nameNode, strict); if (!t || t.kind === "void") lowerer.badType(typeNode, strict); + // An export typed as the empty object literal (tsc's enum output + // `exports.E || (exports.E = {})` after its `exports.E = void 0` + // preamble): TS's `{}` admits any later non-nullish value and the + // export is undefined until then, so it rides checked-dynamic + // storage like the same-shaped var binding (varBindingType). + if (t.kind === "record" && isJsSourceFile(sf)) { + const shape = lowerer.shapes.get(t.shapeId); + if (shape && shape.fields.length === 0 && !shape.indexValue && !shape.tuple) t = DYN; + } // An immutable export source shares its live storage and native // callable ABI. Mutable sources need a separate snapshot slot: // later writes to the binding do not change the exported value. @@ -1611,43 +1724,9 @@ export function collectGlobals( ) ) ) { - if (ts.isVariableStatement(stmt) && (stmt.declarationList.flags & ts.NodeFlags.Const) !== 0) { - for (const decl of stmt.declarationList.declarations.nodes) { - stdlibGlobalAliasDecl(lowerer, decl.name, decl.initializer); - } - } - // `const { codes: { ERR_X } } = require("./errors")`: the outer - // keys stay alias plumbing, but a nested pattern destructures the - // export's value at the require, so its bindings own storage like - // any destructured declaration (lowerVarDeclarator assigns them). - if (ts.isVariableStatement(stmt)) { - const isLetList = - (stmt.declarationList.flags & ts.NodeFlags.Let) !== 0 || - (stmt.declarationList.flags & ts.NodeFlags.BlockScoped) === 0; - for (const decl of stmt.declarationList.declarations.nodes) { - for (const element of requireNestedPatternsOf(decl)) { - for (const nameNode of boundIdentifiersOf(element.name!)) { - const symbol = lowerer.checker.getSymbolAtLocation(nameNode); - if (!symbol || lowerer.globalsBySymbol.has(symbol)) continue; - const mapped = lowerer.mapTypeOf(lowerer.typeOf(nameNode)); - const type = mapped && mapped.kind !== "void" ? mapped : DYN; - const g: IrGlobal = { - id: `%g.${tag}${nameNode.text}`, - name: nameNode.text, - type, - mutable: isLetList, - source: bindingSource(nameNode), - ...((type.kind === "func" || type.kind === "dyn") && !isVarDeclared(decl) - ? { tdz: true as const } - : {}), - }; - lowerer.globalsBySymbol.set(symbol, g); - lowerer.globalsList.push(g); - if (isVarDeclared(decl)) noteVarGlobalEntryInit(lowerer, sf, g); - } - } - } - } + if (ts.isVariableStatement(stmt)) + for (const decl of stmt.declarationList.declarations.nodes) + registerRequireDeclBindings(lowerer, sf, stmt.declarationList, decl, tag); continue; } // `declare const __VERSION__: string` — AMBIENT: no storage exists @@ -1681,6 +1760,16 @@ export function collectGlobals( // probes below would otherwise claim the call-initializer shape // and put its fences on the build. if (provenanceElidedConstDecl(lowerer, decl)) continue; + // A require declarator beside other declarators (`var EventEmitter = + // require("events"), R = class extends EventEmitter {}`): the + // statement lowering treats it as the alias plumbing of a whole + // require statement (lowerVarDeclarator), so it owns no storage + // either. A global here would never be assigned, and a read through + // it would see null. + if (requireAliasDecl(lowerer, decl)) { + registerRequireDeclBindings(lowerer, sf, list, decl, tag); + continue; + } // A stored built-in numeric value iterator has no first-class // global representation. Its same-%init for-of uses are backed by // hidden source/cursor/done locals registered when the declaration @@ -2604,6 +2693,61 @@ function noteVarGlobalEntryInit(lowerer: Lowerer, sf: ts.SourceFile, g: IrGlobal lowerer.varGlobalEntryInits.set(sf, inits); } +/** A JavaScript require declaration the statement lowering treats as alias + * plumbing (lowerVarDeclarator): no storage, the reads resolve through to + * the module. JSON documents and native FFI requires keep their storage. */ +function requireAliasDecl(lowerer: Lowerer, decl: ts.VariableDeclaration): boolean { + return ( + decl.initializer !== undefined && + requireSpecOf(decl.initializer) !== null && + !isNativeFfiRequire(lowerer, decl.initializer) && + !isJsonRequireDecl(lowerer, decl) && + isJsSourceFile(decl.getSourceFile()) + ); +} + +/** The registrations a top-level require declaration does make. `const + * process = require('node:process')` aliases the global process + * (stdlibGlobalAliasDecl's require form) so reads through the binding + * lower via the process surface. In `const { codes: { ERR_X } } = + * require("./errors")` the outer keys stay alias plumbing, but a nested + * pattern destructures the export's value at the require, so its bindings + * own storage like any destructured declaration (lowerVarDeclarator + * assigns them). */ +function registerRequireDeclBindings( + lowerer: Lowerer, + sf: ts.SourceFile, + list: ts.VariableDeclarationList, + decl: ts.VariableDeclaration, + tag: string, +): void { + if ((list.flags & ts.NodeFlags.Const) !== 0) + stdlibGlobalAliasDecl(lowerer, decl.name, decl.initializer); + const isLetList = + (list.flags & ts.NodeFlags.Let) !== 0 || (list.flags & ts.NodeFlags.BlockScoped) === 0; + for (const element of requireNestedPatternsOf(decl)) { + for (const nameNode of boundIdentifiersOf(element.name!)) { + const symbol = lowerer.checker.getSymbolAtLocation(nameNode); + if (!symbol || lowerer.globalsBySymbol.has(symbol)) continue; + const mapped = lowerer.mapTypeOf(lowerer.typeOf(nameNode)); + const type = mapped && mapped.kind !== "void" ? mapped : DYN; + const g: IrGlobal = { + id: `%g.${tag}${nameNode.text}`, + name: nameNode.text, + type, + mutable: isLetList, + source: bindingSource(nameNode), + ...((type.kind === "func" || type.kind === "dyn") && !isVarDeclared(decl) + ? { tdz: true as const } + : {}), + }; + lowerer.globalsBySymbol.set(symbol, g); + lowerer.globalsList.push(g); + if (isVarDeclared(decl)) noteVarGlobalEntryInit(lowerer, sf, g); + } + } +} + /** The nested half of module-scope `var` hoisting: a `var` inside a * top-level BLOCK (an if arm, a loop body, a try) is module-scoped in * JS — other functions in the file read the same binding — so it must @@ -2648,6 +2792,14 @@ function collectNestedVarGlobals( }); for (const list of lists) { for (const decl of list.declarations.nodes) { + // A nested builtin require (`if (c) { var fs = require("fs"); }`) + // lowers to nothing: its bindings are alias plumbing wherever the + // statement sits (lowerVarDeclarator), so they own no storage. + if ( + requireAliasDecl(lowerer, decl) && + canonicalBuiltinModule(requireSpecOf(decl.initializer!)!) !== null + ) + continue; for (const nameNode of boundIdentifiersOf(decl.name)) { const diagsBefore = lowerer.diags.length; try { @@ -2811,6 +2963,23 @@ export function lowerFileInit( loc: loc0, }); } + // Reassigned function declarations are initialized when the module + // starts, as declarations are hoisted: before its dependencies run, so + // a module of an import cycle can call one while it initializes. Their + // statements then lower to nothing (rebindable-functions.ts). + for (const stmt of stmts) { + if (!ts.isFunctionDeclaration(stmt) || !stmt.name || !isRebindableFunction(lowerer, stmt)) + continue; + const g = lowerer.globalsByDeclNode.get(stmt); + if (!g) continue; + header.push({ + kind: "assign", + localId: g.id, + value: lowerer.coerceToExpected(lowerer.lowerLambda(stmt), DYN), + loc: locOf(stmt), + }); + ctx.hoistedFnDecls.add(stmt); + } for (const { stmt, dep } of orderedImportsOf(lowerer.program, sf)) { const npm = lowerer.npmInitActions.get(sf)?.get(stmt); if (npm !== undefined && npm.length > 0) { diff --git a/packages/compiler/src/frontend/lowering/lower-namespaces.ts b/packages/compiler/src/frontend/lowering/lower-namespaces.ts index 9997eb7d9..dca835d77 100644 --- a/packages/compiler/src/frontend/lowering/lower-namespaces.ts +++ b/packages/compiler/src/frontend/lowering/lower-namespaces.ts @@ -37,6 +37,7 @@ * statement is not modeled — same as top-level const reads through early * calls). */ import * as ts from "../ts7/adapter.js"; +import { packageShimFor } from "../package-shims.js"; import type { Lowerer } from "./lowerer.js"; import { boundIdentifiersOf } from "./lowerer.js"; import type { FileParts } from "./lower-modules.js"; @@ -253,6 +254,10 @@ function moduleNsSourceFileOf(lowerer: Lowerer, e: ts.Expression): ts.SourceFile sym = lowerer.checker.getSymbolAtLocation(e.name); } if (!sym) return null; + // The import binding's own declaration stays in view: a package scriptc + // implements itself (package-shims.ts) types through a declaration + // file the alias resolves to, while the import compiles the shim. + const bindingDecls = sym.declarations; if (sym.flags & ts.SymbolFlags.Alias) sym = lowerer.checker.getAliasedSymbol(sym); for (const d of sym.declarations) { if ( @@ -284,6 +289,20 @@ function moduleNsSourceFileOf(lowerer: Lowerer, e: ts.Expression): ts.SourceFile } } } + for (const d of bindingDecls) { + if (!ts.isNamespaceImport(d)) continue; + const stmt = d.parent?.parent; + if ( + stmt === undefined || + !ts.isImportDeclaration(stmt) || + !ts.isStringLiteral(stmt.moduleSpecifier) + ) + continue; + const from = ts.getSourceFileOfNode(d); + if (packageShimFor(from.fileName, stmt.moduleSpecifier.text) === null) continue; + const dep = resolveImport(lowerer.program, from, stmt.moduleSpecifier.text); + if (dep && !dep.isDeclarationFile && lowerer.fileTag.has(dep)) return dep; + } return null; } diff --git a/packages/compiler/src/frontend/lowering/lower-server.ts b/packages/compiler/src/frontend/lowering/lower-server.ts index afae7dd7b..f0da85bf8 100644 --- a/packages/compiler/src/frontend/lowering/lower-server.ts +++ b/packages/compiler/src/frontend/lowering/lower-server.ts @@ -930,6 +930,7 @@ function lowerNetConnectOptions( let port: IrExpr | null = null; let host: IrExpr | null = null; let lookup: IrExpr | null = null; + let dynLookup: IrExpr | null = null; let autoSelect = false; let attempt: IrExpr | null = null; let optionThrow: IrExpr | null = null; @@ -1037,7 +1038,23 @@ function lowerNetConnectOptions( ); } lookup = lowerer.lowerExpr(initializer); - if (!lookupFnShapeOk(lowerer, lookup.type)) { + // A lookup of Node's own type (net.LookupFunction: options and a + // callback taking an address string or list) crosses as a checked- + // dynamic function the runtime calls the way Node's dial does. + if ( + !lookupFnShapeOk(lowerer, lookup.type) && + lookup.type.kind === "func" && + lookup.type.params.length === 3 && + canBoxFuncIntoDyn( + lookup.type, + (id) => lowerer.shapes.get(id), + (id) => lowerer.unions.get(id), + ) + ) { + dynLookup = { kind: "dynFrom", value: lookup, type: DYN, loc: locOf(initializer) }; + } else if (lookup.type.kind === "dyn") { + dynLookup = lookup; + } else if (!lookupFnShapeOk(lowerer, lookup.type)) { lowerer.noLowering( `a ${member} 'lookup' option of '${lowerer.fmt(lookup.type)}' values`, prop, @@ -1080,6 +1097,22 @@ function lowerNetConnectOptions( loc, }; } + if (dynLookup !== null) { + if (args.length !== 1) { + lowerer.noLowering( + `${member} with a lookup and a connect listener`, + expr, + "register the listener separately: socket.once('connect', ...)", + ); + } + return { + kind: "libCall", + fn: "net.connectLookupDyn", + args: [port, host, dynLookup], + type: NETSOCKET_T, + loc, + }; + } if (lookup !== null) { if (!autoSelect) { lowerer.noLowering( diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index d03ba7a0e..a53c0564b 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -1,3 +1,10 @@ +import { + armFallbackSite, + fallbackSplitsAt, + fallbackTainted, + lowerFallbackTail, + openFallbackSite, +} from "./checked-fallback.js"; import { lowerTupleAliasDecl, stripTypeWrappers } from "./typed-arguments.js"; import { declaredMemberOf, isPromisePrototypeRead } from "./expressions/property-access.js"; import { hostsAll } from "./expressions/hosted-narrowing.js"; @@ -7,7 +14,9 @@ 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 { runtimeKeyPatternRead } from "./expressions/keyed-field-targets.js"; +import { declaresHoistedBinding, staticCondition } from "./static-typeof.js"; import { lowerCollectionSpread } from "./containers/collection-methods.js"; import { lowerEventObjectPropertyWrite } from "./lower-event-target.js"; import { lowerDynObjectLiteral } from "./expressions/object-literals.js"; @@ -686,7 +695,10 @@ export function lowerStmts(lowerer: Lowerer, stmts: readonly ts.Statement[]): Ir // position — instead of failing the whole build). TypeScript keeps // compile fences. ICEs (SC9001) always stay compile errors. const sf = stmts.length === 0 ? undefined : ts.getSourceFileOfNode(stmts[0]!); - const deferFences = sf !== undefined && isJsSourceFile(sf); + // The dynamic lowering of a checked-cast fallback tail fences like + // JavaScript: a statement with no `any` lowering throws when a failed + // value reaches it (checked-fallback.ts). + const deferFences = sf !== undefined && (isJsSourceFile(sf) || lowerer.fallbackTaint !== null); // Register this list as open for the forward-capture machinery: a // nested function lowering mid-list may pre-declare a LATER const of // this list as a TDZ box (predeclareForwardCapture), pushing the @@ -733,6 +745,7 @@ export function lowerStmts(lowerer: Lowerer, stmts: readonly ts.Statement[]): Ir lowered = accounting; appended = true; } else { + openFallbackSite(lowerer, stmt, out); lowered = lowerer.lowerStmt(stmt); } // The lib-boundary chokepoint (lib-boundary.ts): checked-dynamic @@ -747,8 +760,17 @@ export function lowerStmts(lowerer: Lowerer, stmts: readonly ts.Statement[]): Ir else body.push(lowered); const rewritten = lowerSurplusCalls(lowerer, body); enforceLibBoundary(lowerer, rewritten); + armFallbackSite(lowerer, stmt, rewritten); if (!appended) out.push(...rewritten); } + // A checked-cast fallback site's tail starts after this + // statement: the rest of the list lowers statically and + // dynamically, selected by the site's flag. + const splits = fallbackSplitsAt(lowerer, stmt); + if (splits.length > 0) { + out.push(...lowerFallbackTail(lowerer, stmts.slice(i + 1), splits)); + break; + } // A JS statement that lowered CLEAN can still have flushed // DEFERRED collection diagnostics (a symbol lookup on an alias // resolves through flushDeferred — informational here, not a @@ -1532,6 +1554,20 @@ export function lowerStmt(lowerer: Lowerer, stmt: ts.Statement): IrStmt | IrStmt } if (ts.isIfStatement(stmt)) { const cond = lowerer.lowerCondition(stmt.expression); + // A global-detection condition (static-typeof.ts) folds; its untaken + // arm is code Node never runs and does not lower, unless it declares a + // hoisted binding. + const decided = staticCondition(lowerer, stmt.expression); + if (decided !== null) { + const taken = decided ? stmt.thenStatement : stmt.elseStatement; + const untaken = decided ? stmt.elseStatement : stmt.thenStatement; + if (untaken === undefined || !declaresHoistedBinding(untaken)) { + const arm = taken === undefined ? [] : lowerer.lowerScopedBlock(taken); + return decided + ? { kind: "if", cond, then: arm, else_: null, loc: locOf(stmt) } + : { kind: "if", cond, then: [], else_: arm, loc: locOf(stmt) }; + } + } // Aliased-typeof narrows (ms's `var type = typeof val` guard): the // then-branch lowers under what a TRUE condition proves, the // else-branch under a FALSE one — the var/let alias narrowing the @@ -2270,12 +2306,21 @@ export function lowerDestructuringDecl( // destructure property 'a' of 'f(...)' as it is null.") for the source // spellings the assignment form reproduces; other sources are refused, // as in the assignment form. JavaScript files keep their existing - // per-read errors. - if ( + // per-read errors, except that a runtime computed first key checks the + // source first: Node throws before it evaluates any key, and V8's bare + // message names the source. + const jsRuntimeKeyCheck = init.type.kind === "dyn" && ts.isObjectBindingPattern(decl.name) && - !isJsSourceFile(ts.getSourceFileOfNode(decl)) && - lowerer.anyOrigin(decl.initializer) + isJsSourceFile(ts.getSourceFileOfNode(decl)) && + startsWithRuntimeKey(lowerer, decl.name) && + destrSpellingOf(decl.initializer) !== null; + if ( + jsRuntimeKeyCheck || + (init.type.kind === "dyn" && + ts.isObjectBindingPattern(decl.name) && + !isJsSourceFile(ts.getSourceFileOfNode(decl)) && + lowerer.anyOrigin(decl.initializer)) ) { const spelling = destrSpellingOf(decl.initializer); if (spelling === null) { @@ -2285,7 +2330,7 @@ export function lowerDestructuringDecl( "destructuring declaration from an `any` source whose expression spelling is unsupported (use a variable, ordinary property chain, or ordinary call)", ); } - const first = decl.name.elements.nodes[0]; + const first = (decl.name as ts.ObjectBindingPattern).elements.nodes[0]; const firstKey = first === undefined || first.dotDotDotToken !== undefined ? undefined @@ -2873,6 +2918,27 @@ export function lowerBindingPattern( return; } if (srcType.kind === "dyn") { + // A runtime computed first key evaluates after RequireObjectCoercible: + // a nullish source throws before the key's side effects run. Later + // keys follow a read that already threw. + if (ts.isObjectBindingPattern(pattern) && startsWithRuntimeKey(lowerer, pattern)) { + const loc = locOf(pattern); + for (const unit of ["null", "undefined"] as const) { + out.push({ + kind: "if", + cond: { kind: "dynTest", test: unit, value: srcRef(), type: BOOL, loc }, + then: [ + { + kind: "exprStmt", + expr: nodeThrowExpr(1, "", `Cannot read properties of ${unit}`, VOID, loc), + loc, + }, + ], + else_: null, + loc, + }); + } + } for (const el of pattern.elements.nodes) { if (el.name === undefined) continue; const loc = locOf(el); @@ -2920,16 +2986,21 @@ export function lowerBindingPattern( ts.isNumericLiteral(prop) ? patternKeyNameOf(lowerer, prop as ts.PropertyName) : null; - if (propName === null) { + // A runtime computed key (`{ [name]: v }`) reads the live property. + const runtimeRead = + propName === null && ts.isComputedPropertyName(prop) + ? runtimeKeyPatternRead(lowerer, prop.expression, srcRef(), out, loc) + : null; + if (propName === null && runtimeRead === null) { lowerer.unsupported( "SC1031", el, "destructuring with computed keys that do not fold to one property name", ); } - let value: IrExpr = { + let value: IrExpr = runtimeRead ?? { kind: "dynKeyGet", - key: { kind: "strLit", value: propName, type: STRING, loc }, + key: { kind: "strLit", value: propName!, type: STRING, loc }, value: srcRef(), type: DYN, loc, @@ -3187,11 +3258,22 @@ export function lowerBindingPattern( ? patternKeyNameOf(lowerer, prop as ts.PropertyName) : null; if (propName === null) { - lowerer.unsupported( - "SC1031", - el, - "destructuring with computed keys that do not fold to one property name", - ); + // A runtime computed key (`{ [name]: v }`): the keyed read of an + // index-signature entry, of the declared field a literal-union key + // names, or of a JavaScript record's live property. + const runtimeRead = ts.isComputedPropertyName(prop) + ? runtimeKeyPatternRead(lowerer, prop.expression, srcRef(), out, locOf(el)) + : null; + if (runtimeRead === null) { + lowerer.unsupported( + "SC1031", + el, + "destructuring with computed keys that do not fold to one property name", + ); + } + const value = el.initializer ? applyBindingDefault(lowerer, el, runtimeRead) : runtimeRead; + lowerer.bindPatternTarget(el.name, value, isLet, out); + continue; } // An ACCESSOR property (%get:/%set: closure slots): the element's // read IS a getter call — once, at the element's pattern position @@ -3467,6 +3549,16 @@ function lowerModuleNamespacePattern( } } +/** Whether a pattern's first element has a computed key that does not + * fold to one property name, so the key evaluates at run time. */ +function startsWithRuntimeKey(lowerer: Lowerer, pattern: ts.ObjectBindingPattern): boolean { + const first = pattern.elements.nodes[0]; + const key = first?.dotDotDotToken === undefined ? first?.propertyName : undefined; + return ( + key !== undefined && ts.isComputedPropertyName(key) && patternKeyNameOf(lowerer, key) === null + ); +} + function patternKeyNameOf(lowerer: Lowerer, prop: ts.PropertyName): string | null { if (ts.isIdentifier(prop) || ts.isPrivateIdentifier(prop)) return prop.text; if (ts.isComputedPropertyName(prop)) return lowerer.foldedStringKeyOf(prop.expression); @@ -5475,7 +5567,29 @@ export function lowerVarDecl( // `let` may be reassigned a different shape later; genuine `any` // only — every other unmappable keeps its own diagnostic. const checkerAny = (lowerer.typeOf(decl.name).flags & ts.TypeFlags.Any) !== 0; + // An annotation the checker could not resolve (a package type alias that + // reads as `any`) over an empty object literal: the binding is a + // dictionary the code fills by key (`files[name] = entry`), which the + // empty struct the literal would adopt cannot hold. It holds the + // checked-dynamic object instead. + let emptyAnyDictionary = false; + if (checkerAny && decl.type && !isLet && init.type.kind === "record") { + let lit: ts.Expression = decl.initializer; + while (ts.isParenthesizedExpression(lit)) lit = lit.expression; + const shape = lowerer.shapes.get(init.type.shapeId); + if ( + ts.isObjectLiteralExpression(lit) && + lit.properties.nodes.length === 0 && + shape && + shape.fields.length === 0 && + !shape.indexValue + ) { + type = DYN; + emptyAnyDictionary = true; + } + } if ( + !emptyAnyDictionary && (type === null || (type.kind === "dyn" && checkerAny)) && !isLet && (checkerAny || @@ -5926,11 +6040,44 @@ export function jsBindingHasOpenWrites(lowerer: Lowerer, decl: ts.VariableDeclar writes = indexed; lowerer.jsBindingWritesByOwner.set(owner, writes); } - return (writes.get(decl.name.text) ?? []).some( - (node) => - lowerer.resolveValueSymbol(node.left as ts.Identifier) === symbol && - (lowerer.typeOf(node.right).flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0, + const candidates = (writes.get(decl.name.text) ?? []).filter( + (node) => lowerer.resolveValueSymbol(node.left as ts.Identifier) === symbol, ); + if ( + candidates.some( + (node) => + (lowerer.typeOf(node.right).flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0, + ) + ) + return true; + // A plain JavaScript write of a value the declared type cannot hold (the + // checker does not report it): buffer-crc32 replaces its number[] table + // with `new Int32Array(table)`. Only a different representation counts; + // writes that coerce into the declared type keep it. + // A write whose checker type is the declaration's own maps to the same + // representation; the declared type is mapped only for the others. + const declaredType = lowerer.typeOf(decl.name); + const differing = candidates.filter( + (node) => + node.operatorToken.kind === ts.SyntaxKind.EqualsToken && + lowerer.typeOf(node.right) !== declaredType, + ); + if (differing.length === 0) return false; + const declared = lowerer.mapTypeOf(declaredType); + if (declared === null || declared.kind === "dyn" || declared.kind === "union") return false; + return differing.some((node) => { + const written = lowerer.mapTypeOf(lowerer.typeOf(node.right)); + return ( + written !== null && + written.kind !== "dyn" && + written.kind !== "union" && + !isUnitType(written) && + lowerer.dynConvertible(written) && + lowerer.dynConvertible(declared) && + !typeEquals(written, declared) && + (written.kind !== declared.kind || written.kind === "bytes") + ); + }); } function immediatelyGuardedAbsenceProbe(lowerer: Lowerer, decl: ts.VariableDeclaration): boolean { @@ -6900,6 +7047,36 @@ export function lowerExprStatement(lowerer: Lowerer, expr: ts.Expression): IrStm ); return { kind: "if", cond, then, else_, loc: locOf(expr) }; } + // A property write to a failed checked value in a fallback tail writes + // the dynamic object (checked-fallback.ts). + if ( + lowerer.fallbackTaint !== null && + ts.isBinaryExpression(expr) && + expr.operatorToken.kind === ts.SyntaxKind.EqualsToken && + ts.isPropertyAccessExpression(expr.left) && + !expr.left.questionDotToken && + fallbackTainted(lowerer, expr.left.expression) + ) { + const loc = locOf(expr); + const recv = lowerer.lowerExpr(expr.left.expression); + if (recv.type.kind === "dyn") { + return { + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "dyn.keySet", + args: [ + recv, + { kind: "strLit", value: expr.left.name.text, type: STRING, loc }, + lowerer.lowerExprExpecting(expr.right, DYN), + ], + type: VOID, + loc, + }, + loc, + }; + } + } // Assignment statements over the no-storage binding families: // - `f1 = f2` where the RHS roots at an ambient-undefined name: // Node evaluates the RHS first and dies on the root's @@ -7020,16 +7197,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, }; } @@ -11435,18 +11608,23 @@ export function lowerForStatement(lowerer: Lowerer, stmt: ts.ForStatement): IrSt } } -/** 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. */ +/** safe-buffer's `var Buffer = buffer.Buffer` (or `var pna = process`, the + * process-nextick-args binding once it compiles as process) at file scope: + * a `var` alias of a surfaced global other than the global object itself + * 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) + !( + bufferNamespaceMember(lowerer, decl.initializer) || + (stdlibGlobalAliasNameOf(lowerer, decl.initializer) ?? "globalThis") !== "globalThis" + ) ) return false; const symbol = lowerer.checker.getSymbolAtLocation(decl.name); diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index a9759b82f..4c0f484d9 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -32,8 +32,12 @@ 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"; +import { + isOptionalProcessStreamProperty, + isOptionalProcessVersionsRead, +} from "./builtins/process.js"; import { isSettledIteratorValueRead } from "./lower-generators.js"; +import { isRebindableFunctionSymbol } from "./rebindable-functions.js"; import { concreteCollectionNarrow } from "./collection-narrowing.js"; import { lowerWorkerMetadata } from "./builtins/workers.js"; import { fenceStaticBlobMember, lowerBlobNew, lowerStaticBlobCall } from "./builtins/blob.js"; @@ -135,6 +139,7 @@ import { earlyReadVarSymbols } from "./var-hoisting.js"; import { recordTextCodecClass } from "../../ir/ir.js"; import { assignLegacyTiers, type DynamicFunction } from "../../ir/tiers.js"; import { promoteJsvals } from "../../ir/jsval.js"; +import { fallbackTainted, type FallbackSite, type FallbackTaint } from "./checked-fallback.js"; import { analyzeTiers, dynamicStatementCount, @@ -166,6 +171,7 @@ import { resolve } from "node:path"; import { bindingSource } from "../binding-source.js"; import { tsgoPath } from "../dts-paths.js"; import * as ts from "../ts7/adapter.js"; +import { isBundledShimForwarder } from "../package-shims.js"; import { erasedTypeOnlyImport } from "../type-only-imports.js"; import { literalValues } from "../literal-values.js"; import { discriminantOwners, literalUnionArm, type UnionLiteral } from "../union-discriminants.js"; @@ -250,6 +256,7 @@ import { requireSpecOf, resolveImport, workspacePackageOfPath, + type CycleEdge, } from "../program.js"; import { containsRecord, @@ -802,6 +809,8 @@ export interface LowerOptions { startupCrash?: StartupCrash | null; /** LoadResult.inlineRequireCycleCalls. */ inlineRequireCycleCalls?: ReadonlySet | undefined; + /** LoadResult.moduleEdges. */ + moduleEdges?: ReadonlyMap | undefined; /** LIBRARY mode's reachability roots: the profile-mapped exports of the * entry module. Executable builds root at the entry's top level alone * (an unreferenced export dead-strips); a library's exports are called from @@ -999,11 +1008,15 @@ export function lowerToIr( ), ); }; + // Inline require() calls closing a cycle inside the appended subgraphs + // join preflight's set (the same run-time refusal by name). + const inlineRequireCycleCalls = new Set(options.inlineRequireCycleCalls ?? []); + const graph = { edges: options.moduleEdges, inlineCycleCalls: inlineRequireCycleCalls }; for (let pass = 0; pass < 32; pass++) { const before = moduleOrder.length; - appendDynamicImportModules(program, moduleOrder, onExtraCycle); - appendForkModules(program, moduleOrder, forkTargets, onExtraCycle); - appendWorkerModules(program, moduleOrder, workerTargets, onExtraCycle); + appendDynamicImportModules(program, moduleOrder, onExtraCycle, graph); + appendForkModules(program, moduleOrder, forkTargets, onExtraCycle, graph); + appendWorkerModules(program, moduleOrder, workerTargets, onExtraCycle, graph); if (moduleOrder.length === before) break; if (pass === 31) throw new Error("dynamic import and fork module discovery did not converge"); } @@ -1018,7 +1031,7 @@ export function lowerToIr( projectCheckJs: options.projectCheckJs ?? false, targetPlatform, startupCrash, - inlineRequireCycleCalls: options.inlineRequireCycleCalls, + inlineRequireCycleCalls, ffiImports, libraryCallbacks, forkTargets, @@ -1042,7 +1055,7 @@ export function lowerToIr( projectCheckJs: options.projectCheckJs ?? false, targetPlatform, startupCrash, - inlineRequireCycleCalls: options.inlineRequireCycleCalls, + inlineRequireCycleCalls, ffiImports, libraryCallbacks, forkTargets, @@ -1074,7 +1087,7 @@ export function lowerToIr( projectCheckJs: options.projectCheckJs ?? false, targetPlatform, startupCrash, - inlineRequireCycleCalls: options.inlineRequireCycleCalls, + inlineRequireCycleCalls, ffiImports, libraryCallbacks, forkTargets, @@ -1118,7 +1131,7 @@ export function lowerToIr( reachable, targetPlatform, startupCrash, - inlineRequireCycleCalls: options.inlineRequireCycleCalls, + inlineRequireCycleCalls, ffiImports, libraryCallbacks, forkTargets, @@ -1215,7 +1228,7 @@ export function lowerToIr( reachable, remainder: true, alreadyFlushed: resultLowerer.flushedSymbols, - inlineRequireCycleCalls: options.inlineRequireCycleCalls, + inlineRequireCycleCalls, targetPlatform, ffiImports, libraryCallbacks, @@ -1833,6 +1846,9 @@ export class Lowerer { * NaN, which lower to plain doubles (see nan-coded-reads.ts). Settled by * analyzeRuntimeOptionalArrayReads before any body lowers. */ nanCodedReads: NanCodedReads | null = null; + /** Whether any implementation source spells `versions` + * (programMentionsVersions in builtins/process.ts). */ + mentionsProcessVersions: boolean | undefined = undefined; private stringIndexBoundsCache: StringIndexBounds | null = null; /** Proofs that a string element read names an existing code unit. The * indexed-read analysis and expression lowering share one instance, so a @@ -1848,6 +1864,9 @@ export class Lowerer { /** Syntactic writes indexed once per JS scope. Symbol and RHS type * checks remain contextual to each binding probe. */ readonly jsBindingWritesByOwner = new Map>(); + /** JavaScript function declarations their own module reassigns + * (rebindable-functions.ts). */ + readonly rebindableFunctions = new Set(); /** TRAP bindings: declarations whose initializer provably throws before * producing a value (its chain roots at an ambient-undefined name — * ambientUndefVarRootOf). Module init unwinds at the declaration, so no @@ -2076,6 +2095,9 @@ export class Lowerer { /** Whether some program source can define a thenable * (programMayDefineThenables). Null until first asked. */ thenablesMayBeDefined: boolean | null = null; + /** Lambdas lowered with checked-dynamic parameters (lambdaSignature): + * callbacks whose arguments are wider than their contextual overload. */ + readonly dynParamLambdas = new Set(); /** Whether some server in this program may listen on a Unix socket: a * listen call whose first argument is a string. Computed once on first * use (null until then); address() answers the path only when true. */ @@ -2212,6 +2234,10 @@ export class Lowerer { runtimeOptionalBindingType(node: ts.Node): IrType | null; runtimeOptionalBindingType(node: ts.Node, fallback: IrType): IrType; runtimeOptionalBindingType(node: ts.Node, fallback?: IrType): IrType | null { + // A binding holding a failed checked value is dynamic: undefined is one + // of its values already (checked-fallback.ts). + if (this.fallbackTaint !== null && ts.isIdentifier(node) && fallbackTainted(this, node)) + return fallback ?? null; const patternType = this.runtimeOptionalPatternTypes.get(node); if (patternType) return patternType; const symbol = ts.isIdentifier(node) @@ -2611,6 +2637,14 @@ export class Lowerer { /** True while re-lowering a base function's 2nd+ instance: the same source * statements were already counted for the first instance. */ suppressStats = false; + /** Checked-cast fallback (checked-fallback.ts): sites opened by their + * root's declaration, by root; armed sites whose tails have not split + * yet; the taint of the dynamic tail being lowered; and the number of + * splits whose tails are being lowered (each lowers its tail twice). */ + fallbackSites = new Map(); + fallbackArmed: FallbackSite[] = []; + fallbackTaint: FallbackTaint | null = null; + fallbackSplitDepth = 0; /** Comparisons interned by operand representations and equality semantics. */ readonly equalityHelpers = new Map(); /** Synthetic array-HOF loop functions (map/filter/forEach desugar), @@ -5088,6 +5122,7 @@ export class Lowerer { if (isIndexedRead(e)) return true; if (ts.isPropertyAccessExpression(e) && isRecordIndexPropertyRead(e)) return true; if (ts.isPropertyAccessExpression(e) && isOptionalProcessStreamProperty(this, e)) return true; + if (isOptionalProcessVersionsRead(this, e)) return true; if (ts.isPropertyAccessExpression(e) && isSettledIteratorValueRead(this, e)) return true; if (ts.isPropertyAccessExpression(e) && isOsConstantsSymbol(symbolOf(e.expression), this)) return true; @@ -5380,19 +5415,40 @@ export class Lowerer { /** `const { a, b } = table` over an index-signature record: a name that * is not a declared field reads the index signature and can be * missing. */ - const scanRecordPattern = (name: ts.ObjectBindingPattern, init: ts.Expression): boolean => { - const sourceType = this.mapTypeOf(this.typeOf(init)); + const scanRecordPattern = ( + name: ts.ObjectBindingPattern, + init: ts.Node, + sourceType: IrType | null = this.mapTypeOf(this.typeOf(init)), + ): boolean => { if (sourceType?.kind !== "record") return false; const shape = this.shapes.get(sourceType.shapeId); - const indexValue = shape?.indexValue; - if (!shape || shape.tuple || !indexValue || indexValue.kind === "dyn") return false; + if (!shape || shape.tuple) return false; + const indexValue = shape.indexValue; let changed = false; for (const el of name.elements.nodes) { + // A nested pattern reads its element the same way + // (`const { limits: { [key]: limit } } = config`). + if (!el.dotDotDotToken && el.name !== undefined && ts.isObjectBindingPattern(el.name)) { + if (scanRecordPattern(el.name, init, this.mapTypeOf(this.typeOf(el.name)))) + changed = true; + continue; + } + if (!indexValue || indexValue.kind === "dyn") continue; if (el.dotDotDotToken || el.initializer || !ts.isIdentifier(el.name)) continue; const key = el.propertyName ?? el.name; - if (!ts.isIdentifier(key) && !ts.isStringLiteral(key) && !ts.isNumericLiteral(key)) + // A computed key that folds to one name reads like that name; a + // runtime key reads the entry of a record with no declared fields + // (runtimeKeyPatternRead), which can be missing too. + const keyName = ts.isComputedPropertyName(key) + ? this.foldedStringKeyOf(key.expression) + : ts.isIdentifier(key) || ts.isStringLiteral(key) || ts.isNumericLiteral(key) + ? key.text + : undefined; + if (keyName === undefined) continue; + if ( + keyName === null ? shape.fields.length > 0 : shape.fields.some((f) => f.name === keyName) + ) continue; - if (shape.fields.some((field) => field.name === key.text)) continue; const symbol = symbolOf(el.name); if (!symbol || optionalSymbols.has(symbol)) continue; const read = this.mapTypeOf(this.typeOf(el.name)); @@ -6003,6 +6059,7 @@ export class Lowerer { ts.walkPreorder(sf, (node) => { if ( ts.isVariableDeclaration(node) || + (ts.isParameter(node) && ts.isObjectBindingPattern(node.name)) || ts.isBinaryExpression(node) || ts.isCallExpression(node) || ts.isNewExpression(node) || @@ -6024,6 +6081,12 @@ export class Lowerer { if (elementStates.scan(node)) changed = true; if (ts.isForOfStatement(node)) presentLoops.set(node, !elementStates.loopMayYieldUndefined(node)); + // A parameter pattern reads its argument like a declaration's + // pattern reads its initializer. + if (ts.isParameter(node) && ts.isObjectBindingPattern(node.name)) { + if (scanRecordPattern(node.name, node, this.mapTypeOf(this.typeOf(node.name)))) + changed = true; + } if (ts.isVariableDeclaration(node)) { if (node.initializer) { const result = optionalPrimitiveResultType(node.initializer); @@ -7921,10 +7984,14 @@ export class Lowerer { ): IrStmt & { kind: "runtimeFence" } { const loc = locOf(node); const code = first?.code ?? fallback?.code ?? "SC1090"; - const baseMessage = + const plainMessage = first?.message ?? fallback?.message ?? "this statement uses a construct with no static lowering"; + const baseMessage = + this.fallbackTaint !== null + ? `a value that failed its checked cast reached code with no dynamic lowering: ${plainMessage}` + : plainMessage; const messageLoc = first?.loc ?? loc; const source = first ? (this.program.getSourceFile(messageLoc.file) ?? ts.getSourceFileOfNode(node)) @@ -8232,6 +8299,9 @@ export class Lowerer { } stdlibMemberFence(access: ts.PropertyAccessExpression): void { + // A member of a failed checked value reads dynamically, whatever the + // static member resolves to (checked-fallback.ts). + if (this.fallbackTaint !== null && fallbackTainted(this, access.expression)) return; return stdlibMemberFence(this, access); } @@ -8281,6 +8351,10 @@ export class Lowerer { } typeOf(node: ts.Node): ts.Type { + // The dynamic lowering of a checked-cast fallback tail reads values + // derived from the failed value as `any`. + if (this.fallbackTaint !== null && fallbackTainted(this, node)) + return this.checker.getAnyType(); if (ts.isPropertyAccessExpression(node) && isJsSourceFile(ts.getSourceFileOfNode(node))) { const receiver = node.expression.kind === ts.SyntaxKind.ThisKeyword && this.fnStack.length > 0 @@ -11686,6 +11760,9 @@ export class Lowerer { isSelfReference(ident: ts.Identifier): boolean { const ctx = this.ctx; if (!ctx.selfSymbol) return false; + // A reassigned JavaScript declaration names its module global, which + // may hold another function by the time the body runs. + if (isRebindableFunctionSymbol(this, ctx.selfSymbol)) return false; // The self binding can be shadowed by a scoped local of the same symbol? // No — a shadow is a different symbol; symbol identity is exact. return this.checker.getSymbolAtLocation(ident) === ctx.selfSymbol; @@ -11952,13 +12029,16 @@ export class Lowerer { readonly isNpmFile = (sf: ts.SourceFile): boolean => sf.isDeclarationFile && (sf.fileName.includes("/node_modules/") || workspacePackageOfPath(sf.fileName) !== null) && - !this.isStdlibFile(sf); + !this.isStdlibFile(sf) && + // A bundled copy's forwarder to a package shim types compiled code. + !isBundledShimForwarder(sf.fileName); npmPackageOf(type: ts.Type): string | null { return npmPackageOf(this, type); } npmMemberFence(access: ts.PropertyAccessExpression): void { + if (this.fallbackTaint !== null && fallbackTainted(this, access.expression)) return; return npmMemberFence(this, access); } @@ -12948,6 +13028,8 @@ export class Lowerer { loc, }; } + if (bi.module === "fs" && (bi.member === "realpathSync" || bi.member === "realpathSync.native")) + return fsRealpathValue(this, bi.member === "realpathSync.native", loc); const fn = builtinModuleFnOf(this, bi.module, bi.member); return fn ? this.lowerNativeCallableValue(fn, `${bi.module}.${bi.member}`, loc) : null; } @@ -13660,3 +13742,51 @@ function droppedMembers(lowerer: Lowerer, from: IrType, to: IrType, seen: Set ({ localId: id, name: id, type: DYN })); + lowerer.liftedFns.push({ + name, + params, + returnType: DYN, + locals: params.map((p) => ({ id: p.localId, name: p.name, type: DYN, mutable: false })), + loc, + body: [ + { + kind: "return", + value: { + kind: "libCall", + fn: "fs.realpathValue", + args: [ + { kind: "varRef", localId: "path", type: DYN, loc }, + { kind: "varRef", localId: "options", type: DYN, loc }, + { kind: "boolLit", value: native, type: BOOL, loc }, + ], + type: DYN, + loc, + }, + loc, + }, + ], + }); + } + return { + kind: "dynFrom", + value: { + kind: "closure", + fnName: name, + captures: [], + type: { kind: "func", params: [DYN, DYN], ret: DYN }, + loc, + }, + fnName: native ? "native" : "realpathSync", + type: DYN, + loc, + }; +} diff --git a/packages/compiler/src/frontend/lowering/rebindable-functions.ts b/packages/compiler/src/frontend/lowering/rebindable-functions.ts new file mode 100644 index 000000000..da41833a6 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/rebindable-functions.ts @@ -0,0 +1,64 @@ +/* JavaScript function declarations that their own module reassigns. + * + * Capability probes in published packages replace a function declaration + * with the implementation that works on the first call (yazl's + * `function bufferFrom(...) { bufferFrom = modern; ... }`). A top-level + * declaration normally lowers to a static function, which cannot change. + * A reassigned one instead becomes a mutable module global holding the + * function as a checked-dynamic value: the module initializer assigns it + * before its first statement (the declaration's hoisting), and reads, + * calls and writes of the name all go through the global. TypeScript + * rejects such writes, so only JavaScript sources take this path. */ +import * as ts from "../ts7/adapter.js"; +import { isJsSourceFile } from "../program.js"; +import type { Lowerer } from "./lowerer.js"; + +/** Top-level function declarations of `sf` that an assignment anywhere in + * the file writes, recorded for the queries below. */ +export function collectRebindableFunctions( + lowerer: Lowerer, + sf: ts.SourceFile, +): Set { + const found = new Set(); + if (!isJsSourceFile(sf)) return found; + const byName = new Map(); + for (const stmt of sf.statements.nodes) { + if ( + ts.isFunctionDeclaration(stmt) && + stmt.name && + stmt.body && + stmt.typeParameters === undefined + ) + byName.set(stmt.name.text, stmt); + } + if (byName.size === 0) return found; + ts.walkPreorder(sf, (node) => { + if ( + !ts.isBinaryExpression(node) || + node.operatorToken.kind !== ts.SyntaxKind.EqualsToken || + !ts.isIdentifier(node.left) + ) + return undefined; + const decl = byName.get(node.left.text); + if (!decl || found.has(decl)) return undefined; + const symbol = lowerer.checker.getSymbolAtLocation(node.left); + if (symbol && symbol.declarations.includes(decl)) found.add(decl); + return undefined; + }); + for (const decl of found) lowerer.rebindableFunctions.add(decl); + return found; +} + +export function isRebindableFunction(lowerer: Lowerer, decl: ts.Node | undefined): boolean { + return ( + decl !== undefined && ts.isFunctionDeclaration(decl) && lowerer.rebindableFunctions.has(decl) + ); +} + +/** Whether the symbol names a reassigned function declaration. */ +export function isRebindableFunctionSymbol( + lowerer: Lowerer, + symbol: ts.Symbol | undefined, +): boolean { + return symbol !== undefined && symbol.declarations.some((d) => isRebindableFunction(lowerer, d)); +} diff --git a/packages/compiler/src/frontend/lowering/static-typeof.ts b/packages/compiler/src/frontend/lowering/static-typeof.ts new file mode 100644 index 000000000..54f2fdfa0 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/static-typeof.ts @@ -0,0 +1,125 @@ +import * as ts from "../ts7/adapter.js"; +import type { Lowerer } from "./lowerer.js"; + +/** The answer `typeof ` gives without evaluating anything, for + * the global-detection idioms (`typeof window < "u"`, `typeof global !== + * "undefined"`): a name nothing declares answers "undefined" (Node's + * answer for an undeclared global such as window or self, and typeof never + * throws for one), and the Node global object and process answer + * "object". Null for every other operand. */ +export function staticTypeofAnswer(lowerer: Lowerer, operand: ts.Expression): string | null { + let e = operand; + while (ts.isParenthesizedExpression(e)) e = e.expression; + if (!ts.isIdentifier(e)) return null; + if (lowerer.checker.getSymbolAtLocation(e) === undefined) return "undefined"; + for (const name of ["global", "globalThis", "process"]) + if (lowerer.isStdlibGlobal(e, name)) return "object"; + return null; +} + +/** A comparison of a statically answered typeof against a string literal + * (`typeof window < "u"`, esbuild's minified undefined test, or + * `typeof self !== "undefined"`), folded: the operand read has no effects, + * so only the comparison's value remains. Null for other shapes. */ +export function staticTypeofComparison(lowerer: Lowerer, e: ts.BinaryExpression): boolean | null { + const unwrap = (x: ts.Expression): ts.Expression => { + while (ts.isParenthesizedExpression(x)) x = x.expression; + return x; + }; + const left = unwrap(e.left); + const right = unwrap(e.right); + const literal = (x: ts.Expression): string | null => + ts.isStringLiteral(x) || ts.isNoSubstitutionTemplateLiteral(x) ? x.text : null; + let answer: string | null = null; + let text: string | null = null; + let flipped = false; + if (ts.isTypeOfExpression(left) && literal(right) !== null) { + answer = staticTypeofAnswer(lowerer, left.expression); + text = literal(right); + } else if (ts.isTypeOfExpression(right) && literal(left) !== null) { + answer = staticTypeofAnswer(lowerer, right.expression); + text = literal(left); + flipped = true; + } + if (answer === null || text === null) return null; + const [a, b] = flipped ? [text, answer] : [answer, text]; + switch (e.operatorToken.kind) { + case ts.SyntaxKind.EqualsEqualsEqualsToken: + case ts.SyntaxKind.EqualsEqualsToken: + return a === b; + case ts.SyntaxKind.ExclamationEqualsEqualsToken: + case ts.SyntaxKind.ExclamationEqualsToken: + return a !== b; + case ts.SyntaxKind.LessThanToken: + return a < b; + case ts.SyntaxKind.LessThanEqualsToken: + return a <= b; + case ts.SyntaxKind.GreaterThanToken: + return a > b; + case ts.SyntaxKind.GreaterThanEqualsToken: + return a >= b; + default: + return null; + } +} + +/** The value of a condition built from statically answered typeof + * comparisons, `!`, `&&` and `||` (the global-detection chains), or null + * when any operand needs evaluation. */ +export function staticCondition(lowerer: Lowerer, expr: ts.Expression): boolean | null { + let e = expr; + while (ts.isParenthesizedExpression(e)) e = e.expression; + if (ts.isPrefixUnaryExpression(e) && e.operator === ts.SyntaxKind.ExclamationToken) { + const inner = staticCondition(lowerer, e.operand); + return inner === null ? null : !inner; + } + if (!ts.isBinaryExpression(e)) return null; + const op = e.operatorToken.kind; + if (op === ts.SyntaxKind.AmpersandAmpersandToken || op === ts.SyntaxKind.BarBarToken) { + const isAnd = op === ts.SyntaxKind.AmpersandAmpersandToken; + const left = staticCondition(lowerer, e.left); + if (left === null) return null; + if (left !== isAnd) return left; + return staticCondition(lowerer, e.right); + } + return staticTypeofComparison(lowerer, e); +} + +/** The expression a value reduces to once statically decided conditions + * select their arms (`(typeof global < "u" ? global : window)` → `global`), + * through parentheses. */ +export function staticallySelected(lowerer: Lowerer, expr: ts.Expression): ts.Expression { + let e = expr; + for (;;) { + while (ts.isParenthesizedExpression(e)) e = e.expression; + if (!ts.isConditionalExpression(e)) return e; + const taken = staticCondition(lowerer, e.condition); + if (taken === null) return e; + e = taken ? e.whenTrue : e.whenFalse; + } +} + +/** True when `stmt` declares a binding that is visible outside it: a `var` + * or a function declaration, outside nested functions. Such an arm still + * lowers when its condition is decided, for the hoisted binding. */ +export function declaresHoistedBinding(stmt: ts.Statement): boolean { + let found = false; + const visit = (n: ts.Node): void => { + if (found) return; + if (ts.isFunctionDeclaration(n)) { + found = true; + return; + } + if (ts.isFunctionLike(n) || ts.isClassDeclaration(n) || ts.isClassExpression(n)) return; + if ( + ts.isVariableDeclarationList(n) && + (n.flags & (ts.NodeFlags.Let | ts.NodeFlags.Const)) === 0 + ) { + found = true; + return; + } + ts.forEachChild(n, visit); + }; + visit(stmt); + return found; +} diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts index a884b39e1..cf7a3c8f0 100644 --- a/packages/compiler/src/frontend/lowering/surfaces.ts +++ b/packages/compiler/src/frontend/lowering/surfaces.ts @@ -1,5 +1,11 @@ import * as ts from "../ts7/adapter.js"; import type { Lowerer } from "./lowerer.js"; +import { staticCondition, staticallySelected } from "./static-typeof.js"; + +function unparenthesized(e: ts.Expression): ts.Expression { + while (ts.isParenthesizedExpression(e)) e = e.expression; + return e; +} import { UNSUPPORTED } from "../../diagnostics/diagnostic.js"; import { BOOL, @@ -2068,6 +2074,9 @@ export const BUILTIN_MODULE_FN_ALIASES: Record< readdirSync: ["fs.readdirTypesSync"], // The { throwIfNoEntry } probe form. statSync: ["fs.statProbeSync"], + // realpathSync and realpathSync.native read as function values. + realpathSync: ["fs.realpathValue"], + "realpathSync.native": ["fs.realpathValue"], lstatSync: ["fs.lstatProbeSync"], globSync: ["fs.globTypesSync"], // The JS-source validation-ladder variant (lowerFsLadderCall): the @@ -3343,6 +3352,14 @@ export function isStdlibSymbol(lowerer: Lowerer, symbol: ts.Symbol | undefined): * builtinClassAliasOf). */ export function stdlibGlobalNameOf(lowerer: Lowerer, expr: ts.Expression): string | null { if (ts.isParenthesizedExpression(expr)) return stdlibGlobalNameOf(lowerer, expr.expression); + // The global-object detection chain (`typeof global < "u" ? global : + // typeof window < "u" ? window : {}`): a condition with static typeof + // answers selects one arm, and only that arm ever evaluates. + if (ts.isConditionalExpression(expr)) { + const taken = staticCondition(lowerer, expr.condition); + if (taken === null) return null; + return stdlibGlobalNameOf(lowerer, taken ? expr.whenTrue : expr.whenFalse); + } if (ts.isIdentifier(expr)) { const symbol = lowerer.checker.getSymbolAtLocation(expr); // `globalThis` itself: a reserved intrinsic — tsc rejects user @@ -3393,7 +3410,14 @@ export function stdlibGlobalNameOf(lowerer: Lowerer, expr: ts.Expression): strin } if (ts.isPropertyAccessExpression(expr) && !expr.questionDotToken) { if (stdlibGlobalNameOf(lowerer, expr.expression) !== "globalThis") return null; - const symbol = lowerer.checker.getSymbolAtLocation(expr.name); + // Through a detection chain the checker types the receiver as the + // union of every arm; the selected arm (the global object) owns the + // member. + const selected = staticallySelected(lowerer, expr.expression); + const symbol = + selected === expr.expression || ts.isIdentifier(unparenthesized(expr.expression)) + ? lowerer.checker.getSymbolAtLocation(expr.name) + : lowerer.checker.getPropertyOfType(lowerer.typeOf(selected), expr.name.text); if (!symbol || !lowerer.isStdlibSymbol(symbol)) return null; return ts.symbolName(symbol) === "global" ? "globalThis" : ts.symbolName(symbol); } diff --git a/packages/compiler/src/frontend/npm-static-builtin-aliases.ts b/packages/compiler/src/frontend/npm-static-builtin-aliases.ts new file mode 100644 index 000000000..f1e3d53d3 --- /dev/null +++ b/packages/compiler/src/frontend/npm-static-builtin-aliases.ts @@ -0,0 +1,68 @@ +/* Requires of polyfill packages that ARE a Node builtin on Node 24 + * (builtin-package-aliases.ts) compile as the builtin expression, at the + * require's position. Two spellings reach here: + * + * - an installed package: `require("isarray")` (or a subpath such as + * `require("readable-stream/transform")`) in an opted-in package's file, + * matched against the version installed where Node's resolution from + * that file would find it; + * - a bundled __commonJS chunk whose path key names the package and its + * version (npm-static-bundled-chunks.ts): each `require_x()` call. + * + * For a module whose export carries another object's members + * (readable-stream's entry is stream.Readable with Stream's classes copied + * onto it), the replacement depends on how the result is used: member reads + * of the carried names read the builtin module's members, value uses read + * the class itself, and a mixture keeps the package's own JavaScript. */ +import * as ts from "./ts7/syntax.js"; +import { tokenStartOfNode } from "./ts7/node-functions.js"; +import { + aliasReplacement, + applyEdits, + countDeclarations, + unparen, + type Edit, +} from "./npm-static-bundled-chunks.js"; +import { BUILTIN_ALIAS_PACKAGES, type BuiltinAlias } from "./builtin-package-aliases.js"; +import { packageNameOfSpecifier } from "./workspace-registry.js"; + +/** True when the text may require an aliased package (a cheap probe). */ +export function mayRequireBuiltinAlias(source: string): boolean { + if (!source.includes("require(")) return false; + for (const name of BUILTIN_ALIAS_PACKAGES) if (source.includes(name)) return true; + return false; +} + +/** The module text with each `require("[/subpath]")` + * respelled as its builtin, or null when none is. `aliasFor` answers the + * alias for a specifier as resolved from this file. */ +export function rewriteBuiltinAliasRequires( + sf: ts.SourceFile, + aliasFor: (specifier: string) => BuiltinAlias | null, +): string | null { + const edits: Edit[] = []; + const declared = countDeclarations(sf, "require") > 0; + if (declared) return null; + ts.walkPreorder(sf, (n) => { + if (n.kind >= ts.SyntaxKind.FirstJSDocNode && n.kind <= ts.SyntaxKind.LastJSDocNode) + return "skip"; + if ( + !ts.isCallExpression(n) || + !ts.isIdentifier(n.expression) || + n.expression.text !== "require" || + n.arguments.nodes.length !== 1 + ) + return undefined; + const arg = unparen(n.arguments.nodes[0]!); + if (!ts.isStringLiteral(arg) && !ts.isNoSubstitutionTemplateLiteral(arg)) return undefined; + const spec = arg.text; + if (!BUILTIN_ALIAS_PACKAGES.has(packageNameOfSpecifier(spec))) return undefined; + const alias = aliasFor(spec); + if (alias === null) return undefined; + const text = aliasReplacement(n, alias); + if (text !== null) edits.push({ start: tokenStartOfNode(n), end: n.end, text }); + return undefined; + }); + if (edits.length === 0) return null; + return applyEdits(sf.text, 0, sf.text.length, edits); +} diff --git a/packages/compiler/src/frontend/npm-static-bundled-chunks.test.ts b/packages/compiler/src/frontend/npm-static-bundled-chunks.test.ts index e6dfe9a56..0779db3ce 100644 --- a/packages/compiler/src/frontend/npm-static-bundled-chunks.test.ts +++ b/packages/compiler/src/frontend/npm-static-bundled-chunks.test.ts @@ -316,3 +316,126 @@ console.log(typeof require_stream().PassThrough, require_user().same); expect(answer.chunks.map((c) => basename(c.path))).toEqual(["index.js.require_user.cjs"]); expect(answer.text).toContain('typeof require("stream").PassThrough'); }); + +test("chunks of polyfill packages that are Node builtins become the builtins", () => { + const key = (name: string, version: string, file: string) => + `node_modules/.pnpm/${name}@${version}/node_modules/${name}/${file}`; + const answer = expectSameBehavior(`${keyedHelper}var require_isarray = __commonJS({ + "${key("isarray", "1.0.0", "index.js")}"(exports2, module2) { + var toString = {}.toString; + module2.exports = Array.isArray || function(arr) { + return toString.call(arr) == "[object Array]"; + }; + } +}); +var require_inherits = __commonJS({ + "${key("inherits", "2.0.4", "inherits.js")}"(exports2, module2) { + module2.exports = require("util").inherits; + } +}); +var require_old = __commonJS({ + "${key("isarray", "3.0.0", "index.js")}"(exports2, module2) { + module2.exports = function isArray() { return "own"; }; + } +}); +var require_user = __commonJS({ + "lib/user.js"(exports2) { + var isArray = require_isarray(); + var inherits = require_inherits(); + exports2.same = isArray === Array.isArray && inherits === require("util").inherits; + } +}); +console.log(require_user().same, require_isarray()([1]), require_old()()); +`); + expect(answer.chunks.map((c) => basename(c.path))).toEqual([ + "index.js.require_old.cjs", + "index.js.require_user.cjs", + ]); + expect(answer.text).toContain("Array.isArray([1])"); + const user = answer.chunks.find((c) => c.path.endsWith("require_user.cjs"))!; + expect(user.source).toContain("var isArray = Array.isArray;"); + expect(user.source).toContain('var inherits = require("util").inherits;'); +}); + +test("a polyfill chunk keeps its module when the bundle declares the builtin's global", () => { + const key = + "node_modules/.pnpm/process-nextick-args@2.0.1/node_modules/process-nextick-args/index.js"; + const answer = expectSameBehavior(`${keyedHelper}var require_pna = __commonJS({ + "${key}"(exports2, module2) { + module2.exports = process; + } +}); +var require_user = __commonJS({ + "lib/user.js"(exports2) { + var process = { local: true }; + var pna = require_pna(); + exports2.same = pna === globalThis.process && process.local; + } +}); +console.log(require_user().same); +`); + expect(answer.chunks.map((c) => basename(c.path))).toEqual([ + "index.js.require_pna.cjs", + "index.js.require_user.cjs", + ]); + const user = answer.chunks.find((c) => c.path.endsWith("require_user.cjs"))!; + expect(user.source).not.toContain("var pna = process;"); +}); + +test("a readable-stream 2.x entry chunk follows its uses", () => { + const key = (file: string) => + `node_modules/.pnpm/readable-stream@2.3.8/node_modules/readable-stream/${file}`; + const chunk = (name: string, file: string, body: string) => + `var ${name} = __commonJS({\n "${key(file)}"(exports2, module2) {\n ${body}\n }\n});\n`; + const result = + split(`${keyedHelper}${chunk("require_readable", "readable.js", 'module2.exports = require("stream").Readable;')}${chunk("require_transform", "transform.js", "module2.exports = require_readable().Transform;")} +var members = require_readable(); +console.log(members.Readable, members.Writable); +var value = require_readable(); +console.log(value.from); +var { Duplex } = require_readable(); +console.log(require_transform()); +`); + const answer = result as BundledChunkSplit; + expect(answer.chunks).toEqual([]); + expect(answer.text).toContain('var members = require("stream");'); + expect(answer.text).toContain('var value = require("stream").Readable;'); + expect(answer.text).toContain('var { Duplex } = require("stream");'); + expect(answer.text).toContain('console.log(require("stream").Transform);'); + // A use the alias cannot answer keeps the package's own module for + // every call site of that chunk. + const mixed = + split(`${keyedHelper}${chunk("require_readable", "readable.js", 'module2.exports = require("stream").Readable;')} +var members = require_readable(); +console.log(members.Readable); +var mixed = require_readable(); +console.log(mixed.Readable, mixed.from); +`) as BundledChunkSplit; + expect(mixed.chunks.map((c) => basename(c.path))).toEqual(["index.js.require_readable.cjs"]); + expect(mixed.text).toContain('var members = require("./index.js.require_readable.cjs");'); + expect(mixed.text).toContain('var mixed = require("./index.js.require_readable.cjs");'); +}); + +test("a bundled copy of a shimmed package's main file requires the package shim", () => { + const key = (version: string, file: string) => + `node_modules/.pnpm/fs-extra@${version}/node_modules/fs-extra/${file}`; + const result = split(`${keyedHelper}var require_lib = __commonJS({ + "${key("9.0.0", "lib/index.js")}"(exports2, module2) { + module2.exports = { readFile() { return "bundled"; } }; + } +}); +var require_old = __commonJS({ + "${key("7.0.1", "lib/index.js")}"(exports2, module2) { + module2.exports = { readFile() { return "old"; } }; + } +}); +console.log(require_lib().readFile(), require_old().readFile()); +`) as BundledChunkSplit; + const forwarder = result.chunks.find((c) => c.path.endsWith(".shim.d.ts")); + expect(forwarder?.path).toBe("/pkg/dist/index.js.require_lib.shim.d.ts"); + expect(forwarder?.source).toMatch(/export \* from ".*shims\/fs-extra\/index\.ts";/); + expect(forwarder?.source).toMatch(/export \{ default \} from ".*shims\/fs-extra\/index\.ts";/); + expect(result.text).toContain('require("./index.js.require_lib.shim").readFile()'); + // fs-extra 7 has no shim: its chunk stays a module. + expect(result.chunks.map((c) => basename(c.path))).toContain("index.js.require_old.cjs"); +}); diff --git a/packages/compiler/src/frontend/npm-static-bundled-chunks.ts b/packages/compiler/src/frontend/npm-static-bundled-chunks.ts index b625c456d..37a74f2be 100644 --- a/packages/compiler/src/frontend/npm-static-bundled-chunks.ts +++ b/packages/compiler/src/frontend/npm-static-bundled-chunks.ts @@ -25,6 +25,12 @@ import * as ts from "./ts7/syntax.js"; import { helperTokens } from "./helper-tokens.js"; import { canonicalBuiltinModule } from "./builtin-modules.js"; import { getTextOfNode, tokenStartOfNode } from "./ts7/node-functions.js"; +import { bundledShimForwarder, packageShimForBundledFile } from "./package-shims.js"; +import { + builtinAliasOf, + versionedPackageOfKey, + type BuiltinAlias, +} from "./builtin-package-aliases.js"; export interface BundledChunkSplit { /** The bundle with chunk declarations blanked and calls respelled. */ @@ -464,6 +470,8 @@ interface Chunk { body: ts.Block; path: string; spec: string; + /** The keyed helper form's module key (the bundled file's path). */ + key: string | null; } interface TopLevel { @@ -584,12 +592,14 @@ export function splitBundledChunks( ) return { degrade: CHUNK_SHAPE_DEGRADE }; let factory: ts.Node = call.arguments.nodes[0]!; + let key: string | null = null; if (form === "keyed") { if (!ts.isObjectLiteralExpression(factory) || factory.properties.nodes.length !== 1) return { degrade: CHUNK_SHAPE_DEGRADE }; factory = factory.properties.nodes[0]!; if (!ts.isMethodDeclaration(factory) || ts.isComputedPropertyName(factory.name)) return { degrade: CHUNK_SHAPE_DEGRADE }; + if (ts.isStringLiteral(factory.name)) key = factory.name.text; } else { factory = unparen(factory as ts.Expression); if (!ts.isArrowFunction(factory) && !ts.isFunctionExpression(factory)) @@ -618,6 +628,7 @@ export function splitBundledChunks( body: fn.body, path: filePath.slice(0, filePath.length - base.length) + file, spec: `./${file}`, + key, }); } if (chunks.size === 0) return null; @@ -625,9 +636,12 @@ export function splitBundledChunks( // Every factory reference is a direct zero-argument call. Calls inside a // chunk body belong to that chunk's text, the rest to the bundle's. const callEdits: Edit[] = []; + const callNames: string[] = []; + const unaliased = new Set(); for (const r of allRefs) { const chunk = chunks.get(r.text); if (chunk === undefined) continue; + callNames.push(chunk.name); const call = r.parent; if ( call === undefined || @@ -639,13 +653,30 @@ export function splitBundledChunks( return { degrade: `its bundled CommonJS module factory '${r.text}' is used other than as a direct call`, }; - const forwarded = forwarderSpec(chunk); + const alias = builtinAliasOfChunk(chunk); + const aliased = alias === null ? null : aliasReplacement(call, alias); + if (alias !== null && aliased === null) unaliased.add(chunk.name); + const forwarded = + forwarderSpec(chunk) ?? (shimOfChunk(chunk) !== null ? shimSpec(chunk) : null); callEdits.push({ start: tokenStartOfNode(call), end: call.end, - text: `require(${JSON.stringify(forwarded ?? chunk.spec)})`, + text: aliased ?? `require(${JSON.stringify(forwarded ?? chunk.spec)})`, }); } + // A chunk keeps its module when any call site's use left the alias + // undetermined; every call site then requires the module. + if (unaliased.size > 0) { + for (let i = 0; i < callEdits.length; i++) { + const name = callNames[i]!; + if (!unaliased.has(name)) continue; + const chunk = chunks.get(name)!; + callEdits[i] = { + ...callEdits[i]!, + text: `require(${JSON.stringify(forwarderSpec(chunk) ?? chunk.spec)})`, + }; + } + } for (const name of chunks.keys()) { if (declaredAnywhere.has(name) && countDeclarations(sf, name) > 1) return { degrade: CHUNK_SHAPE_DEGRADE }; @@ -655,6 +686,23 @@ export function splitBundledChunks( const out: BundledChunkSplit["chunks"] = []; const bundleEdits: Edit[] = [...callEdits]; for (const chunk of chunks.values()) { + if (!unaliased.has(chunk.name) && builtinAliasOfChunk(chunk) !== null) { + // A chunk of a package that IS a builtin on Node 24 is that builtin. + bundleEdits.push({ start: tokenStartOfNode(chunk.stmt), end: chunk.stmt.end, text: "" }); + continue; + } + const shim = shimOfChunk(chunk); + if (shim !== null) { + // A bundled copy of a package scriptc implements itself is the + // package's shim (package-shims.ts), reached through a forwarder + // declaration the checker follows. + out.push({ + path: shimDeclPath(chunk), + source: bundledShimForwarder(shimDeclPath(chunk), shim), + }); + bundleEdits.push({ start: tokenStartOfNode(chunk.stmt), end: chunk.stmt.end, text: "" }); + continue; + } if (forwarderSpec(chunk) !== null) { // A chunk that only re-exports a builtin module is that module. bundleEdits.push({ start: tokenStartOfNode(chunk.stmt), end: chunk.stmt.end, text: "" }); @@ -678,7 +726,7 @@ export function splitBundledChunks( return { text: applyEdits(source, 0, source.length, bundleEdits), chunks: out }; } -export function countDeclarations(sf: ts.SourceFile, name: string): number { +export function countDeclarations(sf: ts.Node, name: string): number { let n = 0; ts.walkPreorder(sf, (node) => { if ( @@ -720,6 +768,132 @@ export function applyEdits(source: string, from: number, to: number, edits: Edit return text + source.slice(at, to); } +/* Use analysis for a require of a builtin-aliased module + * (npm-static-builtin-aliases.ts). */ + +function isAssignmentTarget(node: ts.Node): boolean { + const p = node.parent; + return ( + p !== undefined && + ts.isBinaryExpression(p) && + p.left === node && + p.operatorToken.kind >= ts.SyntaxKind.FirstAssignment && + p.operatorToken.kind <= ts.SyntaxKind.LastAssignment + ); +} + +/** The outermost expression the call's value flows through unchanged + * (parentheses). */ +function valueNode(call: ts.Expression): ts.Expression { + let e: ts.Expression = call; + while (e.parent !== undefined && ts.isParenthesizedExpression(e.parent)) e = e.parent; + return e; +} + +type Use = "member" | "value" | "invalid"; + +/** How one occurrence of the module value is used: a read of a carried + * member, any other value use (member reads of other names included, which + * the class itself answers), or a write that keeps the package. */ +function useOf(node: ts.Expression, names: ReadonlySet): Use { + const p = node.parent; + if (p !== undefined && ts.isPropertyAccessExpression(p) && p.expression === node) { + if (isAssignmentTarget(p) || p.questionDotToken !== undefined) return "invalid"; + return names.has(p.name.text) ? "member" : "value"; + } + if (p !== undefined && ts.isElementAccessExpression(p) && p.expression === node) return "invalid"; + return "value"; +} + +function scopeRoot(node: ts.Node): ts.Node { + for (let p = node.parent; p !== undefined; p = p.parent) { + if (ts.isFunctionLike(p) || ts.isSourceFile(p)) return p; + } + return node; +} + +/** The text replacing a require of an aliased module, or null when its uses + * do not determine one or when the file declares the global the text + * starts with (`process`, `Array`): the text would read that binding + * instead of the builtin. `require` roots are the require the call site + * already uses. */ +export function aliasReplacement(call: ts.CallExpression, alias: BuiltinAlias): string | null { + const text = aliasUseReplacement(call, alias); + if (text === null) return null; + const root = /^[A-Za-z_$][\w$]*/.exec(text)?.[0]; + if ( + root !== undefined && + root !== "require" && + countDeclarations(ts.getSourceFileOfNode(call), root) > 0 + ) + return null; + return text; +} + +function aliasUseReplacement(call: ts.CallExpression, alias: BuiltinAlias): string | null { + const members = alias.members; + if (members === undefined) return alias.whole; + const whole = alias.whole; + const node = valueNode(call); + const p = node.parent; + let uses: Use[]; + if (p !== undefined && ts.isVariableDeclaration(p) && p.initializer === node) { + if (ts.isObjectBindingPattern(p.name)) { + const ok = p.name.elements.nodes.every((el) => { + if (el.dotDotDotToken !== undefined) return false; + const key = el.propertyName ?? el.name; + return ts.isIdentifier(key) && members.names.has(key.text); + }); + return ok ? members.object : null; + } + if (!ts.isIdentifier(p.name)) return null; + const name = p.name.text; + const root = scopeRoot(p); + if (countDeclarations(root as ts.SourceFile, name) !== 1) return null; + const list = p.parent; + // `var` hoisting: a read before the declaration sees undefined. + if (list === undefined || !ts.isVariableDeclarationList(list)) return null; + uses = []; + for (const ref of references(root)) { + if (ref.text !== name) continue; + if (isWritten(ref) || ref.pos < p.end) return null; + uses.push(useOf(ref, members.names)); + } + } else { + uses = [useOf(node, members.names)]; + } + if (uses.includes("invalid")) return null; + if (uses.length > 0 && uses.every((u) => u === "member")) return members.object; + if (uses.every((u) => u === "value")) return whole; + return null; +} + +/** The builtin a chunk's module is on Node 24, when its key names an + * aliased package at a checked version (builtin-package-aliases.ts). */ +function builtinAliasOfChunk(chunk: Chunk): BuiltinAlias | null { + if (chunk.key === null) return null; + const pkg = versionedPackageOfKey(chunk.key); + return pkg === null ? null : builtinAliasOf(pkg.name, pkg.version, pkg.subpath); +} + +/** The shim module standing in for a chunk that is a bundled copy of a + * shimmed package's main file at a supported version, or null. */ +function shimOfChunk(chunk: Chunk): string | null { + if (chunk.key === null) return null; + const pkg = versionedPackageOfKey(chunk.key); + return pkg === null ? null : packageShimForBundledFile(pkg.name, pkg.version, pkg.subpath); +} + +/** The forwarder declaration beside the bundle for a shimmed chunk, and + * the specifier its call sites require. */ +function shimDeclPath(chunk: Chunk): string { + return chunk.path.replace(/\.cjs$/, ".shim.d.ts"); +} + +function shimSpec(chunk: Chunk): string { + return chunk.spec.replace(/\.cjs$/, ".shim"); +} + /** The builtin module a chunk forwards, or null: a body that is exactly * `module.exports = require("")` (readable-stream's * internal/streams/stream.js). Requiring the chunk is then requiring the diff --git a/packages/compiler/src/frontend/npm-static-bundled-esm.test.ts b/packages/compiler/src/frontend/npm-static-bundled-esm.test.ts index 381538087..c5b556821 100644 --- a/packages/compiler/src/frontend/npm-static-bundled-esm.test.ts +++ b/packages/compiler/src/frontend/npm-static-bundled-esm.test.ts @@ -228,3 +228,140 @@ test("files without __esm initializers and ES-module bundles are left alone", () lower(`${helpers}var init_a = __esm({ "a.ts"() {} });\ninit_a();\nexport {};\n`), ).toBeNull(); }); + +test("a class an initializer defines into a module binding becomes a class declaration", () => { + const lowered = expectSameBehavior(`"use strict"; +${helpers}var import_stream, Shout, Loud, Plain; +var init_shout = __esm({ + "src/shout.ts"() { + "use strict"; + import_stream = require("stream"); + console.log("shout init"); + Shout = class extends import_stream.Transform { + _transform(chunk, encoding, callback) { + callback(null, String(chunk).toUpperCase()); + } + }; + Loud = class Loud extends Shout { + static of() { return new Loud(); } + }, Plain = class { + label() { return "plain"; } + }; + console.log("after", typeof Shout, Loud.name, new Plain().label()); + } +}); +function main() { + init_shout(); + const s = Loud.of(); + s.on("data", (c) => console.log("data", String(c))); + s.end("hi"); +} +console.log("start"); +main(); +`); + expect(lowered).toContain('class Shout extends require("stream").Transform'); + expect(lowered).toContain("class Loud extends Shout"); + expect(lowered).toContain("class Plain "); + // The bindings' declarators are gone with the interop binding's. + expect(lowered).not.toMatch(/var [^;=]*\b(?:import_stream|Shout|Loud|Plain)\b/); +}); + +test("classes whose definition or binding is observable stay assignments", () => { + const lowered = expectSameBehavior(`${helpers}var A, B, C, D; +var init_m = __esm({ + "src/m.ts"() { + A = class { static made = console.log("static field runs at definition") }; + B = class extends globalThis.Array {}; + console.log(typeof D); + D = class {}; + C = class {}; + } +}); +console.log(typeof C); +init_m(); +console.log(typeof A, typeof B, typeof C, typeof D); +`); + expect(lowered).toContain("A = class"); + expect(lowered).toContain("B = class"); + expect(lowered).toContain("C = class"); + expect(lowered).toContain("D = class"); +}); + +test("an interop binding a new expression constructs through stays the callee", () => { + const lowered = expectSameBehavior(`${helpers}function make() { + return new import_events.EventEmitter(); +} +var import_events; +var init_m = __esm({ + "src/m.ts"() { + import_events = require("events"); + } +}); +init_m(); +console.log(typeof make().on); +`); + expect(lowered).toContain('new (require("events")).EventEmitter()'); +}); + +test("a hoisted class may extend a global constructor, past esbuild's export annotation", () => { + const lowered = expectSameBehavior(`${helpers}var AppError; +var init_errors = __esm({ + "src/errors.ts"() { + AppError = class extends Error { + code; + constructor(message, code) { + super(message); + this.code = code; + } + }; + } +}); +function fail() { + init_errors(); + throw new AppError("failed", "E1"); +} +function main() { + try { + fail(); + } catch (err) { + console.log(err instanceof AppError, err instanceof Error, err.code, err.message); + } +} +main(); +0 && (module.exports = { AppError }); +`); + expect(lowered).toContain("class AppError extends Error"); +}); + +test("an initializer declared last in a shared var statement still hoists its classes", () => { + const lowered = expectSameBehavior(`"use strict"; +${helpers}var BuildError, hints, init_errors = __esm({ + "src/errors.ts"() { + "use strict"; + BuildError = class extends Error { + constructor(code) { + super("failed: " + code); + this.code = code; + } + }, hints = { retry: "try again" }; + } +}); +function run() { + init_errors(); + try { + throw new BuildError("E2"); + } catch (err) { + console.log(err instanceof BuildError, err.code, err.message, hints.retry); + } +} +run(); +`); + expect(lowered).toContain("class BuildError extends Error"); +}); + +test("a minified initializer that ends with its class still hoists it", () => { + const lowered = expectSameBehavior( + `${helpers}var Counter,init_counter=__esm({"src/counter.ts"(){console.log("counter init"),Counter=class{n=0;next(){return++this.n}}}});function run(){init_counter();const c=new Counter;c.next();console.log(c.next())}run();`, + ); + expect(lowered).toContain("class Counter{"); +}); diff --git a/packages/compiler/src/frontend/npm-static-bundled-esm.ts b/packages/compiler/src/frontend/npm-static-bundled-esm.ts index ac572000d..83cb1c1e1 100644 --- a/packages/compiler/src/frontend/npm-static-bundled-esm.ts +++ b/packages/compiler/src/frontend/npm-static-bundled-esm.ts @@ -29,6 +29,7 @@ import * as ts from "./ts7/syntax.js"; import { bareRequireSpecOf } from "./cjs-syntax.js"; import { helperTokens } from "./helper-tokens.js"; +import { canonicalBuiltinModule } from "./builtin-modules.js"; import { applyEdits, countDeclarations, @@ -37,6 +38,7 @@ import { helperBase, isWritten, references, + strictPrologue, unparen, type Edit, } from "./npm-static-bundled-chunks.js"; @@ -46,6 +48,19 @@ export interface EsmInitHost { requireTargetEsModuleStamped(fromFile: string, specifier: string): boolean; } +/** A top-level binding of the bundle: its statement and declaration. */ +interface Top { + stmt: ts.Statement; + node: ts.VariableDeclaration | ts.FunctionDeclaration; +} + +/** A lazy initializer: its declaration's `__esm(...)` call and body. */ +interface InitBody { + name: string; + call: ts.CallExpression; + body: ts.Block; +} + const ESM_SHAPE_DEGRADE = "its shipped JS initializes bundled ES modules through the __esm helper in a shape the " + "recognizer cannot verify"; @@ -134,6 +149,17 @@ function removeDeclarators(sf: ts.SourceFile, dead: ReadonlySet(); const duplicate = new Set(); for (const stmt of sf.statements.nodes) { @@ -311,12 +333,7 @@ export function lowerBundledEsmInits( } // The initializers: `NAME = HELPER(factory)` declarators. - interface Init { - name: string; - call: ts.CallExpression; - body: ts.Block; - } - const inits = new Map(); + const inits = new Map(); for (const [name, t] of top) { if (!ts.isVariableDeclaration(t.node) || t.node.initializer === undefined) continue; const call = unparen(t.node.initializer); @@ -383,6 +400,7 @@ export function lowerBundledEsmInits( else list.push(r); } const deadDeclarators = new Set(); + const interopSpecs = new Map(); for (const init of inits.values()) { for (const stmt of init.body.statements.nodes) { if (!ts.isExpressionStatement(stmt)) continue; @@ -444,14 +462,27 @@ export function lowerBundledEsmInits( } if (defaultIsModule && access.name.text === "default") uses.push({ start: tokenStartOfNode(access), end: access.end, text: `(${req})` }); - else uses.push({ start: tokenStartOfNode(r), end: r.end, text: req }); + // `new import_x.C()` must stay `new (require("x")).C()`: a bare + // `new require("x").C()` constructs the require call itself. + else + uses.push({ + start: tokenStartOfNode(r), + end: r.end, + text: inNewCallee(r) ? `(${req})` : req, + }); } if (!ok) continue; edits.push({ start: tokenStartOfNode(el), end: el.end, text: req }, ...uses); deadDeclarators.add(decl); + interopSpecs.set(name, spec); } } } + const hoisted = hoistInitClasses(sf, inits, top, duplicate, writes, refsByName, interopSpecs); + for (const h of hoisted.values()) { + edits.push(...h.edits); + for (const d of h.declarators) deadDeclarators.add(d); + } /* Namespaces of lazily initialized modules read through members. */ let mainTarget: string | null = null; @@ -813,14 +844,231 @@ export function lowerBundledEsmInits( { start: tokenStartOfNode(call), end: tokenStartOfNode(body) + 1, - text: "(() => { let __scriptc_done = false; function __scriptc_init() {", + text: `(() => { let __scriptc_done = false; function __scriptc_init() {`, }, { start: body.end - 1, end: call.end, - text: "} return function () { if (__scriptc_done) return; __scriptc_done = true; __scriptc_init(); }; })()", + // A body split around hoisted classes ends in its last tail function. + text: hoisted.has(init.name) ? "}" : `} ${RUN_ONCE_TAIL}`, }, ); } return applyEdits(source, 0, source.length, edits); } + +/** Global constructors a hoisted class may extend: reading one runs no + * code. */ +const GLOBAL_BASES: ReadonlySet = new Set([ + "Error", + "TypeError", + "RangeError", + "SyntaxError", + "ReferenceError", + "EvalError", + "URIError", + "AggregateError", + "Array", + "Map", + "Set", + "Promise", + "Object", +]); + +/** The run-once closure's closing text after the initializer function. */ +const RUN_ONCE_TAIL = + "return function () { if (__scriptc_done) return; __scriptc_done = true; __scriptc_init(); }; })()"; + +/* Classes an initializer defines into a module binding, + * + * var Multi; + * var init_m = __esm({ "src/m.ts"() { Multi = class extends import_stream.Readable { … }; } }); + * + * become class declarations in the bundle's scope, at the same text + * position: the initializer's body splits around each class into run-once + * pieces that call one another in order (`… __scriptc_rest_init_m_1(); } + * return …; })(); class Multi extends … { … } function + * __scriptc_rest_init_m_1() { … }`), so the class's own text stays where + * it was and only its binding changes. A stored class value with a + * runtime-provided base has no static representation, while a declared + * class does. + * + * Exact where defining the class earlier is unobservable: its heritage is + * absent, a builtin module member, or a class hoisted before it; it has no + * static field initializers, static blocks or computed member names, so + * evaluating the definition runs no code; the binding is declared once, + * assigned only there, and read only inside functions (which esbuild's + * code reaches only after the initializer ran, the assumption the interop + * respelling makes too); and the body declares nothing, so its statements + * can move between functions. */ +function hoistInitClasses( + sf: ts.SourceFile, + inits: ReadonlyMap, + top: ReadonlyMap, + duplicate: ReadonlySet, + writes: ReadonlyMap, + refsByName: ReadonlyMap, + interopSpecs: ReadonlyMap, +): Map { + const out = new Map(); + const hoistedNames = new Set(); + /** esbuild's `0 && (module.exports = { … })` annotation for Node's + * CommonJS lexer: code that never runs. */ + const inDeadAnnotation = (n: ts.Node): boolean => { + for (let child: ts.Node = n, p = n.parent; p !== undefined; child = p, p = p.parent) { + if ( + ts.isBinaryExpression(p) && + p.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken && + p.right === child && + ts.isNumericLiteral(unparen(p.left)) && + (unparen(p.left) as ts.NumericLiteral).text === "0" + ) + return true; + } + return false; + }; + const insideFunction = (n: ts.Node): boolean => { + for (let p = n.parent; p !== undefined && !ts.isSourceFile(p); p = p.parent) + if (ts.isFunctionLike(p)) return true; + return false; + }; + const heritageOk = (cls: ts.ClassExpression): boolean => { + const clause = cls.heritageClauses?.nodes.find((c) => c.token === ts.SyntaxKind.ExtendsKeyword); + if (clause === undefined) return true; + const type = clause.types.nodes[0]; + if (type === undefined || clause.types.nodes.length !== 1 || type.typeArguments !== undefined) + return false; + let root: ts.Expression = type.expression; + while (ts.isPropertyAccessExpression(root) && !root.questionDotToken) root = root.expression; + if (ts.isIdentifier(root)) { + if (root === type.expression) + return ( + hoistedNames.has(root.text) || + // A standard global constructor the bundle never rebinds + // (`class extends Error`, an error base class). + (GLOBAL_BASES.has(root.text) && countDeclarations(sf, root.text) === 0) + ); + const spec = interopSpecs.get(root.text); + return spec !== undefined && canonicalBuiltinModule(spec) !== null; + } + const spec = bareRequireSpecOf(root); + return spec !== null && canonicalBuiltinModule(spec) !== null && root !== type.expression; + }; + const definitionInert = (cls: ts.ClassExpression): boolean => + (cls.modifiers?.nodes.length ?? 0) === 0 && + cls.members.nodes.every((m) => { + if (ts.isClassStaticBlockDeclaration(m)) return false; + if (m.name !== undefined && ts.isComputedPropertyName(m.name)) return false; + if (m.modifiers?.nodes.some((x) => x.kind === ts.SyntaxKind.Decorator)) return false; + const isStatic = m.modifiers?.nodes.some((x) => x.kind === ts.SyntaxKind.StaticKeyword); + return !(isStatic && ts.isPropertyDeclaration(m)); + }); + + for (const init of inits.values()) { + const stmts = init.body.statements.nodes; + if (stmts.some((st) => !ts.isExpressionStatement(st) && !ts.isEmptyStatement(st))) continue; + // The initializer's declarator is its statement's last one. + const declarator = init.call.parent; + const list = declarator?.parent; + if ( + declarator === undefined || + !ts.isVariableDeclaration(declarator) || + list === undefined || + !ts.isVariableDeclarationList(list) || + // The initializer ends its statement: the split pieces follow it + // (`var A, B, init_x = __esm(…)` keeps A and B declared before). + list.declarations.nodes[list.declarations.nodes.length - 1] !== declarator || + unparen(declarator.initializer!) !== init.call + ) + continue; + const strict = strictPrologue(stmts) ? '"use strict"; ' : ""; + const edits: Edit[] = []; + const declarators: ts.VariableDeclaration[] = []; + let piece = 0; + for (const stmt of stmts) { + if (!ts.isExpressionStatement(stmt)) continue; + const els = commaElements(stmt.expression); + const hoistedEls = new Set(); + for (let i = 0; i < els.length; i++) { + const el = els[i]!; + if ( + !ts.isBinaryExpression(el) || + el.operatorToken.kind !== ts.SyntaxKind.EqualsToken || + !ts.isIdentifier(el.left) || + !ts.isClassExpression(el.right) + ) + continue; + const name = el.left.text; + const cls = el.right; + const target = el.left; + const decl = top.get(name)?.node; + if ( + decl === undefined || + !ts.isVariableDeclaration(decl) || + decl.initializer !== undefined || + duplicate.has(name) || + writes.get(name) !== 1 || + (cls.name !== undefined && cls.name.text !== name) || + // The binding's declarator (and the class's own name, if spelled). + countDeclarations(sf, name) !== (cls.name === undefined ? 1 : 2) || + !definitionInert(cls) || + !heritageOk(cls) || + !(refsByName.get(name) ?? []).every( + (r) => + r === target || + // The class's own methods name the class itself. + (tokenStartOfNode(r) >= tokenStartOfNode(cls) && r.end <= cls.end) || + inDeadAnnotation(r) || + (insideFunction(r) && + // The initializer's own earlier code reads the unassigned binding. + !( + tokenStartOfNode(r) >= tokenStartOfNode(init.body) && tokenStartOfNode(r) < el.end + )), + ) + ) + continue; + piece++; + const rest = `__scriptc_rest_${init.name}_${piece}`; + const close = piece === 1 ? `} ${RUN_ONCE_TAIL};` : "}"; + // `a, X = class …, b`: the elements around the class end and begin + // the neighboring pieces. + // After a hoisted neighbor the separating comma is already gone. + const fresh = i === 0 || hoistedEls.has(els[i - 1]!); + const from = + i === 0 ? tokenStartOfNode(stmt) : fresh ? tokenStartOfNode(el) : els[i - 1]!.end; + const sep = fresh ? "" : ";"; + hoistedEls.add(el); + edits.push({ + start: from, + end: tokenStartOfNode(cls), + text: `${sep} ${rest}(); ${close} `, + }); + const next = els[i + 1]; + // The class's own closing brace carries the next piece's opener: an + // empty insertion at its end would share its position with the + // initializer body's closing edit in minified text (`…}}})`). + edits.push( + next === undefined + ? { + start: cls.end - 1, + end: cls.end, + text: `} function ${rest}() { ${strict}`, + } + : { + start: cls.end, + end: tokenStartOfNode(next), + text: ` function ${rest}() { ${strict}`, + }, + ); + if (cls.name === undefined) { + const keyword = tokenStartOfNode(cls); + edits.push({ start: keyword, end: keyword + "class".length, text: `class ${name}` }); + } + declarators.push(decl); + hoistedNames.add(name); + } + } + if (piece > 0) out.set(init.name, { edits, declarators }); + } + return out; +} diff --git a/packages/compiler/src/frontend/npm-static-enum-iife.test.ts b/packages/compiler/src/frontend/npm-static-enum-iife.test.ts new file mode 100644 index 000000000..b27c53345 --- /dev/null +++ b/packages/compiler/src/frontend/npm-static-enum-iife.test.ts @@ -0,0 +1,135 @@ +import { spawnSync } from "node:child_process"; +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterAll, expect, test } from "vitest"; +import { nodeFrontendServices } from "./services-node.js"; + +const dirs: string[] = []; +afterAll(() => { + for (const dir of dirs) rmSync(dir, { recursive: true, force: true }); +}); + +function rewrite(source: string): string | null { + return nodeFrontendServices().enumIifes(source, "/pkg/index.js"); +} + +function run(file: string): string { + const result = spawnSync(process.execPath, [file], { encoding: "utf8" }); + expect(result.status).toBe(0); + return result.stdout; +} + +const probe = ` +for (const name of Object.keys(module.exports)) { + const v = module.exports[name]; + console.log(name, JSON.stringify(v), Object.keys(v ?? {}).join(",")); +} +`; + +/** The rewritten module exports what the original exports, keys in the + * same order. */ +function expectSameExports(source: string): string { + const result = rewrite(source); + expect(typeof result).toBe("string"); + const dir = mkdtempSync(join(tmpdir(), "scriptc-enum-iife-")); + dirs.push(dir); + const original = join(dir, "original.cjs"); + const rewritten = join(dir, "rewritten.cjs"); + writeFileSync(original, source + probe); + writeFileSync(rewritten, result! + probe); + expect(run(rewritten)).toBe(run(original)); + // Line positions survive the respelling. + expect(result!.split("\n").length).toBe(source.split("\n").length); + return result!; +} + +test("a TypeScript 5 exported string enum becomes its object literal", () => { + const out = expectSameExports(`"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.Kind = void 0; +var Kind; +(function (Kind) { + Kind["Identifier"] = "Identifier"; + Kind["Literal"] = "Literal"; +})(Kind || (exports.Kind = Kind = {})); +`); + expect(out).toContain(`var Kind = { "Identifier": "Identifier", "Literal": "Literal" };`); + expect(out).toContain("exports.Kind = Kind;"); +}); + +test("numeric enums keep their reverse mapping and Node's key order", () => { + const out = expectSameExports(`"use strict"; +var Level; +(function (Level) { + Level[Level["High"] = 2] = "High"; + Level[Level["Low"] = 0] = "Low"; + Level[Level["Below"] = -1] = "Below"; + Level[Level["Also"] = 2] = "Also"; +})(Level = exports.Level || (exports.Level = {})); +`); + expect(out).toContain( + `var Level = { 0: "Low", 2: "Also", "High": 2, "Low": 0, "Below": -1, "-1": "Below", "Also": 2 };`, + ); +}); + +test("an ES module's exported enum keeps its export", () => { + const source = `export var Kind; +(function (Kind) { + Kind["Identifier"] = "Identifier"; + Kind[Kind["Zero"] = 0] = "Zero"; +})(Kind || (Kind = {})); +`; + const out = rewrite(source); + expect(out).toContain(`export var Kind = { 0: "Zero", "Identifier": "Identifier", "Zero": 0 };`); + const dir = mkdtempSync(join(tmpdir(), "scriptc-enum-iife-")); + dirs.push(dir); + writeFileSync(join(dir, "original.mjs"), source); + writeFileSync(join(dir, "rewritten.mjs"), out!); + const reader = (file: string) => + `import { Kind } from "./${file}"; console.log(JSON.stringify(Kind), Object.keys(Kind).join(","));`; + writeFileSync(join(dir, "read-original.mjs"), reader("original.mjs")); + writeFileSync(join(dir, "read-rewritten.mjs"), reader("rewritten.mjs")); + expect(run(join(dir, "read-rewritten.mjs"))).toBe(run(join(dir, "read-original.mjs"))); + expect(out!.split("\n").length).toBe(source.split("\n").length); +}); + +test("an enum the module does not export becomes a local object", () => { + const out = expectSameExports(`"use strict"; +var Mode; +(function (Mode) { + Mode.Fast = "fast"; +})(Mode || (Mode = {})); +exports.mode = Mode.Fast; +`); + expect(out).toContain(`var Mode = { "Fast": "fast" };`); +}); + +test("merged, computed and otherwise written enums keep their text", () => { + // A second declaration merges into the first. + expect( + rewrite(`var E; +(function (E) { E["A"] = "a"; })(E || (E = {})); +(function (E) { E["B"] = "b"; })(E || (E = {})); +`), + ).toBeNull(); + // A member referencing another member. + expect( + rewrite(`var F; +(function (F) { F[F["A"] = 1] = "A"; F[F["B"] = F.A << 1] = "B"; })(F || (exports.F = F = {})); +`), + ).toBeNull(); + // A statement other than a member write. + expect( + rewrite(`var G; +(function (G) { G["A"] = "a"; console.log("side effect"); })(G || (exports.G = G = {})); +`), + ).toBeNull(); + // The variable is reassigned later. + expect( + rewrite(`var H; +(function (H) { H["A"] = "a"; })(H || (exports.H = H = {})); +H = null; +`), + ).toBeNull(); +}); diff --git a/packages/compiler/src/frontend/npm-static-enum-iife.ts b/packages/compiler/src/frontend/npm-static-enum-iife.ts new file mode 100644 index 000000000..e1b9220a9 --- /dev/null +++ b/packages/compiler/src/frontend/npm-static-enum-iife.ts @@ -0,0 +1,285 @@ +/* TypeScript's enum emit in a compiled package: a bare `var` filled by an + * immediately invoked function that writes each member onto its argument. + * + * var Kind; + * (function (Kind) { + * Kind["Identifier"] = "Identifier"; + * Kind[Kind["Zero"] = 0] = "Zero"; + * })(Kind || (exports.Kind = Kind = {})); + * + * Inference types the variable from the empty object the argument creates, + * so every member read lands on `{}`. When every member value is a literal, + * the enum is that object literal: the declaration becomes + * `var Kind = { ... };` and the call becomes the export assignment it + * performs. Keys keep Node's own-property order (integer keys ascending, + * then the rest in insertion order, a later write replacing an earlier + * value in place), so enumeration observes the same object. + * + * Accepted argument spellings: `Kind || (exports.Kind = Kind = {})` + * (TypeScript 5), `Kind = exports.Kind || (exports.Kind = {})` (earlier + * releases) and `Kind || (Kind = {})` (an enum the module does not + * export). Merged declarations, computed or referencing members, and any + * other write to the variable keep the original text. */ +import * as ts from "./ts7/syntax.js"; +import { tokenStartOfNode } from "./ts7/node-functions.js"; +import { applyEdits, unparen, type Edit } from "./npm-static-bundled-chunks.js"; + +/** True when the text may hold an enum IIFE (a cheap probe). */ +export function mayHoldEnumIife(source: string): boolean { + return source.includes("(function (") && source.includes("|| ("); +} + +/** The module text with each literal-valued enum IIFE respelled as an + * object literal, or null when there is none. */ +export function rewriteEnumIifes(sf: ts.SourceFile): string | null { + const stmts = sf.statements.nodes; + const edits: Edit[] = []; + for (let i = 1; i < stmts.length; i++) { + const decl = bareVarName(stmts[i - 1]!); + if (decl === null) continue; + const iife = enumIife(stmts[i]!, decl); + if (iife === null || writtenElsewhere(sf, decl, stmts[i]!)) continue; + // Modifiers before `var` stay: `export var Kind;` (an ES module's + // enum) keeps exporting the binding. + const declaration = stmts[i - 1] as ts.VariableStatement; + const modifiers = sf.text.slice( + tokenStartOfNode(declaration), + tokenStartOfNode(declaration.declarationList), + ); + edits.push({ + start: tokenStartOfNode(declaration), + end: declaration.end, + text: `${modifiers}var ${decl} = ${objectLiteral(iife.members)};`, + }); + edits.push({ + start: tokenStartOfNode(stmts[i]!), + end: stmts[i]!.end, + text: iife.exportName === null ? "" : `exports.${iife.exportName} = ${decl};`, + }); + } + if (edits.length === 0) return null; + return applyEdits(sf.text, 0, sf.text.length, edits); +} + +/** `var Name;`: one declarator, no initializer. */ +function bareVarName(stmt: ts.Statement): string | null { + if (!ts.isVariableStatement(stmt)) return null; + const list = stmt.declarationList; + if ((list.flags & (ts.NodeFlags.Let | ts.NodeFlags.Const)) !== 0) return null; + const decls = list.declarations.nodes; + if (decls.length !== 1) return null; + const d = decls[0]!; + if (d.initializer !== undefined || !ts.isIdentifier(d.name)) return null; + return d.name.text; +} + +type Members = Map; + +function enumIife( + stmt: ts.Statement, + name: string, +): { members: Members; exportName: string | null } | null { + if (!ts.isExpressionStatement(stmt)) return null; + const call = unparen(stmt.expression); + if (!ts.isCallExpression(call) || call.arguments.nodes.length !== 1) return null; + const fn = unparen(call.expression); + if (!ts.isFunctionExpression(fn) || fn.name !== undefined || fn.asteriskToken !== undefined) + return null; + if (fn.modifiers !== undefined && fn.modifiers.nodes.length > 0) return null; + const params = fn.parameters.nodes; + if (params.length !== 1) return null; + const param = params[0]!; + if (!ts.isIdentifier(param.name) || param.initializer !== undefined) return null; + if (param.dotDotDotToken !== undefined) return null; + const exportName = enumArgument(call.arguments.nodes[0]!, name); + if (exportName === undefined) return null; + const members = enumMembers(fn.body.statements.nodes, param.name.text); + return members === null ? null : { members, exportName }; +} + +/** The export name the argument assigns (null: not exported), or + * undefined when it is not one of the enum spellings. */ +function enumArgument(arg: ts.Expression, name: string): string | null | undefined { + const e = unparen(arg); + if (!ts.isBinaryExpression(e)) return undefined; + // `Kind = exports.Kind || (exports.Kind = {})` + if (e.operatorToken.kind === ts.SyntaxKind.EqualsToken) { + if (!isName(e.left, name)) return undefined; + const or = unparen(e.right); + if (!ts.isBinaryExpression(or) || or.operatorToken.kind !== ts.SyntaxKind.BarBarToken) + return undefined; + const exported = exportsMember(or.left); + if (exported === null) return undefined; + const init = unparen(or.right); + if ( + !ts.isBinaryExpression(init) || + init.operatorToken.kind !== ts.SyntaxKind.EqualsToken || + exportsMember(init.left) !== exported || + !isEmptyObject(init.right) + ) + return undefined; + return exported; + } + if (e.operatorToken.kind !== ts.SyntaxKind.BarBarToken || !isName(e.left, name)) return undefined; + const init = unparen(e.right); + if (!ts.isBinaryExpression(init) || init.operatorToken.kind !== ts.SyntaxKind.EqualsToken) + return undefined; + // `Kind || (Kind = {})` + if (isName(init.left, name)) return isEmptyObject(init.right) ? null : undefined; + // `Kind || (exports.Kind = Kind = {})` + const exported = exportsMember(init.left); + const inner = unparen(init.right); + if ( + exported === null || + !ts.isBinaryExpression(inner) || + inner.operatorToken.kind !== ts.SyntaxKind.EqualsToken || + !isName(inner.left, name) || + !isEmptyObject(inner.right) + ) + return undefined; + return exported; +} + +/** The members the body writes, or null when a statement is anything but + * a literal member write (`E["K"] = v`, `E.K = v`, or the reverse-mapped + * `E[E["K"] = v] = "K"`). */ +function enumMembers(body: readonly ts.Statement[], param: string): Members | null { + const members: Members = new Map(); + for (const stmt of body) { + if (!ts.isExpressionStatement(stmt)) return null; + const e = stmt.expression; + if (!ts.isBinaryExpression(e) || e.operatorToken.kind !== ts.SyntaxKind.EqualsToken) + return null; + const value = literalValue(e.right); + if (value === null) return null; + const key = memberKey(e.left, param); + if (key !== null) { + members.set(key, value); + continue; + } + // E[E["K"] = v] = "K" + const target = e.left; + if (!ts.isElementAccessExpression(target) || !isName(target.expression, param)) return null; + const inner = unparen(target.argumentExpression); + if (!ts.isBinaryExpression(inner) || inner.operatorToken.kind !== ts.SyntaxKind.EqualsToken) + return null; + const innerKey = memberKey(inner.left, param); + const innerValue = literalValue(inner.right); + if (innerKey === null || innerValue === null) return null; + members.set(innerKey, innerValue); + members.set(propertyKey(innerValue), value); + } + return members.size > 0 ? members : null; +} + +function memberKey(target: ts.Expression, param: string): string | null { + if (ts.isElementAccessExpression(target) && isName(target.expression, param)) { + const k = unparen(target.argumentExpression); + return ts.isStringLiteral(k) || ts.isNoSubstitutionTemplateLiteral(k) ? k.text : null; + } + if ( + ts.isPropertyAccessExpression(target) && + isName(target.expression, param) && + ts.isIdentifier(target.name) + ) + return target.name.text; + return null; +} + +function literalValue(expr: ts.Expression): string | number | null { + const e = unparen(expr); + if (ts.isStringLiteral(e) || ts.isNoSubstitutionTemplateLiteral(e)) return e.text; + if (ts.isNumericLiteral(e)) return numberOf(e.text); + if ( + ts.isPrefixUnaryExpression(e) && + e.operator === ts.SyntaxKind.MinusToken && + ts.isNumericLiteral(e.operand) + ) { + const n = numberOf(e.operand.text); + return n === null ? null : -n; + } + return null; +} + +function numberOf(text: string): number | null { + if (!/^(?:\d+(?:\.\d+)?(?:[eE][+-]?\d+)?|0[xX][0-9a-fA-F]+)$/.test(text)) return null; + const n = Number(text); + return Number.isFinite(n) ? n : null; +} + +/** ToPropertyKey of a member value. */ +function propertyKey(value: string | number): string { + return typeof value === "string" ? value : String(value); +} + +/** The canonical array-index keys JavaScript enumerates first. */ +function isIndexKey(key: string): boolean { + return /^(?:0|[1-9]\d*)$/.test(key) && Number(key) < 4294967295; +} + +function objectLiteral(members: Members): string { + const keys = [...members.keys()]; + const ordered = [ + ...keys.filter(isIndexKey).sort((a, b) => Number(a) - Number(b)), + ...keys.filter((k) => !isIndexKey(k)), + ]; + const parts = ordered.map((k) => { + const v = members.get(k)!; + const value = typeof v === "string" ? JSON.stringify(v) : Object.is(v, -0) ? "-0" : String(v); + return `${isIndexKey(k) ? k : JSON.stringify(k)}: ${value}`; + }); + return `{ ${parts.join(", ")} }`; +} + +/** True when the module writes `name` anywhere outside the enum call. */ +function writtenElsewhere(sf: ts.SourceFile, name: string, iife: ts.Statement): boolean { + const start = tokenStartOfNode(iife); + const end = iife.end; + let written = false; + ts.walkPreorder(sf, (n) => { + if (written) return "stop"; + if (n.pos >= start && n.end <= end) return "skip"; + if (ts.isBinaryExpression(n) && isAssignmentOperator(n.operatorToken.kind)) { + if (isName(n.left, name)) written = true; + } else if ( + (ts.isPrefixUnaryExpression(n) || ts.isPostfixUnaryExpression(n)) && + (n.operator === ts.SyntaxKind.PlusPlusToken || + n.operator === ts.SyntaxKind.MinusMinusToken) && + isName(n.operand, name) + ) { + written = true; + } else if ( + (ts.isFunctionDeclaration(n) || ts.isClassDeclaration(n)) && + n.name !== undefined && + n.name.text === name + ) { + written = true; + } + return undefined; + }); + return written; +} + +function isAssignmentOperator(kind: ts.SyntaxKind): boolean { + return kind >= ts.SyntaxKind.FirstAssignment && kind <= ts.SyntaxKind.LastAssignment; +} + +function isName(e: ts.Expression, name: string): boolean { + const u = unparen(e); + return ts.isIdentifier(u) && u.text === name; +} + +function exportsMember(e: ts.Expression): string | null { + const u = unparen(e); + return ts.isPropertyAccessExpression(u) && + ts.isIdentifier(u.expression) && + u.expression.text === "exports" && + ts.isIdentifier(u.name) + ? u.name.text + : null; +} + +function isEmptyObject(e: ts.Expression): boolean { + const u = unparen(e); + return ts.isObjectLiteralExpression(u) && u.properties.nodes.length === 0; +} diff --git a/packages/compiler/src/frontend/npm-static.ts b/packages/compiler/src/frontend/npm-static.ts index ae8a4a8f4..1bf84e734 100644 --- a/packages/compiler/src/frontend/npm-static.ts +++ b/packages/compiler/src/frontend/npm-static.ts @@ -4,6 +4,7 @@ * Eligibility failures and package preflight errors become explicit refusals. */ import { dirname } from "node:path"; import type { FrontendServices } from "./services.js"; +import { isBundledShimForwarder } from "./package-shims.js"; import type { NpmStaticDeclarationOverloads, NpmStaticDeclarationProperties, @@ -451,16 +452,28 @@ export function npmStaticFsShadow(services: FrontendServices): NpmStaticFsShadow } else if (esm !== null) { original = esm; } + // Polyfill packages that ARE a Node builtin compile as the builtin. + const aliased = original === null ? null : services.builtinAliasRequires(original, path); + if (aliased !== null) original = aliased; // A module exporting one of its functions reaches its members // through that function. const fnExports = original === null ? null : services.cjsFunctionExports(original, path); if (fnExports !== null) original = fnExports; + // TypeScript's enum IIFEs with literal members become the object + // literals they build. + const enums = original === null ? null : services.enumIifes(original, path); + if (enums !== null) original = enums; // A module that names its export object through a variable reaches // its members through `exports`. const exportsVar = original === null ? null : services.cjsExportsVar(original, path); if (exportsVar !== null) original = exportsVar; const respelled = - split !== null || esm !== null || fnExports !== null || exportsVar !== null; + split !== null || + esm !== null || + aliased !== null || + fnExports !== null || + enums !== null || + exportsVar !== null; if (original !== null) { const namepaths = services.jsDocNamepaths(path, original); const source = namepaths ?? original; @@ -510,12 +523,43 @@ export function npmStaticFsShadow(services: FrontendServices): NpmStaticFsShadow hideFile: (path) => { const target = shadowTargetOf(path); if (!target) return false; + if (isBundledShimForwarder(path)) return false; if (target.viaTypes) return true; // the @types twin hides whole return isDeclarationFileName(path) || isUnloadableInstalledTsSource(path); }, }; } +const programAliasCache = new Map(); + +/** The always-on program-file half of the builtin aliases: a CommonJS + * program file's `require("")` compiles as the builtin + * the package is on Node 24 (npm-static-builtin-aliases.ts), as in an + * opted-in package's files. Undefined for every other path, so TypeScript + * programs and JavaScript without such a require read the file as is. */ +export function programBuiltinAliasSource( + services: FrontendServices, + path: string, +): string | undefined { + if (!/\.c?js$/.test(path) || isNodeModulesPathNorm(path)) return undefined; + const hit = programAliasCache.get(path); + if (hit !== undefined) return hit ?? undefined; + let rewritten: string | null = null; + try { + const source = trackedReadFile(path); + if (source !== null) rewritten = services.builtinAliasRequires(source, path); + } catch { + rewritten = null; + } + programAliasCache.set(path, rewritten); + return rewritten ?? undefined; +} + +/** Resets the program-file alias cache (once per program load). */ +export function clearProgramBuiltinAliasCache(): void { + programAliasCache.clear(); +} + /* Only a bundler RUNTIME disqualifies now: webpack's module registry has * no static story (the graph lives in numbered closures). The tsc/babel * __esModule stamp and esbuild's helper preambles used to disqualify too, 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..86a41d3a6 --- /dev/null +++ b/packages/compiler/src/frontend/package-shims.ts @@ -0,0 +1,323 @@ +/** 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. + * + * - builtin alias: a polyfill package whose module IS a Node builtin on + * Node 24 at its installed major version (builtin-package-aliases.ts). + * A require of it compiles as the builtin expression at the require's + * position (npm-static-builtin-aliases.ts), so 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"; +import { + BUILTIN_ALIAS_PACKAGES, + builtinAliasOf, + type BuiltinAlias, +} from "./builtin-package-aliases.js"; + +interface ImplementationShim { + /** npm package name. */ + name: string; + /** Supported major versions and their entry modules, relative to the + * shim root. */ + versions: ReadonlyMap; + /** The entry has a default export (a CommonJS package's whole export + * object, which a default import reads). */ + defaultExport?: boolean; + /** The package's main file in every supported version: a bundler's copy + * of it (a __commonJS chunk whose key names the package, its version + * and this file) is the package itself. */ + mainFile: string; +} + +const IMPLEMENTATION_SHIMS: ReadonlyMap = new Map([ + [ + "fs-extra", + { + name: "fs-extra", + defaultExport: true, + mainFile: "lib/index.js", + versions: new Map([ + [8, "fs-extra/index.ts"], + [9, "fs-extra/index.ts"], + [10, "fs-extra/index.ts"], + [11, "fs-extra/index.ts"], + ]), + }, + ], + [ + "undici", + { + name: "undici", + mainFile: "index.js", + versions: new Map([ + [5, "undici/v5.ts"], + [7, "undici/v7.ts"], + ]), + }, + ], +]); + +/** The shim module that stands in for a bundled copy of a package's main + * file (a __commonJS chunk keyed `…/@/…//`), or + * null when the package, version or file is not one the registry serves. */ +export function packageShimForBundledFile( + name: string, + version: string, + file: string, +): string | null { + const shim = IMPLEMENTATION_SHIMS.get(name); + if (shim === undefined || file.replace(/^\.\//, "") !== shim.mainFile) return null; + const major = Number(/^(\d+)\./.exec(version)?.[1] ?? NaN); + const entry = shim.versions.get(major); + return entry === undefined ? null : join(packageShimRoot(), entry); +} + +/** Bundled-copy forwarders: a virtual declaration module beside a bundle + * (`..shim.d.ts`) that re-exports the shim standing in + * for a bundled package, so the checker loads the shim through it, and + * module resolution answers the shim itself for its path. */ +const bundledForwarders = new Map(); + +/** The forwarder declaration text for a bundled copy of a shimmed package + * and records its path; the bundle's call sites require the forwarder's + * extensionless path. */ +export function bundledShimForwarder(declPath: string, shimPath: string): string { + bundledForwarders.set(declPath.split("\\").join("/"), shimPath); + const target = JSON.stringify(tsgoPath(shimPath)); + const shim = [...IMPLEMENTATION_SHIMS.values()].find((entry) => + [...entry.versions.values()].some((v) => join(packageShimRoot(), v) === shimPath), + ); + const defaultLine = shim?.defaultExport === true ? `export { default } from ${target};\n` : ""; + return `// scriptc: this bundled package compiles from its built-in implementation.\nexport * from ${target};\n${defaultLine}`; +} + +/** True for a bundled-copy forwarder's path (the --npm-static shadow keeps + * it visible where it hides the package's other declaration files). */ +export function isBundledShimForwarder(path: string): boolean { + return bundledForwarders.has(path.split("\\").join("/")); +} + +/** The shim a resolved module path forwards to, or the path itself. */ +export function throughBundledShimForwarder(resolved: string): string { + return bundledForwarders.get(resolved.split("\\").join("/")) ?? resolved; +} + +/** Every implementation module the registry ships (absolute paths), for + * the guard test that compiles each one. */ +export function packageShimEntries(): string[] { + const entries = new Set(); + for (const shim of IMPLEMENTATION_SHIMS.values()) + for (const entry of shim.versions.values()) entries.add(join(packageShimRoot(), entry)); + return [...entries].sort(); +} + +/** 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); +} + +/** The builtin a bare specifier's module is on Node 24, as installed where + * Node's resolution from `fromFile` finds it, or null. A package the + * project marks unloaded has no alias: its require throws MODULE_NOT_FOUND. */ +export function builtinPackageAliasFor(fromFile: string, specifier: string): BuiltinAlias | null { + if (specifier.startsWith(".") || specifier.startsWith("/") || specifier.startsWith("#")) + return null; + if (isUnloadedPackage(specifier)) return null; + const name = packageNameOfSpecifier(specifier); + if (!BUILTIN_ALIAS_PACKAGES.has(name)) return null; + const dir = installedPackageDir(fromFile, name); + if (dir === null) return null; + const text = trackedReadFile(join(dir, "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 (pkg.name !== name || typeof pkg.version !== "string") return null; + return builtinAliasOf(name, pkg.version, specifier.slice(name.length).replace(/^\//, "")); +} + +/** 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); + const shim = IMPLEMENTATION_SHIMS.get(name); + if (shim === undefined) return undefined; + // A DefinitelyTyped twin (`@types/fs-extra`) types a package that ships no + // declarations. Nothing at its path names the installed runtime version, + // so it reads as the shim only when every supported version shares one + // module. + const viaTypes = dirname(pkgDir).endsWith("/@types"); + const entries = new Set(shim.versions.values()); + const entry = viaTypes + ? entries.size === 1 + ? join(packageShimRoot(), [...entries][0]!) + : null + : shimEntryForPackageDir(pkgDir, name); + if (entry === null) return undefined; + const target = JSON.stringify(tsgoPath(entry)); + const defaultLine = shim.defaultExport === true ? `export { default } from ${target};\n` : ""; + return `// scriptc: this package compiles from its built-in implementation.\nexport * from ${target};\n${defaultLine}`; +} + +/** 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(); + bundledForwarders.clear(); +} diff --git a/packages/compiler/src/frontend/pipeline.ts b/packages/compiler/src/frontend/pipeline.ts index 16c96247f..193e8adc0 100644 --- a/packages/compiler/src/frontend/pipeline.ts +++ b/packages/compiler/src/frontend/pipeline.ts @@ -11,8 +11,10 @@ import { checkPreflight, isNodeTypesPath, locOf, + npmStaticDepSf7, npmStaticLoadableFiles, requiresOf, + resolveImport, resolveNpmImport, type LoadResult, } from "./program.js"; @@ -24,6 +26,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"; @@ -36,6 +39,7 @@ import { isStringLiteral, isStringLiteralLike, SyntaxKind, + type SourceFile, } from "./ts7/adapter.js"; import { moduleSourceCandidates } from "./module-source-candidates.js"; import { CONTRACT_TYPES_MODULE } from "./contract-types.js"; @@ -113,6 +117,65 @@ export type FrontendFactory = ( libraryNpmStatic?: readonly string[], ) => Frontend; +/** Adds the program modules a literal `import()` reaches to `scanned`, + * with their own static import graphs: they load at the call, outside the + * startup module order, and their npm imports are dependencies all the + * same (a workspace package loaded on demand, the vercel.ts config parser + * shape). An opted-in package's JavaScript reached that way joins too, + * with every file its literal require() and import edges load: those + * files are outside the module order as well, and the packages THEY + * require (a compiled CommonJS dist requiring a sibling package) are + * dependencies the selection must see. */ +function addDynamicImportGraph(load: LoadResult, scanned: Set): void { + const queue = [...scanned]; + const added = new Set(); + while (queue.length > 0) { + const sf = queue.pop()!; + const packageJs = sf.fileName.includes("/node_modules/"); + if (packageJs && !added.has(sf)) continue; + const specs: { spec: string; kind: "import" | "require" }[] = []; + // Every literal import() in the file, read from the syntax: a comment + // can stand between the keyword and its parenthesis. A file without + // the keyword has none. Package JavaScript loads its literal require() + // calls the same way, nested ones included. + const calls = + packageJs || sf.text.includes("import") ? moduleSourceCandidates(load.program, sf).calls : []; + for (const call of calls) { + const arg = call.arguments.nodes[0]; + if (arg === undefined || !isStringLiteralLike(arg)) continue; + if (call.expression.kind === SyntaxKind.ImportKeyword) + specs.push({ spec: arg.text, kind: "import" }); + else if (packageJs && isIdentifier(call.expression) && call.expression.text === "require") + specs.push({ spec: arg.text, kind: "require" }); + } + // The startup graph's static edges are already in the module order. + if (added.has(sf)) { + for (const stmt of sf.statements.nodes) { + if ( + (isImportDeclaration(stmt) || isExportDeclaration(stmt)) && + stmt.moduleSpecifier && + isStringLiteral(stmt.moduleSpecifier) && + !(isImportDeclaration(stmt) ? erasedTypeOnlyImport(stmt) : stmt.isTypeOnly) + ) + specs.push({ spec: stmt.moduleSpecifier.text, kind: "import" }); + } + } + for (const { spec, kind } of specs) { + if (canonicalBuiltinModule(spec) !== null) continue; + // Only the JavaScript of a package the selection already opted in: + // a package's declarations load nothing. + const dep = + resolveImport(load.program, sf, spec, kind) ?? + npmStaticDepSf7(load.program, sf, spec, kind); + if (dep === null || scanned.has(dep)) continue; + if (dep.fileName.includes("/node_modules/") && !isJsSourceFileName(dep.fileName)) continue; + scanned.add(dep); + added.add(dep); + queue.push(dep); + } + } +} + function detectAutoPackages( load: LoadResult, statuses: NpmStaticStatus[], @@ -135,6 +198,7 @@ function detectAutoPackages( // nested require() calls the graph walk does not model) still name its // dependencies. const scanned = new Set([...load.moduleOrder, load.entry]); + addDynamicImportGraph(load, scanned); const transitiveFiles = transitive.size === 0 ? [] @@ -192,6 +256,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 @@ -624,6 +691,7 @@ function loadFrontend( frontendServices: finalLoad.services, startupCrash: finalLoad.startupCrash ?? null, inlineRequireCycleCalls: finalLoad.inlineRequireCycleCalls, + moduleEdges: finalLoad.moduleEdges, projectCheckJs: finalLoad.typeWorld?.projectCheckJs ?? false, externalTypes: finalLoad.externalTypes, externalTypeSpecifiersByFile: finalLoad.externalTypeSpecifiersByFile, diff --git a/packages/compiler/src/frontend/program.ts b/packages/compiler/src/frontend/program.ts index 2e75542d7..218a31116 100644 --- a/packages/compiler/src/frontend/program.ts +++ b/packages/compiler/src/frontend/program.ts @@ -68,6 +68,8 @@ import { isNpmStaticPackage, npmStaticActive, npmStaticFsShadow, + programBuiltinAliasSource, + clearProgramBuiltinAliasCache, npmStaticLexerVisibleNames, npmStaticLiveExportTable, npmStaticPackageOfPath, @@ -89,6 +91,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 { @@ -348,6 +356,11 @@ export interface LoadResult { * at run time instead of refusing the whole program. Filled by * checkPreflight. */ inlineRequireCycleCalls?: ReadonlySet; + /** Every program file's module edges as preflight resolved them, + * require() edges included: the lowering orders the modules only a + * literal import(), fork() or Worker reaches with them. Filled by + * checkPreflight. */ + moduleEdges?: ReadonlyMap | undefined; /** Node's startup refusal, when the graph carries an import edge Node * rejects before ANY module evaluates — a RESOLUTION refusal (a bare * specifier nothing installed resolves, an exports/imports map that @@ -914,7 +927,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 @@ -1037,8 +1056,10 @@ export function loadProgram( // the checker, so its resolution lands on the JS Node actually loads; // resolve.ts answers the same sibling for scriptc's own edges). const npmShadow = npmStaticFsShadow(services); + clearProgramBuiltinAliasCache(); const fsShadow = { - readFile: (path: string) => npmShadow?.readFile(path), + readFile: (path: string) => + npmShadow?.readFile(path) ?? programBuiltinAliasSource(services, path), hideFile: (path: string) => (npmShadow?.hideFile(path) ?? false) || projectDtsRuntimeSibling(path) !== null, }; @@ -1088,10 +1109,12 @@ export function loadProgram( * preflight diagnostics. Preserve the caller's complete load shape when * mutating it, including its owned services and disposal callback. */ export function checkPreflight(load: T): ScrDiagnostic[] { - const { diags, moduleOrder, startupCrash, inlineRequireCycleCalls } = preflight7(load); + const { diags, moduleOrder, startupCrash, inlineRequireCycleCalls, moduleEdges } = + preflight7(load); load.moduleOrder = moduleOrder; load.startupCrash = startupCrash; load.inlineRequireCycleCalls = inlineRequireCycleCalls; + load.moduleEdges = moduleEdges; return diags; } @@ -3534,6 +3557,9 @@ export interface CycleEarlyBinding { /** Modules whose TOP-LEVEL code runs only after the declaration ran: * modules outside the cycle and cycle members evaluated later. */ settled: ReadonlySet; + /** The local name each importing file gives the binding when it differs + * from the declared one, by file name: Node's ReferenceError names it. */ + localNames?: ReadonlyMap; } /** Every binding of the program's exactly-initialized cycles that can be @@ -3654,6 +3680,7 @@ function computeCycleEarlyBindings7( end: number; reference: ts.Identifier | null; folded: ts.Identifier | null; + localNames: Map; } const candidates = new Map(); const names = new Set(); @@ -3693,6 +3720,7 @@ function computeCycleEarlyBindings7( end: stmt.end, reference: null, folded: null, + localNames: new Map(), }); names.add(name.text); } @@ -3701,13 +3729,33 @@ function computeCycleEarlyBindings7( }); if (candidates.size === 0) continue; const rankOf = new Map(evaluation.map((m, i) => [m, i] as const)); - // References: every identifier spelled like a candidate, resolved - // through import aliases to the declaration's symbol. + // References: every identifier spelled like a candidate or like a + // local name the file imports one under (`import { x as y }`, a + // default import), resolved through import aliases to the + // declaration's symbol. for (const sf of order) { + const local = new Set(); + for (const stmt of sf.statements.nodes) { + if (!ts.isImportDeclaration(stmt) || stmt.importClause === undefined) continue; + const clause = stmt.importClause; + const bound: ts.Identifier[] = clause.name !== undefined ? [clause.name] : []; + if (clause.namedBindings !== undefined && ts.isNamedImports(clause.namedBindings)) + for (const element of clause.namedBindings.elements.nodes) bound.push(element.name); + for (const name of bound) { + if (names.has(name.text)) continue; + const imported = checker.getSymbolAtLocation(name); + const candidate = + imported === undefined ? undefined : candidates.get(checker.getAliasedSymbol(imported)); + if (candidate === undefined) continue; + local.add(name.text); + candidate.localNames.set(sf.fileName, name.text); + } + } const refs: ts.Identifier[] = []; ts.walkPreorder(sf, (node) => { if (ts.isImportDeclaration(node)) return "skip"; - if (ts.isIdentifier(node) && names.has(node.text)) refs.push(node); + if (ts.isIdentifier(node) && (names.has(node.text) || local.has(node.text))) + refs.push(node); return undefined; }); if (refs.length === 0) continue; @@ -3759,6 +3807,7 @@ function computeCycleEarlyBindings7( reference: c.reference, ...(c.folded !== null ? { folded: c.folded } : {}), settled, + ...(c.localNames.size > 0 ? { localNames: c.localNames } : {}), }); } } @@ -4316,6 +4365,10 @@ function resolveNpmImport7( // 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)) { @@ -4428,6 +4481,7 @@ function preflight7(load: LoadResult): { moduleOrder: ts.SourceFile[]; startupCrash: StartupCrash | null; inlineRequireCycleCalls: ReadonlySet; + moduleEdges?: ReadonlyMap; } { const { program, entry } = load; const diags: ScrDiagnostic[] = [...load.configDiags]; @@ -4564,6 +4618,25 @@ function preflight7(load: LoadResult): { return d.code === 2300 && commentDup.has(`${d.fileName}:${sf.text.slice(d.pos, d.end)}`); }; + /* `typeof window` / `typeof self` in a JavaScript file: the operand of + * typeof may name a global nothing declares (Node answers "undefined" + * and never throws), the feature-detection idiom every isomorphic + * package uses, and the guarded uses of that name (`typeof window < "u" + * ? window : …`) are code Node never runs. A name the file feature-tests + * this way is therefore not a type error in JavaScript; a reached use + * still meets the lowering's located refusal. TypeScript files keep the + * error and its hint. */ + const typeofTestedNameSuppressed = (p: ts.Program, d: ts.Diagnostic): boolean => { + if (d.code !== 2304 && d.code !== 2552 && d.code !== 2584) return false; + if (d.fileName === undefined || !isJsSourceFileName(d.fileName)) return false; + if (d.pos === undefined || d.end === undefined) return false; + const sf = p.getSourceFile(d.fileName); + if (!sf) return false; + const name = sf.text.slice(d.pos, d.end); + if (!/^[A-Za-z_$][\w$]*$/.test(name)) return false; + return new RegExp(`\\btypeof\\s*\\(?\\s*${name.replace(/\$/g, "\\$")}(?![\\w$])`).test(sf.text); + }; + const workspaceImplicitAnySuppressed = (p: ts.Program, d: ts.Diagnostic): boolean => { if (d.code !== 7016 || d.fileName === undefined || d.pos === undefined || d.end === undefined) return false; @@ -4604,10 +4677,12 @@ 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) && !workspaceImplicitAnySuppressed(p, d) && + !typeofTestedNameSuppressed(p, d) && !jsdocTypeSuppressed(p, d, commentDup), ); }; @@ -5523,6 +5598,7 @@ function preflight7(load: LoadResult): { moduleOrder: order, startupCrash: resolveCrash ?? linkCrash, inlineRequireCycleCalls, + moduleEdges: edges, }; } diff --git a/packages/compiler/src/frontend/resolve.ts b/packages/compiler/src/frontend/resolve.ts index 36470a643..3b32dad3a 100644 --- a/packages/compiler/src/frontend/resolve.ts +++ b/packages/compiler/src/frontend/resolve.ts @@ -22,6 +22,11 @@ import { npmStaticPackageOfPath, npmStaticTransformPkgJson, } from "./npm-static.js"; +import { + clearPackageShimCaches, + packageShimFor, + throughBundledShimForwarder, +} from "./package-shims.js"; import { provenanceEntryFor } from "./provenance-registry.js"; import { trackedAccessibleEntries, @@ -609,6 +614,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); @@ -662,6 +671,11 @@ export function resolveProjectImport(fromFile: string, specifier: string): strin * answer null for their dedicated callers. */ export function resolveProjectModule(fromFile: string, specifier: string): string | null { if (isRelativeSpecifier(specifier) || isAbsolute(specifier)) { + // A bundled copy's forwarder (a virtual declaration module) answers + // the shim it forwards to. + const forwarder = join(dirname(resolve(fromFile)), `${specifier}.d.ts`); + const forwarded = throughBundledShimForwarder(forwarder); + if (forwarded !== forwarder) return forwarded; return resolveRelativeModule(fromFile, specifier); } if (specifier.startsWith("node:")) return null; @@ -1051,6 +1065,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/services.ts b/packages/compiler/src/frontend/services.ts index 07bc8ca69..be3881e13 100644 --- a/packages/compiler/src/frontend/services.ts +++ b/packages/compiler/src/frontend/services.ts @@ -8,6 +8,12 @@ import { splitBundledChunks, type BundledChunkSplit, } from "./npm-static-bundled-chunks.js"; +import { + mayRequireBuiltinAlias, + rewriteBuiltinAliasRequires, +} from "./npm-static-builtin-aliases.js"; +import { mayHoldEnumIife, rewriteEnumIifes } from "./npm-static-enum-iife.js"; +import { builtinPackageAliasFor } from "./package-shims.js"; import * as syntax from "./npm-static-declaration-syntax.js"; import { normalizeCjsExportsVar } from "./npm-static-cjs-exports-var.js"; import type { SourceFile } from "./ts7/ast-types.js"; @@ -100,6 +106,22 @@ export class FrontendServices { if (!source.includes("module.exports")) return null; return normalizeCjsFunctionExports(this.parse(path, source, "js")); } + /** A package module with its literal-valued TypeScript enum IIFEs + * respelled as object literals (null: none). */ + enumIifes(source: string, path: string): string | null { + this.ensureOpen(); + if (!mayHoldEnumIife(source)) return null; + return rewriteEnumIifes(this.parse(path, source, "js")); + } + /** A package module with its requires of builtin-aliased polyfill + * packages respelled as the builtins (null: none). */ + builtinAliasRequires(source: string, path: string): string | null { + this.ensureOpen(); + if (!mayRequireBuiltinAlias(source)) return null; + return rewriteBuiltinAliasRequires(this.parse(path, source, "js"), (specifier) => + builtinPackageAliasFor(path, specifier), + ); + } /** A CommonJS module that names its export object through a variable * (`var ip = exports`), its member accesses respelled through `exports` * (null: none). */ diff --git a/packages/compiler/src/frontend/ts7/program-host.ts b/packages/compiler/src/frontend/ts7/program-host.ts index dbc0804d5..928b2bb58 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 5d1bab78e..125e324c7 100644 --- a/packages/compiler/src/frontend/ts7/semantic-model.ts +++ b/packages/compiler/src/frontend/ts7/semantic-model.ts @@ -557,6 +557,14 @@ export class SemanticType { // this snapshot's type with the same panic instead of a new // request. if (e instanceof Error && e.message.startsWith("panic:")) { + // The widened union (the cloned empty tuple widens to a plain + // array type) is readable and describes the same values; answer + // with its members when typescript-go can serialize them. + const widened = this.widenedConstituents(); + if (widened !== undefined) { + this.constituents = widened; + return widened; + } (e as { unreadableConstituents?: true }).unreadableConstituents = true; this.constituentsPanic = e; } @@ -565,6 +573,20 @@ export class SemanticType { } return this.constituents; } + /** The members of this union's widened type, or undefined when widening + * changes nothing or its members are unreadable too. */ + private widenedConstituents(): SemanticType[] | undefined { + try { + const data = this.project.request("getWidenedType", { type: this.id }); + if (data === null || data.id === this.id) return undefined; + const widened = this.project.type(data); + if ((widened.flags & TypeFlags.Union) === 0) return [widened]; + return this.project.fetchTypes(widened.id, "getTypesOfType"); + } catch (e) { + if (e instanceof Error && e.message.startsWith("panic:")) return undefined; + throw e; + } + } dispose(): void { this.constituents = undefined; this.constituentsPanic = undefined; diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index c9a1cc9df..b7bdcf56e 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -2138,7 +2138,9 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { (ts.isInterfaceDeclaration(d) || ts.isTypeAliasDeclaration(d) || ts.isTypeLiteralNode(d) || - ts.isMappedTypeNode(d)), + ts.isMappedTypeNode(d) || + isDeclaredDataClass(d) || + (decls.some(isDeclaredDataClass) && isTypeOnlyNamespace(d))), ); const aliasDecls = packageOnly(widened.getAliasSymbol()); const ownDecls = packageOnly(widened.getSymbol()); @@ -2471,6 +2473,16 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { psym.declarations.some( (d) => ts.isInterfaceDeclaration(d) && ctx.isStdlibFile(ts.getSourceFileOfNode(d)), ); + // 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 !== undefined && + ts.symbolName(psym) === name && + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(ts.getSourceFileOfNode(d)), + ); // 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 @@ -3004,11 +3016,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") || @@ -4613,7 +4625,18 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { const emptyTuple = (part: ts.Type): boolean => checker.isTupleType(part) && checker.getTypeArguments(part as ts.TypeReference).length === 0; - const hasArrayPart = parts.some((part) => checker.isArrayType(part)); + // The match-result fallback `s.match(re) || []` (and `?? []`) widens + // to `RegExpMatchArray | []`: the empty tuple is one of the match + // array's own values too. + const matchArray = (part: ts.Type): boolean => { + // The names have no leading `__`, so the escaped name is the name. + const name = part.getSymbol()?.escapedName; + return ( + (name === "RegExpMatchArray" || name === "RegExpExecArray") && + (part.getSymbol()?.declarations.some((d) => ctx.isStdlibFile(d.getSourceFile())) ?? false) + ); + }; + const hasArrayPart = parts.some((part) => checker.isArrayType(part) || matchArray(part)); for (const part of parts) { if (hasArrayPart && emptyTuple(part)) continue; // TypeScript's client can retain impossible intersections in a @@ -6520,6 +6543,48 @@ function isMappedShape(t: ts.Type): boolean { * of a package the program does not compile. Its pure type shapes — * interfaces, aliases, type literals — describe data a program can build * itself (`import type` from a types-only package such as type-fest). */ +/** A class an SDK declares only to describe data (aws-sdk's + * `declare class PresignedPost { url: string; fields: Fields }`, which + * arrives as JSON and is never constructed): public instance fields and + * nothing else — no constructor, methods, accessors, index signatures, + * static, private or protected members, base classes, implemented + * interfaces or type parameters. Its instances are exactly the records + * of those fields, so the type maps structurally like an interface. */ +function isDeclaredDataClass(d: ts.Node): boolean { + if (!ts.isClassDeclaration(d) || !d.getSourceFile().isDeclarationFile) return false; + if (d.heritageClauses !== undefined && d.heritageClauses.nodes.length > 0) return false; + if (d.typeParameters !== undefined && d.typeParameters.nodes.length > 0) return false; + return d.members.nodes.every( + (member) => + ts.isPropertyDeclaration(member) && + !ts.isPrivateIdentifier(member.name) && + !(member.modifiers?.nodes ?? []).some( + (modifier) => + modifier.kind === ts.SyntaxKind.StaticKeyword || + modifier.kind === ts.SyntaxKind.PrivateKeyword || + modifier.kind === ts.SyntaxKind.ProtectedKeyword || + modifier.kind === ts.SyntaxKind.AbstractKeyword, + ), + ); +} + +/** A namespace merged into a declared data class that holds only types + * (`namespace PresignedPost { interface Fields { … } }`): it adds no value + * the program could reach. */ +function isTypeOnlyNamespace(d: ts.Node): boolean { + if (!ts.isModuleDeclaration(d)) return false; + const body = d.body; + if (body === undefined) return true; + if (ts.isModuleDeclaration(body)) return isTypeOnlyNamespace(body); + if (!ts.isModuleBlock(body)) return false; + return body.statements.nodes.every( + (statement) => + ts.isInterfaceDeclaration(statement) || + ts.isTypeAliasDeclaration(statement) || + isTypeOnlyNamespace(statement), + ); +} + function packageTypeFile(sf: ts.SourceFile, ctx: Pick): boolean { return ( sf.isDeclarationFile && /[\\/]node_modules[\\/]/.test(sf.fileName) && !ctx.isStdlibFile(sf) @@ -7287,10 +7352,11 @@ function mapRecordTypeInner( let v: IrType | null = null; for (const info of indexInfos) { if (!stringKey(info.keyType) && !(info.keyType.flags & ts.TypeFlags.Number)) return null; - const iv = mapType(info.type, ctx); - - // JSONValue>` (a package's own JSON alias) and `Record` - + // An `any`-valued signature (`{ [key: string]: any }`, an SDK's + // `Dictionary`) holds whatever the program stores: its + // overflow values are checked-dynamic, exactly `Record`'s. + const iv = info.type.flags & ts.TypeFlags.Any ? DYN : mapType(info.type, ctx); if (!iv || !isSupportedIndexValue(iv)) return null; if (v !== null && !typeEquals(v, iv)) return null; v = iv; @@ -7517,6 +7583,22 @@ function mapRecordTypeInner( pt = arrayOf(unitOnlyUnion(ctx.unions)); } if (pt === null && (fieldTs.flags & ts.TypeFlags.Any) !== 0) pt = DYN; + // An `any[]` field (`list: any[]` in an SDK's message type, also + // optional or nullable) is the checked-dynamic array, exactly as + // `unknown[]` maps; the checked-dynamic value holds the units too. + // Declared members only: an object literal's inferred `any[]` member + // keeps its existing story. + if ( + pt === null && + declaredMember(p) && + (fieldTs.isUnionType() ? ts.constituentTypes(fieldTs) : [fieldTs]).every( + (part) => + isAnyArrayType(part, checker) || + (part.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Null)) !== 0, + ) && + !isUnitOnlyTsType(fieldTs, ctx.resolveTypeParam) + ) + pt = DYN; // Unit-only FIELDS (`{ msg?: undefined }` — the discriminated-union // absent-field idiom — and `{ p: undefined }` spellings): the // unit-only union. The runtime value is the interned unit, JSON @@ -7613,6 +7695,10 @@ function dynSubsumableUnionArm(arm: IrType, ctx: TypeMapperCtx): boolean { return true; case "record": case "array": + // `T | Promise` with T bound to `unknown`: the checked-dynamic value + // holds a promise by reference, so `unknown` absorbs the promise arm + // exactly as the checker's own union reduction does. + case "promise": return canConvertToDyn( arm, (id) => ctx.shapes.get(id), @@ -7907,15 +7993,118 @@ export function describeRecordMemberBlocker(widened: ts.Type, ctx: TypeMapperCtx if (erasedOverloadMemberType(fieldTs, ctx) !== undefined) continue; return `the record shape is supported, but its member '${ts.symbolName(p)}' ${describeOverloadSetBlocker(fieldTs, ctx)}`; } - let pt = isGenericCallableMemberType(fieldTs, checker) - ? erasedGenericMemberType(fieldTs, ctx) - : mapType(fieldTs, ctx); - if (pt === undefined) continue; - if (pt?.kind === "void" && isUnitOnlyTsType(fieldTs, ctx.resolveTypeParam)) - pt = unitOnlyUnion(ctx.unions); - if (!pt || pt.kind === "void") { - return `the record shape is supported, but its member '${ts.symbolName(p)}' has type '${checker.typeToString(fieldTs)}', which does not compile`; + if (memberMaps(fieldTs, ctx, p)) continue; + // Name the deepest member the failure comes from (`order.deployment. + // builds[].config`): a large message type otherwise points only at + // its outermost member. + const inner = innerBlockedMember(fieldTs, ctx, new Set()); + if (inner !== null && inner.path !== "") { + return `the record shape is supported, but its member '${ts.symbolName(p)}${inner.path}' has type '${checker.typeToString(inner.type)}', which does not compile`; } + return `the record shape is supported, but its member '${ts.symbolName(p)}' has type '${checker.typeToString(fieldTs)}', which does not compile`; } return null; } + +/** A member an interface, type literal or class declares (not one an + * object literal's inferred type carries). */ +function declaredMember(member: ts.Symbol): boolean { + return ( + member.declarations.length > 0 && + member.declarations.every((d) => ts.isPropertySignature(d) || ts.isPropertyDeclaration(d)) + ); +} + +/** Whether a record member of type `fieldTs` maps, by the member rules of + * mapRecordTypeInner (generic members erase; `any` and `any[]` members + * are checked-dynamic; unit-only members are the unit union). Overloaded + * members answer true: describeOverloadSetBlocker tells their story. */ +function memberMaps(fieldTs: ts.Type, ctx: TypeMapperCtx, member?: ts.Symbol): boolean { + const { checker } = ctx; + if (isOverloadedCallableMemberType(fieldTs, checker)) return true; + const pt = isGenericCallableMemberType(fieldTs, checker) + ? erasedGenericMemberType(fieldTs, ctx) + : mapType(fieldTs, ctx); + if (pt === undefined) return true; + if (pt === null && (fieldTs.flags & ts.TypeFlags.Any) !== 0) return true; + if ( + pt === null && + (member === undefined || declaredMember(member)) && + (fieldTs.isUnionType() ? ts.constituentTypes(fieldTs) : [fieldTs]).every( + (part) => + isAnyArrayType(part, checker) || + (part.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Null)) !== 0, + ) && + !isUnitOnlyTsType(fieldTs, ctx.resolveTypeParam) + ) + return true; + if (pt?.kind === "void" && isUnitOnlyTsType(fieldTs, ctx.resolveTypeParam)) return true; + return pt !== null && pt.kind !== "void"; +} + +/** The member path inside the unmapped type `t` that its failure comes + * from, and that member's type: `.name` steps into record members, `[]` + * into array elements, `()` into a function's return; union arms and + * promise payloads are transparent. The empty path means `t` itself is + * the blocker; null means `t` maps (or is `any`). */ +function innerBlockedMember( + t: ts.Type, + ctx: TypeMapperCtx, + seen: Set, +): { path: string; type: ts.Type } | null { + const { checker } = ctx; + // `any` is no component to name: the member holding it is the blocker. + if ((t.flags & ts.TypeFlags.Any) !== 0 || mapType(t, ctx) !== null) return null; + const here = { path: "", type: t }; + if (seen.has(t) || seen.size >= 16) return here; + seen.add(t); + const step = (prefix: string, inner: ts.Type): { path: string; type: ts.Type } => { + const found = innerBlockedMember(inner, ctx, seen); + return found === null ? here : { path: prefix + found.path, type: found.type }; + }; + if (t.isUnionType()) { + const UNIT = ts.TypeFlags.Undefined | ts.TypeFlags.Void | ts.TypeFlags.Null; + for (const part of ts.constituentTypes(t)) { + if ((part.flags & UNIT) !== 0 || mapType(part, ctx) !== null) continue; + return step("", part); + } + return here; + } + if ((t.flags & ts.TypeFlags.Object) === 0) return here; + if (checker.isArrayType(t)) { + const elem = checker.getTypeArguments(t as ts.TypeReference)[0]; + return elem === undefined ? here : step("[]", elem); + } + const symbol = t.getSymbol(); + if ( + symbol !== undefined && + ts.symbolName(symbol) === "Promise" && + symbol.declarations.some((d) => ctx.isStdlibFile(d.getSourceFile())) + ) { + const payload = checker.getTypeArguments(t as ts.TypeReference)[0]; + return payload === undefined ? here : step("", payload); + } + const calls = checker.getCallSignatures(t); + if (calls.length === 1) { + const sig = calls[0]!; + if (sig.getTypeParameters().length > 0) return here; + if (sig.getParameters().some((p) => mapType(checker.getTypeOfSymbol(p), ctx) === null)) + return here; + return step("()", checker.getReturnTypeOfSignature(sig)); + } + if ( + calls.length > 0 || + checker.getConstructSignatures(t).length > 0 || + checker.isTupleType(t) || + checker.getIndexInfosOfType(t).length > 0 + ) + return here; + for (const p of checker.getPropertiesOfType(t)) { + if ((p.flags & (ts.SymbolFlags.GetAccessor | ts.SymbolFlags.SetAccessor)) !== 0) continue; + if (ts.symbolName(p).startsWith("__@")) continue; + const fieldTs = checker.getTypeOfSymbol(p); + if (memberMaps(fieldTs, ctx, p)) continue; + return step(`.${ts.symbolName(p)}`, fieldTs); + } + return here; +} diff --git a/packages/compiler/src/index.ts b/packages/compiler/src/index.ts index 0f144ca2e..9c8ce5920 100644 --- a/packages/compiler/src/index.ts +++ b/packages/compiler/src/index.ts @@ -547,6 +547,8 @@ async function compileExecutableNative( ...(features.dynTier ? { dynTier: true } : {}), ...(features.streamDyn ? { streamDyn: true } : {}), ...(features.eventsOnceSignal ? { eventsOnceSignal: true } : {}), + ...(features.dynBufferNum ? { dynBufferNum: true } : {}), + ...(features.builtinValues ? { builtinValues: true } : {}), events: features.events, emitter: features.emitter, symbol: features.symbol, diff --git a/packages/compiler/src/ir/array-length-stability.test.ts b/packages/compiler/src/ir/array-length-stability.test.ts index 44e461ca2..a0d0e49a7 100644 --- a/packages/compiler/src/ir/array-length-stability.test.ts +++ b/packages/compiler/src/ir/array-length-stability.test.ts @@ -4,6 +4,8 @@ import { BOOL, F64, arrayOf, + mapOf, + setOf, type IrExpr, type IrFunction, type IrLocal, @@ -111,6 +113,32 @@ test("captured receivers have no local proof", () => { expect(preserves([sum], "i", new Set(), boxed)).toBe(false); }); +test("Map and Set reads and checked union extractions run no user code", () => { + const counts: IrExpr = { kind: "varRef", localId: "m", type: mapOf(F64, F64), loc }; + const seen: IrExpr = { kind: "varRef", localId: "v", type: setOf(F64), loc }; + const onMap = (name: string, args: IrExpr[]): IrExpr => + ({ kind: "mapIntrinsic", method: name, receiver: counts, args, type: F64, loc }) as IrExpr; + const onSet = (name: string, args: IrExpr[]): IrExpr => + ({ kind: "setIntrinsic", method: name, receiver: seen, args, type: BOOL, loc }) as IrExpr; + const call = (callee: string): IrExpr => ({ + kind: "call", + callee, + args: [ref("s")], + type: F64, + loc, + }); + const key = read("a", ref("i")); + expect( + preserves([stmt(onMap("has", [key])), stmt(onMap("get", [key])), stmt(onMap("size", []))]), + ).toBe(true); + expect(preserves([stmt(onSet("has", [key])), stmt(onSet("size", []))])).toBe(true); + expect(preserves([stmt(call("%union.narrow.0"))])).toBe(true); + // Writes release replaced values; other calls run user code. + expect(preserves([stmt(onMap("set", [key, num(1)]))])).toBe(false); + expect(preserves([stmt(onSet("add", [key]))])).toBe(false); + expect(preserves([stmt(call("g"))])).toBe(false); +}); + function fn(body: IrStmt[], params: string[] = []): IrFunction { return { loc, diff --git a/packages/compiler/src/ir/array-length-stability.ts b/packages/compiler/src/ir/array-length-stability.ts index 2ac196d30..3f9c3b9ca 100644 --- a/packages/compiler/src/ir/array-length-stability.ts +++ b/packages/compiler/src/ir/array-length-stability.ts @@ -17,6 +17,11 @@ const READING_ARRAY_METHODS = new Set([ /** String methods that read an immutable string and run no user code. */ const READING_STRING_METHODS = new Set(["length", "charCodeAt", "codePointAt"]); +/** Map and Set methods that read the receiver and run no user code: keys + * hash and compare by value or identity, and no array is touched. */ +const READING_MAP_METHODS = new Set(["get", "has", "size"]); +const READING_SET_METHODS = new Set(["has", "size"]); + /** Expressions that compute from their operands and run no user code. Their * children are checked separately. */ const PLAIN_EXPRESSIONS = new Set([ @@ -162,8 +167,9 @@ export interface ArrayLengthLoop { /** Whether running `nodes` (a loop's condition, body and update) can never * change the length of the array held by local `loop.receiver`. The proof is * syntactic and conservative: every expression and statement must be one - * that runs no user code (no calls, callbacks, closures, iterators, - * suspension or exception regions); the binding is never written and only + * that runs no user code (no calls other than checked union extractions, no + * callbacks, closures, iterators, suspension or exception regions; Map and + * Set reads are accepted); the binding is never written and only * ever read as the receiver of an element access or a reading array method, * so the array cannot escape to code that resizes it; and nothing in the * nodes resizes any array that may be the same one. Element writes are @@ -215,6 +221,14 @@ export function preservesArrayLength( e.args.every(expr) ); } + case "call": + // A checked union extraction (`x!`, coercions/unions.ts): the + // asserted arm's payload or a TypeError, and no user code. + return e.callee.startsWith("%union.narrow.") && e.args.every(expr); + case "mapIntrinsic": + return READING_MAP_METHODS.has(e.method) && expr(e.receiver) && e.args.every(expr); + case "setIntrinsic": + return READING_SET_METHODS.has(e.method) && expr(e.receiver) && e.args.every(expr); case "libCall": return ( isBorrowSafeMath(e.fn) && diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index d4e96a35a..d830f3784 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -85,6 +85,9 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "bytes.constructor", "bytes.instanceOf", "buffer.byteLenDyn", + "buffer.byteLenValue", + "buffer.allocValue", + "fs.realpathValue", "bytes.construct", "arrayBuffer.new", "sharedArrayBuffer.new", @@ -453,6 +456,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ // The caller's lookup runs synchronously inside the connect call — a // throw there propagates like Node's. "net.connectLookup", + "net.connectLookupDyn", "cp.execSync", "cp.execCapture", // The client request constructors validate the method token @@ -769,6 +773,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "buffer.poolFromBytes", "buffer.poolFromArray", "buffer.poolFromDyn", + "buffer.poolFromDynRef", // The checked-dynamic compare/equals validators: Node's argument // ladders throw ERR_INVALID_ARG_TYPE / ERR_OUT_OF_RANGE catchably. "buffer.compareChk", diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index cd3db784d..278dbbc1f 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -687,6 +687,10 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ /** realpath(3) with Node's error shape (syscall "lstat" in the message, * Node's own spelling for realpathSync failures). +1 fresh string. */ "fs.realpathSync": { argTypes: [STRING], result: STRING }, + /** fs.realpathSync (or its .native when the flag is true) as a function + * value: path and options arrive unchecked; the result is a string or a + * Buffer by the options' encoding. */ + "fs.realpathValue": { argTypes: [DYN, DYN, BOOL], result: DYN }, /** Native realpath with syscall "realpath" on failure. +1 fresh string. */ "fs.realpathNativeSync": { argTypes: [STRING], result: STRING }, /** The os.userInfo() field trio (uv_os_get_passwd's slices): pw_name, @@ -1437,6 +1441,11 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ * 'error'. A synchronous throw INSIDE the lookup propagates out of the * connect call (may-throw seed). */ "net.connectLookup": { argTypes: [F64, STRING, null], result: NETSOCKET_T }, + /** connect({ host, port, lookup }) with a lookup of Node's own type, as + * a checked-dynamic function: the runtime calls it with { hints, all: + * true } and a callback taking an address string or list, then dials + * the answers in order. MAY THROW (host validation, a lookup throw). */ + "net.connectLookupDyn": { argTypes: [F64, STRING, DYN], result: NETSOCKET_T }, "net.sockWrite": { argTypes: [NETSOCKET_T, STRING], result: VOID }, "net.sockWriteBytes": { argTypes: [NETSOCKET_T, BYTES_U8], result: VOID }, "net.sockEnd": { argTypes: [NETSOCKET_T], result: VOID }, @@ -2872,6 +2881,9 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ * allocUnsafeSlow never pool. The size forms validate like Node * (ERR_OUT_OF_RANGE). poolSize reads and writes Buffer.poolSize. */ "buffer.alloc": { argTypes: [F64], result: BYTES_U8 }, + /** Buffer.alloc as a function value: the size, fill and encoding arrive + * unchecked, and the runtime fills as Node's alloc does. */ + "buffer.allocValue": { argTypes: [DYN, DYN, DYN], result: BYTES_U8 }, "buffer.allocUnsafe": { argTypes: [F64], result: BYTES_U8 }, "buffer.allocUnsafeSlow": { argTypes: [F64], result: BYTES_U8 }, "buffer.poolFromStr": { argTypes: [STRING, STRING], result: BYTES_U8 }, @@ -2879,12 +2891,16 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "buffer.poolFromBytes": { argTypes: [BYTES_U8], result: BYTES_U8 }, "buffer.poolFromArray": { argTypes: [arrayOf(F64)], result: BYTES_U8 }, "buffer.poolFromDyn": { argTypes: [DYN, STRING], result: BYTES_U8 }, + /** poolFromDyn that also copies an array crossing by reference. */ + "buffer.poolFromDynRef": { argTypes: [DYN, STRING], result: BYTES_U8 }, "buffer.poolSize": { argTypes: [], result: F64 }, "buffer.setPoolSize": { argTypes: [F64], result: VOID }, /** Buffer.byteLength(string, enc) — enc a NORMALIZED literal like * fromStr's — and Buffer.isEncoding(name) over a runtime string * (case-insensitive against Node's alias set). Pure; never throw. */ "buffer.byteLenDyn": { argTypes: [DYN, STRING], result: F64 }, + /** Buffer.byteLength as a function value: the encoding arrives at run time. */ + "buffer.byteLenValue": { argTypes: [DYN, DYN], result: F64 }, "buffer.byteLenStr": { argTypes: [STRING, STRING], result: F64 }, "buffer.isEncoding": { argTypes: [STRING], result: BOOL }, /** Buffer.concat(list, totalLength): the concatenation truncated or @@ -4153,6 +4169,9 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ /** A numeric typed array that is not a Buffer (any element kind). */ "insp.typedArray": { argTypes: [null, F64, F64], result: STRING }, "insp.error": { argTypes: [null, F64, F64], result: STRING }, + "insp.errorHasCause": { argTypes: [null], result: BOOL }, + "insp.errorBagHasProperties": { argTypes: [DYN], result: BOOL }, + "insp.errorCause": { argTypes: [null, F64, F64], result: STRING }, "insp.dyn": { argTypes: [DYN, F64, F64], result: STRING }, "insp.dynS": { argTypes: [DYN, F64], result: STRING }, "insp.begin": { argTypes: [F64], result: VOID }, @@ -4176,6 +4195,11 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ * (inspect, Object.keys/values/entries) then answer through its live * checked view. The receiver is any such record type. Never throws. */ "record.sourceHas": { argTypes: [null], result: BOOL }, + /** A checked-cast fallback site's reads right after the check + * (dynCheck.fallback): whether its value failed validation, and that + * value (+1), taken once. */ + "checkedCast.fallbackTaken": { argTypes: [], result: BOOL }, + "checkedCast.fallbackTake": { argTypes: [], result: DYN }, "insp.key": { argTypes: [STRING], result: STRING }, "insp.moreItems": { argTypes: [F64], result: STRING }, "insp.end": { argTypes: [STRING, STRING, STRING, F64, BOOL, BOOL], result: STRING }, diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts index 4c5bbd183..ccb00c37e 100644 --- a/packages/compiler/src/ir/ir.ts +++ b/packages/compiler/src/ir/ir.ts @@ -647,6 +647,9 @@ export function isSupportedArrayElem(t: IrType): boolean { case "regex": case "child": case "netServer": + // fs.Stats snapshots (`await Promise.all(paths.map(stat))`): immutable + // refcounted handles with their own retain/release adapters. + case "stats": case "symbol": case "classval": case "moduleNs": @@ -3824,11 +3827,20 @@ export type IrExpr = /** `exact`: the value is known to be a capsule this program boxed from * exactly the class `type` names (a `dyn.typedRefKeyIs` test guards the * expression), so the check is one load of the capsule's instance. */ + /** `fallback`: a checked-cast fallback site (frontend + * checked-fallback.ts). A value that fails validation does not throw: the + * result is the dummy, and the checked dyn value parks for the + * `checkedCast.fallbackTaken`/`checkedCast.fallbackTake` reads the + * frontend emits right after the site, whose code then continues in the + * dynamic lowering of the region the value flows through. Other + * exceptions (a JSON syntax error) still throw. Only record, array, + * number, string and boolean targets carry it. */ | { kind: "dynCheck"; value: IrExpr; preserveRefs?: true; exact?: true; + fallback?: true; type: IrType; loc: SrcLoc; }; @@ -4214,6 +4226,10 @@ export function canConvertToDyn( (a) => a.kind === "undefinedT" || isJsonSafeType(a, getRecord, getUnion) || + // Composite arms (records holding dyn, Map or Set members) box + // through the walker beside the promise arm: a method answering + // either its result or a promise of it. + canBoxDynComposite(a, getRecord, getUnion, visiting) || isDynTypedRefType(a) || a.kind === "classval" || DYN_HANDLE_KINDS.has(a.kind) || diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts index fd7c98cf6..4914e0f28 100644 --- a/packages/compiler/src/ir/runtime-features.ts +++ b/packages/compiler/src/ir/runtime-features.ts @@ -9,97 +9,118 @@ import { } from "./ir.js"; import { everyModuleNode } from "./traverse.js"; +/** The fixed-width Buffer read/write method names (both "UInt" and "Uint" + * spellings) a checked-dynamic method call routes to scr_bytes_dyn.c. */ +export const DYN_BUFFER_NUM_METHODS: ReadonlySet = new Set( + ["read", "write"].flatMap((rw) => [ + ...["UInt", "Uint"].flatMap((u) => + ["8", "16LE", "16BE", "32LE", "32BE"].map((w) => `${rw}${u}${w}`), + ), + ...[ + "Int8", + "Int16LE", + "Int16BE", + "Int32LE", + "Int32BE", + "FloatLE", + "FloatBE", + "DoubleLE", + "DoubleBE", + ].map((n) => `${rw}${n}`), + ]), +); + const DYN_ASYNC_LIB_FNS: ReadonlySet = new Set([ // DEP0040 rides the dyn-async unit's warning machinery. - "punycode.warn", - "fs.callbackValue", - "fs.callbackCall", - "fs.callbackGlob", "async.awaitDyn", - "timers.immediatePromise", "crypto.native", "dyn.promiseAll", "dyn.promiseAllSettled", "dyn.promiseAny", "dyn.promiseResolve", - "process.onUncaughtException", - "process.offUncaughtException", - "process.onUnhandledRejection", - "process.offUnhandledRejection", - "process.onRejectionHandled", - "process.offRejectionHandled", - "process.onWarning", - "process.offWarning", + "fs.callbackCall", + "fs.callbackGlob", + "fs.callbackValue", + "process.deprecationFlag", "process.emitWarning", "process.emitWarningText", - "process.deprecationFlag", + "process.offRejectionHandled", + "process.offUncaughtException", + "process.offUnhandledRejection", + "process.offWarning", + "process.onRejectionHandled", + "process.onUncaughtException", + "process.onUnhandledRejection", + "process.onWarning", "process.setDeprecationFlag", "process.setEmitWarning", + "punycode.warn", + "timers.immediatePromise", // The performance timeline (scr_perf.c, linked with this unit for its // MaxPerformanceEntryBufferExceededWarning). - "perf.mark", - "perf.measure", - "perf.entries", "perf.clearMarks", "perf.clearMeasures", - "perf.entryName", - "perf.entryType", - "perf.entryStart", - "perf.entryDuration", + "perf.entries", "perf.entryDetail", + "perf.entryDuration", "perf.entryKeep", + "perf.entryName", "perf.entryRef", + "perf.entryStart", + "perf.entryType", + "perf.mark", + "perf.measure", // Blob and File (scr_blob.c, linked with this unit, which boxes their // reads as dyn promises). - "blob.new", + "als.disable", + "als.enterWith", + "als.exitRun", + "als.get", + "als.new", + "als.run", + "blob.arrayBuffer", + "blob.bytes", "blob.fileNew", + "blob.is", + "blob.new", "blob.slice", "blob.text", - "blob.bytes", - "blob.arrayBuffer", - "blob.is", - "util.styleText", - "util.inherits", - "util.deprecate", + "dc.chanBindStore", + "dc.chanRunStores", + "dc.chanUnbindStore", "util.callbackify", "util.debuglog", - "als.new", - "als.get", - "als.run", - "als.exitRun", - "als.enterWith", - "als.disable", - "dc.chanBindStore", - "dc.chanUnbindStore", - "dc.chanRunStores", + "util.deprecate", + "util.inherits", + "util.styleText", ]); /** The libCalls that enter AsyncLocalStorage stores (RuntimeFeatures * asyncContext). */ const ASYNC_CONTEXT_LIB_FNS: ReadonlySet = new Set([ - "als.run", - "als.exitRun", "als.enterWith", + "als.exitRun", + "als.run", "dc.chanBindStore", "dc.chanRunStores", // getTestContext() reads the test runner's entry in the same snapshot. - "test.hasContext", "test.context", + "test.hasContext", ]); const LEGACY_URL_LIB_FNS: ReadonlySet = new Set([ - "url.legacyParse", - "url.legacyFormat", - "url.legacyResolve", "url.domainToASCII", "url.domainToUnicode", + "url.legacyFormat", + "url.legacyParse", + "url.legacyResolve", ]); const UTIL_CALLABLE_LIB_FNS: ReadonlySet = new Set([ - "util.inherits", - "util.deprecate", "util.callbackify", "util.debuglog", + "util.deprecate", + "util.inherits", ]); /** Entry points that link the regex unit: the regex and glob matchers (fs.glob's walk compiles its patterns with the glob matcher), and readline's terminal unit (scr_readline_tty.c), which measures with the regex unit's stripVTControlCharacters. */ @@ -167,14 +188,14 @@ const STREAM_LIB_FNS: ReadonlySet = new Set([ "stream.addAbortSignalFn", "stream.destroy", "stream.destroyErr", - "stream.errored", - "stream.prop", "stream.dynNative", "stream.dynNativePipe", + "stream.errored", "stream.pipeDyn", - "stream.unpipeDyn", + "stream.prop", "stream.stdioWritable", "stream.unboxDyn", + "stream.unpipeDyn", "stream.workerStdio", ]); @@ -209,10 +230,6 @@ const READABLE_DYN_ASYNC_PREFIXES: readonly string[] = ["readable.fromGen", "rea /** Entry points that link the dyn-invoke unit. */ const DYN_INVOKE_LIB_FNS: ReadonlySet = new Set([ - "stream.dynNative", - "stream.dynNativePipe", - "stream.pipeDyn", - "stream.unpipeDyn", "dyn.arrayProtoCall", "dyn.arrayPrototype", "dyn.builtinFunction", @@ -224,6 +241,10 @@ const DYN_INVOKE_LIB_FNS: ReadonlySet = new Set([ "dyn.globalConstructor", "dyn.objCreateWithProperties", "dyn.objectTag", + "stream.dynNative", + "stream.dynNativePipe", + "stream.pipeDyn", + "stream.unpipeDyn", ]); /** Member names whose constant-key dynamic reads answer the inherited Object.prototype member through the dyn-invoke unit. Mirrors expr-dynamic.ts INHERITED_OBJECT_MEMBERS (the IR layer does not import the backend). */ @@ -258,8 +279,8 @@ const CHILD_PROCESS_LIB_PREFIXES: readonly string[] = ["child.", "cp.", "spawnRe const BUILTIN_CLASS_LIB_FNS: readonly IrLibFn[] = [ "dyn.builtinClass", // the stream classes, through scr_dyn_builtin_stream.c - "dyn.builtinStreamClass", "dyn.builtinInstanceOf", + "dyn.builtinStreamClass", "dyn.functionInstanceOf", ]; @@ -387,64 +408,64 @@ function boxesPromise(t: IrType, tables: TypeTables, seen: Set): boolean /** The libCall fns served by the OPTIONAL events unit (scr_events.c): * process signal/exit listeners and the piped-stdin surface. */ const PROCESS_EVENT_LIB_FNS: ReadonlySet = new Set([ - "process.stdio", - "process.onSignal", + "process.offExit", "process.offSignal", "process.onExit", - "process.offExit", + "process.onSignal", + "process.stdio", + "stdin.nextChunk", "stdin.onData", "stdin.onEnd", "stdin.onError", - "stdin.nextChunk", // node:readline rides the stdin unit (scr_readline.c links beside // scr_events.c under the same gate). + "rl.checkCompleter", + "rl.checkOptions", + "rl.close", "rl.create", "rl.createSignal", - "rl.question", - "rl.questionAsync", - "rl.questionAsyncSignal", - "rl.questionDyn", - "rl.questionAsyncDyn", - "rl.questionAsyncSignalDyn", - "rl.close", - "rl.pause", - "rl.resume", - "rl.prompt", - "rl.promptWith", - "rl.setPrompt", - "rl.setPromptDyn", - "rl.getPrompt", - "rl.write", - "rl.writeKey", - "rl.on", - "rl.iterOpen", - "rl.iterNext", - "rl.iterReturn", - "rl.checkOptions", - "rl.checkCompleter", - "rl.setOptions", - "rl.setHistory", - "rl.history", - "rl.numProp", - "rl.terminal", - "rl.line", - "rl.isTerminal", - "rl.isPromises", "rl.cursor", - "rl.seq", - "rl.moves", "rl.cursorPos", + "rl.getPrompt", + "rl.history", + "rl.isPromises", + "rl.isTerminal", + "rl.iterNext", + "rl.iterOpen", + "rl.iterReturn", "rl.keypressStdin", "rl.keypressStream", - "stdin.onKeypress", - "rl.uiOpen", - "rl.uiOpenStream", - "rl.uiCursorTo", - "rl.uiMoveCursor", + "rl.line", + "rl.moves", + "rl.numProp", + "rl.on", + "rl.pause", + "rl.prompt", + "rl.promptWith", + "rl.question", + "rl.questionAsync", + "rl.questionAsyncDyn", + "rl.questionAsyncSignal", + "rl.questionAsyncSignalDyn", + "rl.questionDyn", + "rl.resume", + "rl.seq", + "rl.setHistory", + "rl.setOptions", + "rl.setPrompt", + "rl.setPromptDyn", + "rl.terminal", "rl.uiClearLine", "rl.uiClearScreenDown", - "rl.uiRollback", "rl.uiCommit", + "rl.uiCursorTo", + "rl.uiMoveCursor", + "rl.uiOpen", + "rl.uiOpenStream", + "rl.uiRollback", + "rl.write", + "rl.writeKey", + "stdin.onKeypress", ]); /** The legacy http2.* libCalls implemented by scr_http.c/scr_tls.c (the @@ -485,7 +506,9 @@ const NET_PREFIXES: readonly string[] = ["net.", "http.", "tls.", "https.", "htt /** One rule per runtime feature, sorted by name. A new feature is an * entry here and its line in noRuntimeFeatures; RuntimeFeatures, the scan - * and its early exit derive from this table. */ + * and its early exit derive from this table. The lists above keep one + * entry per line, sorted within each commented group, so changes that add + * different entries touch different lines. */ const RUNTIME_FEATURE_RULES = { assert: rule({ prefixes: ["assert."] }), /** The program enters AsyncLocalStorage stores (store.run, enterWith, @@ -573,6 +596,13 @@ const RUNTIME_FEATURE_RULES = { streamDyn: rule({ fns: ["stream.dynNative", "stream.dynNativePipe", "stream.pipeDyn", "stream.unpipeDyn"], }), + /** Fixed-width Buffer read/write methods called on checked-dynamic + * values (scr_bytes_dyn.c). */ + dynBufferNum: rule({ + expr: (node, _tables) => node.kind === "dynInvoke" && DYN_BUFFER_NUM_METHODS.has(node.method), + }), + /** Builtin functions read as values (scr_builtin_values.c). */ + builtinValues: rule({ fns: ["buffer.byteLenValue", "buffer.allocValue", "fs.realpathValue"] }), /** The stream unit emits through the emitter unit, also when the * program constructs no stream (a pipeline of functions). */ emitter: rule({ @@ -739,13 +769,13 @@ function noRuntimeFeatures(): RuntimeFeatures { fetch: false, fileHandle: false, fsWatch: false, - netLookup: false, - http: false, http2: false, + http: false, inspect: false, legacyTextDecoder: false, mockTimers: false, net: false, + netLookup: false, nodeTest: false, parseArgs: false, perfLoop: false, @@ -756,6 +786,8 @@ function noRuntimeFeatures(): RuntimeFeatures { stdioExtended: false, stream: false, streamDyn: false, + dynBufferNum: false, + builtinValues: false, symbol: false, tls: false, tlsCa: false, diff --git a/packages/compiler/src/native/sanitizer.test.ts b/packages/compiler/src/native/sanitizer.test.ts index 7607034b2..70aa08d52 100644 --- a/packages/compiler/src/native/sanitizer.test.ts +++ b/packages/compiler/src/native/sanitizer.test.ts @@ -79,6 +79,8 @@ test.skipIf(process.platform !== "linux" && process.platform !== "darwin")( dynTier: false, streamDyn: false, eventsOnceSignal: false, + dynBufferNum: false, + builtinValues: false, warningReplace: false, stdioExtended: false, regex: true, diff --git a/packages/compiler/test/array-induction-read-emission.test.ts b/packages/compiler/test/array-induction-read-emission.test.ts new file mode 100644 index 000000000..f264c31dc --- /dev/null +++ b/packages/compiler/test/array-induction-read-emission.test.ts @@ -0,0 +1,45 @@ +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { compile } from "../src/index.js"; + +async function emitLlvm(source: string): Promise { + const dir = await mkdtemp(join(tmpdir(), "scriptc-array-induction-read-")); + try { + const entry = join(dir, "main.ts"); + const outPath = join(dir, "main.ll"); + await writeFile(entry, source); + const result = await compile(entry, { outDir: dir, outPath, outputKind: "llvm" }); + expect(result.ok, JSON.stringify(result.diagnostics)).toBe(true); + return await readFile(outPath, "utf8"); + } finally { + await rm(dir, { recursive: true, force: true }); + } +} + +const source = ` +function sum(values: number[]): number { + let total = 0; + for (let i = 0; i < values.length; i++) total += values[i]!; + return total; +} +function at(values: number[], k: number): number { + return values[k]!; +} +console.log(sum([1, 2, 3]), sum(process.argv.map((a) => a.length)), at([4, 5], 1)); +`; + +test("an element read indexed by a loop counter reads above the prefix through the runtime", async () => { + const llvm = await emitLlvm(source); + const sum = /^define internal double @sc_bf_sum\([^]*?^}/m.exec(llvm)?.[0] ?? ""; + // No state address (data + cap * 9 - 1 - i) inside the loop: slots at or + // above the hole-free prefix take scr_arr_get_f64. + expect(sum).toMatch(/call double @scr_arr_get_f64\(/); + expect(sum).not.toMatch(/getelementptr inbounds \[9 x i8\], ptr /); + // The loop bound compares the counter with the i32 length field in 32 bits. + expect(sum).toMatch(/(%\S+) = trunc i64 %\S+ to i32\n\s*%\S+ = icmp ult i32 \1, %\S+/); + // Other indices keep the inline state check. + const at = /^define internal double @sc_bf_at[.(][^]*?^}/m.exec(llvm)?.[0] ?? ""; + expect(at).toMatch(/getelementptr inbounds \[9 x i8\], ptr /); +}); diff --git a/packages/compiler/test/array-length-range-emission.test.ts b/packages/compiler/test/array-length-range-emission.test.ts index 4628298dd..e24ebce45 100644 --- a/packages/compiler/test/array-length-range-emission.test.ts +++ b/packages/compiler/test/array-length-range-emission.test.ts @@ -41,7 +41,10 @@ test.runIf(process.arch === "x64" || process.arch === "arm64")( async () => { const llvm = await emit(source, "llvm"); const body = /^define internal double @sc_bf_count\([^]*?^}/m.exec(llvm)?.[0] ?? ""; - expect(body).toMatch(/= load i64, ptr %\S+(, !tbaa !\d+)?, !range !\{i64 0, i64 4294967296\}/); + // The length is an i32 field: its zero extension carries the range. + expect(body).toMatch( + /(%\S+) = load i32, ptr %\S+(, !tbaa !\d+)?\n\s*%\S+ = zext i32 \1 to i64/, + ); }, ); @@ -49,7 +52,9 @@ test.runIf(process.arch === "x64")( "a length used as an index is not converted to double and back", async () => { const asm = await emit(source, "asm"); - const body = /^sc_bf_count:[^]*?\.cfi_endproc/m.exec(asm)?.[0] ?? ""; - expect(body).not.toMatch(/cvtsi2sd|cvttsd2si/); + // `parameters[length - 1]` loads from data + length * 8 - 8: the index + // stays the integer length, folded into the address (whether or not + // `count` is inlined into its callers). + expect(asm).toMatch(/movq\t-8\(%r\w+,%r\w+,8\), %r\w+/); }, ); diff --git a/packages/compiler/test/checked-cast-fallback.test.ts b/packages/compiler/test/checked-cast-fallback.test.ts new file mode 100644 index 000000000..1eda2a7af --- /dev/null +++ b/packages/compiler/test/checked-cast-fallback.test.ts @@ -0,0 +1,172 @@ +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { compile, deserializeModule } from "../src/index.js"; +import type { IrExpr, IrFunction, IrModule, IrStmt } from "../src/ir/ir.js"; +import { everyStmtList } from "../src/ir/traverse.js"; +import type { TierMode } from "../src/frontend/tier-analysis.js"; + +async function lowerSource(source: string, tier?: TierMode): Promise { + const dir = await mkdtemp(join(tmpdir(), "scriptc-cast-fallback-")); + try { + const entry = join(dir, "main.ts"); + await writeFile(entry, source); + const outPath = join(dir, "main.ir.json"); + const result = await compile(entry, { + outPath, + outDir: dir, + outputKind: "ir", + ...(tier ? { tier } : {}), + }); + if (!result.ok) throw new Error(result.diagnostics.map((d) => d.message).join("\n")); + return deserializeModule(await readFile(outPath, "utf8")); + } finally { + await rm(dir, { recursive: true, force: true }); + } +} + +function fnNamed(mod: IrModule, name: string): IrFunction { + const fn = mod.functions.find((f) => f.name === name); + if (!fn) throw new Error(`no function ${name}`); + return fn; +} + +function checks(fn: IrFunction): (IrExpr & { kind: "dynCheck" })[] { + const found: (IrExpr & { kind: "dynCheck" })[] = []; + everyStmtList(fn.body, { + expr: (e) => { + if (e.kind === "dynCheck") found.push(e); + return true; + }, + stmt: () => true, + }); + return found; +} + +/** The `if (flag) dynamic else static` split a fallback tail lowers to. */ +function splits(fn: IrFunction): (IrStmt & { kind: "if" })[] { + const found: (IrStmt & { kind: "if" })[] = []; + everyStmtList(fn.body, { + expr: () => true, + stmt: (s) => { + if ( + s.kind === "if" && + s.cond.kind === "varRef" && + s.cond.localId.startsWith("%fb.") && + s.else_ !== null + ) + found.push(s); + return true; + }, + }); + return found; +} + +const listener = ` +interface Order { deployment: { id: string; plan: string } } +interface Req { buildOrder: Order; traceCarrier?: Record } +function accept(body: string): string { + let request: Partial; + try { + request = JSON.parse(body) as Partial; + } catch { + return "400"; + } + const id = request.buildOrder?.deployment?.id; + return id ? "200 " + id : "400 missing"; +} +console.log(accept('{"buildOrder":{"deployment":{"id":"d"}}}')); +`; + +test("a block-local checked cast continues in a dynamic tail behind one flag", async () => { + const fn = fnNamed(await lowerSource(listener), "accept"); + const [check] = checks(fn); + expect(check?.fallback).toBe(true); + const split = splits(fn); + expect(split).toHaveLength(1); + // The static tail reads the record natively; the dynamic one reads the + // failed value's shadow. + const shadowReads = (body: IrStmt[]): number => { + let n = 0; + everyStmtList(body, { + expr: (e) => { + if (e.kind === "varRef" && e.localId.startsWith("%fbv.")) n++; + return true; + }, + stmt: () => true, + }); + return n; + }; + expect(shadowReads(split[0]!.then)).toBeGreaterThan(0); + expect(shadowReads(split[0]!.else_!)).toBe(0); +}); + +test("--tier=static-only keeps the throwing cast", async () => { + const fn = fnNamed(await lowerSource(listener, "static-only"), "accept"); + expect(checks(fn).some((c) => c.fallback)).toBe(false); + expect(splits(fn)).toHaveLength(0); + expect(fn.locals.some((l) => l.id.startsWith("%fb"))).toBe(false); +}); + +test("casts whose binding is read outside the code after them, or written again, keep throwing", async () => { + const mod = await lowerSource(` +interface P { x: number } +const top = JSON.parse('{"x":1}') as P; +console.log(top.x); +function before(text: string): number { + let p: P | undefined; + for (let i = 0; i < 2; i++) { + if (p) console.log(p.x); + p = JSON.parse(text) as P; + } + return p?.x ?? 0; +} +function rewritten(text: string): number { + let p = JSON.parse(text) as P; + p = { x: 2 }; + return p.x; +} +function hoisted(text: string): number { + const p = JSON.parse(text) as P; + function read(): number { return p.x; } + return read(); +} +console.log(before('{"x":1}'), rewritten('{"x":1}'), hoisted('{"x":1}')); +`); + for (const name of ["before", "rewritten", "hoisted"]) { + const fn = fnNamed(mod, name); + expect( + checks(fn).some((c) => c.fallback), + name, + ).toBe(false); + expect( + fn.locals.some((l) => l.id.startsWith("%fb")), + name, + ).toBe(false); + } + expect(mod.functions.flatMap(checks).some((c) => c.fallback)).toBe(false); +}); + +test("sequential casts split the code after them at most twice", async () => { + const source = ` +interface P { x: number } +function sum(a: string, b: string, c: string, d: string): number { + const p = JSON.parse(a) as P; + const q = JSON.parse(b) as P; + const r = JSON.parse(c) as P; + const s = JSON.parse(d) as P; + return p.x + q.x + r.x + s.x; +} +console.log(sum('{"x":1}', '{"x":2}', '{"x":3}', '{"x":4}')); +`; + const fn = fnNamed(await lowerSource(source), "sum"); + // p splits the code after it, and q splits each copy of that code; r and + // s keep the throwing cast instead of doubling the copies again. + expect(splits(fn)).toHaveLength(3); + const binding = (c: { loc: { start: number } }) => + /const (\w) = [^\n]*$/.exec(source.slice(0, c.loc.start))?.[1]; + const casts = checks(fn); + expect(new Set(casts.filter((c) => c.fallback).map(binding))).toEqual(new Set(["p", "q"])); + expect(new Set(casts.filter((c) => !c.fallback).map(binding))).toEqual(new Set(["r", "s"])); +}); diff --git a/packages/compiler/test/json-callbacks.test.ts b/packages/compiler/test/json-callbacks.test.ts index 308d626e3..d10721d0b 100644 --- a/packages/compiler/test/json-callbacks.test.ts +++ b/packages/compiler/test/json-callbacks.test.ts @@ -53,7 +53,8 @@ for (const backend of ["llvm"] as const) { } catch (error) { if (error instanceof Error) console.log(error.message); } - // A checked cast still validates the result AFTER revival. + // A checked cast validates the result AFTER revival; a revived value + // that does not fit continues as that value (checked-cast fallback). try { const value = JSON.parse('{"n":1}', (key: string, value: unknown): unknown => key === "n" ? "wrong" : value) as { n: number }; console.log(value.n); @@ -75,6 +76,8 @@ for (const backend of ["llvm"] as const) { console.log(result === undefined, typeof result); } local(); + // Values that do not fit continue as their parsed values, every + // iteration (and release cleanly under the sanitizer lane). for (let i = 0; i < 20; i++) { try { const record = JSON.parse('{"opaque":{"deep":[1,2]},"typed":"wrong"}') as { opaque: unknown; typed: number }; @@ -105,13 +108,12 @@ for (const backend of ["llvm"] as const) { expect(result.stdout.toString()).toBe( [ "JSON.parse reviver deleting array elements is not supported yet", - "true", + "wrong", "true", "true", "true", "true undefined", - "true", - "true", + ...Array.from({ length: 20 }, () => ["wrong", "2"]).flat(), "1", "", ].join("\n"), diff --git a/packages/compiler/test/native-codegen-integration.test.ts b/packages/compiler/test/native-codegen-integration.test.ts index 1c5f9e3e7..c0f2cfe84 100644 --- a/packages/compiler/test/native-codegen-integration.test.ts +++ b/packages/compiler/test/native-codegen-integration.test.ts @@ -378,7 +378,7 @@ done: expect(await symbols(helperObject, ["-u"])).toEqual(await symbols(clangObject, ["-u"])); expect(await symbols(helperObject, ["-gU"])).toEqual(await symbols(clangObject, ["-gU"])); expect(await symbols(helperObject, ["-gU"])).toEqual(["0000000000000000 T _main"]); - expect(await symbols(helperObject, ["-u"])).toContain("_scr_runtime_abi_v22"); + expect(await symbols(helperObject, ["-u"])).toContain("_scr_runtime_abi_v23"); const clangExe = join(dir, "clang-program"); const linkDriver = join(dir, "clang-driver.c"); @@ -712,7 +712,7 @@ done: .split("\n"); expect(undefinedSymbols).toContain("_sf_scale"); expect(undefinedSymbols).toContain("_sf_callback_mix"); - expect(undefinedSymbols).toContain("_scr_runtime_abi_v22"); + expect(undefinedSymbols).toContain("_scr_runtime_abi_v23"); }); test("helper and clang object paths retain the same outbound FFI C ABI", async () => { diff --git a/packages/compiler/test/package-shim-guard.test.ts b/packages/compiler/test/package-shim-guard.test.ts new file mode 100644 index 000000000..ff01acbb8 --- /dev/null +++ b/packages/compiler/test/package-shim-guard.test.ts @@ -0,0 +1,64 @@ +/* Every package shim (packages/compiler/shims, registered in + * src/frontend/package-shims.ts) compiles without a lowering refusal or a + * runtime fence anywhere in its own files. + * + * Shim files suppress TypeScript errors (like library declarations under + * skipLibCheck), but never lowering diagnostics: a refusal in code the + * program reaches fails its build, located in the shim, and one in code it + * does not reach would fail a later program. This test reaches all of it: + * the analysis lowers the unreached remainder too, so every function body + * of every shim module is checked, under both type worlds a project can + * bring (scriptc's bundled Node declarations, and @types/node). + */ +import { mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterAll, describe, expect, test } from "vitest"; +import { analyze } from "../src/index.js"; +import { packageShimEntries } from "../src/frontend/package-shims.js"; +import type { ScrDiagnostic } from "../src/diagnostics/diagnostic.js"; + +const repoRoot = join(import.meta.dirname, "../../.."); +const shimRoot = join(repoRoot, "packages/compiler/shims"); +const scratch = mkdtempSync(join(tmpdir(), "scriptc-shim-guard-")); +afterAll(() => rmSync(scratch, { recursive: true, force: true })); + +function shimFindings(entry: string, world: "bundled" | "types-node"): string[] { + const dir = join(scratch, `${world}-${entry.slice(shimRoot.length + 1).replace(/[/.]/g, "_")}`); + mkdirSync(dir, { recursive: true }); + if (world === "types-node") { + mkdirSync(join(dir, "node_modules/@types"), { recursive: true }); + symlinkSync(join(repoRoot, "node_modules/@types/node"), join(dir, "node_modules/@types/node")); + } + const main = join(dir, "main.ts"); + writeFileSync(main, `import ${JSON.stringify(entry)};\nconsole.log("loaded");\n`); + const { coverage } = analyze(main); + expect(coverage.preflightFailed, "the shim type-checks for the analysis").toBe(false); + const all: ScrDiagnostic[] = [ + ...coverage.diagnostics, + ...(coverage.runtimeFences ?? []), + ...(coverage.unreached?.diagnostics ?? []), + ...(coverage.unreached?.runtimeFences ?? []), + ]; + return all + .filter((d) => d.loc.file.startsWith(shimRoot)) + .map((d) => `${d.loc.file.slice(shimRoot.length + 1)}: ${d.code} ${d.message}`); +} + +describe("package shims compile without refusals", () => { + const entries = packageShimEntries(); + test("the registry lists its shim modules", () => { + expect(entries.map((e) => e.slice(shimRoot.length + 1))).toEqual([ + "fs-extra/index.ts", + "undici/v5.ts", + "undici/v7.ts", + ]); + }); + for (const entry of entries) { + for (const world of ["bundled", "types-node"] as const) { + test(`${entry.slice(shimRoot.length + 1)} (${world})`, () => { + expect(shimFindings(entry, world)).toEqual([]); + }, 300_000); + } + } +}); 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/6412-any-valued-dictionaries.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6412-any-valued-dictionaries.ts.json new file mode 100644 index 000000000..70a5823f4 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6412-any-valued-dictionaries.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6412-any-valued-dictionaries.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6413-sdk-declared-data-class/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6413-sdk-declared-data-class/main.ts.json new file mode 100644 index 000000000..83fe76101 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6413-sdk-declared-data-class/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6413-sdk-declared-data-class/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6414-pick-class-method-views.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6414-pick-class-method-views.ts.json new file mode 100644 index 000000000..c5b440992 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6414-pick-class-method-views.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6414-pick-class-method-views.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6419-untyped-async-package/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6419-untyped-async-package/main.ts.json new file mode 100644 index 000000000..ab4ee5819 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6419-untyped-async-package/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6419-untyped-async-package/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6440-cycle-class-late-use/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6440-cycle-class-late-use/main.ts.json new file mode 100644 index 000000000..a81642ee8 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6440-cycle-class-late-use/main.ts.json @@ -0,0 +1,8 @@ +{ + "order": [ + "/tests/corpus/6440-cycle-class-late-use/stats.ts", + "/tests/corpus/6440-cycle-class-late-use/log.ts", + "/tests/corpus/6440-cycle-class-late-use/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6441-cycle-class-early-access/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6441-cycle-class-early-access/main.ts.json new file mode 100644 index 000000000..76ab0b5b4 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6441-cycle-class-early-access/main.ts.json @@ -0,0 +1,8 @@ +{ + "order": [ + "/tests/corpus/6441-cycle-class-early-access/stats.ts", + "/tests/corpus/6441-cycle-class-early-access/log.ts", + "/tests/corpus/6441-cycle-class-early-access/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6442-cycle-class-factory/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6442-cycle-class-factory/main.ts.json new file mode 100644 index 000000000..530a6661c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6442-cycle-class-factory/main.ts.json @@ -0,0 +1,8 @@ +{ + "order": [ + "/tests/corpus/6442-cycle-class-factory/pets.ts", + "/tests/corpus/6442-cycle-class-factory/dogs.ts", + "/tests/corpus/6442-cycle-class-factory/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6452-dns-lookup-runtime-options.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6452-dns-lookup-runtime-options.ts.json new file mode 100644 index 000000000..350a2fa6e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6452-dns-lookup-runtime-options.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6452-dns-lookup-runtime-options.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6453-dns-lookup-runtime-callbacks.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6453-dns-lookup-runtime-callbacks.ts.json new file mode 100644 index 000000000..c8e2dfd98 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6453-dns-lookup-runtime-callbacks.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6453-dns-lookup-runtime-callbacks.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6476-in-literal-key-index-records.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6476-in-literal-key-index-records.ts.json new file mode 100644 index 000000000..c030a0076 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6476-in-literal-key-index-records.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6476-in-literal-key-index-records.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6477-fresh-array-spread-insertion.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6477-fresh-array-spread-insertion.ts.json new file mode 100644 index 000000000..3e3775faa --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6477-fresh-array-spread-insertion.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6477-fresh-array-spread-insertion.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6478-any-annotated-dictionary.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6478-any-annotated-dictionary.ts.json new file mode 100644 index 000000000..8925564a5 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6478-any-annotated-dictionary.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6478-any-annotated-dictionary.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6481-set-add-chaining.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6481-set-add-chaining.ts.json new file mode 100644 index 000000000..1168a1cbc --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6481-set-add-chaining.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6481-set-add-chaining.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6492-runtime-key-destructuring.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6492-runtime-key-destructuring.ts.json new file mode 100644 index 000000000..79506d7b1 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6492-runtime-key-destructuring.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6492-runtime-key-destructuring.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6493-runtime-key-destructuring.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6493-runtime-key-destructuring.cjs.json new file mode 100644 index 000000000..5c31e0541 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6493-runtime-key-destructuring.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6493-runtime-key-destructuring.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6494-default-export-class-members/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6494-default-export-class-members/main.ts.json new file mode 100644 index 000000000..24634cff1 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6494-default-export-class-members/main.ts.json @@ -0,0 +1,9 @@ +{ + "order": [ + "/tests/corpus/6494-default-export-class-members/lib/date.js", + "/tests/corpus/6494-default-export-class-members/lib/error.js", + "/tests/corpus/6494-default-export-class-members/lib/index.js", + "/tests/corpus/6494-default-export-class-members/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6495-generic-method-overloads.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6495-generic-method-overloads.ts.json new file mode 100644 index 000000000..98806a53c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6495-generic-method-overloads.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6495-generic-method-overloads.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6496-overload-signatures.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6496-overload-signatures.ts.json new file mode 100644 index 000000000..8bc601bd6 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6496-overload-signatures.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6496-overload-signatures.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6497-generic-overload-bindings.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6497-generic-overload-bindings.ts.json new file mode 100644 index 000000000..06e7bb17f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6497-generic-overload-bindings.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6497-generic-overload-bindings.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6520-match-or-empty-widened.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6520-match-or-empty-widened.ts.json new file mode 100644 index 000000000..562b72174 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6520-match-or-empty-widened.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6520-match-or-empty-widened.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6521-tuple-spread-into-array.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6521-tuple-spread-into-array.ts.json new file mode 100644 index 000000000..c464e1921 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6521-tuple-spread-into-array.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6521-tuple-spread-into-array.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6522-stats-arrays.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6522-stats-arrays.ts.json new file mode 100644 index 000000000..6b1bc14e5 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6522-stats-arrays.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6522-stats-arrays.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6523-then-primitive-constructors.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6523-then-primitive-constructors.ts.json new file mode 100644 index 000000000..35ec854b9 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6523-then-primitive-constructors.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6523-then-primitive-constructors.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6524-match-or-empty-tuple.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6524-match-or-empty-tuple.ts.json new file mode 100644 index 000000000..8a6728d33 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6524-match-or-empty-tuple.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6524-match-or-empty-tuple.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6525-error-subclass-fields.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6525-error-subclass-fields.ts.json new file mode 100644 index 000000000..8ff1df128 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6525-error-subclass-fields.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6525-error-subclass-fields.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6526-error-subclass-cycles.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6526-error-subclass-cycles.ts.json new file mode 100644 index 000000000..9353afa09 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6526-error-subclass-cycles.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6526-error-subclass-cycles.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6530-error-subclass-header.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6530-error-subclass-header.ts.json new file mode 100644 index 000000000..084c4817a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6530-error-subclass-header.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6530-error-subclass-header.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6550-npm-boolean-bitwise/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6550-npm-boolean-bitwise/main.ts.json new file mode 100644 index 000000000..a34eeedc0 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6550-npm-boolean-bitwise/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6550-npm-boolean-bitwise/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6560-npm-js-capability-probes/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6560-npm-js-capability-probes/main.ts.json new file mode 100644 index 000000000..62b900d3c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6560-npm-js-capability-probes/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6560-npm-js-capability-probes/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6565-rebindable-function-import-cycle/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6565-rebindable-function-import-cycle/main.ts.json new file mode 100644 index 000000000..f0d46c0bf --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6565-rebindable-function-import-cycle/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6565-rebindable-function-import-cycle/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6570-npm-builtin-function-bind/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6570-npm-builtin-function-bind/main.ts.json new file mode 100644 index 000000000..3db56c8ab --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6570-npm-builtin-function-bind/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6570-npm-builtin-function-bind/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6570-require-mixed-declarations.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6570-require-mixed-declarations.cjs.json new file mode 100644 index 000000000..fd070103c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6570-require-mixed-declarations.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6570-require-mixed-declarations.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6592-promise-armed-method-values/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6592-promise-armed-method-values/main.ts.json new file mode 100644 index 000000000..1fb3f21bc --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6592-promise-armed-method-values/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6592-promise-armed-method-values/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/array-length-limits.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/array-length-limits.ts.json new file mode 100644 index 000000000..08bab6d2a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/array-length-limits.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/array-length-limits.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/class-id-instances-as-streams.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/class-id-instances-as-streams.ts.json new file mode 100644 index 000000000..01d871cb3 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/class-id-instances-as-streams.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/class-id-instances-as-streams.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/fs-extra-package/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/fs-extra-package/main.ts.json new file mode 100644 index 000000000..d48b63a86 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/fs-extra-package/main.ts.json @@ -0,0 +1,8 @@ +{ + "order": [ + "/packages/compiler/shims/fs-extra/core/api.ts", + "/packages/compiler/shims/fs-extra/index.ts", + "/tests/corpus/fs-extra-package/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/global-detection-typeof.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/global-detection-typeof.cjs.json new file mode 100644 index 000000000..93ea8e3a7 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/global-detection-typeof.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/global-detection-typeof.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/json-parse-fallback-extra-keys.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/json-parse-fallback-extra-keys.ts.json new file mode 100644 index 000000000..8f5694905 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/json-parse-fallback-extra-keys.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/json-parse-fallback-extra-keys.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/json-parse-fallback-flows.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/json-parse-fallback-flows.ts.json new file mode 100644 index 000000000..2084d8a65 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/json-parse-fallback-flows.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/json-parse-fallback-flows.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/json-parse-fallback-missing.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/json-parse-fallback-missing.ts.json new file mode 100644 index 000000000..17562fe47 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/json-parse-fallback-missing.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/json-parse-fallback-missing.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/json-parse-fallback-nested.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/json-parse-fallback-nested.ts.json new file mode 100644 index 000000000..3585687e2 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/json-parse-fallback-nested.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/json-parse-fallback-nested.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/json-parse-fallback-partial.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/json-parse-fallback-partial.ts.json new file mode 100644 index 000000000..26bae0445 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/json-parse-fallback-partial.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/json-parse-fallback-partial.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/json-parse-fallback-wrong-types.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/json-parse-fallback-wrong-types.ts.json new file mode 100644 index 000000000..fd445ea1c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/json-parse-fallback-wrong-types.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/json-parse-fallback-wrong-types.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/readable-stream-3/main.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/readable-stream-3/main.cjs.json new file mode 100644 index 000000000..8a308528a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/readable-stream-3/main.cjs.json @@ -0,0 +1,27 @@ +{ + "order": [ + "/tests/corpus/readable-stream-3/main.cjs" + ], + "diags": [ + { + "code": "SC0001", + "message": "Could not find a declaration file for module 'readable-stream'. '/tests/corpus/readable-stream-3/node_modules/readable-stream/readable.js' implicitly has an 'any' type.\n Try `npm i --save-dev @types/readable-stream` if it exists or add a new declaration (.d.ts) file containing `declare module 'readable-stream';`", + "loc": { + "file": "/tests/corpus/readable-stream-3/main.cjs", + "start": 554, + "end": 571 + } + }, + { + "code": "SC1010", + "message": "the 'readable-stream' module is not supported yet", + "loc": { + "file": "/tests/corpus/readable-stream-3/main.cjs", + "start": 541, + "end": 572 + }, + "milestone": "M4", + "hint": "relative imports (./file, ../dir/file), package.json-mediated project imports (#alias via the imports field, self-name references via exports), statically compiled npm packages, and the built-in fs, fs/promises, path, os, url, crypto, zlib, child_process, net, http, tls, https, http2, dgram, dns, dns/promises, util, util/types, string_decoder, querystring, readline, events, stream, stream/promises, stream/consumers, stream/web, buffer, assert, assert/strict, worker_threads, cluster, tty, async_hooks, timers, timers/promises, diagnostics_channel, perf_hooks, module, v8, and console modules (bare or node:-prefixed) and node:test (node:-prefixed only, like in Node) are supported" + } + ] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/readable-stream-disable/main.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/readable-stream-disable/main.cjs.json new file mode 100644 index 000000000..21f85bd01 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/readable-stream-disable/main.cjs.json @@ -0,0 +1,87 @@ +{ + "order": [ + "/tests/corpus/readable-stream-disable/main.cjs" + ], + "diags": [ + { + "code": "SC0001", + "message": "Could not find a declaration file for module 'readable-stream'. '/tests/corpus/readable-stream-disable/node_modules/readable-stream/readable.js' implicitly has an 'any' type.\n Try `npm i --save-dev @types/readable-stream` if it exists or add a new declaration (.d.ts) file containing `declare module 'readable-stream';`", + "loc": { + "file": "/tests/corpus/readable-stream-disable/main.cjs", + "start": 581, + "end": 598 + } + }, + { + "code": "SC0001", + "message": "Could not find a declaration file for module 'readable-stream/transform'. '/tests/corpus/readable-stream-disable/node_modules/readable-stream/transform.js' implicitly has an 'any' type.\n Try `npm i --save-dev @types/readable-stream` if it exists or add a new declaration (.d.ts) file containing `declare module 'readable-stream/transform';`", + "loc": { + "file": "/tests/corpus/readable-stream-disable/main.cjs", + "start": 625, + "end": 652 + } + }, + { + "code": "SC0001", + "message": "Could not find a declaration file for module 'readable-stream/passthrough'. '/tests/corpus/readable-stream-disable/node_modules/readable-stream/passthrough.js' implicitly has an 'any' type.\n Try `npm i --save-dev @types/readable-stream` if it exists or add a new declaration (.d.ts) file containing `declare module 'readable-stream/passthrough';`", + "loc": { + "file": "/tests/corpus/readable-stream-disable/main.cjs", + "start": 681, + "end": 710 + } + }, + { + "code": "SC0001", + "message": "Could not find a declaration file for module 'readable-stream/writable'. '/tests/corpus/readable-stream-disable/node_modules/readable-stream/writable.js' implicitly has an 'any' type.\n Try `npm i --save-dev @types/readable-stream` if it exists or add a new declaration (.d.ts) file containing `declare module 'readable-stream/writable';`", + "loc": { + "file": "/tests/corpus/readable-stream-disable/main.cjs", + "start": 736, + "end": 762 + } + }, + { + "code": "SC1010", + "message": "the 'readable-stream' module is not supported yet", + "loc": { + "file": "/tests/corpus/readable-stream-disable/main.cjs", + "start": 568, + "end": 599 + }, + "milestone": "M4", + "hint": "relative imports (./file, ../dir/file), package.json-mediated project imports (#alias via the imports field, self-name references via exports), statically compiled npm packages, and the built-in fs, fs/promises, path, os, url, crypto, zlib, child_process, net, http, tls, https, http2, dgram, dns, dns/promises, util, util/types, string_decoder, querystring, readline, events, stream, stream/promises, stream/consumers, stream/web, buffer, assert, assert/strict, worker_threads, cluster, tty, async_hooks, timers, timers/promises, diagnostics_channel, perf_hooks, module, v8, and console modules (bare or node:-prefixed) and node:test (node:-prefixed only, like in Node) are supported" + }, + { + "code": "SC1010", + "message": "the 'readable-stream/transform' module is not supported yet", + "loc": { + "file": "/tests/corpus/readable-stream-disable/main.cjs", + "start": 605, + "end": 653 + }, + "milestone": "M4", + "hint": "relative imports (./file, ../dir/file), package.json-mediated project imports (#alias via the imports field, self-name references via exports), statically compiled npm packages, and the built-in fs, fs/promises, path, os, url, crypto, zlib, child_process, net, http, tls, https, http2, dgram, dns, dns/promises, util, util/types, string_decoder, querystring, readline, events, stream, stream/promises, stream/consumers, stream/web, buffer, assert, assert/strict, worker_threads, cluster, tty, async_hooks, timers, timers/promises, diagnostics_channel, perf_hooks, module, v8, and console modules (bare or node:-prefixed) and node:test (node:-prefixed only, like in Node) are supported" + }, + { + "code": "SC1010", + "message": "the 'readable-stream/passthrough' module is not supported yet", + "loc": { + "file": "/tests/corpus/readable-stream-disable/main.cjs", + "start": 659, + "end": 711 + }, + "milestone": "M4", + "hint": "relative imports (./file, ../dir/file), package.json-mediated project imports (#alias via the imports field, self-name references via exports), statically compiled npm packages, and the built-in fs, fs/promises, path, os, url, crypto, zlib, child_process, net, http, tls, https, http2, dgram, dns, dns/promises, util, util/types, string_decoder, querystring, readline, events, stream, stream/promises, stream/consumers, stream/web, buffer, assert, assert/strict, worker_threads, cluster, tty, async_hooks, timers, timers/promises, diagnostics_channel, perf_hooks, module, v8, and console modules (bare or node:-prefixed) and node:test (node:-prefixed only, like in Node) are supported" + }, + { + "code": "SC1010", + "message": "the 'readable-stream/writable' module is not supported yet", + "loc": { + "file": "/tests/corpus/readable-stream-disable/main.cjs", + "start": 717, + "end": 763 + }, + "milestone": "M4", + "hint": "relative imports (./file, ../dir/file), package.json-mediated project imports (#alias via the imports field, self-name references via exports), statically compiled npm packages, and the built-in fs, fs/promises, path, os, url, crypto, zlib, child_process, net, http, tls, https, http2, dgram, dns, dns/promises, util, util/types, string_decoder, querystring, readline, events, stream, stream/promises, stream/consumers, stream/web, buffer, assert, assert/strict, worker_threads, cluster, tty, async_hooks, timers, timers/promises, diagnostics_channel, perf_hooks, module, v8, and console modules (bare or node:-prefixed) and node:test (node:-prefixed only, like in Node) are supported" + } + ] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/checker-panic-tuple.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/checker-panic-tuple.ts.json deleted file mode 100644 index 4a57502d9..000000000 --- a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/checker-panic-tuple.ts.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "order": [ - "/tests/diagnostics/checker-panic-tuple.ts" - ], - "diags": [] -} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/error-fields-cause-fences.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/error-fields-cause-fences.ts.json new file mode 100644 index 000000000..5e0eef5d2 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/error-fields-cause-fences.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/error-fields-cause-fences.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/error-header-accessor-fences.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/error-header-accessor-fences.ts.json new file mode 100644 index 000000000..1055edd13 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/error-header-accessor-fences.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/error-header-accessor-fences.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/error-header-prototype-fences.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/error-header-prototype-fences.ts.json new file mode 100644 index 000000000..f670a2444 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/error-header-prototype-fences.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/error-header-prototype-fences.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/fresh-spread-exported/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/fresh-spread-exported/main.ts.json new file mode 100644 index 000000000..2dd42cd27 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/fresh-spread-exported/main.ts.json @@ -0,0 +1,7 @@ +{ + "order": [ + "/tests/diagnostics/fresh-spread-exported/items.ts", + "/tests/diagnostics/fresh-spread-exported/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/fresh-spread-repeated.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/fresh-spread-repeated.ts.json new file mode 100644 index 000000000..cb4b2f25d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/fresh-spread-repeated.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/fresh-spread-repeated.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/nested-member-blocker-path.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/nested-member-blocker-path.ts.json new file mode 100644 index 000000000..b52b29f92 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/nested-member-blocker-path.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/nested-member-blocker-path.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/overload-fences.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/overload-fences.ts.json deleted file mode 100644 index 1515ceeb9..000000000 --- a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/overload-fences.ts.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "order": [ - "/tests/diagnostics/overload-fences.ts" - ], - "diags": [] -} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/pick-view-generic-methods.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/pick-view-generic-methods.ts.json new file mode 100644 index 000000000..1dafe9a3a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/pick-view-generic-methods.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/pick-view-generic-methods.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/process-versions-absent.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/process-versions-absent.ts.json new file mode 100644 index 000000000..1d842a3fe --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/process-versions-absent.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/node-types/process-versions-absent.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2585-builtin-polyfill-aliases/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2585-builtin-polyfill-aliases/main.ts.json new file mode 100644 index 000000000..6ccd6c657 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2585-builtin-polyfill-aliases/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/npm/cases/2585-builtin-polyfill-aliases/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2586-readable-stream-builtin/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2586-readable-stream-builtin/main.ts.json new file mode 100644 index 000000000..db2264f1a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2586-readable-stream-builtin/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/npm/cases/2586-readable-stream-builtin/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2587-bundled-polyfill-aliases/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2587-bundled-polyfill-aliases/main.ts.json new file mode 100644 index 000000000..f8fcef230 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2587-bundled-polyfill-aliases/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/npm/cases/2587-bundled-polyfill-aliases/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2588-bundled-esm-classes/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2588-bundled-esm-classes/main.ts.json new file mode 100644 index 000000000..6a031d215 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2588-bundled-esm-classes/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/npm/cases/2588-bundled-esm-classes/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2589-bundled-error-subclass/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2589-bundled-error-subclass/main.ts.json new file mode 100644 index 000000000..0d5592a51 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2589-bundled-error-subclass/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/npm/cases/2589-bundled-error-subclass/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2590-bundled-fs-extra/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2590-bundled-fs-extra/main.ts.json new file mode 100644 index 000000000..7763781d1 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2590-bundled-fs-extra/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/npm/cases/2590-bundled-fs-extra/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/opentelemetry-api/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/opentelemetry-api/main.ts.json new file mode 100644 index 000000000..cf29b6130 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/opentelemetry-api/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/npm/cases/opentelemetry-api/main.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/baselines/order-parity/tests/fixtures/npm/cases/workspace-dynamic-import-deps/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/workspace-dynamic-import-deps/main.ts.json new file mode 100644 index 000000000..9206e9bfc --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/workspace-dynamic-import-deps/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/npm/cases/workspace-dynamic-import-deps/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/workspace-dynamic-import/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/workspace-dynamic-import/main.ts.json new file mode 100644 index 000000000..0f6c04a58 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/workspace-dynamic-import/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/npm/cases/workspace-dynamic-import/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/resolver-parity.test.ts b/packages/compiler/test/ts7/resolver-parity.test.ts index b1788d4da..61a84baa9 100644 --- a/packages/compiler/test/ts7/resolver-parity.test.ts +++ b/packages/compiler/test/ts7/resolver-parity.test.ts @@ -22,6 +22,7 @@ import { resolveTypeDirective, setProjectPathMappings, } from "../../src/frontend/resolve.js"; +import { packageShimFor } from "../../src/frontend/package-shims.js"; import { isRelativeSpecifier } from "../../src/frontend/workspace-registry.js"; import { options5 } from "./harness.js"; @@ -138,14 +139,21 @@ test("relative and bare specifiers across the whole test tree resolve identicall // the resolver follows Node — corpus 2124 pins the behavior // differentially, so parity has nothing to check here. if (spec.startsWith("#/")) continue; + // A package scriptc implements itself (package-shims.ts) resolves + // to its built-in module by design; 5.9.3 answers the installed + // package. + if (packageShimFor(file, spec) !== null) continue; bare++; // PROJECT imports (#alias / self-name): ts5's NON-external answers // for bare specifiers are exactly the package.json-mediated // resolutions inside the project — resolveProjectImport must agree. + // A package scriptc implements itself (package-shims.ts) answers its + // shim module where 5.9.3 answers the installed package, so only the + // external-library answer below is compared for it. const oursProj = resolveProjectImport(file, spec); const theirsProj = reference && !reference.isExternalLibraryImport ? reference.resolvedFileName : null; - if (oursProj !== theirsProj) { + if (oursProj !== theirsProj && packageShimFor(file, spec) === null) { failures.push(`${file} '${spec}' (project): ours=${oursProj} ts5=${theirsProj}`); } const ours = resolveBareModule(file, spec); diff --git a/packages/compiler/test/undici-shim-framing.test.ts b/packages/compiler/test/undici-shim-framing.test.ts new file mode 100644 index 000000000..ee61115ee --- /dev/null +++ b/packages/compiler/test/undici-shim-framing.test.ts @@ -0,0 +1,165 @@ +/* Message framing in the undici shim (packages/compiler/shims/undici), + * against the real undici 5.12 and 7.30 the undici5/undici7 npm fixtures + * vendor: + * + * - a chunk-size line is read as it arrives: an extension streamed in many + * pieces is skipped without keeping its bytes, and an oversized chunk + * size fails the response; + * - a request with two Content-Length fields is rejected in both profiles + * before anything is written. + */ +import { createServer, type Server, type Socket } from "node:net"; +import { join } from "node:path"; +import { afterAll, beforeAll, describe, expect, test } from "vitest"; +import * as shim5 from "../shims/undici/v5.ts"; +import * as shim7 from "../shims/undici/v7.ts"; + +const repoRoot = join(import.meta.dirname, "../../.."); +const real7 = (await import( + join(repoRoot, "tests/fixtures/npm/cases/undici7/node_modules/undici/index.js") +)) as typeof shim7; +const real5 = (await import( + join(repoRoot, "tests/fixtures/npm/cases/undici5/node_modules/undici/index.js") +)) as typeof shim7; + +type Respond = (socket: Socket) => void; +let respond: Respond = () => {}; +let server: Server; +let origin = ""; + +beforeAll(async () => { + server = createServer((socket) => { + socket.once("data", () => respond(socket)); + socket.on("error", () => {}); + }); + await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); + const address = server.address(); + if (address === null || typeof address === "string") throw new Error("no port"); + origin = `http://127.0.0.1:${address.port}`; +}); + +afterAll(() => new Promise((resolve) => server.close(() => resolve()))); + +/** The body text, or the error's name, code and message. */ +async function outcome(undici: typeof shim7, path: string): Promise { + const agent = new undici.Agent(); + try { + const res = await undici.request(`${origin}${path}`, { dispatcher: agent }); + return `body ${await res.body.text()}`; + } catch (err) { + const e = err as Error & { code?: string }; + return `${e.name} ${e.code ?? ""} ${e.message}`; + } finally { + await agent.close(); + } +} + +const HEAD = "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\nConnection: close\r\n\r\n"; + +describe("undici shim chunked framing", () => { + test("a chunk extension streamed in many pieces is skipped", async () => { + // The bytes the shim's parser holds while the extension streams in. + let parser: { buf: Buffer; queue: Buffer[] } | null = null; + let retained = 0; + respond = (socket) => { + socket.write(HEAD + "5;name="); + let left = 2048; + const piece = "x".repeat(1024); + const next = (): void => { + if (parser !== null) { + const held = parser.buf.length + parser.queue.reduce((n, b) => n + b.length, 0); + retained = Math.max(retained, held); + } + if (left-- > 0) { + socket.write(piece); + setImmediate(next); + } else { + socket.end("\r\nhello\r\n0\r\n\r\n"); + } + }; + next(); + }; + const expected = await outcome(real7, "/extension"); + expect(expected).toBe("body hello"); + + const client = new shim7.Client(origin) as unknown as { + __parser: { buf: Buffer; queue: Buffer[] } | null; + request(options: object): Promise<{ body: { text(): Promise } }>; + close(): Promise; + }; + const pending = client.request({ path: "/extension", method: "GET" }); + const watch = setInterval(() => { + if (client.__parser !== null) parser = client.__parser; + }, 0); + const res = await pending; + expect(await res.body.text()).toBe("hello"); + clearInterval(watch); + await client.close(); + expect(parser).not.toBeNull(); + expect(retained).toBeLessThan(64 * 1024); + }, 60_000); + + for (const [name, real, undici] of [ + ["5.x", real5, shim5 as unknown as typeof shim7], + ["7.x", real7, shim7], + ] as const) { + test(`${name}: an oversized chunk size fails the response`, async () => { + respond = (socket) => socket.end(HEAD + "fffffffffffffffffff\r\n"); + const expected = await outcome(real, "/overflow"); + expect(expected).toContain("Chunk size overflow"); + expect(await outcome(undici, "/overflow")).toBe(expected); + }); + + test(`${name}: an invalid chunk-size character fails the response`, async () => { + respond = (socket) => socket.end(HEAD + "5z\r\nhello\r\n0\r\n\r\n"); + const expected = await outcome(real, "/invalid"); + expect(expected).toContain("Invalid character in chunk size"); + expect(await outcome(undici, "/invalid")).toBe(expected); + }); + } +}); + +describe("undici shim Content-Length", () => { + const headers = { "content-length": "5", "Content-Length": "5" }; + for (const [name, undici] of [ + ["5.x", shim5 as unknown as typeof shim7], + ["7.x", shim7], + ] as const) { + test(`${name} rejects a duplicate Content-Length`, async () => { + let wrote = false; + respond = (socket) => { + wrote = true; + socket.end("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"); + }; + const agent = new undici.Agent(); + const err = await undici + .request(`${origin}/duplicate`, { + dispatcher: agent, + method: "POST", + headers, + body: "hello", + }) + .then( + () => null, + (e: Error & { code?: string }) => e, + ); + await agent.close(); + expect(err?.message).toBe("duplicate content-length header"); + expect(err?.code).toBe("UND_ERR_INVALID_ARG"); + expect(wrote).toBe(false); + }); + } + + test("real undici 7.x rejects it with the same error", async () => { + const agent = new real7.Agent(); + const err = await real7 + .request(`${origin}/duplicate`, { dispatcher: agent, method: "POST", headers, body: "hello" }) + .then( + () => null, + (e: Error & { code?: string }) => e, + ); + await agent.close(); + expect(err?.message).toBe("duplicate content-length header"); + expect(err?.code).toBe("UND_ERR_INVALID_ARG"); + }); +}); 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/scripts/build.mjs b/packages/runtime-darwin-arm64/scripts/build.mjs index dbde1564f..86ac887e4 100644 --- a/packages/runtime-darwin-arm64/scripts/build.mjs +++ b/packages/runtime-darwin-arm64/scripts/build.mjs @@ -335,7 +335,7 @@ async function build() { package: packageManifest.name, version: packageManifest.version, target: RUNTIME_PACK_MATRIX.target, - runtime_abi: { version: 22, marker: "scr_runtime_abi_v22" }, + runtime_abi: { version: 23, marker: "scr_runtime_abi_v23" }, compiler: { command: compiler, identity: compilerVersion, diff --git a/packages/runtime-pack-common/runtime-pack-matrix.mjs b/packages/runtime-pack-common/runtime-pack-matrix.mjs index 258df8dec..aa71d5fb8 100644 --- a/packages/runtime-pack-common/runtime-pack-matrix.mjs +++ b/packages/runtime-pack-common/runtime-pack-matrix.mjs @@ -61,6 +61,10 @@ export const OPTIONAL_RUNTIME_SOURCES = [ ["scr_inspect.c", "inspect"], ["scr_console_native.c", "inspect"], ["scr_dyn_invoke.c", any("dynInvoke", "nativeFetch")], + // Fixed-width Buffer methods called on checked-dynamic values. + ["scr_bytes_dyn.c", all("dynBufferNum", "dynInvoke")], + // Builtin functions read as values in JavaScript dependencies. + ["scr_builtin_values.c", "builtinValues"], ["scr_dc.c", "dc"], // The dynamic tier's jsval values (two-tier design I3): linked only into // programs whose IR has dynamic-tier operations. diff --git a/packages/runtime-pack-common/scripts/build.mjs b/packages/runtime-pack-common/scripts/build.mjs index a0f117cec..aa38b4715 100644 --- a/packages/runtime-pack-common/scripts/build.mjs +++ b/packages/runtime-pack-common/scripts/build.mjs @@ -432,7 +432,7 @@ async function build() { package: packageManifest.name, version: packageManifest.version, target: matrix.target, - runtime_abi: { version: 22, marker: "scr_runtime_abi_v22" }, + runtime_abi: { version: 23, marker: "scr_runtime_abi_v23" }, compiler: { command: compiler, identity: compilerVersion, diff --git a/packages/runtime/src/scr_array.c b/packages/runtime/src/scr_array.c index 1f4efc321..c21589031 100644 --- a/packages/runtime/src/scr_array.c +++ b/packages/runtime/src/scr_array.c @@ -162,7 +162,7 @@ static uint64_t scr_arr_retain_state_slot(const ScrArr *a, uint64_t slot, typedef struct { uint64_t *data; - uint8_t *present; + uint8_t *states; /* the end of the dense states (scr_arr_states_end) */ size_t cap; ScrArrSparseSlot *sparse; size_t sparse_len; @@ -318,18 +318,18 @@ static void scr_arr_sparse_drop_prefix(ScrArr *a, size_t count) { static bool scr_arr_inline(const ScrArr *a) { return (a->spare & SCR_ARR_INLINE_SLOTS) != 0; } /* Head room: removing elements from the front of a dense array of 8-byte - * slots with its own allocation (shift, splice(0, n)) advances `data` and - * `present` past them and lowers `cap`, instead of moving the tail down, so - * draining a queue costs O(1) per element. The vacated slots stay in the - * allocation in front of `data` (values) and of `present` (states, all - * holes) until an unshift reuses them, the array empties, or growth moves - * the elements back to the start of the allocation (scr_arr_drop_head). - * `spare` records that an array may have head room; its size is implied, - * because the states still follow the whole value area: - * present == (uint8_t *)(data + cap) + head. - * Every reader indexes from `data`, `present` and `cap` as before (emitted - * code included); only the code that frees, reallocates or swaps the - * allocation needs its start. Static and inline storage never has head room. */ + * slots with its own allocation (shift, splice(0, n)) advances `data` past + * them and lowers `cap`, instead of moving the tail down, so draining a + * queue costs O(1) per element. The states run backward from the end of the + * allocation (scr_arr_states_end), so the remaining ones stay where they + * are. The vacated slots (in front of `data`) and states (past the states + * end, all holes) stay in the allocation until an unshift reuses them, the + * array empties, or growth moves the elements back to the start of the + * allocation (scr_arr_drop_head). `spare` records head room, and the vacated + * slot just before `data` holds its size (scr_arr_head). Every reader + * indexes from `data` and `cap` as before (emitted code included); only the + * code that frees, reallocates or swaps the allocation needs its start. + * Static and inline storage never has head room. */ #define SCR_ARR_HEAD_ROOM 2u /* An unshift had to move every element: the next one that would have to * move them again opens head room instead (scr_arr_open_head). */ @@ -338,8 +338,17 @@ static bool scr_arr_inline(const ScrArr *a) { return (a->spare & SCR_ARR_INLINE_ #define SCR_ARR_HEAD_MIN ((size_t)16) static size_t scr_arr_head(const ScrArr *a) { - if (!(a->spare & SCR_ARR_HEAD_ROOM)) return 0; - return (size_t)(a->present - (uint8_t *)(a->data + a->cap)); + return (a->spare & SCR_ARR_HEAD_ROOM) ? (size_t)a->data[-1] : 0; +} + +/* Record `head` vacated slots in front of `data` (none: no head room). */ +static void scr_arr_set_head(ScrArr *a, size_t head) { + if (head == 0) { + a->spare &= (uint16_t)~SCR_ARR_HEAD_ROOM; + return; + } + a->data[-1] = head; + a->spare |= SCR_ARR_HEAD_ROOM; } /* The start of the dense allocation. */ @@ -352,17 +361,18 @@ static void scr_arr_drop_head(ScrArr *a) { a->spare &= (uint16_t)~SCR_ARR_HEAD_ROOM; if (head == 0) return; size_t cap = a->cap, total = cap + head, live = a->len < cap ? a->len : cap; + uint8_t *end = scr_arr_states_end(a); uint64_t *data = a->data - head; - uint8_t *present = (uint8_t *)(data + total); + /* The states end moves to the end of the allocation, past the `head` + * vacated states: the live states move up with it, and every other state + * becomes a hole. */ + uint8_t *full = end + head; if (live) { memmove(data, a->data, live * sizeof(uint64_t)); - memmove(present, a->present, live); + memmove(full - live, end - live, live); } - /* States past `live` were holes already, except the `head` that now sit - * where the moved states were. */ - memset(present + live, SCR_ARR_HOLE, head); + memset(full - total, SCR_ARR_HOLE, total - live); a->data = data; - a->present = present; a->cap = total; } @@ -373,29 +383,36 @@ static void scr_arr_drop_head(ScrArr *a) { static bool scr_arr_drop_front(ScrArr *a, size_t count) { if (a->elem == SCR_ELEM_BOOL || scr_arr_inline(a)) return false; if (count == 0) return true; - memset(a->present, SCR_ARR_HOLE, count); + size_t head = scr_arr_head(a); + memset(scr_arr_state_run(a, 0, count), SCR_ARR_HOLE, count); memset(a->data, 0, count * sizeof(uint64_t)); + if (head) a->data[-1] = 0; a->data += count; - a->present += count; a->cap -= count; a->len -= count; a->filled = a->filled > count ? (uint32_t)(a->filled - count) : 0; - a->spare |= SCR_ARR_HEAD_ROOM; + head += count; if (a->len == 0) { /* Every state in the allocation is a hole: start over at its front. */ - size_t head = scr_arr_head(a); a->data -= head; - a->present -= head; a->cap += head; a->spare &= (uint16_t)~SCR_ARR_HEAD_ROOM; + } else { + scr_arr_set_head(a, head); } return true; } -/* Slots requested by the small constructions, rounded up to fill a block - * on the 16-byte grid: 5 eight-byte slots (or 8 booleans) after a plain - * header. */ +/* Slots requested by the small constructions. The small allocator rounds + * them up to fill a block on the 16-byte grid: 7 eight-byte slots after a + * plain header (6 after a collector header) in a 112-byte block, or 8 + * booleans in a 64-byte block. Programs that free and reuse many small + * arrays run slower with 96-byte blocks of 5 slots. */ +#if SCR_SMALL_ALLOC +#define SCR_ARR_SMALL_SLOTS ((size_t)6) +#else #define SCR_ARR_SMALL_SLOTS ((size_t)4) +#endif /* Inline capacity of a small construction with `prefix` bytes of block * before the slots. */ @@ -413,9 +430,8 @@ static size_t scr_arr_small_cap(size_t prefix, bool bools) { static void scr_arr_use_inline(ScrArr *a, size_t cap) { a->data = (uint64_t *)((char *)a + SCR_ARR_INLINE_OFFSET); - a->present = (uint8_t *)a->data + cap * scr_arr_slot_width(a); - memset(a->present, SCR_ARR_HOLE, cap); - a->cap = cap; + a->cap = (uint32_t)cap; + memset(scr_arr_state_run(a, 0, cap), SCR_ARR_HOLE, cap); a->spare |= SCR_ARR_INLINE_SLOTS; } @@ -425,12 +441,11 @@ static void scr_arr_move_inline(ScrArr *a, size_t cap, size_t unit) { size_t width = scr_arr_slot_width(a); uint64_t *data = scr_mem_alloc(cap * unit); if (!data) scr_arr_oom(); - uint8_t *present = (uint8_t *)data + cap * width; + uint8_t *end = (uint8_t *)data + cap * (width + 1); memcpy(data, a->data, a->cap * width); - memcpy(present, a->present, a->cap); - memset(present + a->cap, SCR_ARR_HOLE, cap - a->cap); + memcpy(end - a->cap, scr_arr_state_run(a, 0, a->cap), a->cap); + memset(end - cap, SCR_ARR_HOLE, cap - a->cap); a->data = data; - a->present = present; a->cap = cap; a->spare &= (uint16_t)~SCR_ARR_INLINE_SLOTS; } @@ -495,12 +510,11 @@ static inline void scr_arr_grow_dense_to(ScrArr *a, size_t need, ScrArrGrow mode uint64_t *data = a->data ? scr_mem_realloc(a->data, cap * SCR_ARR_SLOT_BYTES) : scr_mem_alloc(cap * SCR_ARR_SLOT_BYTES); if (!data) scr_arr_oom(); - /* The states follow the values: move them past the larger value area. */ - uint8_t *present = (uint8_t *)(data + cap); - if (old) memmove(present, (uint8_t *)(data + old), old); - memset(present + old, SCR_ARR_HOLE, cap - old); + /* The states end the allocation: move them to its new end. */ + uint8_t *end = (uint8_t *)(data + cap) + cap; + if (old) memmove(end - old, (uint8_t *)(data + old), old); + memset(end - cap, SCR_ARR_HOLE, cap - old); a->data = data; - a->present = present; a->cap = cap; } @@ -509,7 +523,7 @@ static void scr_arr_absorb_sparse(ScrArr *a) { size_t moved = scr_arr_sparse_lower_bound(SCR_AX(a)->sparse, SCR_AX(a)->sparse_len, a->cap); for (size_t i = 0; i < moved; i++) { scr_arr_dense_put(a, SCR_AX(a)->sparse[i].index, SCR_AX(a)->sparse[i].slot); - a->present[SCR_AX(a)->sparse[i].index] = SCR_AX(a)->sparse[i].state; + *scr_arr_state_ptr(a, SCR_AX(a)->sparse[i].index) = SCR_AX(a)->sparse[i].state; } scr_arr_sparse_drop_prefix(a, moved); } @@ -560,21 +574,19 @@ static void scr_arr_grow_dense_slow(ScrArr *a, size_t need, ScrArrGrow mode) { uint64_t *data = scr_mem_alloc(cap * unit); if (!data) scr_arr_oom(); a->data = data; - a->present = (uint8_t *)data + cap * width; - memset(a->present, SCR_ARR_HOLE, cap); - a->cap = cap; + a->cap = (uint32_t)cap; + memset(scr_arr_state_run(a, 0, cap), SCR_ARR_HOLE, cap); return; } - /* Values and states share one allocation. Move the old states before - * publishing the larger capacity: their previous location is now part - * of the value area. Existing data and state pointers keep their ABI. */ + /* Values and states share one allocation. Move the old states to its new + * end before publishing the larger capacity: their previous location is + * now part of the value area. */ uint64_t *data = scr_mem_realloc(a->data, cap * unit); if (!data) scr_arr_oom(); - uint8_t *present = (uint8_t *)data + cap * width; - if (a->cap) memmove(present, (uint8_t *)data + a->cap * width, a->cap); - memset(present + a->cap, SCR_ARR_HOLE, cap - a->cap); + uint8_t *end = (uint8_t *)data + cap * (width + 1); + if (a->cap) memmove(end - a->cap, (uint8_t *)data + a->cap * width, a->cap); + memset(end - cap, SCR_ARR_HOLE, cap - a->cap); a->data = data; - a->present = present; a->cap = cap; scr_arr_absorb_sparse(a); } @@ -584,7 +596,7 @@ static void scr_arr_grow_dense(ScrArr *a, size_t need) { } /* Room for `count` values in front of the first element of a dense array: - * moves `data` and `present` back into head room, raising `cap`; the caller + * moves `data` back into head room, raising `cap`; the caller * stores the new first `count` slots and states. A shifted array reuses the * room its shifts left. An array that unshift already had to move once moves * into a new allocation with head room for half its length again, so a run @@ -607,19 +619,20 @@ static bool scr_arr_open_head(ScrArr *a, size_t count) { size_t total = room + a->cap; uint64_t *block = scr_mem_alloc(total * SCR_ARR_SLOT_BYTES); if (!block) scr_arr_oom(); - uint8_t *states = (uint8_t *)(block + total); + /* The window's states end `room` bytes before the allocation does; the + * room's states past it are holes. */ + uint8_t *end = (uint8_t *)(block + total) + a->cap; memcpy(block + room, a->data, len * sizeof(uint64_t)); - memset(states, SCR_ARR_HOLE, room); - memcpy(states + room, a->present, len); - memset(states + room + len, SCR_ARR_HOLE, a->cap - len); + memset(end, SCR_ARR_HOLE, room); + memcpy(end - len, scr_arr_state_run(a, 0, len), len); + memset(end - a->cap, SCR_ARR_HOLE, a->cap - len); scr_mem_free(scr_arr_block(a)); a->data = block + room; - a->present = states + room; - a->spare |= SCR_ARR_HEAD_ROOM; + head = room; } a->data -= count; - a->present -= count; a->cap += count; + scr_arr_set_head(a, head - count); return true; } @@ -628,9 +641,10 @@ static bool scr_arr_open_head(ScrArr *a, size_t count) { * within that gap past capacity. */ static bool scr_arr_dense_tail_populated(const ScrArr *a) { size_t window = a->cap < SCR_ARR_DENSE_GAP ? a->cap : SCR_ARR_DENSE_GAP; - if (window == 0 || a->present[a->cap - 1] == SCR_ARR_HOLE) return false; + const uint8_t *states = scr_arr_states_end(a); + if (window == 0 || *scr_arr_state_in(states, a->cap - 1) == SCR_ARR_HOLE) return false; size_t populated = 0; - for (size_t i = a->cap - window; i < a->cap; i++) populated += a->present[i] != SCR_ARR_HOLE; + for (size_t i = a->cap - window; i < a->cap; i++) populated += *scr_arr_state_in(states, i) != SCR_ARR_HOLE; return populated * 16 >= window * 3; } @@ -672,9 +686,9 @@ static void scr_arr_grow_for_index(ScrArr *a, size_t index) { static uint8_t scr_arr_state_at_storage(const ScrArrStorage *s, size_t index, uint64_t *out) { if (index < s->cap) { - if (s->present[index] == SCR_ARR_HOLE) return SCR_ARR_HOLE; + if (*scr_arr_state_in(s->states, index) == SCR_ARR_HOLE) return SCR_ARR_HOLE; if (out) *out = scr_arr_storage_get(s, index); - return s->present[index]; + return *scr_arr_state_in(s->states, index); } size_t pos = scr_arr_sparse_lower_bound(s->sparse, s->sparse_len, index); if (pos == s->sparse_len || s->sparse[pos].index != index) return SCR_ARR_HOLE; @@ -688,13 +702,13 @@ static bool scr_arr_slot_at_storage(const ScrArrStorage *s, size_t index, } static bool scr_arr_slot_at(const ScrArr *a, size_t index, uint64_t *out) { - ScrArrStorage s = {a->data, a->present, a->cap, SCR_AX(a)->sparse, + ScrArrStorage s = {a->data, scr_arr_states_end(a), a->cap, SCR_AX(a)->sparse, SCR_AX(a)->sparse_len, SCR_AX(a)->sparse_cap, a->elem, NULL}; return scr_arr_slot_at_storage(&s, index, out); } static uint8_t scr_arr_state_at(const ScrArr *a, size_t index, uint64_t *out) { - ScrArrStorage s = {a->data, a->present, a->cap, SCR_AX(a)->sparse, + ScrArrStorage s = {a->data, scr_arr_states_end(a), a->cap, SCR_AX(a)->sparse, SCR_AX(a)->sparse_len, SCR_AX(a)->sparse_cap, a->elem, NULL}; return scr_arr_state_at_storage(&s, index, out); } @@ -707,7 +721,7 @@ static void scr_arr_store_state_owned(ScrArr *a, size_t index, uint64_t slot, scr_arr_grow_for_index(a, index); if (index < a->cap) { scr_arr_dense_put(a, index, slot); - a->present[index] = state; + *scr_arr_state_ptr(a, index) = state; return; } size_t pos = scr_arr_sparse_lower_bound(SCR_AX(a)->sparse, SCR_AX(a)->sparse_len, index); @@ -726,11 +740,11 @@ static void scr_arr_store_owned(ScrArr *a, size_t index, uint64_t slot) { static bool scr_arr_take_state(ScrArr *a, size_t index, uint64_t *out, uint8_t *state_out) { if (index < a->cap) { - uint8_t state = a->present[index]; + uint8_t state = *scr_arr_state_ptr(a, index); if (state == SCR_ARR_HOLE) return false; scr_arr_cut(a, index); *out = scr_arr_dense_get(a, index); - a->present[index] = SCR_ARR_HOLE; + *scr_arr_state_ptr(a, index) = SCR_ARR_HOLE; scr_arr_dense_put(a, index, 0); if (state_out) *state_out = state; return true; @@ -754,10 +768,10 @@ static void scr_arr_replace_state_owned(ScrArr *a, size_t index, uint64_t slot, uint64_t old = 0; if (state != SCR_ARR_VALUE) scr_arr_cut(a, index); if (index < a->cap) { - old_state = a->present[index]; + old_state = *scr_arr_state_ptr(a, index); if (old_state == SCR_ARR_VALUE) old = scr_arr_dense_get(a, index); scr_arr_dense_put(a, index, slot); - a->present[index] = state; + *scr_arr_state_ptr(a, index) = state; } else { size_t pos = scr_arr_sparse_lower_bound(SCR_AX(a)->sparse, SCR_AX(a)->sparse_len, index); if (pos == SCR_AX(a)->sparse_len || SCR_AX(a)->sparse[pos].index != index) { @@ -788,7 +802,7 @@ static void scr_arr_refill_to(ScrArr *a, size_t limit) { size_t end = a->len < a->cap ? a->len : a->cap; if (limit < end) end = limit; size_t f = a->filled; - if (f < end) f = scr_arr_value_run(a->present, f, end); + if (f < end) f = scr_arr_value_run(scr_arr_states_end(a), f, end); a->filled = (uint32_t)f; } @@ -867,12 +881,11 @@ static bool scr_arr_prop_delete(ScrArr *a, double key) { } static ScrArrStorage scr_arr_take_storage(ScrArr *a) { - ScrArrStorage old = {a->data, a->present, a->cap, SCR_AX(a)->sparse, + ScrArrStorage old = {a->data, scr_arr_states_end(a), a->cap, SCR_AX(a)->sparse, SCR_AX(a)->sparse_len, SCR_AX(a)->sparse_cap, a->elem, scr_arr_inline(a) ? NULL : scr_arr_block(a)}; a->spare &= (uint16_t)~(SCR_ARR_INLINE_SLOTS | SCR_ARR_HEAD_ROOM); a->data = NULL; - a->present = NULL; a->cap = 0; a->filled = 0; if (a->ext) { @@ -900,16 +913,81 @@ static void scr_arr_move_dense(ScrArr *a, size_t to, size_t from, size_t count) if (count == 0) return; scr_arr_cut(a, to); memmove(scr_arr_dense_at(a, to), scr_arr_dense_at(a, from), count * scr_arr_slot_width(a)); - memmove(a->present + to, a->present + from, count); + memmove(scr_arr_state_run(a, to, count), scr_arr_state_run(a, from, count), count); } static void scr_arr_clear_dense(ScrArr *a, size_t from, size_t count) { if (count == 0) return; scr_arr_cut(a, from); - memset(a->present + from, SCR_ARR_HOLE, count); + memset(scr_arr_state_run(a, from, count), SCR_ARR_HOLE, count); memset(scr_arr_dense_at(a, from), 0, count * scr_arr_slot_width(a)); } +/* Copy `n` state bytes between distinct arrays. Most runs are a few bytes, + * which overlapping word copies move with fewer instructions than a call. */ +static inline void scr_arr_copy_states(uint8_t *dst, const uint8_t *src, size_t n) { + if (n >= 8 && n <= 16) { + uint64_t head, tail; + memcpy(&head, src, sizeof head); + memcpy(&tail, src + n - 8, sizeof tail); + memcpy(dst, &head, sizeof head); + memcpy(dst + n - 8, &tail, sizeof tail); + } else if (n >= 4 && n < 8) { + uint32_t head, tail; + memcpy(&head, src, sizeof head); + memcpy(&tail, src + n - 4, sizeof tail); + memcpy(dst, &head, sizeof head); + memcpy(dst + n - 4, &tail, sizeof tail); + } else if (n < 4) { + for (size_t i = 0; i < n; i++) dst[i] = src[i]; + } else { + memcpy(dst, src, n); + } +} + +/* Set `n` state bytes to `state`, with overlapping word stores for short + * runs. */ +static inline void scr_arr_set_states(uint8_t *dst, uint8_t state, size_t n) { + if (n >= 8 && n <= 16) { + uint64_t word = 0x0101010101010101ull * state; + memcpy(dst, &word, sizeof word); + memcpy(dst + n - 8, &word, sizeof word); + } else if (n >= 4 && n < 8) { + uint32_t word = 0x01010101u * state; + memcpy(dst, &word, sizeof word); + memcpy(dst + n - 4, &word, sizeof word); + } else if (n < 4) { + for (size_t i = 0; i < n; i++) dst[i] = state; + } else { + memset(dst, state, n); + } +} + +/* Copy `n` 8-byte slots between distinct arrays: up to 8 slots move as + * overlapping 16- or 32-byte blocks rather than through a call. */ +static inline void scr_arr_copy_slots(uint64_t *dst, const uint64_t *src, size_t n) { + size_t bytes = n * sizeof(uint64_t); + char *d = (char *)dst; + const char *s = (const char *)src; + if (bytes >= 32 && bytes <= 64) { + char head[32], tail[32]; + memcpy(head, s, 32); + memcpy(tail, s + bytes - 32, 32); + memcpy(d, head, 32); + memcpy(d + bytes - 32, tail, 32); + } else if (bytes >= 16 && bytes < 32) { + char head[16], tail[16]; + memcpy(head, s, 16); + memcpy(tail, s + bytes - 16, 16); + memcpy(d, head, 16); + memcpy(d + bytes - 16, tail, 16); + } else if (bytes == 8) { + memcpy(d, s, 8); + } else if (bytes) { + memcpy(d, s, bytes); + } +} + /* The destination owns no values in this range. Only retain adapters run * while copying references: no release can collect an intermediate copy. * Self-appends use disjoint ranges after reserving capacity. Both arrays @@ -919,29 +997,36 @@ static void scr_arr_copy_dense(ScrArr *dst, size_t to, const ScrArr *src, bool materialize_holes) { if (count == 0) return; scr_arr_cut(dst, to); - if (!reverse && dst->elem != SCR_ELEM_BOOL) { - /* 8-byte slots: a constant slot size. */ - memcpy(dst->data + to, src->data + from, count * sizeof(uint64_t)); - memcpy(dst->present + to, src->present + from, count); + /* 8-byte slots: a constant slot size, and constant-width state ends. */ + bool wide = dst->elem != SCR_ELEM_BOOL; + uint8_t *dst_states = wide ? scr_arr_states_end_w(dst, 8) : scr_arr_states_end_w(dst, 1); + const uint8_t *src_states = wide ? scr_arr_states_end_w(src, 8) : scr_arr_states_end_w(src, 1); + if (!reverse && wide) { + scr_arr_copy_slots(dst->data + to, src->data + from, count); + /* A range below the source's hole-free prefix holds only values. */ + if (from + count <= src->filled) + scr_arr_set_states(dst_states - to - count, SCR_ARR_VALUE, count); + else + scr_arr_copy_states(dst_states - to - count, src_states - from - count, count); } else if (!reverse) { memcpy(scr_arr_dense_at(dst, to), scr_arr_dense_at(src, from), count * scr_arr_slot_width(dst)); - memcpy(dst->present + to, src->present + from, count); + scr_arr_copy_states(dst_states - to - count, src_states - from - count, count); } else { for (size_t i = 0; i < count; i++) { size_t source = from + count - i - 1; scr_arr_dense_put(dst, to + i, scr_arr_dense_get(src, source)); - dst->present[to + i] = src->present[source]; + *scr_arr_state_in(dst_states, to + i) = *scr_arr_state_in(src_states, source); } } bool refs = scr_elem_is_ref(src->elem); if (!refs && !materialize_holes) return; for (size_t i = to; i < to + count; i++) { - if (dst->present[i] == SCR_ARR_VALUE) { + if (*scr_arr_state_in(dst_states, i) == SCR_ARR_VALUE) { if (refs) dst->data[i] = scr_elem_retain_slot(src, dst->data[i]); - } else if (materialize_holes && dst->present[i] == SCR_ARR_HOLE) { + } else if (materialize_holes && *scr_arr_state_in(dst_states, i) == SCR_ARR_HOLE) { scr_arr_dense_put(dst, i, 0); - dst->present[i] = SCR_ARR_UNDEFINED; + *scr_arr_state_in(dst_states, i) = SCR_ARR_UNDEFINED; } } } @@ -949,13 +1034,17 @@ static void scr_arr_copy_dense(ScrArr *dst, size_t to, const ScrArr *src, /* Store `count` 8-byte value slots into dense storage starting at `to`. */ static void scr_arr_put_slots(ScrArr *a, size_t to, size_t count, const uint64_t *slots) { if (a->elem != SCR_ELEM_BOOL) { - memcpy(a->data + to, slots, count * sizeof(*slots)); + scr_arr_copy_slots(a->data + to, slots, count); return; } for (size_t i = 0; i < count; i++) scr_arr_dense_put(a, to + i, slots[i]); } -static ScrArr *scr_arr_make(ScrElemKind elem, size_t initial_cap, bool small) { +/* Inlined into each constructor, so `small` and the slot unit fold to + * constants there. */ +static inline __attribute__((always_inline)) ScrArr *scr_arr_make(ScrElemKind elem, + size_t initial_cap, + bool small) { size_t unit = elem == SCR_ELEM_BOOL ? SCR_ARR_BOOL_SLOT_BYTES : SCR_ARR_SLOT_BYTES; size_t inline_cap = small ? scr_arr_small_cap(SCR_ARR_INLINE_OFFSET, elem == SCR_ELEM_BOOL) : 0; ScrArr *a = scr_mem_alloc(inline_cap ? SCR_ARR_INLINE_OFFSET + inline_cap * unit : sizeof(ScrArr)); @@ -969,7 +1058,6 @@ static ScrArr *scr_arr_make(ScrElemKind elem, size_t initial_cap, bool small) { a->filled = 0; a->ops = NULL; a->data = NULL; - a->present = NULL; a->ext = NULL; if (inline_cap) scr_arr_use_inline(a, inline_cap); else if (initial_cap > 0) scr_arr_grow_dense_to(a, initial_cap, SCR_ARR_GROW_EXACT); @@ -1004,7 +1092,6 @@ ScrArr *scr_arr_new_inline(ScrElemKind elem, size_t cap) { a->filled = 0; a->ops = NULL; a->data = NULL; - a->present = NULL; a->ext = NULL; scr_arr_use_inline(a, cap); #ifdef SCR_RC_AUDIT @@ -1051,8 +1138,9 @@ void scr_arr_trace_v(void *a0, ScrTraceVisit visit, void *ctx) { if (SCR_AX(a)->metadata) visit(SCR_AX(a)->metadata, ctx); /* Growth and truncation leave spare capacity empty. Sparse slots and * named properties are separate edges, independent of dense length. */ + const uint8_t *states = scr_arr_states_end(a); for (size_t i = 0, end = a->len < a->cap ? a->len : a->cap; i < end; i++) { - if (a->present[i] == SCR_ARR_VALUE) visit(scr_slot_to_ptr(a->data[i]), ctx); + if (*scr_arr_state_in(states, i) == SCR_ARR_VALUE) visit(scr_slot_to_ptr(a->data[i]), ctx); } for (size_t i = 0; i < SCR_AX(a)->sparse_len; i++) { if (SCR_AX(a)->sparse[i].state == SCR_ARR_VALUE) { @@ -1109,7 +1197,6 @@ static ScrArr *scr_arr_make_ops(const ScrArrOps *ops, size_t initial_cap, bool s a->filled = 0; a->ops = ops; a->data = NULL; - a->present = NULL; a->ext = NULL; if (inline_cap) scr_arr_use_inline(a, inline_cap); else if (initial_cap > 0) scr_arr_grow_dense_to(a, initial_cap, SCR_ARR_GROW_EXACT); @@ -1138,18 +1225,25 @@ static void scr_arr_destroy(void *object) { * first (and the storage cannot move under the loop); immortal strings * (interned characters, literals) need no call. */ const uint64_t *data = a->data; - const uint8_t *present = a->present; - for (size_t i = 0; i < end; i++) { - if (present[i] != SCR_ARR_VALUE) continue; + const uint8_t *states = scr_arr_states_end_w(a, 8); + /* Slots below the hole-free prefix bound hold values. */ + size_t filled = a->filled < end ? a->filled : end; + for (size_t i = 0; i < filled; i++) { + ScrStr *s = (ScrStr *)scr_slot_to_ptr(data[i]); + if (s && s->rc != SIZE_MAX) scr_str_release(s); + } + for (size_t i = filled; i < end; i++) { + if (*scr_arr_state_in(states, i) != SCR_ARR_VALUE) continue; ScrStr *s = (ScrStr *)scr_slot_to_ptr(data[i]); if (s && s->rc != SIZE_MAX) scr_str_release(s); } } else { + uint8_t *states = scr_arr_states_end(a); for (size_t i = 0; i < end; i++) { - if (a->present[i] != SCR_ARR_HOLE) { + if (*scr_arr_state_in(states, i) != SCR_ARR_HOLE) { uint64_t old = a->data[i]; - uint8_t state = a->present[i]; - a->present[i] = 0; + uint8_t state = *scr_arr_state_in(states, i); + *scr_arr_state_in(states, i) = 0; a->data[i] = 0; if (state == SCR_ARR_VALUE) scr_elem_release(a, old); } @@ -1221,8 +1315,9 @@ double scr_arr_next_present(const ScrArr *a, double start) { if (!(start < SCR_ARR_LEN_F64(a->len))) return SCR_ARR_LEN_F64(a->len); size_t i = start > 0 ? (size_t)ceil(start) : 0; size_t end = a->cap < a->len ? a->cap : a->len; + const uint8_t *states = scr_arr_states_end(a); for (; i < end; i++) { - if (a->present[i] != SCR_ARR_HOLE) return (double)i; + if (*scr_arr_state_in(states, i) != SCR_ARR_HOLE) return (double)i; } size_t pos = scr_arr_sparse_lower_bound(SCR_AX(a)->sparse, SCR_AX(a)->sparse_len, i); if (pos < SCR_AX(a)->sparse_len && SCR_AX(a)->sparse[pos].index < a->len) { @@ -1252,10 +1347,10 @@ void scr_arr_set_len(ScrArr *a, double length) { size_t dense_stop = next < dense_end ? next : dense_end; if (scr_elem_is_ref(a->elem)) { for (size_t i = dense_stop; i < dense_end; i++) { - if (a->present[i] != SCR_ARR_HOLE) { + if (*scr_arr_state_ptr(a, i) != SCR_ARR_HOLE) { uint64_t old = a->data[i]; - uint8_t state = a->present[i]; - a->present[i] = 0; + uint8_t state = *scr_arr_state_ptr(a, i); + *scr_arr_state_ptr(a, i) = 0; a->data[i] = 0; if (state == SCR_ARR_VALUE) scr_elem_release(a, old); } @@ -1272,7 +1367,7 @@ void scr_arr_set_len(ScrArr *a, double length) { } } else { if (dense_stop < dense_end) { - memset(a->present + dense_stop, 0, dense_end - dense_stop); + memset(scr_arr_state_run(a, dense_stop, dense_end - dense_stop), 0, dense_end - dense_stop); memset(scr_arr_dense_at(a, dense_stop), 0, (dense_end - dense_stop) * scr_arr_slot_width(a)); } @@ -1313,8 +1408,9 @@ static void scr_arr_copy_range_states(ScrArr *dst, size_t dst_start, const ScrAr bool materialize_holes) { if (src != dst && src_start <= src->cap && count <= src->cap - src_start && dst_start <= dst->cap && count <= dst->cap - dst_start) { + const uint8_t *dst_states = scr_arr_states_end(dst); size_t empty = 0; - while (empty < count && dst->present[dst_start + empty] == SCR_ARR_HOLE) empty++; + while (empty < count && *scr_arr_state_in(dst_states, dst_start + empty) == SCR_ARR_HOLE) empty++; if (empty == count) { scr_arr_copy_dense(dst, dst_start, src, src_start, count, reverse, materialize_holes); @@ -1401,7 +1497,7 @@ static ScrArr *scr_arr_fill_slot(ScrArr *a, uint64_t slot, uint8_t state, if (state != SCR_ARR_VALUE) scr_arr_cut(a, from); if (a->elem == SCR_ELEM_BOOL) memset(scr_arr_dense_at(a, from), (int)slot, until - from); else for (size_t i = from; i < until; i++) a->data[i] = slot; - memset(a->present + from, state, until - from); + memset(scr_arr_state_run(a, from, until - from), state, until - from); } else { for (size_t i = from; i < until; i++) { uint64_t value = refs && state == SCR_ARR_VALUE ? scr_elem_retain_slot(a, slot) : slot; @@ -1555,7 +1651,7 @@ void *scr_arr_peek_ref(const ScrArr *a, double i) { size_t idx = (size_t)i; if ((double)idx == i) { if (idx < a->filled) return scr_slot_to_ptr(a->data[idx]); - if (idx >= a->len || a->present[idx] != SCR_ARR_VALUE) return NULL; + if (idx >= a->len || *scr_arr_state_ptr(a, idx) != SCR_ARR_VALUE) return NULL; return scr_slot_to_ptr(a->data[idx]); } } @@ -1585,8 +1681,8 @@ bool scr_arr_index_eq(const ScrArr *a, double i, const ScrArr *b, double j) { if (i >= 0 && i < (double)a->cap && i == trunc(i) && j >= 0 && j < (double)b->cap && j == trunc(j)) { size_t ai = (size_t)i, bi = (size_t)j; - bool ap = ai < a->len && a->present[ai] == SCR_ARR_VALUE; - bool bp = bi < b->len && b->present[bi] == SCR_ARR_VALUE; + bool ap = ai < a->len && *scr_arr_state_ptr(a, ai) == SCR_ARR_VALUE; + bool bp = bi < b->len && *scr_arr_state_ptr(b, bi) == SCR_ARR_VALUE; if (!ap || !bp) return ap == bp; av = scr_arr_dense_get(a, ai); bv = scr_arr_dense_get(b, bi); @@ -1700,12 +1796,17 @@ double scr_arr_push_many(ScrArr *a, size_t count, const uint64_t *slots) { scr_arr_grow_dense(a, next); if (count) { scr_arr_put_slots(a, from, count, slots); - memset(a->present + from, SCR_ARR_VALUE, count); + scr_arr_set_states(scr_arr_state_run(a, from, count), SCR_ARR_VALUE, count); } - } else { - for (size_t i = 0; i < count; i++) scr_arr_store_owned(a, from + i, slots[i]); + a->len = (uint32_t)next; + /* The appended states are values: a bound at the old length moves to + * the new one without reading them back. */ + if (a->filled == from) a->filled = (uint32_t)next; + else scr_arr_refill(a); + return (double)next; } - a->len = next; + for (size_t i = 0; i < count; i++) scr_arr_store_owned(a, from + i, slots[i]); + a->len = (uint32_t)next; scr_arr_refill(a); return (double)next; } @@ -1786,7 +1887,7 @@ double scr_arr_concat_copy(ScrArr *a, const ScrArr *src) { } a->len = old_len + add; for (size_t i = 0; i < src->cap; i++) { - if (src->present[i] != SCR_ARR_HOLE) { + if (*scr_arr_state_ptr(src, i) != SCR_ARR_HOLE) { scr_arr_copy_index(a, old_len + i, src, i); } } @@ -1813,7 +1914,7 @@ static double scr_arr_unshift_slot(ScrArr *a, uint64_t slot) { /* The prefix stays put in front of the new first value. */ size_t f = a->filled; a->data[0] = slot; - a->present[0] = SCR_ARR_VALUE; + *scr_arr_state_ptr(a, 0) = SCR_ARR_VALUE; a->len++; a->filled = (uint32_t)(f + 1); return SCR_ARR_LEN_F64(a->len); @@ -1824,7 +1925,7 @@ static double scr_arr_unshift_slot(ScrArr *a, uint64_t slot) { size_t f = a->filled; scr_arr_move_dense(a, 1, 0, old_len); scr_arr_dense_put(a, 0, slot); - a->present[0] = SCR_ARR_VALUE; + *scr_arr_state_ptr(a, 0) = SCR_ARR_VALUE; a->len++; a->filled = (uint32_t)(f + 1); return SCR_ARR_LEN_F64(a->len); @@ -1833,8 +1934,8 @@ static double scr_arr_unshift_slot(ScrArr *a, uint64_t slot) { a->len = 0; scr_arr_store_owned(a, 0, slot); for (size_t i = 0; i < old.cap; i++) { - if (old.present[i]) - scr_arr_store_state_owned(a, i + 1, scr_arr_storage_get(&old, i), old.present[i]); + if (*scr_arr_state_in(old.states, i)) + scr_arr_store_state_owned(a, i + 1, scr_arr_storage_get(&old, i), *scr_arr_state_in(old.states, i)); } for (size_t i = 0; i < old.sparse_len; i++) { scr_arr_store_state_owned(a, old.sparse[i].index + 1, old.sparse[i].slot, @@ -1865,7 +1966,7 @@ double scr_arr_unshift_many(ScrArr *a, size_t count, const uint64_t *slots) { if (scr_arr_is_dense(a) && scr_arr_open_head(a, count)) { size_t f = a->filled; scr_arr_put_slots(a, 0, count, slots); - memset(a->present, SCR_ARR_VALUE, count); + memset(scr_arr_state_run(a, 0, count), SCR_ARR_VALUE, count); a->len = old_len + count; a->filled = (uint32_t)(f + count); return SCR_ARR_LEN_F64(a->len); @@ -1875,7 +1976,7 @@ double scr_arr_unshift_many(ScrArr *a, size_t count, const uint64_t *slots) { size_t f = a->filled; scr_arr_move_dense(a, count, 0, old_len); scr_arr_put_slots(a, 0, count, slots); - memset(a->present, SCR_ARR_VALUE, count); + memset(scr_arr_state_run(a, 0, count), SCR_ARR_VALUE, count); a->len = old_len + count; a->filled = (uint32_t)(f + count); return SCR_ARR_LEN_F64(a->len); @@ -1883,8 +1984,8 @@ double scr_arr_unshift_many(ScrArr *a, size_t count, const uint64_t *slots) { ScrArrStorage old = scr_arr_take_storage(a); for (size_t i = 0; i < count; i++) scr_arr_store_owned(a, i, slots[i]); for (size_t i = 0; i < old.cap; i++) { - if (old.present[i] != SCR_ARR_HOLE) - scr_arr_store_state_owned(a, i + count, scr_arr_storage_get(&old, i), old.present[i]); + if (*scr_arr_state_in(old.states, i) != SCR_ARR_HOLE) + scr_arr_store_state_owned(a, i + count, scr_arr_storage_get(&old, i), *scr_arr_state_in(old.states, i)); } for (size_t i = 0; i < old.sparse_len; i++) { scr_arr_store_state_owned(a, old.sparse[i].index + count, @@ -1933,8 +2034,8 @@ double scr_arr_unshift_spread(ScrArr *a, const ScrArr *src) { } } for (size_t i = 0; i < old.cap; i++) { - if (old.present[i] != SCR_ARR_HOLE) { - scr_arr_store_state_owned(a, i + add, scr_arr_storage_get(&old, i), old.present[i]); + if (*scr_arr_state_in(old.states, i) != SCR_ARR_HOLE) { + scr_arr_store_state_owned(a, i + add, scr_arr_storage_get(&old, i), *scr_arr_state_in(old.states, i)); } } for (size_t i = 0; i < old.sparse_len; i++) { @@ -1952,14 +2053,15 @@ ScrArr *scr_arr_reverse(ScrArr *a) { if (scr_arr_is_dense(a)) { /* A hole-free array stays hole-free; any other prefix is recomputed. */ if (a->filled != len) scr_arr_cut(a, 0); + uint8_t *states = scr_arr_states_end(a); for (size_t i = 0; i < len / 2; i++) { size_t j = len - i - 1; uint64_t slot = scr_arr_dense_get(a, i); scr_arr_dense_put(a, i, scr_arr_dense_get(a, j)); scr_arr_dense_put(a, j, slot); - uint8_t state = a->present[i]; - a->present[i] = a->present[j]; - a->present[j] = state; + uint8_t state = *scr_arr_state_in(states, i); + *scr_arr_state_in(states, i) = *scr_arr_state_in(states, j); + *scr_arr_state_in(states, j) = state; } scr_arr_refill(a); return scr_arr_retain(a); @@ -1967,8 +2069,8 @@ ScrArr *scr_arr_reverse(ScrArr *a) { ScrArrStorage old = scr_arr_take_storage(a); a->len = len; for (size_t i = 0; i < old.cap; i++) { - if (old.present[i]) - scr_arr_store_state_owned(a, len - 1 - i, scr_arr_storage_get(&old, i), old.present[i]); + if (*scr_arr_state_in(old.states, i)) + scr_arr_store_state_owned(a, len - 1 - i, scr_arr_storage_get(&old, i), *scr_arr_state_in(old.states, i)); } for (size_t i = 0; i < old.sparse_len; i++) { scr_arr_store_state_owned(a, len - 1 - old.sparse[i].index, @@ -2018,7 +2120,7 @@ uint8_t scr_arr_shift_state(ScrArr *a, uint64_t *slot_out) { if (a->len == 0) return SCR_ARR_HOLE; size_t old_len = a->len; if (scr_arr_is_dense(a)) { - uint8_t state = a->present[0]; + uint8_t state = *scr_arr_state_ptr(a, 0); if (slot_out && state == SCR_ARR_VALUE) *slot_out = scr_arr_dense_get(a, 0); /* The first slot becomes head room; the tail stays where it is. */ if (scr_arr_drop_front(a, 1)) return state; @@ -2035,8 +2137,8 @@ uint8_t scr_arr_shift_state(ScrArr *a, uint64_t *slot_out) { uint8_t state = scr_arr_state_at_storage(&old, 0, &s); a->len = 0; for (size_t i = 1; i < old.cap; i++) { - if (old.present[i]) - scr_arr_store_state_owned(a, i - 1, scr_arr_storage_get(&old, i), old.present[i]); + if (*scr_arr_state_in(old.states, i)) + scr_arr_store_state_owned(a, i - 1, scr_arr_storage_get(&old, i), *scr_arr_state_in(old.states, i)); } for (size_t i = 0; i < old.sparse_len; i++) { if (old.sparse[i].index > 0) { @@ -2094,7 +2196,7 @@ ScrArr *scr_arr_splice(ScrArr *a, double start, double deleteCount) { if (scr_arr_is_dense(a)) { if (n) { memcpy(out->data, scr_arr_dense_at(a, from), n * scr_arr_slot_width(a)); - memcpy(out->present, a->present + from, n); + memcpy(scr_arr_state_run(out, 0, n), scr_arr_state_run(a, from, n), n); } scr_arr_refill(out); /* A removed prefix becomes head room. */ @@ -2112,8 +2214,8 @@ ScrArr *scr_arr_splice(ScrArr *a, double start, double deleteCount) { ScrArrStorage old = scr_arr_take_storage(a); a->len = 0; for (size_t i = 0; i < old.cap; i++) { - if (old.present[i] == SCR_ARR_HOLE) continue; - uint8_t state = old.present[i]; + if (*scr_arr_state_in(old.states, i) == SCR_ARR_HOLE) continue; + uint8_t state = *scr_arr_state_in(old.states, i); uint64_t slot = scr_arr_storage_get(&old, i); size_t dst; if (i < from) { @@ -2171,7 +2273,7 @@ ScrArr *scr_arr_splice_insert(ScrArr *a, double start, double deleteCount, : scr_arr_new(a->elem, n); if (n) { memcpy(removed->data, scr_arr_dense_at(a, from), n * scr_arr_slot_width(a)); - memcpy(removed->present, a->present + from, n); + memcpy(scr_arr_state_run(removed, 0, n), scr_arr_state_run(a, from, n), n); } removed->len = n; scr_arr_refill(removed); @@ -2229,8 +2331,9 @@ void scr_arr_splice_drop(ScrArr *a, double start, double deleteCount, size_t cou removed = scr_mem_alloc(n * sizeof(uint64_t)); if (!removed) scr_arr_oom(); } + const uint8_t *states = scr_arr_states_end(a); for (size_t i = from; i < from + n; i++) { - if (a->present[i] == SCR_ARR_VALUE) removed[kept++] = a->data[i]; + if (*scr_arr_state_in(states, i) == SCR_ARR_VALUE) removed[kept++] = a->data[i]; } } size_t old_len = a->len; @@ -2245,7 +2348,7 @@ void scr_arr_splice_drop(ScrArr *a, double start, double deleteCount, size_t cou scr_arr_move_dense(a, from + count, from + n, old_len - from - n); if (count) { scr_arr_put_slots(a, from, count, slots); - memset(a->present + from, SCR_ARR_VALUE, count); + memset(scr_arr_state_run(a, from, count), SCR_ARR_VALUE, count); } if (next_len < old_len) scr_arr_clear_dense(a, next_len, old_len - next_len); a->len = next_len; @@ -2302,8 +2405,9 @@ static bool scr_arr_ref_eq(const ScrArr *a, uint64_t slot, void *v) { } double scr_arr_index_of_f64(ScrArr *a, double v) { + const uint8_t *states = scr_arr_states_end(a); for (size_t i = 0; i < a->cap && i < a->len; i++) { - if (a->present[i] == SCR_ARR_VALUE && scr_slot_to_f64(a->data[i]) == v) return (double)i; + if (*scr_arr_state_in(states, i) == SCR_ARR_VALUE && scr_slot_to_f64(a->data[i]) == v) return (double)i; } for (size_t i = 0; i < SCR_AX(a)->sparse_len; i++) { if (SCR_AX(a)->sparse[i].state == SCR_ARR_VALUE && scr_slot_to_f64(SCR_AX(a)->sparse[i].slot) == v) return (double)SCR_AX(a)->sparse[i].index; @@ -2312,8 +2416,9 @@ double scr_arr_index_of_f64(ScrArr *a, double v) { } double scr_arr_index_of_bool(ScrArr *a, bool v) { + const uint8_t *states = scr_arr_states_end(a); for (size_t i = 0; i < a->cap && i < a->len; i++) { - if (a->present[i] == SCR_ARR_VALUE && ((scr_arr_dense_get(a, i) != 0) == v)) return (double)i; + if (*scr_arr_state_in(states, i) == SCR_ARR_VALUE && ((scr_arr_dense_get(a, i) != 0) == v)) return (double)i; } for (size_t i = 0; i < SCR_AX(a)->sparse_len; i++) { if (SCR_AX(a)->sparse[i].state == SCR_ARR_VALUE && (SCR_AX(a)->sparse[i].slot != 0) == v) return (double)SCR_AX(a)->sparse[i].index; @@ -2322,8 +2427,9 @@ double scr_arr_index_of_bool(ScrArr *a, bool v) { } double scr_arr_index_of_ref(ScrArr *a, void *v) { + const uint8_t *states = scr_arr_states_end(a); for (size_t i = 0; i < a->cap && i < a->len; i++) { - if (a->present[i] == SCR_ARR_VALUE && scr_arr_ref_eq(a, a->data[i], v)) return (double)i; + if (*scr_arr_state_in(states, i) == SCR_ARR_VALUE && scr_arr_ref_eq(a, a->data[i], v)) return (double)i; } for (size_t i = 0; i < SCR_AX(a)->sparse_len; i++) { if (SCR_AX(a)->sparse[i].state == SCR_ARR_VALUE && scr_arr_ref_eq(a, SCR_AX(a)->sparse[i].slot, v)) return (double)SCR_AX(a)->sparse[i].index; @@ -2332,8 +2438,9 @@ double scr_arr_index_of_ref(ScrArr *a, void *v) { } bool scr_arr_includes_f64(ScrArr *a, double v) { + const uint8_t *states = scr_arr_states_end(a); for (size_t i = 0; i < a->cap && i < a->len; i++) { - if (a->present[i] != SCR_ARR_VALUE) continue; + if (*scr_arr_state_in(states, i) != SCR_ARR_VALUE) continue; double x = scr_slot_to_f64(a->data[i]); if (x == v || (x != x && v != v)) return true; /* SameValueZero: NaN hits */ } @@ -2353,8 +2460,9 @@ bool scr_arr_includes_ref(ScrArr *a, void *v) { if (a->elem == SCR_ELEM_REF && a->ops->retain == scr_dyn_retain_v) { const ScrDyn *needle = (const ScrDyn *)v; if (needle->kind == SCR_DYN_NUM && needle->v.num != needle->v.num) { + const uint8_t *states = scr_arr_states_end(a); for (size_t i = 0; i < a->cap && i < a->len; i++) { - if (a->present[i] != SCR_ARR_VALUE) continue; + if (*scr_arr_state_in(states, i) != SCR_ARR_VALUE) continue; const ScrDyn *d = (const ScrDyn *)scr_slot_to_ptr(a->data[i]); if (d->kind == SCR_DYN_NUM && d->v.num != d->v.num) return true; } @@ -2416,6 +2524,13 @@ ScrArr *scr_arr_slice(ScrArr *a, double start, double end) { ? scr_arr_new_ops(a->ops, hint ? hint : 1) : scr_arr_new(a->elem, hint ? hint : 1); out->len = n; + if (to <= a->cap && to <= a->filled && a->elem != SCR_ELEM_BOOL && !scr_elem_is_ref(a->elem)) { + /* Numbers below the source's hole-free prefix: values only. */ + scr_arr_copy_slots(out->data, a->data + from, n); + scr_arr_set_states(scr_arr_states_end_w(out, 8) - n, SCR_ARR_VALUE, n); + out->filled = (uint32_t)n; + return out; + } if (to <= a->cap) { scr_arr_copy_dense(out, 0, a, from, n, false, false); /* States copied from below the source's bound are present values. */ @@ -2424,9 +2539,10 @@ ScrArr *scr_arr_slice(ScrArr *a, double start, double end) { scr_arr_refill(out); return out; } + const uint8_t *states = scr_arr_states_end(a); for (size_t i = 0; i < a->cap && i < to; i++) { - if (i < from || a->present[i] == SCR_ARR_HOLE) continue; - uint8_t state = a->present[i]; + if (i < from || *scr_arr_state_in(states, i) == SCR_ARR_HOLE) continue; + uint8_t state = *scr_arr_state_in(states, i); scr_arr_store_state_owned(out, i - from, scr_arr_retain_state_slot(a, scr_arr_dense_get(a, i), state), state); @@ -2552,9 +2668,10 @@ ScrArr *scr_arr_sort_primitive(ScrArr *a, bool copy) { if (a->len == 0) return copy ? scr_arr_new(a->elem, 0) : scr_arr_retain(a); size_t dense = a->len < a->cap ? a->len : a->cap; size_t count = 0, undefined_count = 0; + const uint8_t *states = scr_arr_states_end(a); for (size_t i = 0; i < dense; i++) { - count += a->present[i] == SCR_ARR_VALUE; - undefined_count += a->present[i] == SCR_ARR_UNDEFINED; + count += *scr_arr_state_in(states, i) == SCR_ARR_VALUE; + undefined_count += *scr_arr_state_in(states, i) == SCR_ARR_UNDEFINED; } for (size_t i = 0; i < SCR_AX(a)->sparse_len; i++) { count += SCR_AX(a)->sparse[i].state == SCR_ARR_VALUE; @@ -2565,7 +2682,7 @@ ScrArr *scr_arr_sort_primitive(ScrArr *a, bool copy) { if (count && !values) scr_arr_oom(); size_t used = 0; for (size_t i = 0; i < dense; i++) - if (a->present[i] == SCR_ARR_VALUE) values[used++] = scr_arr_dense_get(a, i); + if (*scr_arr_state_in(states, i) == SCR_ARR_VALUE) values[used++] = scr_arr_dense_get(a, i); for (size_t i = 0; i < SCR_AX(a)->sparse_len; i++) if (SCR_AX(a)->sparse[i].state == SCR_ARR_VALUE) values[used++] = SCR_AX(a)->sparse[i].slot; ScrNumberSortKey *numbers = NULL; @@ -2591,8 +2708,8 @@ ScrArr *scr_arr_sort_primitive(ScrArr *a, bool copy) { if (!copy && scr_arr_is_dense(a)) { if (count) scr_arr_put_slots(a, 0, count, values); scr_arr_cut(a, count); - memset(a->present, SCR_ARR_VALUE, count); - memset(a->present + count, SCR_ARR_UNDEFINED, undefined_count); + memset(scr_arr_state_run(a, 0, count), SCR_ARR_VALUE, count); + memset(scr_arr_state_run(a, count, undefined_count), SCR_ARR_UNDEFINED, undefined_count); scr_arr_clear_dense(a, count + undefined_count, a->len - count - undefined_count); if (undefined_count) memset(scr_arr_dense_at(a, count), 0, undefined_count * scr_arr_slot_width(a)); @@ -4211,9 +4328,9 @@ void scr_arr_sort_values(ScrArr *a, double count, ScrClosure *f, uint32_t arity, static bool scr_arr_dense_values(const ScrArr *a) { if (!scr_arr_is_dense(a)) return false; if (a->filled == a->len) return true; - const uint8_t *present = a->present; + const uint8_t *states = scr_arr_states_end(a); bool all = true; - for (size_t i = 0; i < a->len; i++) all &= present[i] == SCR_ARR_VALUE; + for (size_t i = 0; i < a->len; i++) all &= *scr_arr_state_in(states, i) == SCR_ARR_VALUE; return all; } @@ -4285,16 +4402,16 @@ bool scr_arr_sort_dense(ScrArr *a, ScrClosure *f, uint32_t arity, bool owned, in } else if (a->len >= n && a->cap >= n) { if (!retained) { memcpy(a->data, src, n * sizeof(*src)); - memset(a->present, SCR_ARR_VALUE, n); + memset(scr_arr_state_run(a, 0, n), SCR_ARR_VALUE, n); } else { /* Publish each new edge before releasing the value it replaces: a * release can collect cycles. The buffer still holds the slots not * yet written. */ for (size_t i = 0; i < n; i++) { - bool held = a->present[i] == SCR_ARR_VALUE; + bool held = *scr_arr_state_ptr(a, i) == SCR_ARR_VALUE; uint64_t old = held ? a->data[i] : 0; a->data[i] = src[i]; - a->present[i] = SCR_ARR_VALUE; + *scr_arr_state_ptr(a, i) = SCR_ARR_VALUE; if (held) scr_elem_release(a, old); } } diff --git a/packages/runtime/src/scr_array.h b/packages/runtime/src/scr_array.h index 5289f8c13..909f9ae8c 100644 --- a/packages/runtime/src/scr_array.h +++ b/packages/runtime/src/scr_array.h @@ -64,8 +64,10 @@ typedef struct ScrArrOps { typedef struct ScrArr { size_t rc; /* SIZE_MAX = immortal (unused for arrays; kept per convention) */ - size_t len; - size_t cap; /* dense capacity; sparse entries cover indices >= cap */ + /* Length and dense capacity are at most SCR_ARR_MAX_LENGTH (2^32 - 1); + * sparse entries cover indices >= cap. */ + uint32_t len; + uint32_t cap; /* One ScrElemKind. */ uint8_t elem; /* 1 exactly when ops->trace is non-NULL: the element type carries a cycle @@ -88,10 +90,10 @@ typedef struct ScrArr { /* SCR_ELEM_REF only; NULL for every other element kind. */ const ScrArrOps *ops; /* Dense values: `cap` 8-byte slots, or `cap` bytes for SCR_ELEM_BOOL - * (scr_arr_slot_width). Sparse entries and properties keep 8-byte slots - * for every kind; a bool slot is 0 or 1 wherever it lives. */ + * (scr_arr_slot_width), followed in the same allocation by one state byte + * per slot in reverse slot order (scr_arr_states_end). Sparse entries and properties keep 8-byte + * slots for every kind; a bool slot is 0 or 1 wherever it lives. */ uint64_t *data; - uint8_t *present; /* one state per dense slot; heap arrays share data allocation */ /* Rarely used storage (sparse indices, noncanonical numeric properties, * match-result metadata) lives out of line: NULL until first needed, so * the common dense array stays small. */ @@ -114,20 +116,58 @@ typedef struct ScrArrExt { } ScrArrExt; /* The LLVM backend mirrors ScrArr as %ScrArr: its inline retain/release read * `cyc` (field 4, i8) to decide mark-live / candidate buffering exactly like - * scr_arr_retain and scr_arr_release; its inline element access uses filled - * (field 6, i32), data (field 8) and present (field 9). */ -_Static_assert(offsetof(ScrArr, cyc) == 3 * sizeof(size_t) + 1, + * scr_arr_retain and scr_arr_release; its inline element access uses len and + * cap (fields 1 and 2, i32), filled (field 6, i32) and data (field 8), and + * finds the state of slot i at data + cap * (slot width + 1) - 1 - i. */ +_Static_assert(offsetof(ScrArr, len) == sizeof(size_t) && sizeof(((ScrArr *)0)->len) == 4, + "emitted length reads load ScrArr.len as %ScrArr field 1, an i32"); +_Static_assert(offsetof(ScrArr, cap) == sizeof(size_t) + 4 && sizeof(((ScrArr *)0)->cap) == 4, + "emitted capacity reads load ScrArr.cap as %ScrArr field 2, an i32"); +_Static_assert(offsetof(ScrArr, cyc) == sizeof(size_t) + 9, "inline RC fast paths read ScrArr.cyc as %ScrArr field 4"); -_Static_assert(offsetof(ScrArr, filled) == 3 * sizeof(size_t) + 4, +_Static_assert(offsetof(ScrArr, filled) == sizeof(size_t) + 12, "inline element access reads ScrArr.filled as %ScrArr field 6"); -_Static_assert(offsetof(ScrArr, data) == 3 * sizeof(size_t) + 8 + sizeof(void *), +_Static_assert(offsetof(ScrArr, data) == sizeof(size_t) + 16 + sizeof(void *), "inline element reads use ScrArr.data as %ScrArr field 8"); +_Static_assert(sizeof(ScrArr) == sizeof(size_t) + 16 + 3 * sizeof(void *), + "ScrArr is 48 bytes on 64-bit targets"); /* Bytes per dense value slot. */ static inline size_t scr_arr_slot_width(const ScrArr *a) { return a->elem == SCR_ELEM_BOOL ? 1 : sizeof(uint64_t); } +/* The dense states, one byte per slot, share the values' allocation (inline + * slots included). They follow the `cap` value slots in reverse slot order: + * the state of slot i is the byte i + 1 before scr_arr_states_end(a). Removing + * slots from the front (head room: `data` advances, `cap` drops) leaves every + * remaining state at its address. The address is computed on integers, so a + * NULL `data` stays NULL. */ +static inline uint8_t *scr_arr_states_end(const ScrArr *a) { + return (uint8_t *)((uintptr_t)a->data + (uintptr_t)a->cap * (scr_arr_slot_width(a) + 1)); +} + +/* scr_arr_states_end for an array whose slots are known to be `width` + * bytes wide: a constant multiple, with no element-kind test. */ +static inline uint8_t *scr_arr_states_end_w(const ScrArr *a, size_t width) { + return (uint8_t *)((uintptr_t)a->data + (uintptr_t)a->cap * (width + 1)); +} + +/* The state of dense slot `index` (< cap), given the end of the states. */ +static inline uint8_t *scr_arr_state_in(const uint8_t *end, size_t index) { + return (uint8_t *)end - 1 - index; +} + +static inline uint8_t *scr_arr_state_ptr(const ScrArr *a, size_t index) { + return scr_arr_state_in(scr_arr_states_end(a), index); +} + +/* The states of dense slots [from, from + count) are contiguous, in reverse + * order; this is their first byte (the state of slot from + count - 1). */ +static inline uint8_t *scr_arr_state_run(const ScrArr *a, size_t from, size_t count) { + return scr_arr_states_end(a) - from - count; +} + /* The dense value at `index` (< cap) as an 8-byte slot, and its store. */ static inline uint64_t scr_arr_dense_get(const ScrArr *a, size_t index) { if (a->elem == SCR_ELEM_BOOL) return ((const uint8_t *)a->data)[index]; @@ -145,18 +185,18 @@ static inline void scr_arr_cut(ScrArr *a, size_t index) { if (a->filled > index) a->filled = (uint32_t)index; } -/* The first index in [f, end) whose state is not a present value (or end). - * Compares eight states per step: bulk copies (slice, concat, spread) end - * with this scan over the whole copied range. */ -static inline size_t scr_arr_value_run(const uint8_t *present, size_t f, size_t end) { +/* The first index in [f, end) whose state is not a present value (or end), + * given the end of the states. Compares eight states per step: bulk copies + * (slice, concat, spread) end with this scan over the whole copied range. */ +static inline size_t scr_arr_value_run(const uint8_t *states_end, size_t f, size_t end) { const uint64_t values = 0x0101010101010101ull * SCR_ARR_VALUE; while (end - f >= 8) { uint64_t word; - memcpy(&word, present + f, sizeof word); + memcpy(&word, states_end - f - 8, sizeof word); if (word != values) break; f += 8; } - while (f < end && present[f] == SCR_ARR_VALUE) f++; + while (f < end && *scr_arr_state_in(states_end, f) == SCR_ARR_VALUE) f++; return f; } @@ -165,7 +205,7 @@ static inline size_t scr_arr_value_run(const uint8_t *present, size_t f, size_t static inline void scr_arr_refill(ScrArr *a) { size_t end = a->len < a->cap ? a->len : a->cap; size_t f = a->filled; - if (f < end) f = scr_arr_value_run(a->present, f, end); + if (f < end) f = scr_arr_value_run(scr_arr_states_end(a), f, end); a->filled = (uint32_t)f; } diff --git a/packages/runtime/src/scr_builtin_values.c b/packages/runtime/src/scr_builtin_values.c new file mode 100644 index 000000000..ea240359a --- /dev/null +++ b/packages/runtime/src/scr_builtin_values.c @@ -0,0 +1,261 @@ +/* Builtin functions called as function values in JavaScript dependencies + * (`Buffer.byteLength.bind(Buffer)`, `var alloc = Buffer.alloc`, a stored + * `fs.realpathSync`): the arguments arrive unchecked, so these entries apply + * Node's argument handling before the ordinary operation. A separate unit, + * linked only by programs that read these values. */ +#include "scr_runtime.h" + +#include +#include +#include + +/* Buffer.byteLength called as a function value (`Buffer.byteLength.bind( + * Buffer)` in truncate-utf8-bytes), with the encoding known only at run + * time. Node's ladder: a non-string source answers its byte length or + * throws before the encoding is read; an empty string answers 0; a falsy + * encoding, or one getEncodingOps does not know after ToString (any ASCII + * case), counts UTF-8. */ +double scr_buffer_byte_length_value(ScrDyn *value, ScrDyn *encoding) { + static const struct { const char *from; const char *to; } map[] = { + {"utf8", "utf8"}, {"utf-8", "utf8"}, {"hex", "hex"}, + {"base64", "base64"}, {"base64url", "base64url"}, + {"latin1", "latin1"}, {"binary", "latin1"}, {"ascii", "ascii"}, + {"utf16le", "utf16le"}, {"utf-16le", "utf16le"}, + {"ucs2", "utf16le"}, {"ucs-2", "utf16le"}, + }; + const char *name = "utf8"; + if (value->kind == SCR_DYN_STR && value->v.str->len > 0 && scr_dyn_truthy(encoding)) { + ScrStr *raw = scr_dyn_string_coerce_js(encoding); + if (!raw) return 0; + for (size_t i = 0; i < sizeof map / sizeof map[0]; i++) { + size_t n = strlen(map[i].from); + bool same = raw->len == n; + for (size_t k = 0; same && k < n; k++) { + char c = raw->data[k]; + if (c >= 'A' && c <= 'Z') c = (char)(c + ('a' - 'A')); + same = c == map[i].from[k]; + } + if (same) { + name = map[i].to; + break; + } + } + scr_str_release(raw); + } + ScrStr *canonical = scr_str_new(name, strlen(name)); + double length = scr_buffer_byte_length_dyn(value, canonical); + scr_str_release(canonical); + return length; +} + +/* Node's normalizeEncoding: the canonical name for a string encoding + * (any ASCII case; null, undefined and "" read as utf8), or NULL. */ +static const char *scr_bv_encoding(const ScrDyn *encoding) { + static const struct { const char *from; const char *to; } map[] = { + {"utf8", "utf8"}, {"utf-8", "utf8"}, {"hex", "hex"}, + {"base64", "base64"}, {"base64url", "base64url"}, + {"latin1", "latin1"}, {"binary", "latin1"}, {"ascii", "ascii"}, + {"utf16le", "utf16le"}, {"utf-16le", "utf16le"}, + {"ucs2", "utf16le"}, {"ucs-2", "utf16le"}, + }; + if (encoding->kind == SCR_DYN_UNDEF || encoding->kind == SCR_DYN_NULL) return "utf8"; + if (encoding->kind != SCR_DYN_STR) return NULL; + const ScrStr *name = encoding->v.str; + if (name->len == 0) return "utf8"; + for (size_t i = 0; i < sizeof map / sizeof map[0]; i++) { + size_t n = strlen(map[i].from); + bool same = name->len == n; + for (size_t k = 0; same && k < n; k++) { + char c = name->data[k]; + if (c >= 'A' && c <= 'Z') c = (char)(c + ('a' - 'A')); + same = c == map[i].from[k]; + } + if (same) return map[i].to; + } + return NULL; +} + +static void scr_bv_invalid_value(const ScrDyn *value) { + char rendered[160]; + /* The rejected views are empty, which util.inspect prints whole. */ + const char *text = value->kind == SCR_DYN_BYTES && value->v.bytes->len == 0 && value->buffer ? "" + : scr_dyn_inspect_lite(value, rendered, sizeof rendered); + char message[224]; + int n = snprintf(message, sizeof message, "The argument 'value' is invalid. Received %s", text); + scr_throw_error_msg_code(SCR_ERR_TYPE, message, (size_t)n, "ERR_INVALID_ARG_VALUE"); +} + +/* Buffer.alloc called as a function value (`var alloc = Buffer.alloc` in + * tar-stream), with Node's alloc and _fill: the size must be a number, + * ERR_INVALID_ARG_TYPE otherwise, then the allocation and range checks. + * A fill that is undefined or 0, or a size of 0, leaves the buffer zeroed. + * A string fill encodes with the encoding (ERR_INVALID_ARG_TYPE for a + * non-string encoding Node cannot normalize, ERR_UNKNOWN_ENCODING for an + * unknown name), an empty string zero-fills, and a non-empty string that + * encodes to no bytes throws ERR_INVALID_ARG_VALUE. A view fills with + * its bytes (ERR_INVALID_ARG_VALUE when it is empty); any other value + * fills with ToUint32(ToNumber(value)) & 255. +1, or NULL with the + * exception pending. */ +ScrBytes *scr_buffer_alloc_value(const ScrDyn *size, const ScrDyn *fill, const ScrDyn *encoding) { + if (size->kind != SCR_DYN_NUM) { + scr_dyn_arg_type_fail("size", "of type number", size); + return NULL; + } + ScrBytes *buf = scr_buffer_alloc(size->v.num); + if (!buf) return NULL; + if (fill->kind == SCR_DYN_UNDEF || (fill->kind == SCR_DYN_NUM && fill->v.num == 0) || buf->len == 0) + return buf; + if (fill->kind == SCR_DYN_STR) { + const char *name = scr_bv_encoding(encoding); + if (!name) { + if (encoding->kind != SCR_DYN_STR) { + scr_dyn_arg_type_fail("encoding", "of type string", encoding); + } else { + ScrJsonBuf b; + scr_jb_init(&b); + scr_jb_puts(&b, "Unknown encoding: "); + scr_jb_put_str(&b, encoding->v.str); + ScrStr *message = scr_jb_finish(&b); + scr_throw_error_msg_code(SCR_ERR_TYPE, message->data, message->len, "ERR_UNKNOWN_ENCODING"); + scr_str_release(message); + } + scr_bytes_release(buf); + return NULL; + } + if (fill->v.str->len == 0) return buf; + ScrStr *canonical = scr_str_new(name, strlen(name)); + ScrBytes *pattern = scr_bytes_from_str(fill->v.str, canonical); + scr_str_release(canonical); + if (!pattern || pattern->len == 0) { + scr_bytes_release(pattern); + scr_bytes_release(buf); + scr_bv_invalid_value(fill); + return NULL; + } + ScrBytes *filled = scr_bytes_fill(buf, pattern, 0, 0, 0); + scr_bytes_release(pattern); + scr_bytes_release(filled); + return buf; + } + if (fill->kind == SCR_DYN_BYTES) { + ScrBytes *pattern = scr_bytes_buffer_source(fill); + if (!pattern || pattern->len == 0) { + scr_bytes_release(pattern); + scr_bytes_release(buf); + scr_bv_invalid_value(fill); + return NULL; + } + ScrBytes *filled = scr_bytes_fill(buf, pattern, 0, 0, 0); + scr_bytes_release(pattern); + scr_bytes_release(filled); + return buf; + } + double value; + if (!scr_dyn_number_coerce_js(fill, &value)) { + scr_bytes_release(buf); + return NULL; + } + double byte = isfinite(value) ? fmod(trunc(value), 256.0) : 0; + if (byte < 0) byte += 256; + memset(buf->data, (int)byte, buf->len); + return buf; +} + +/* fs.realpathSync and fs.realpathSync.native called as function values + * (fs.realpath keeps `var origRealpathSync = fs.realpathSync`). Node's + * argument handling: options that are null, undefined or a function mean + * none, a string is the encoding, an object supplies `encoding`, anything + * else throws ERR_INVALID_ARG_TYPE; a file URL is its path (toPathIfFileURL, + * with fileURLToPath's errors for other URLs). The JavaScript form converts + * any other path that is not a string with ToString (a Buffer decodes as + * UTF-8); the native form takes a Uint8Array's bytes and throws + * ERR_INVALID_ARG_TYPE for other values. The result is a string for + * utf8, a Buffer for "buffer", and the UTF-8 bytes decoded in any other + * encoding, whose name must be one Node knows. +1 or NULL. */ +ScrDyn *scr_fs_realpath_value(const ScrDyn *path, const ScrDyn *options, bool native) { + const ScrDyn *encoding = NULL; + if (options->kind == SCR_DYN_STR) { + encoding = options; + } else if (options->kind == SCR_DYN_OBJ) { + encoding = scr_dyn_obj_get(options, "encoding", 8); + } else if (options->kind != SCR_DYN_UNDEF && options->kind != SCR_DYN_NULL && + options->kind != SCR_DYN_FUNC) { + scr_dyn_arg_type_fail("options", "of type string or an instance of Object", options); + return NULL; + } + static const struct { const char *from; const char *to; } names[] = { + {"utf8", "utf8"}, {"utf-8", "utf8"}, {"hex", "hex"}, {"base64", "base64"}, + {"base64url", "base64url"}, {"latin1", "latin1"}, {"binary", "latin1"}, + {"ascii", "ascii"}, {"utf16le", "utf16le"}, {"utf-16le", "utf16le"}, + {"ucs2", "utf16le"}, {"ucs-2", "utf16le"}, {"buffer", "buffer"}, + }; + const char *canonical = "utf8"; + if (encoding && encoding->kind != SCR_DYN_UNDEF && encoding->kind != SCR_DYN_NULL && + !(encoding->kind == SCR_DYN_STR && encoding->v.str->len == 0)) { + const char *found = NULL; + if (encoding->kind == SCR_DYN_STR) { + for (size_t i = 0; i < sizeof names / sizeof names[0] && !found; i++) { + size_t n = strlen(names[i].from); + if (encoding->v.str->len != n) continue; + bool same = true; + for (size_t k = 0; k < n && same; k++) { + char c = encoding->v.str->data[k]; + if (c >= 'A' && c <= 'Z') c = (char)(c + ('a' - 'A')); + same = c == names[i].from[k]; + } + if (same) found = names[i].to; + } + } + if (!found) { + ScrStr *shown = scr_dyn_string_coerce_js(encoding); + if (!shown) return NULL; + char msg[256]; + int n = snprintf(msg, sizeof msg, "The argument 'encoding' is invalid encoding. Received '%.*s'", + (int)(shown->len > 160 ? 160 : shown->len), shown->data); + scr_str_release(shown); + scr_throw_error_msg_code(SCR_ERR_TYPE, msg, (size_t)n, "ERR_INVALID_ARG_VALUE"); + return NULL; + } + canonical = found; + } + ScrStr *p; + if (scr_dyn_native_url_is(path)) { + p = scr_url_to_path(path->v.handle.ptr); + } else if (path->kind == SCR_DYN_STR) { + p = scr_str_retain(path->v.str); + } else if (native && path->kind == SCR_DYN_BYTES && path->v.bytes->elem == SCR_BYTES_U8) { + /* getValidatedPath takes a Uint8Array's bytes as the path. */ + ScrStr *utf8 = scr_str_new("utf8", 4); + p = scr_bytes_to_str(path->v.bytes, utf8); + scr_str_release(utf8); + } else if (native) { + scr_dyn_arg_type_fail("path", "of type string or an instance of Buffer or URL", path); + return NULL; + } else { + p = scr_dyn_string_coerce_js(path); + } + if (!p) return NULL; + ScrStr *resolved = native ? scr_fs_realpath_promise(p) : scr_fs_realpath(p); + scr_str_release(p); + if (!resolved) return NULL; + ScrDyn *out; + if (!strcmp(canonical, "utf8")) { + out = scr_dyn_new_str(resolved); + } else { + ScrStr *utf8 = scr_str_new("utf8", 4); + ScrBytes *bytes = scr_buffer_from_str(resolved, utf8); + scr_str_release(utf8); + if (!strcmp(canonical, "buffer")) { + out = scr_dyn_new_buffer(bytes); + } else { + ScrStr *enc = scr_str_new(canonical, strlen(canonical)); + ScrStr *text = scr_bytes_to_str(bytes, enc); + scr_str_release(enc); + out = scr_dyn_new_str(text); + scr_str_release(text); + } + scr_bytes_release(bytes); + } + scr_str_release(resolved); + return out; +} diff --git a/packages/runtime/src/scr_bytes.c b/packages/runtime/src/scr_bytes.c index 64fc36484..999e75fb0 100644 --- a/packages/runtime/src/scr_bytes.c +++ b/packages/runtime/src/scr_bytes.c @@ -3116,7 +3116,7 @@ static void scr_bytes_fill_from_arr(uint8_t *out, ScrBytesElem elem, const ScrAr /* Sparse tails and explicit undefined retain ordinary iteration * semantics. Numeric array slots own no conversion callbacks. */ if (index < arr->cap) { - if (arr->present[index] == SCR_ARR_VALUE) + if (*scr_arr_state_ptr(arr, index) == SCR_ARR_VALUE) memcpy(&values[i], &arr->data[index], sizeof(double)); else values[i] = NAN; } else values[i] = scr_arr_get_number(arr, (double)index); diff --git a/packages/runtime/src/scr_bytes.h b/packages/runtime/src/scr_bytes.h index b7b913d7f..e0fa7b857 100644 --- a/packages/runtime/src/scr_bytes.h +++ b/packages/runtime/src/scr_bytes.h @@ -389,11 +389,15 @@ ScrBytes *scr_buffer_from_str_checked(const ScrStr *s, const ScrStr *enc); ScrBytes *scr_buffer_from_bytes(const ScrBytes *src); /* a copy of any typed array's values */ ScrBytes *scr_buffer_from_arr(const ScrArr *arr); /* number[] */ ScrBytes *scr_buffer_from_dyn_pooled(const ScrDyn *value, const ScrStr *encoding); +ScrBytes *scr_buffer_from_dyn_ref_pooled(const ScrDyn *value, const ScrStr *encoding); ScrBytes *scr_buffer_concat(const ScrArr *list); ScrBytes *scr_buffer_concat_len(const ScrArr *list, double total); /* The untyped-list forms: Node's ERR_INVALID_ARG_TYPE checks on the list * and each element, then the typed concat. NULL with the exception pending. */ ScrBytes *scr_buffer_concat_dyn(const struct ScrDyn *list); +ScrBytes *scr_buffer_alloc_value(const struct ScrDyn *size, const struct ScrDyn *fill, + const struct ScrDyn *encoding); +struct ScrDyn *scr_fs_realpath_value(const struct ScrDyn *path, const struct ScrDyn *options, bool native); ScrBytes *scr_buffer_concat_len_dyn(const struct ScrDyn *list, double total); /* Drops the pool's slab reference and restores Buffer.poolSize (process * exit audits and library session resets). */ @@ -408,6 +412,7 @@ ScrBytes *scr_bytes_from_str_checked(const ScrStr *s, const ScrStr *enc); * Borrow; never throw. */ double scr_bytes_byte_length_str(ScrStr *s, const ScrStr *enc); double scr_buffer_byte_length_dyn(ScrDyn *value, ScrStr *enc); +double scr_buffer_byte_length_value(ScrDyn *value, ScrDyn *encoding); bool scr_bytes_is_encoding(const ScrStr *s); /* equals / compare / indexOf-lastIndexOf / fill / copy / swap / write on diff --git a/packages/runtime/src/scr_bytes_dyn.c b/packages/runtime/src/scr_bytes_dyn.c new file mode 100644 index 000000000..d4b29574b --- /dev/null +++ b/packages/runtime/src/scr_bytes_dyn.c @@ -0,0 +1,101 @@ +/* The fixed-width read/write methods of a Buffer held as a checked-dynamic + * value: a JavaScript dependency's `buf.writeUInt32LE(v, 0)` on a buffer it + * got back untyped (yazl). The compiler routes only the dyn method calls + * that name one of these methods here, so programs without them link + * nothing new; any other receiver takes the ordinary dispatch. */ +#include "scr_runtime.h" + +#include +#include +#include + +static const struct { + const char *name; + ScrBytesNumKind kind; + bool le; +} scr_bytes_dyn_names[] = { + { "UInt8", SCR_BN_U8, false }, { "Uint8", SCR_BN_U8, false }, + { "Int8", SCR_BN_I8, false }, { "UInt16LE", SCR_BN_U16, true }, + { "Uint16LE", SCR_BN_U16, true }, { "UInt16BE", SCR_BN_U16, false }, + { "Uint16BE", SCR_BN_U16, false }, { "Int16LE", SCR_BN_I16, true }, + { "Int16BE", SCR_BN_I16, false }, { "UInt32LE", SCR_BN_U32, true }, + { "Uint32LE", SCR_BN_U32, true }, { "UInt32BE", SCR_BN_U32, false }, + { "Uint32BE", SCR_BN_U32, false }, { "Int32LE", SCR_BN_I32, true }, + { "Int32BE", SCR_BN_I32, false }, { "FloatLE", SCR_BN_F32, true }, + { "FloatBE", SCR_BN_F32, false }, { "DoubleLE", SCR_BN_F64, true }, + { "DoubleBE", SCR_BN_F64, false }, +}; + +static size_t scr_bytes_dyn_width(ScrBytesNumKind kind) { + switch (kind) { + case SCR_BN_U8: + case SCR_BN_I8: + return 1; + case SCR_BN_U16: + case SCR_BN_I16: + return 2; + case SCR_BN_F64: + return 8; + default: + return 4; + } +} + +/* Node's checkInt range test and message, before the offset is looked at. */ +static bool scr_bytes_dyn_value_in_range(double value, size_t width, bool sign) { + double max = sign ? exp2((double)(8 * width - 1)) - 1 : exp2((double)(8 * width)) - 1; + double min = sign ? -exp2((double)(8 * width - 1)) : 0; + if (!(value > max || value < min)) return true; + char recv[48]; + scr_num_received(value, recv); + char minb[32], maxb[32]; + scr_f64_to_str(min, minb); + scr_f64_to_str(max, maxb); + char msg[160]; + int mlen = snprintf(msg, sizeof msg, + "The value of \"value\" is out of range. It must be >= %s and <= %s. Received %s", + minb, maxb, recv); + scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)mlen, "ERR_OUT_OF_RANGE"); + return false; +} + +/* Node's order: a write converts the value with ToNumber and checks its + * range, then the offset must be a number (absent means 0), then the + * bounds. +1, or NULL with the exception pending. */ +static ScrDyn *scr_bytes_dyn_num_method(ScrBytes *b, bool read, size_t at, ScrDyn *const *args, + size_t argc) { + ScrBytesNumKind kind = scr_bytes_dyn_names[at].kind; + double value = 0; + if (!read) { + if (!scr_dyn_number_coerce_js(argc > 0 ? args[0] : scr_dyn_undefined(), &value)) return NULL; + if (kind != SCR_BN_F32 && kind != SCR_BN_F64) { + bool sign = kind == SCR_BN_I8 || kind == SCR_BN_I16 || kind == SCR_BN_I32; + if (!scr_bytes_dyn_value_in_range(value, scr_bytes_dyn_width(kind), sign)) return NULL; + } + } + size_t offset_at = read ? 0 : 1; + double offset = 0; + if (argc > offset_at && args[offset_at]->kind != SCR_DYN_UNDEF) { + if (args[offset_at]->kind != SCR_DYN_NUM) { + scr_dyn_arg_type_fail("offset", "of type number", args[offset_at]); + return NULL; + } + offset = args[offset_at]->v.num; + } + bool le = scr_bytes_dyn_names[at].le; + double out = read ? scr_bytes_read_num(b, offset, kind, le) : scr_bytes_write_num(b, value, offset, kind, le); + return scr_exc_pending() ? NULL : scr_dyn_new_num(out); +} + +ScrDyn *scr_dyn_invoke_buffer_num(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, + size_t argc, const char *what) { + if (recv->kind == SCR_DYN_BYTES && recv->buffer && !recv->v.bytes->is_data_view) { + bool read = !strncmp(method, "read", 4); + const char *rest = method + (read ? 4 : 5); + for (size_t at = 0; at < sizeof scr_bytes_dyn_names / sizeof scr_bytes_dyn_names[0]; at++) { + if (!strcmp(rest, scr_bytes_dyn_names[at].name)) + return scr_bytes_dyn_num_method(recv->v.bytes, read, at, args, argc); + } + } + return scr_dyn_invoke_prepared(recv, callee, method, args, argc, what); +} diff --git a/packages/runtime/src/scr_bytes_io.c b/packages/runtime/src/scr_bytes_io.c index 4d7ef5f67..c852388e9 100644 --- a/packages/runtime/src/scr_bytes_io.c +++ b/packages/runtime/src/scr_bytes_io.c @@ -1350,6 +1350,24 @@ ScrBytes *scr_buffer_from_dyn_pooled(const ScrDyn *value, const ScrStr *encoding return scr_buffer_pool_adopt(out); } +/* scr_buffer_from_dyn_pooled for values that can be a typed array crossing + * an untyped slot by reference (a number[] passed to an untyped parameter, + * as yazl's Buffer.from(something, encoding) receives it): the array copies + * like the array it is. Used by the runtime-encoding Buffer.from forms. */ +ScrBytes *scr_buffer_from_dyn_ref_pooled(const ScrDyn *value, const ScrStr *encoding) { + if (value->kind == SCR_DYN_TYPED_REF && scr_dyn_typed_ref_array_ops(value) != NULL) { + ScrDyn *materialized = scr_dyn_typed_ref_materialize(value); + if (scr_exc_pending()) { + scr_dyn_release(materialized); + return NULL; + } + ScrBytes *out = scr_buffer_from_dyn_pooled(materialized, encoding); + scr_dyn_release(materialized); + return out; + } + return scr_buffer_from_dyn_pooled(value, encoding); +} + /* ── the checked-dynamic Buffer compare/equals validators ────────────── * Node's argument ladders for buf.equals / buf.compare / Buffer.compare * over dyn-boxed arguments (the invalid-input probes: string needles, diff --git a/packages/runtime/src/scr_console.c b/packages/runtime/src/scr_console.c index 1e031a2d0..0afb8981a 100644 --- a/packages/runtime/src/scr_console.c +++ b/packages/runtime/src/scr_console.c @@ -148,7 +148,7 @@ static void scr_enable_utf8_console_output(void) { } #endif -void scr_runtime_abi_v22(void) {} +void scr_runtime_abi_v23(void) {} void scr_init(void) { #ifdef SCR_WORKERS diff --git a/packages/runtime/src/scr_core.h b/packages/runtime/src/scr_core.h index 6d251e8ce..cdf1061d9 100644 --- a/packages/runtime/src/scr_core.h +++ b/packages/runtime/src/scr_core.h @@ -81,7 +81,7 @@ void scr_init(void); /* Program objects emitted by the bundled LLVM helper reference this symbol. * Its versioned spelling makes a mismatched manual runtime link fail before * the program can start. */ -void scr_runtime_abi_v22(void); +void scr_runtime_abi_v23(void); void scr_runtime_workers_v8(void); /* ── the trap funnel (scr_console.c; scr_library.c under -DSCR_LIB) ────── diff --git a/packages/runtime/src/scr_cycle.c b/packages/runtime/src/scr_cycle.c index fd5d64c81..9085bf9c9 100644 --- a/packages/runtime/src/scr_cycle.c +++ b/packages/runtime/src/scr_cycle.c @@ -165,8 +165,9 @@ void scr_weak_dispose(void *object) { #include #include -/* Every heap object's first member is `size_t rc`. */ -#define SCR_RC(obj) (*(size_t *)(obj)) +/* Every heap object's first member is `size_t rc`; the collector works on + * its count (SCR_RC_COUNT). */ +#define SCR_RC(obj) SCR_RC_COUNT(obj) static void scr_cyc_oom(void) { scr_trap("scriptc: out of memory\n"); @@ -1171,7 +1172,7 @@ void scr_cyc_on_release(void *obj) { #define SCR_CYC_WALK_GEN(h) scr_cyc_gen(h) #endif #define SCR_CYC_SKIP(child) \ - ((child) == NULL || SCR_RC(child) == SIZE_MAX \ + ((child) == NULL || SCR_RC(child) == SCR_RC_IMMORTAL \ || SCR_CYC_WALK_GEN(scr_cyc_hdr(child)) > scr_gen_limit) /* The walks are depth-first and recursive up to SCR_CYC_WALK_DEPTH, where @@ -1190,8 +1191,10 @@ void scr_cyc_on_release(void *obj) { size_t end = arr->len < arr->cap ? arr->len : arr->cap; \ size_t filled = arr->filled < end ? arr->filled : end; \ for (size_t i = 0; i < filled; i++) visit((void *)(uintptr_t)arr->data[i], ctx); \ + const uint8_t *states = scr_arr_states_end(arr); \ for (size_t i = filled; i < end; i++) \ - if (arr->present[i] == SCR_ARR_VALUE) visit((void *)(uintptr_t)arr->data[i], ctx); \ + if (*scr_arr_state_in(states, i) == SCR_ARR_VALUE) \ + visit((void *)(uintptr_t)arr->data[i], ctx); \ } /* Visit every child of `obj` with VISIT, through ARRAY_WALK for a dense @@ -1213,7 +1216,7 @@ SCR_CYC_ARRAY_WALK(scr_scan_array, scr_scan_visit) static inline __attribute__((always_inline)) void scr_mark_gray(void *obj, uintptr_t depth); static void scr_mg_visit(void *child, void *ctx) { - if (child == NULL || SCR_RC(child) == SIZE_MAX) return; + if (child == NULL || SCR_RC(child) == SCR_RC_IMMORTAL) return; ScrCycHdr *h = scr_cyc_hdr(child); if (SCR_CYC_WALK_GEN(h) > scr_gen_limit) { scr_cyc_push_work(&scr_xgen_edges, scr_gray_holder); @@ -1372,7 +1375,7 @@ static SCR_TL size_t scr_xg_freed = 0; static void scr_xg_release(void *obj); static void scr_xg_child_visit(void *child, void *ctx) { (void)ctx; - if (child == NULL || SCR_RC(child) == SIZE_MAX) return; + if (child == NULL || SCR_RC(child) == SCR_RC_IMMORTAL) return; scr_xg_release(child); } @@ -1597,7 +1600,7 @@ static void scr_yin_leave(ScrCycHdr *h, void *obj) { static void scr_yin_count_visit(void *child, void *ctx) { (void)ctx; - if (child == NULL || SCR_RC(child) == SIZE_MAX) return; + if (child == NULL || SCR_RC(child) == SCR_RC_IMMORTAL) return; ScrCycHdr *h = scr_cyc_hdr(child); if (scr_cyc_gen(h) == SCR_CYC_YOUNG) scr_yin_counts[scr_cyc_buf_index(h)]++; } @@ -1605,7 +1608,7 @@ static void scr_yin_count_visit(void *child, void *ctx) { /* Marked young objects are GRAY (young objects are otherwise BLACK). */ static void scr_yin_mark_visit(void *child, void *ctx) { (void)ctx; - if (child == NULL || SCR_RC(child) == SIZE_MAX) return; + if (child == NULL || SCR_RC(child) == SCR_RC_IMMORTAL) return; ScrCycHdr *h = scr_cyc_hdr(child); if (scr_cyc_gen(h) != SCR_CYC_YOUNG || scr_cyc_color(h) == SCR_CYC_GRAY) return; scr_cyc_set_color(h, SCR_CYC_GRAY); @@ -1617,7 +1620,7 @@ static void scr_yin_mark_visit(void *child, void *ctx) { * the count until every teardown has run. */ static void scr_yin_dead_visit(void *child, void *ctx) { (void)ctx; - if (child == NULL || SCR_RC(child) == SIZE_MAX) return; + if (child == NULL || SCR_RC(child) == SCR_RC_IMMORTAL) return; ScrCycHdr *h = scr_cyc_hdr(child); if (scr_cyc_gen(h) == SCR_CYC_YOUNG && scr_cyc_color(h) != SCR_CYC_GRAY) return; scr_yin_push(&scr_yin_xrel, child); diff --git a/packages/runtime/src/scr_dgram.c b/packages/runtime/src/scr_dgram.c index c34923607..0707edecf 100644 --- a/packages/runtime/src/scr_dgram.c +++ b/packages/runtime/src/scr_dgram.c @@ -1394,6 +1394,7 @@ static void scr_dns_lookup_plain(const ScrDyn *hostname, const ScrDyn *options, double family = 0; bool all = false; int order = scr_dns_default_order; + int ai_flags = 0; if (options->kind == SCR_DYN_NUM) { double f = options->v.num; if (f != 0 && f != 4 && f != 6) { @@ -1412,10 +1413,15 @@ static void scr_dns_lookup_plain(const ScrDyn *hostname, const ScrDyn *options, scr_dyn_prop_type_fail("options.hints", "of type number", v); return; } - if (v->v.num != 0) { - scr_throw_error_msg_code(SCR_ERR_ERROR, "dns.lookup hints are not supported yet", 38, "SC2020"); + /* Node's validateHints: only dns.ADDRCONFIG, dns.V4MAPPED and + * dns.ALL (getaddrinfo's own flags), which then reach getaddrinfo */ + double h = v->v.num; + int allowed = AI_ADDRCONFIG | AI_V4MAPPED | AI_ALL; + if (!(h == h && trunc(h) == h && h >= 0 && h <= 0x7fffffff) || ((int)h & ~allowed) != 0) { + scr_dyn_arg_value_fail("hints", "is invalid", v); return; } + ai_flags = (int)h; } v = scr_dyn_obj_get(options, "family", 6); if (v != NULL && v->kind != SCR_DYN_UNDEF && v->kind != SCR_DYN_NULL) { @@ -1519,6 +1525,7 @@ static void scr_dns_lookup_plain(const ScrDyn *hostname, const ScrDyn *options, memset(&hints, 0, sizeof hints); hints.ai_family = family == 4 ? AF_INET : family == 6 ? AF_INET6 : AF_UNSPEC; hints.ai_socktype = SOCK_STREAM; + hints.ai_flags = ai_flags; struct addrinfo *res = NULL; int rc = getaddrinfo(host->data, NULL, &hints, &res); if (rc != 0 || res == NULL) { diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index 401bf2b39..0ac280436 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -1134,6 +1134,10 @@ bool scr_dyn_strict_eq(const ScrDyn *a, const ScrDyn *b); * result owned (+1). MAY THROW (NULL with the exception pending). */ ScrDyn *scr_dyn_prepare_method(ScrDyn *recv, const char *method); ScrDyn *scr_dyn_invoke_prepared(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, size_t argc, const char *what); +/* scr_dyn_invoke_prepared for the fixed-width Buffer read/write method names + * (scr_bytes_dyn.c): a Buffer receiver runs the method, any other takes the + * ordinary dispatch. */ +ScrDyn *scr_dyn_invoke_buffer_num(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, size_t argc, const char *what); ScrDyn *scr_dyn_invoke(ScrDyn *recv, const char *method, ScrDyn *const *args, size_t argc, const char *what); /* Keyed read on a FUNC node: the own-property table first (defineProperties * writes land there), then "name" (the box's best-effort static name; "" @@ -1241,6 +1245,16 @@ ScrClassObj *scr_dyn_class_check(const ScrDyn *value, const char *type_key, cons * scriptc-specific behavior — JS `as` never checks (SEMANTICS.md). */ void scr_dyn_check_fail(const ScrDynPath *path, const char *want, const ScrDyn *got); +/* Checked-cast fallback (a `JSON.parse(text) as T` whose value fails + * validation keeps its dynamic value instead of throwing). Right after a + * fallback site's check, scr_dyn_check_recover takes back the exception the + * check raised (Node runs nothing at a cast, so nothing it raises is + * observable) and parks the checked value, which the lowered code reads + * with scr_dyn_check_fallback_taken and takes (+1) with _take. */ +bool scr_dyn_check_recover(ScrDyn *d); +bool scr_dyn_check_fallback_taken(void); +ScrDyn *scr_dyn_check_fallback_take(void); + /* Matchers and builders of recursive shapes (records and arrays whose type * reaches itself) track the dyn values they visit, so cyclic values match * and convert without unbounded recursion. Every matcher and builder of a diff --git a/packages/runtime/src/scr_dyn_builtin_stream.c b/packages/runtime/src/scr_dyn_builtin_stream.c index b7295f59a..209c8c39a 100644 --- a/packages/runtime/src/scr_dyn_builtin_stream.c +++ b/packages/runtime/src/scr_dyn_builtin_stream.c @@ -140,9 +140,11 @@ static void *bcs_capsule_retain(void *p) { return scr_stream_retain(p); } /* Also the capsule's tag. */ static void bcs_capsule_release(void *p) { scr_stream_release(p); } -static bool bcs_stream_vt(const ScrVt *vt) { - return (vt->pre >= scr_readable_vt.pre && vt->pre <= scr_readable_vt.post) || - (vt->pre >= scr_writable_vt.pre && vt->pre <= scr_writable_vt.post); +/* Whether `stream`, boxed as an "object:" typed ref, is a runtime stream. */ +static bool bcs_stream_vt(const ScrStream *stream) { + const ScrVt *vt = scr_stream_vt_of(stream); + return vt != NULL && ((vt->pre >= scr_readable_vt.pre && vt->pre <= scr_readable_vt.post) || + (vt->pre >= scr_writable_vt.pre && vt->pre <= scr_writable_vt.post)); } static void bcs_rec_release(void *p); @@ -154,7 +156,7 @@ static ScrStream *bcs_of(const ScrDyn *value) { if (value != NULL && value->kind == SCR_DYN_TYPED_REF && value->v.typed_ref.type_key_len > 7 && memcmp(value->v.typed_ref.type_key, "object:", 7) == 0) { ScrStream *stream = value->v.typed_ref.ptr; - return bcs_stream_vt(stream->vt) && scr_stream_native(stream, bcs_rec_release) ? stream : NULL; + return bcs_stream_vt(stream) && scr_stream_native(stream, bcs_rec_release) ? stream : NULL; } if (value == NULL || value->kind != SCR_DYN_OBJ || value->symbol_properties == NULL) return NULL; ScrDyn *capsule = scr_dyn_obj_get(value->symbol_properties, bcs_hidden_key, BCS_HIDDEN_LEN); @@ -184,7 +186,7 @@ static ScrStream *bcs_native_of(const ScrDyn *value) { memcmp(value->v.typed_ref.type_key, "object:", 7) != 0) return NULL; ScrStream *stream = scr_dyn_typed_ref_unbox(value); - if (bcs_stream_vt(stream->vt)) return stream; + if (bcs_stream_vt(stream)) return stream; value->v.typed_ref.release(stream); return NULL; } diff --git a/packages/runtime/src/scr_inspect.c b/packages/runtime/src/scr_inspect.c index c1673891b..223517e92 100644 --- a/packages/runtime/src/scr_inspect.c +++ b/packages/runtime/src/scr_inspect.c @@ -860,6 +860,67 @@ static ScrStr *scr_insp_data_view(const ScrBytes *b, double recurse, double dept static ScrStr *scr_insp_dyn_error(ScrDyn *d, double recurse, double depth); +/* An error's own `cause` property and whether it is enumerable, read where + * `e.cause` reads it: the error's checked-dynamic node when it has one, + * else its slot. NULL when it has none (borrowed; `view` holds a + * materialized node the caller releases). */ +static ScrDyn *insp_error_cause(ScrError *e, bool *enumerable, ScrDyn **view) { + *view = NULL; + ScrDyn *node = scr_errdyn_view_of(e); + if (node != NULL && node->kind == SCR_DYN_TYPED_REF) node = *view = scr_dyn_typed_ref_materialize(node); + if (node != NULL && node->kind == SCR_DYN_OBJ) { + for (size_t i = 0; i < node->v.obj.len; i++) { + ScrDynEntry *ent = &node->v.obj.entries[i]; + if (ent->key_len == 5 && memcmp(ent->key, "cause", 5) == 0) { + *enumerable = ent->enumerable; + return ent->value; + } + } + return NULL; + } + *enumerable = e->cause_enumerable; + return e->error_cause; +} + +/* Whether an error subclass instance's property bag (its properties + * created through untyped references) holds an enumerable property other + * than `cause`, which the error renderers print. Node's inspector shows no + * other non-enumerable property of an error. */ +bool scr_insp_error_bag_has_properties(ScrDyn *bag) { + if (bag == NULL || bag->kind != SCR_DYN_OBJ) return false; + for (size_t i = 0; i < bag->v.obj.len; i++) { + const ScrDynEntry *ent = &bag->v.obj.entries[i]; + if (ent->enumerable && !(ent->key_len == 5 && memcmp(ent->key, "cause", 5) == 0)) return true; + } + return false; +} + +/* Whether a program-declared error subclass instance has an own `cause`. */ +bool scr_insp_error_has_cause(ScrError *e) { + bool enumerable; + ScrDyn *view; + bool has = insp_error_cause(e, &enumerable, &view) != NULL; + scr_dyn_release(view); + return has; +} + +/* The entry formatError prints for an error's own `cause`: `cause: value` + * when the property is enumerable (an assignment), `[cause]: value` when it + * is not (the constructor's options). The caller checked that it has one + * (scr_insp_error_has_cause). +1 */ +ScrStr *scr_insp_error_cause(ScrError *e, double recurse, double depth) { + bool enumerable; + ScrDyn *view; + ScrDyn *cause = insp_error_cause(e, &enumerable, &view); + ScrStr *value = cause != NULL ? scr_insp_dyn(cause, recurse, depth) : scr_str_new("undefined", 9); + scr_dyn_release(view); + InspBuf out = {0}; + ib_cstr(&out, enumerable ? "cause: " : "[cause]: "); + ib_bytes(&out, value->data, value->len); + scr_str_release(value); + return ib_take(&out); +} + /* A runtime error through its node: the cached one, or one built from its * fields. */ ScrStr *scr_insp_error_node(ScrError *e, double recurse, double depth) { @@ -1009,6 +1070,16 @@ static ScrDyn *insp_assertion_ellipsis(ScrDyn *value) { * scr_insp_error: the stackless bracket form `[Name: message]`, then the * remaining enumerable own properties (code, errno, syscall, ...) in * Node's `{ ... }` tail. */ +/* An own non-enumerable entry of the error encoding, NULL when absent or + * enumerable (an enumerable one prints with the other own keys). */ +static const ScrDynEntry *insp_error_hidden_entry(const ScrDyn *d, const char *key, size_t len) { + for (size_t i = 0; i < d->v.obj.len; i++) { + const ScrDynEntry *ent = &d->v.obj.entries[i]; + if (ent->key_len == len && memcmp(ent->key, key, len) == 0) return ent->enumerable ? NULL : ent; + } + return NULL; +} + static ScrStr *scr_insp_dyn_error(ScrDyn *d, double recurse, double depth) { /* An error already on the traversal stack (a spawnSync error's own * `error`): Node's [Circular *N], and on the outer rendering. */ @@ -1029,6 +1100,12 @@ static ScrStr *scr_insp_dyn_error(ScrDyn *d, double recurse, double depth) { continue; extra++; } + /* formatError shows a non-enumerable `cause`, and an AggregateError's + * `errors` array, after the enumerable keys: `[cause]`, `[errors]`. */ + const ScrDynEntry *cause = insp_error_hidden_entry(d, "cause", 5); + const ScrDynEntry *errors = insp_error_hidden_entry(d, "errors", 6); + if (errors && errors->value->kind != SCR_DYN_ARR) errors = NULL; + extra += (cause != NULL) + (errors != NULL); InspBuf base = {0}; ib_char(&base, '['); if (name != NULL) ib_str(&base, name->v.str); @@ -1071,6 +1148,20 @@ static ScrStr *scr_insp_dyn_error(ScrDyn *d, double recurse, double depth) { scr_insp_entry(entry, false); scr_str_release(entry); } + const ScrDynEntry *hidden[] = {cause, errors}; + for (size_t i = 0; i < 2; i++) { + if (!hidden[i]) continue; + ScrStr *val = scr_insp_dyn(hidden[i]->value, recurse + 1, value_depth); + InspBuf eb = {0}; + ib_char(&eb, '['); + ib_bytes(&eb, hidden[i]->key, hidden[i]->key_len); + ib_cstr(&eb, "]: "); + ib_bytes(&eb, val->data, val->len); + scr_str_release(val); + ScrStr *entry = ib_take(&eb); + scr_insp_entry(entry, false); + scr_str_release(entry); + } ScrStr *b0 = scr_str_new("{", 1); ScrStr *b1 = scr_str_new("}", 1); ScrStr *body = scr_insp_end(base_str, b0, b1, recurse + 1, false, false); diff --git a/packages/runtime/src/scr_inspect.h b/packages/runtime/src/scr_inspect.h index 4eb607a5c..5ac3aa9dd 100644 --- a/packages/runtime/src/scr_inspect.h +++ b/packages/runtime/src/scr_inspect.h @@ -35,6 +35,9 @@ ScrStr *scr_insp_error(ScrError *e, double recurse, double depth); * one, or one built from its fields), so Node's own properties follow the * bracket form. */ ScrStr *scr_insp_error_node(ScrError *e, double recurse, double depth); +bool scr_insp_error_bag_has_properties(ScrDyn *bag); +bool scr_insp_error_has_cause(ScrError *e); +ScrStr *scr_insp_error_cause(ScrError *e, double recurse, double depth); /* The checked-dynamic tree: shape lives in the value, so the whole * traversal is here. dyn-boxed bytes render in the checked-dynamic tree's documented * Uint8Array identity. */ diff --git a/packages/runtime/src/scr_itable.c b/packages/runtime/src/scr_itable.c index 4842bf79c..7b5202b81 100644 --- a/packages/runtime/src/scr_itable.c +++ b/packages/runtime/src/scr_itable.c @@ -472,7 +472,7 @@ static ScrArrState scr_itable_element(ScrITableElements *w, size_t i, uint64_t * } if (i < a->cap) { *slot = a->data[i]; - return (ScrArrState)a->present[i]; + return (ScrArrState)*scr_arr_state_ptr(a, i); } if (a->ext) { while (w->sparse < a->ext->sparse_len && a->ext->sparse[w->sparse].index < i) w->sparse++; diff --git a/packages/runtime/src/scr_itable.test.c b/packages/runtime/src/scr_itable.test.c index 5e5f22bc9..f6d98654a 100644 --- a/packages/runtime/src/scr_itable.test.c +++ b/packages/runtime/src/scr_itable.test.c @@ -54,8 +54,10 @@ static ScrArr *make_f64s(const double *values, size_t n) { a->len = a->cap = n; a->filled = (uint32_t)n; a->elem = SCR_ELEM_F64; - a->data = calloc(n ? n : 1, sizeof(uint64_t)); + /* Values, then one state byte per slot (scr_arr_states_end). */ + a->data = calloc(n ? n : 1, sizeof(uint64_t) + 1); for (size_t i = 0; i < n; i++) memcpy(&a->data[i], &values[i], sizeof(double)); + memset(scr_arr_state_run(a, 0, n), SCR_ARR_VALUE, n); return a; } @@ -65,8 +67,9 @@ static ScrArr *make_strs(ScrStr *const *values, size_t n) { a->filled = (uint32_t)n; a->elem = SCR_ELEM_STR; free(a->data); - a->data = calloc(n, sizeof(uint64_t)); + a->data = calloc(n ? n : 1, sizeof(uint64_t) + 1); for (size_t i = 0; i < n; i++) a->data[i] = (uint64_t)(uintptr_t)values[i]; + memset(scr_arr_state_run(a, 0, n), SCR_ARR_VALUE, n); return a; } @@ -157,10 +160,7 @@ int main(void) { ScrArr *shorter = make_f64s((double[]){1, 0.0}, 2); ScrArr *holed = make_f64s((double[]){1, 0.0, 0.0}, 3); holed->filled = 1; - holed->present = calloc(3, 1); - holed->present[0] = SCR_ARR_VALUE; - holed->present[1] = SCR_ARR_VALUE; - holed->present[2] = SCR_ARR_HOLE; + *scr_arr_state_ptr(holed, 2) = SCR_ARR_HOLE; ScrArr *empty_list = make_f64s(NULL, 0); const ScrITableEntry *l1 = scr_itable_insert_arr(lists, one, 1); assert(scr_itable_find_arr(lists, same) == l1 && scr_itable_insert_arr(lists, same, 2) == l1); @@ -207,7 +207,6 @@ int main(void) { ScrArr *arrays[] = {one, same, shorter, holed, empty_list, big, longest, huge, p1, p2, p3, heavy}; for (unsigned i = 0; i < sizeof arrays / sizeof *arrays; i++) { free(arrays[i]->data); - free(arrays[i]->present); free(arrays[i]); } free(ab); diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 9746e8eb1..4b0ef7d1c 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -8567,6 +8567,34 @@ static const char *scr_dyn_kind_name(const ScrDyn *d) { return "unknown"; } +static SCR_TL ScrDyn *scr_dyn_fallback_value; +static SCR_TL bool scr_dyn_fallback_registered; + +static void scr_dyn_fallback_cleanup(void) { + scr_dyn_release(scr_dyn_fallback_value); + scr_dyn_fallback_value = NULL; +} + +bool scr_dyn_check_recover(ScrDyn *d) { + if (!scr_exc_pending() || scr_exc_active->kind == SCR_EXC_TERMINATE) return false; + scr_exc_clear(); + if (!scr_dyn_fallback_registered) { + scr_dyn_fallback_registered = true; + scr_atexit(scr_dyn_fallback_cleanup); + } + scr_dyn_release(scr_dyn_fallback_value); + scr_dyn_fallback_value = scr_dyn_retain(d); + return true; +} + +bool scr_dyn_check_fallback_taken(void) { return scr_dyn_fallback_value != NULL; } + +ScrDyn *scr_dyn_check_fallback_take(void) { + ScrDyn *value = scr_dyn_fallback_value; + scr_dyn_fallback_value = NULL; + return value; +} + void scr_dyn_check_fail(const ScrDynPath *path, const char *want, const ScrDyn *got) { ScrJsonBuf b; scr_jb_init(&b); diff --git a/packages/runtime/src/scr_lib.c b/packages/runtime/src/scr_lib.c index 99f0128c9..8d3b668c1 100644 --- a/packages/runtime/src/scr_lib.c +++ b/packages/runtime/src/scr_lib.c @@ -12721,10 +12721,11 @@ static bool scr_set_to_arr_sparse(const ScrMap *s, ScrArr *out) { ScrArr *scr_set_to_arr_f64(const ScrMap *s) { ScrArr *out = scr_arr_new(SCR_ELEM_F64, s->nlive); if (scr_set_to_arr_sparse(s, out)) return out; + uint8_t *states = scr_arr_states_end(out); for (size_t i = 0; i < s->nentries; i++) { if (!s->entries[i].hash) continue; out->data[out->len] = s->entries[i].key; - out->present[out->len++] = SCR_ARR_VALUE; + *scr_arr_state_in(states, out->len++) = SCR_ARR_VALUE; } return out; } @@ -12732,10 +12733,11 @@ ScrArr *scr_set_to_arr_f64(const ScrMap *s) { ScrArr *scr_set_to_arr_bool(const ScrMap *s) { ScrArr *out = scr_arr_new(SCR_ELEM_BOOL, s->nlive); if (scr_set_to_arr_sparse(s, out)) return out; + uint8_t *states = scr_arr_states_end(out); for (size_t i = 0; i < s->nentries; i++) { if (!s->entries[i].hash) continue; scr_arr_dense_put(out, out->len, s->entries[i].key != 0); - out->present[out->len++] = SCR_ARR_VALUE; + *scr_arr_state_in(states, out->len++) = SCR_ARR_VALUE; } return out; } @@ -12743,11 +12745,12 @@ ScrArr *scr_set_to_arr_bool(const ScrMap *s) { ScrArr *scr_set_to_arr_str(const ScrMap *s) { ScrArr *out = scr_arr_new(SCR_ELEM_STR, s->nlive); if (scr_set_to_arr_sparse(s, out)) return out; + uint8_t *states = scr_arr_states_end(out); for (size_t i = 0; i < s->nentries; i++) { if (!s->entries[i].hash) continue; scr_str_retain((ScrStr *)(uintptr_t)s->entries[i].key); out->data[out->len] = s->entries[i].key; - out->present[out->len++] = SCR_ARR_VALUE; + *scr_arr_state_in(states, out->len++) = SCR_ARR_VALUE; } return out; } @@ -12755,11 +12758,12 @@ ScrArr *scr_set_to_arr_str(const ScrMap *s) { ScrArr *scr_set_to_arr_ref(const ScrMap *s, const ScrArrOps *ops) { ScrArr *out = scr_arr_new_ops(ops, s->nlive); if (scr_set_to_arr_sparse(s, out)) return out; + uint8_t *states = scr_arr_states_end(out); for (size_t i = 0; i < s->nentries; i++) { if (!s->entries[i].hash) continue; ops->retain((void *)(uintptr_t)s->entries[i].key); out->data[out->len] = s->entries[i].key; - out->present[out->len++] = SCR_ARR_VALUE; + *scr_arr_state_in(states, out->len++) = SCR_ARR_VALUE; } return out; } diff --git a/packages/runtime/src/scr_map.c b/packages/runtime/src/scr_map.c index 28922296e..647f559dc 100644 --- a/packages/runtime/src/scr_map.c +++ b/packages/runtime/src/scr_map.c @@ -1504,7 +1504,7 @@ void scr_set_add_all(ScrMap *set, ScrArr *values) { /* Packed values stay owned by the seed array throughout construction. * Avoid taking a temporary owner only to release it after insertion. * Sparse/missing positions retain the existing accessor semantics. */ - if (i < values->cap && values->present[i] == SCR_ARR_VALUE) { + if (i < values->cap && *scr_arr_state_ptr(values, i) == SCR_ARR_VALUE) { uint64_t key = scr_arr_dense_get(values, i); if (set->key_kind == SCR_MAP_KEY_F64) key = scr_map_f64_bits(scr_map_slot_to_f64(key)); scr_map_set(set, scr_map_hash_key(set, key), key, 0); diff --git a/packages/runtime/src/scr_message.c b/packages/runtime/src/scr_message.c index 374b5469a..cb9bd8a23 100644 --- a/packages/runtime/src/scr_message.c +++ b/packages/runtime/src/scr_message.c @@ -282,7 +282,7 @@ static size_t scr_message_value(ScrMessageEncoder *encoder, const ScrDyn *input) /* Published values (immortal) travel by pointer: a typed value as a * capsule over the same object, a dyn graph as itself. */ if (input->kind == SCR_DYN_TYPED_REF && input->v.typed_ref.ptr && - *(const size_t *)input->v.typed_ref.ptr == SIZE_MAX) { + SCR_RC_COUNT(input->v.typed_ref.ptr) == SCR_RC_IMMORTAL) { scr_sa_thread_shares(); encoder->visits[id].view = (ScrDyn *)input; ScrMessageNode *node = &encoder->message->nodes[id]; diff --git a/packages/runtime/src/scr_net.c b/packages/runtime/src/scr_net.c index 9902033f2..894862ccc 100644 --- a/packages/runtime/src/scr_net.c +++ b/packages/runtime/src/scr_net.c @@ -2894,6 +2894,81 @@ ScrNetSocket *scr_net_connect_lookup(double port, ScrStr *host /*borrowed*/, return s; } +/* The callback a checked-dynamic lookup function receives (Node's own + * shape, `(err, address, family)`): with `all: true` the address is a list + * of { address, family } records, otherwise one address string. caps[0] + * boxes the socket, as for the typed answer closure. */ +static ScrDyn *scr_net_lookup_dyn_thunk(ScrClosure *clo, ScrDyn *const *args, size_t argc) { + const ScrDyn *err = argc > 0 ? args[0] : scr_dyn_undefined(); + const ScrDyn *addr = argc > 1 ? args[1] : scr_dyn_undefined(); + ScrStr *msg = NULL; + ScrArr *ips = NULL; + bool failed = scr_dyn_truthy(err); + if (failed) { + const ScrDyn *m = err->kind == SCR_DYN_OBJ ? scr_dyn_obj_get(err, "message", 7) : NULL; + msg = m != NULL && m->kind == SCR_DYN_STR ? scr_str_retain(m->v.str) : scr_dyn_string_coerce_js(err); + } else { + ScrDyn *view = addr->kind == SCR_DYN_TYPED_REF ? scr_dyn_typed_ref_materialize(addr) : NULL; + const ScrDyn *list = view != NULL ? view : addr; + ips = scr_arr_new(SCR_ELEM_STR, 0); + if (list->kind == SCR_DYN_STR) { + scr_arr_push_ref(ips, scr_str_retain(list->v.str)); + } else if (list->kind == SCR_DYN_ARR) { + for (size_t i = 0; i < list->v.arr.len; i++) { + const ScrDyn *entry = list->v.arr.items[i]; + ScrDyn *ev = entry->kind == SCR_DYN_TYPED_REF ? scr_dyn_typed_ref_materialize(entry) : NULL; + const ScrDyn *rec = ev != NULL ? ev : entry; + const ScrDyn *a = rec->kind == SCR_DYN_OBJ ? scr_dyn_obj_get(rec, "address", 7) : NULL; + if (a != NULL && a->kind == SCR_DYN_STR) scr_arr_push_ref(ips, scr_str_retain(a->v.str)); + scr_dyn_release(ev); + } + } + scr_dyn_release(view); + } + scr_net_lookup_answer(clo, failed, msg, ips); + scr_str_release(msg); + if (ips != NULL) scr_arr_release(ips); + return scr_dyn_retain(scr_dyn_undefined()); +} + +/* net.connect({ host, port, lookup }) with a lookup of Node's own type + * (`(hostname, options, callback)` whose callback takes a string or an + * address list), held as a checked-dynamic function: the runtime calls it + * as Node's autoSelectFamily dial does, with { hints, all: true }, and dials + * the answered addresses in order. NULL with the exception pending. */ +ScrNetSocket *scr_net_connect_lookup_dyn(double port, ScrStr *host /*borrowed*/, + const ScrDyn *lookup /*borrowed*/) { + if (!scr_net_host_chk(host)) return NULL; + ScrNetSocket *s = scr_net_sock_new(); + s->peer_port = (int)port; + snprintf(s->peer_ip, sizeof s->peer_ip, "%.*s", + (int)(host->len < 63 ? host->len : 63), host->data); + if (!scr_net_poller_init()) { + fputs("scriptc: event poller init failed\n", stderr); + abort(); + } + s->connecting = true; + s->lookup_wait = true; + scr_net_sock_register(s); + ScrClosure *ans = scr_closure_new((void *)&scr_net_lookup_dyn_thunk, 1); + ScrBox *box = scr_box_new_obj(&scr_net_sock_retain_v, &scr_net_sock_release_v, NULL); + scr_box_set_ref(box, scr_net_sock_retain(s)); + ans->caps[0] = box; + ScrDyn *callback = scr_dyn_new_func(ans, &scr_net_lookup_dyn_thunk, 3, "(err, address, family)", "callback"); + ScrDyn *options = scr_dyn_new_obj(); +#ifndef _WIN32 + scr_dyn_obj_set(options, "hints", 5, scr_dyn_new_num((double)AI_ADDRCONFIG)); +#endif + scr_dyn_obj_set(options, "all", 3, scr_dyn_new_bool(true)); + ScrDyn *name = scr_dyn_new_str(host); + ScrDyn *args[3] = { name, options, callback }; + scr_dyn_release(scr_dyn_call(lookup, args, 3, "lookup")); + scr_dyn_release(name); + scr_dyn_release(options); + scr_dyn_release(callback); + return s; +} + /* The answer closure's runtime half (its fn is the emitted per-shape * thunk; caps[0] boxes the socket). msg/ips are borrowed (nullable). */ void scr_net_lookup_answer(ScrClosure *self, bool has_err, ScrStr *msg /*borrowed, nullable*/, diff --git a/packages/runtime/src/scr_net.h b/packages/runtime/src/scr_net.h index d78ae48f0..33638f699 100644 --- a/packages/runtime/src/scr_net.h +++ b/packages/runtime/src/scr_net.h @@ -242,6 +242,7 @@ bool scr_net_sock_readable(ScrNetSocket *s); /* The deferred dial (the http agent's maxSockets queue): the socket * registers "connecting" and buffers writes; dial_start runs the dial. */ ScrNetSocket *scr_net_connect_deferred(double port, ScrStr *host /*borrowed, nullable*/); /* +1 */ +ScrNetSocket *scr_net_connect_lookup_dyn(double port, ScrStr *host /*borrowed*/, const ScrDyn *lookup /*borrowed*/); /* +1, NULL on throw */ /* A client socket for the Unix socket at `path`, dialed now or (deferred) * by scr_net_sock_dial_start. A failure is the deferred 'error'. */ ScrNetSocket *scr_net_connect_path(ScrStr *path /*borrowed*/, bool deferred); /* +1 */ diff --git a/packages/runtime/src/scr_publish.c b/packages/runtime/src/scr_publish.c index 9dedc6844..5bd4b198a 100644 --- a/packages/runtime/src/scr_publish.c +++ b/packages/runtime/src/scr_publish.c @@ -37,9 +37,14 @@ typedef struct { uintptr_t obj; uint32_t rc; - uint32_t buf; /* a candidate taken out of its buffer: its index + 1, else 0 */ + uint32_t buf; /* a candidate taken out of its buffer: its index + 1, else 0; + * SCR_PUB_LOG_COUNT: `rc` is the count of the low half */ } ScrPubLog; +/* In ScrPubLog.buf: the object keeps its class id in the high half of + * its rc word (SCR_PUB_CLASS_ID), and `rc` is the count of the low half. */ +#define SCR_PUB_LOG_COUNT UINT32_C(0x80000000) + #define SCR_PUB_DYN ((uintptr_t)1) #define SCR_PUB_RC_LIFETIME UINT32_C(0xfffffffe) #define SCR_PUB_RC_BIG UINT32_C(0xffffffff) @@ -103,7 +108,7 @@ static void *scr_pub_grow(void *p, size_t *cap, size_t size, void *inline_buf) { void scr_pub_str_fn(void *v, ScrPub *ctx); void scr_pub_push(ScrPub *ctx, void *v, ScrPubFn fn) { - if (!v || *(size_t *)v == SIZE_MAX || ctx->refusal) return; + if (!v || SCR_RC_COUNT(v) == SCR_RC_IMMORTAL || ctx->refusal) return; if (fn == scr_pub_str_fn) { /* a leaf: mark it now */ scr_pub_str_fn(v, ctx); return; @@ -120,7 +125,9 @@ void scr_pub_push(ScrPub *ctx, void *v, ScrPubFn fn) { * touch the objects again. */ bool scr_pub_mark(ScrPub *ctx, void *obj, bool cyc, int kind) { size_t *rc = obj; - if (*rc == SIZE_MAX || ctx->refusal) return false; + if (SCR_RC_COUNT(obj) == SCR_RC_IMMORTAL || ctx->refusal) return false; + bool class_id = (kind & SCR_PUB_CLASS_ID) != 0; + kind &= ~SCR_PUB_CLASS_ID; if (ctx->logged == ctx->log_cap) ctx->log = scr_pub_grow(ctx->log, &ctx->log_cap, sizeof *ctx->log, ctx->log_inline); #ifdef SCR_RC_AUDIT @@ -135,10 +142,11 @@ bool scr_pub_mark(ScrPub *ctx, void *obj, bool cyc, int kind) { buf = scr_cyc_pub_take(obj) + 1; ctx->taken++; } - size_t count = *rc; + size_t count = class_id ? SCR_RC_COUNT(obj) : *rc; uint32_t logged = (uint32_t)count; + if (class_id) buf |= SCR_PUB_LOG_COUNT; #if SIZE_MAX > UINT32_MAX - if (count >= SCR_PUB_RC_LIFETIME) { + if (!class_id && count >= SCR_PUB_RC_LIFETIME) { if (count == (size_t)1 << 63) logged = SCR_PUB_RC_LIFETIME; else { if (ctx->nbig == ctx->big_cap) ctx->big = scr_pub_grow(ctx->big, &ctx->big_cap, sizeof *ctx->big, NULL); @@ -148,7 +156,8 @@ bool scr_pub_mark(ScrPub *ctx, void *obj, bool cyc, int kind) { } #endif *e = (ScrPubLog){(uintptr_t)obj, logged, buf}; - *rc = SIZE_MAX; + if (class_id) SCR_RC_COUNT(obj) = SCR_RC_IMMORTAL; + else *rc = SIZE_MAX; return true; } @@ -196,8 +205,9 @@ void scr_pub_arr(void *v, ScrPub *ctx, ScrPubFn elem) { ScrPubFn fn = a->elem == SCR_ELEM_STR ? scr_pub_str_fn : a->elem == SCR_ELEM_REF ? elem : NULL; if (!fn) return; size_t dense = a->len < a->cap ? a->len : a->cap; + const uint8_t *states = scr_arr_states_end(a); for (size_t i = 0; i < dense; i++) - if (!a->present || a->present[i]) scr_pub_slot(ctx, a->data[i], fn); + if (!states || *scr_arr_state_in(states, i)) scr_pub_slot(ctx, a->data[i], fn); if (a->ext) for (size_t i = 0; i < a->ext->sparse_len; i++) if (a->ext->sparse[i].state) scr_pub_slot(ctx, a->ext->sparse[i].slot, fn); @@ -372,7 +382,7 @@ static bool scr_pub_keep_ok(size_t bytes) { } void scr_publish(void *root, ScrPubFn fn) { - if (!root || *(size_t *)root == SIZE_MAX) return; + if (!root || SCR_RC_COUNT(root) == SCR_RC_IMMORTAL) return; ScrPub ctx; if (scr_pub_keep.stack) { ctx.stack = scr_pub_keep.stack; @@ -417,13 +427,18 @@ void scr_publish(void *root, ScrPubFn fn) { for (size_t i = ctx.logged; i-- > 0;) { const ScrPubLog *e = &ctx.log[i]; void *obj = scr_pub_log_obj(e); - size_t rc = e->rc; + uint32_t buf = e->buf & ~SCR_PUB_LOG_COUNT; + if (e->buf & SCR_PUB_LOG_COUNT) { + SCR_RC_COUNT(obj) = (ScrRcCount)e->rc; + } else { + size_t rc = e->rc; #if SIZE_MAX > UINT32_MAX - if (e->rc == SCR_PUB_RC_LIFETIME) rc = (size_t)1 << 63; - else if (e->rc == SCR_PUB_RC_BIG) rc = ctx.big[--ctx.nbig]; + if (e->rc == SCR_PUB_RC_LIFETIME) rc = (size_t)1 << 63; + else if (e->rc == SCR_PUB_RC_BIG) rc = ctx.big[--ctx.nbig]; #endif - *(size_t *)obj = rc; - if (e->buf) scr_cyc_pub_restore(obj, e->buf - 1); + *(size_t *)obj = rc; + } + if (buf) scr_cyc_pub_restore(obj, buf - 1); } if (ctx.taken) scr_cyc_pub_fill(); scr_throw_error_msg(SCR_ERR_TYPE, ctx.refusal, strlen(ctx.refusal)); diff --git a/packages/runtime/src/scr_rc.h b/packages/runtime/src/scr_rc.h index 8611741da..10eb96d18 100644 --- a/packages/runtime/src/scr_rc.h +++ b/packages/runtime/src/scr_rc.h @@ -6,6 +6,41 @@ #include "scr_core.h" +/* ── reference counts ───────────────────────────────────────────────── + * Every heap object's first member is its `size_t rc` word. On 64-bit + * targets the reference count is the word's low 32 bits: an emitted + * instance of a class in a program-rooted hierarchy keeps its class id (its + * class's preorder number + 1) in the high 32 bits (llvm/classes.ts + * classIdRcWord), and no other object's count reaches 2^32. A count of SCR_RC_IMMORTAL marks + * an immortal object; for every object but those class instances the whole + * word is then SIZE_MAX. Code that reads or writes the count of an object of + * any kind goes through SCR_RC_COUNT. */ +#if SIZE_MAX > UINT32_MAX +typedef uint32_t ScrRcCount; +#ifdef __BYTE_ORDER__ +_Static_assert(__BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__, + "the reference count is the low half of the rc word, at offset 0"); +#endif +#else +typedef size_t ScrRcCount; +#endif +#define SCR_RC_IMMORTAL ((ScrRcCount)-1) +#define SCR_RC_COUNT(obj) (*(ScrRcCount *)(obj)) + +/* The class id in the high half of `obj`'s rc word, or 0 for an object + * without one: the high half of any other object's word is 0, or 0xffffffff + * (immortal) or 0x80000000 (program-lifetime), and a class id is never one of + * these. */ +static inline uint32_t scr_rc_class_id(const void *obj) { +#if SIZE_MAX > UINT32_MAX + uint32_t high = (uint32_t)(*(const size_t *)obj >> 32); + return high == UINT32_MAX || high == UINT32_C(0x80000000) ? 0 : high; +#else + (void)obj; + return 0; +#endif +} + /* ── cycle collection (scr_cycle.c) ─────────────────────────────────── * Reference counting alone cannot free cycles, so every object that can * participate in one — capture boxes, heap closures, unions, promises, and @@ -591,6 +626,10 @@ enum { SCR_PUB_KIND_OBJECT = 4, SCR_PUB_KIND_UNION = 5, SCR_PUB_KIND_DYN = 6, + /* Or'ed into a kind: an object whose rc word keeps its class id in + * the high half (SCR_RC_COUNT). The mark, and a refusal's restore, write + * only the count. */ + SCR_PUB_CLASS_ID = 0x100, }; void scr_publish(void *root, ScrPubFn fn); void scr_pub_push(ScrPub *ctx, void *v, ScrPubFn fn); diff --git a/packages/runtime/src/scr_regex.c b/packages/runtime/src/scr_regex.c index fa104058f..e4867ddf3 100644 --- a/packages/runtime/src/scr_regex.c +++ b/packages/runtime/src/scr_regex.c @@ -691,7 +691,8 @@ static ScrDyn *scr_regex_groups_of(const ScrArr *a, uint8_t *bc) { const char *name = groupnames; for (int k = 1; k < capture_count; k++) { size_t length = strlen(name); - bool matched = (size_t)k < a->len && (size_t)k < a->cap && a->present[k] == SCR_ARR_VALUE; + bool matched = (size_t)k < a->len && (size_t)k < a->cap && + *scr_arr_state_ptr(a, (size_t)k) == SCR_ARR_VALUE; /* ES2025 duplicates sit in distinct alternatives: at most one * participates, and it wins over an earlier undefined. */ if (length && (matched || !scr_dyn_obj_get(groups, name, length))) @@ -803,7 +804,7 @@ static uint64_t scr_regex_mask_bits(double mask) { * present undefined (SCR_RE_NO_SPAN when lazy), a lazy capture of an ASCII * subject is a span word, and every other capture a fresh string. */ static inline void scr_regex_fill_slot(const ScrRegexInput *input, uint64_t *data, - uint8_t *present, int k, uint64_t mask, uint64_t lazy, + uint8_t *states, int k, uint64_t mask, uint64_t lazy, bool spans) { const uint8_t *cs = input->capture[2 * k], *ce = input->capture[2 * k + 1]; bool deferred = k < 64 && (lazy >> k & 1); @@ -820,7 +821,7 @@ static inline void scr_regex_fill_slot(const ScrRegexInput *input, uint64_t *dat state = SCR_ARR_VALUE; } data[k] = word; - present[k] = state; + *scr_arr_state_in(states, (size_t)k) = state; } /* re.exec(s): a fresh string[] of [whole match, ...captures], or NULL @@ -851,29 +852,29 @@ static ScrArr *scr_regex_exec_core(ScrStr *s, ScrRegex *re, uint64_t mask, bool * they stay in registers across the slot stores. */ size_t n = (size_t)count + spans; uint64_t *data = out->data; - uint8_t *present = out->present; + uint8_t *states = scr_arr_states_end(out); if (count <= 64) { /* Captures neither selected nor lazy are present undefined with a * zero payload; only the selected and lazy ones need a decision. */ memset(data, 0, (size_t)count * sizeof *data); - memset(present, SCR_ARR_UNDEFINED, (size_t)count); + memset(states - count, SCR_ARR_UNDEFINED, (size_t)count); uint64_t visit = mask | lazy; if (count < 64) visit &= (UINT64_C(1) << count) - 1; while (visit) { int k = __builtin_ctzll(visit); visit &= visit - 1; - scr_regex_fill_slot(&input, data, present, k, mask, lazy, spans); + scr_regex_fill_slot(&input, data, states, k, mask, lazy, spans); } } else { for (int k = 0; k < count; k++) - scr_regex_fill_slot(&input, data, present, k, mask, lazy, spans); + scr_regex_fill_slot(&input, data, states, k, mask, lazy, spans); } if (spans) { data[count] = (uint64_t)(uintptr_t)scr_str_retain(s); - present[count] = SCR_ARR_VALUE; + *scr_arr_state_in(states, count) = SCR_ARR_VALUE; } size_t filled = 0; - while (filled < n && present[filled] == SCR_ARR_VALUE) filled++; + while (filled < n && *scr_arr_state_in(states, filled) == SCR_ARR_VALUE) filled++; out->filled = (uint32_t)filled; out->len = n; if (metadata) { @@ -904,14 +905,14 @@ static bool scr_regex_group_bytes(const ScrArr *m, double k, const char **p, siz /* Array lengths stay below 2^53, so signed conversions are exact. */ if (!(k >= 0 && k < (double)(int64_t)m->len)) return false; size_t i = (size_t)(int64_t)k; - if (m->present[i] == SCR_ARR_VALUE) { + if (*scr_arr_state_ptr(m, i) == SCR_ARR_VALUE) { const ScrStr *str = (const ScrStr *)(uintptr_t)m->data[i]; *p = str->data; *n = str->len; return true; } uint64_t span = m->data[i]; - if (m->present[i] != SCR_ARR_UNDEFINED || span == SCR_RE_NO_SPAN) return false; + if (*scr_arr_state_ptr(m, i) != SCR_ARR_UNDEFINED || span == SCR_RE_NO_SPAN) return false; const ScrStr *subject = (const ScrStr *)(uintptr_t)m->data[m->len - 1]; *p = subject->data + (span >> 32); *n = (size_t)((span & UINT32_MAX) - (span >> 32)); @@ -1045,21 +1046,22 @@ static ScrArr *scr_regex_match_all_core(ScrStr *s, ScrRegex *re, ScrArr *indices * directly, then the hole-free prefix is the run of captures from 0. */ ScrArr *row = scr_arr_new_inline(SCR_ELEM_STR, (size_t)capture_count); uint64_t *data = row->data; - uint8_t *present = row->present; + uint8_t *states = scr_arr_states_end_w(row, 8); for (int k = 0; k < capture_count; k++) { const uint8_t *cs = capture[2 * k], *ce = capture[2 * k + 1]; if (cs == NULL || ce == NULL) { data[k] = 0; - present[k] = SCR_ARR_UNDEFINED; + *scr_arr_state_in(states, (size_t)k) = SCR_ARR_UNDEFINED; } else { ScrStr *text = scr_regex_span(&input, scr_capture_index(&input, cs), scr_capture_index(&input, ce)); data[k] = (uint64_t)(uintptr_t)text; - present[k] = SCR_ARR_VALUE; + *scr_arr_state_in(states, (size_t)k) = SCR_ARR_VALUE; } } size_t filled = 0; - while (filled < (size_t)capture_count && present[filled] == SCR_ARR_VALUE) filled++; + while (filled < (size_t)capture_count && *scr_arr_state_in(states, filled) == SCR_ARR_VALUE) + filled++; row->filled = (uint32_t)filled; row->len = (size_t)capture_count; if (source) scr_regex_defer_metadata(row, source, start); diff --git a/packages/runtime/src/scr_region.c b/packages/runtime/src/scr_region.c index cc094ef0b..455b23dbb 100644 --- a/packages/runtime/src/scr_region.c +++ b/packages/runtime/src/scr_region.c @@ -666,11 +666,11 @@ static void scr_rgn_queue(ScrRgnWalk *w, size_t entry, void *p, int kind, void ( * region counted as explained. RC-audit builds still enter it, and stop if * the walk reaches it twice. */ static void scr_rgn_enter(ScrRgnWalk *w, void *p, int kind, void (*walk)(void *, ScrRgnWalk *)) { - if (!p || *(size_t *)p == SIZE_MAX) return; /* immortal: never freed, not walked */ + if (!p || SCR_RC_COUNT(p) == SCR_RC_IMMORTAL) return; /* immortal: never freed, not walked */ #ifdef SCR_RGN_DEBUG - bool single = w->src == 0 && *(size_t *)p == 1 && !w->debug; + bool single = w->src == 0 && SCR_RC_COUNT(p) == 1 && !w->debug; #else - bool single = w->src == 0 && *(size_t *)p == 1; + bool single = w->src == 0 && SCR_RC_COUNT(p) == 1; #endif #ifndef SCR_RC_AUDIT if (single) { @@ -805,8 +805,9 @@ static void scr_rgn_walk_arr(ScrRgnWalk *w, ScrArr *a) { ScrRgnSlotKind k = a->elem == SCR_ELEM_ARR ? (ScrRgnSlotKind){SCR_RGN_W_ARR, NULL} : scr_rgn_slot_kind(a->ops ? a->ops->release : NULL); size_t dense = a->len < a->cap ? a->len : a->cap; + const uint8_t *states = scr_arr_states_end(a); for (size_t i = 0; i < dense; i++) - if (!a->present || a->present[i] == SCR_ARR_VALUE) scr_rgn_slot_as(w, scr_rgn_slot_ptr(a->data[i]), k); + if (!states || *scr_arr_state_in(states, i) == SCR_ARR_VALUE) scr_rgn_slot_as(w, scr_rgn_slot_ptr(a->data[i]), k); if (!a->ext) return; for (size_t i = 0; i < a->ext->sparse_len; i++) if (a->ext->sparse[i].state == SCR_ARR_VALUE) scr_rgn_slot_as(w, scr_rgn_slot_ptr(a->ext->sparse[i].slot), k); @@ -922,7 +923,7 @@ static int64_t scr_rgn_settle(ScrRgnWalk *w, size_t *black_out) { size_t top = 0; for (size_t i = 0; i < n; i++) { ScrRgnEntry *x = &w->ents[i]; - if (*(size_t *)x->obj != (size_t)x->found) { + if (SCR_RC_COUNT(x->obj) != (ScrRcCount)x->found) { x->black = true; stack[top++] = i; } @@ -1041,7 +1042,7 @@ static void scr_rgn_report(const ScrRegion *r, const ScrRgnWalk *w, int64_t unex } for (size_t i = 0; i < w->nents; i++) { const ScrRgnEntry *x = &w->ents[i]; - if (*(size_t *)x->obj == (size_t)x->found) continue; /* not referenced from outside itself */ + if (SCR_RC_COUNT(x->obj) == (ScrRcCount)x->found) continue; /* not referenced from outside itself */ if (printed == lines) { more++; continue; @@ -1055,7 +1056,7 @@ static void scr_rgn_report(const ScrRegion *r, const ScrRgnWalk *w, int64_t unex } const ScrRgnFrom *f = &w->from[at]; fprintf(stderr, " held by %s.%s, with %zu references, %lld of them from what the region owns\n", - f->cls ? f->cls->name : "?", f->cls ? scr_rgn_field_name(f->cls, f->field) : "?", *(size_t *)x->obj, + f->cls ? f->cls->name : "?", f->cls ? scr_rgn_field_name(f->cls, f->field) : "?", (size_t)SCR_RC_COUNT(x->obj), (long long)x->found); } if (more) fprintf(stderr, " %zu more\n", more); @@ -1111,7 +1112,7 @@ static bool scr_rgn_try_free(ScrRgnThread *t, ScrRegion *r, bool tentative) { for (ScrRgnChunk *c = r->chunks; c; c = c->next) c->found = 0; size_t objects = 0; SCR_RGN_EACH(r, s, o) { - if (*(size_t *)o == SIZE_MAX) { + if (SCR_RC_COUNT(o) == SCR_RC_IMMORTAL) { /* Published: other threads may hold it, uncounted. */ r->state = SCR_RGN_PINNED; t->stats.pinned++; diff --git a/packages/runtime/src/scr_slots.c b/packages/runtime/src/scr_slots.c index 7c5bc7f88..84236c0d5 100644 --- a/packages/runtime/src/scr_slots.c +++ b/packages/runtime/src/scr_slots.c @@ -29,7 +29,7 @@ static uint32_t scr_slots_index(void *obj, uint32_t *word) { if (index != 0) return index; index = atomic_fetch_add_explicit(&scr_slots_next, 1, memory_order_relaxed); if (index == 0) scr_trap("scriptc: ThreadSlots ran out of slot indices\n"); - if (*(size_t *)obj != SIZE_MAX) { + if (SCR_RC_COUNT(obj) != SCR_RC_IMMORTAL) { __atomic_store_n(word, index, __ATOMIC_RELAXED); return index; } diff --git a/packages/runtime/src/scr_stream.c b/packages/runtime/src/scr_stream.c index 7c501f20c..7d7ec04ed 100644 --- a/packages/runtime/src/scr_stream.c +++ b/packages/runtime/src/scr_stream.c @@ -4740,12 +4740,14 @@ ScrStream *scr_stream_rl_from_dyn(const ScrDyn *value, bool output) { if (value->kind == SCR_DYN_TYPED_REF && value->v.typed_ref.type_key_len > 7 && !memcmp(value->v.typed_ref.type_key, "object:", 7)) { ScrStream *stream = scr_dyn_typed_ref_unbox(value); - const ScrVt *vt = stream->vt; - bool duplex = vt == &scr_duplex_vt || vt == &scr_transform_vt || vt == &scr_passthrough_vt || - scr_vt_within(vt, &scr_duplex_vt); - bool ok = duplex || (output ? vt == &scr_writable_vt || scr_vt_within(vt, &scr_writable_vt) - : vt == &scr_readable_vt || scr_vt_within(vt, &scr_readable_vt)); - if (ok) return stream; + const ScrVt *vt = scr_stream_vt_of(stream); + if (vt) { + bool duplex = vt == &scr_duplex_vt || vt == &scr_transform_vt || vt == &scr_passthrough_vt || + scr_vt_within(vt, &scr_duplex_vt); + bool ok = duplex || (output ? vt == &scr_writable_vt || scr_vt_within(vt, &scr_writable_vt) + : vt == &scr_readable_vt || scr_vt_within(vt, &scr_readable_vt)); + if (ok) return stream; + } value->v.typed_ref.release(stream); } static const char in_m[] = "createInterface with an input that is not a readable stream has no lowering"; @@ -6411,9 +6413,9 @@ static ScrStream *scr_stream_view_receiver(const ScrDyn *value) { if (value->kind == SCR_DYN_TYPED_REF && value->v.typed_ref.type_key_len > 7 && !memcmp(value->v.typed_ref.type_key, "object:", 7)) { ScrStream *stream = scr_dyn_typed_ref_unbox(value); - const ScrVt *vt = stream->vt; - if ((vt->pre >= scr_readable_vt.pre && vt->pre <= scr_readable_vt.post) || - (vt->pre >= scr_writable_vt.pre && vt->pre <= scr_writable_vt.post)) return stream; + const ScrVt *vt = scr_stream_vt_of(stream); + if (vt && ((vt->pre >= scr_readable_vt.pre && vt->pre <= scr_readable_vt.post) || + (vt->pre >= scr_writable_vt.pre && vt->pre <= scr_writable_vt.post))) return stream; value->v.typed_ref.release(stream); } scr_dyn_arg_type_fail("destination", "an instance of Stream", value); @@ -6716,9 +6718,9 @@ ScrStream *scr_stream_unbox_dyn(const ScrDyn *value, const ScrStr *message) { if (value->kind == SCR_DYN_TYPED_REF && value->v.typed_ref.type_key_len > 7 && !memcmp(value->v.typed_ref.type_key, "object:", 7)) { ScrStream *stream = scr_dyn_typed_ref_unbox(value); - const ScrVt *vt = stream->vt; - if ((vt->pre >= scr_readable_vt.pre && vt->pre <= scr_readable_vt.post) || - (vt->pre >= scr_writable_vt.pre && vt->pre <= scr_writable_vt.post)) return stream; + const ScrVt *vt = scr_stream_vt_of(stream); + if (vt && ((vt->pre >= scr_readable_vt.pre && vt->pre <= scr_readable_vt.post) || + (vt->pre >= scr_writable_vt.pre && vt->pre <= scr_writable_vt.post))) return stream; value->v.typed_ref.release(stream); } scr_throw_error_msg_code(SCR_ERR_ERROR, message->data, message->len, "SC2020"); diff --git a/packages/runtime/src/scr_stream.h b/packages/runtime/src/scr_stream.h index 78290d8c4..99f58331c 100644 --- a/packages/runtime/src/scr_stream.h +++ b/packages/runtime/src/scr_stream.h @@ -48,6 +48,13 @@ typedef struct ScrStream { ScrStreamState *st; } ScrStream; +/* The vtable of an object boxed as an "object:" typed ref, which may be any + * program class instance: NULL for one with a class id in its rc word + * (scr_rc.h), which has no vtable word and is no stream. */ +static inline const ScrVt *scr_stream_vt_of(const ScrStream *s) { + return scr_rc_class_id(s) == 0 ? s->vt : NULL; +} + extern SCR_TL ScrVt scr_readable_vt, scr_writable_vt, scr_duplex_vt, scr_transform_vt, scr_passthrough_vt; /* main() stamps pre/post */ diff --git a/packages/runtime/src/scr_stream_dyn.c b/packages/runtime/src/scr_stream_dyn.c index 0859c4e07..11dcf6ee2 100644 --- a/packages/runtime/src/scr_stream_dyn.c +++ b/packages/runtime/src/scr_stream_dyn.c @@ -23,7 +23,11 @@ ScrStream *scr_stream_dyn_native(const ScrDyn *value, double side) { memcmp(value->v.typed_ref.type_key, "object:", 7) != 0) return NULL; ScrStream *stream = scr_dyn_typed_ref_unbox(value); - const ScrVt *vt = stream->vt; + const ScrVt *vt = scr_stream_vt_of(stream); + if (vt == NULL) { + value->v.typed_ref.release(stream); + return NULL; + } bool duplex = scr_stream_dyn_within(vt, &scr_duplex_vt) || scr_stream_dyn_within(vt, &scr_transform_vt) || scr_stream_dyn_within(vt, &scr_passthrough_vt); diff --git a/packages/runtime/src/scr_string.c b/packages/runtime/src/scr_string.c index 77c020ee0..2b4752b34 100644 --- a/packages/runtime/src/scr_string.c +++ b/packages/runtime/src/scr_string.c @@ -1846,9 +1846,10 @@ static uint16_t *scr_str_units(const ScrStr *s, size_t *n) { * UTF-16 units to bytes through the cursor and checkpoints. Once its * reads would have paid for converting it (one read per 16 bytes), a * receiver up to SCR_SIDX_UNITS_MAX units keeps its code units in its - * entry and reads become loads. The array is freed with the entry, and an - * append drops it (scr_sidx_concat_append). */ -#define SCR_SIDX_UNITS_MAX ((size_t)1 << 20) + * entry and reads become loads. The bound admits source texts of several + * megabytes, which scanners read in full. The array is freed with the + * entry, and an append drops it (scr_sidx_concat_append). */ +#define SCR_SIDX_UNITS_MAX ((size_t)1 << 24) static bool scr_sidx_units(const ScrStr *s, ScrSidx *e) { if (!e->units) { if (++e->reads < 32 || e->reads * 16 < s->len || s->len > SCR_SIDX_UNITS_MAX) return false; diff --git a/packages/runtime/src/scr_string_methods.h b/packages/runtime/src/scr_string_methods.h index 6ed2e09df..1f25b8766 100644 --- a/packages/runtime/src/scr_string_methods.h +++ b/packages/runtime/src/scr_string_methods.h @@ -27,7 +27,8 @@ double scr_str_utf16_len(ScrStr *s); * holds (scr_string.c), published so emitted length and charCodeAt reads of * that string are a compare and a load. Cleared whenever its entry drops * the units (release, append, eviction); all fields zero otherwise. Emitted - * code reads it only where SCR_TL is empty. */ + * code reads it where SCR_TL is empty, and in worker executables on ELF + * targets, whose thread-locals live in the executable's static TLS block. */ typedef struct ScrStrScan { const ScrStr *s; const uint16_t *units; diff --git a/packages/runtime/test/test_array.c b/packages/runtime/test/test_array.c index 1f86be80e..a3ad4735b 100644 --- a/packages/runtime/test/test_array.c +++ b/packages/runtime/test/test_array.c @@ -1283,7 +1283,7 @@ static void test_exact_construction(void) { static bool filled_ok(const ScrArr *a) { if (a->filled > a->len || a->filled > a->cap) return false; for (size_t i = 0; i < a->filled; i++) - if (a->present[i] != SCR_ARR_VALUE) return false; + if (*scr_arr_state_ptr(a, i) != SCR_ARR_VALUE) return false; return true; } @@ -1358,7 +1358,7 @@ static void test_filled_prefix(void) { scr_arr_release(scr_arr_sort_primitive(b, false)); check(filled_ok(b), "sorting moves holes past the bound"); for (size_t i = 0; i < b->len; i++) { - if (i < b->cap && b->present[i] != SCR_ARR_VALUE) break; + if (i < b->cap && *scr_arr_state_ptr(b, i) != SCR_ARR_VALUE) break; check(i < b->filled, "sorted values form the bound"); } while (scr_arr_len(b) > 0) { @@ -1378,11 +1378,11 @@ static void test_filled_prefix(void) { } /* Boolean arrays store one byte per dense element, followed by the states in - * the same allocation. Growth moves the states past the larger value area, + * the same allocation. Growth moves the states to the end of the larger area, * densifying absorbs sparse entries into bytes, and every moving and copying * operation keeps values, holes and present undefined in place. */ static bool bool_layout_ok(const ScrArr *a) { - return a->cap == 0 || a->present == (const uint8_t *)a->data + a->cap; + return a->cap == 0 || scr_arr_states_end(a) == (const uint8_t *)a->data + 2 * (size_t)a->cap; } static bool bool_states(const ScrArr *a, const char *want) { @@ -1495,7 +1495,8 @@ static void test_inline_slots(void) { check(!inline_slots(plain), "plain constructions keep separate storage"); scr_arr_release(plain); ScrArr *a = scr_arr_new_small(SCR_ELEM_F64); - check(inline_slots(a) && a->cap >= 4 && a->len == 0 && a->present == (uint8_t *)(a->data + a->cap), + check(inline_slots(a) && a->cap >= 4 && a->len == 0 && + scr_arr_states_end(a) == (uint8_t *)(a->data + a->cap) + a->cap, "small construction places slots and states in the array block"); size_t inline_cap = a->cap; for (size_t i = 0; i < inline_cap; i++) scr_arr_push_f64(a, (double)i); @@ -1503,12 +1504,13 @@ static void test_inline_slots(void) { scr_arr_push_f64(a, 100); check(!inline_slots(a) && a->cap > inline_cap && scr_arr_get_f64(a, 0) == 0 && scr_arr_get_f64(a, (double)inline_cap - 1) == (double)inline_cap - 1 && - scr_arr_get_f64(a, (double)inline_cap) == 100 && a->present == (uint8_t *)(a->data + a->cap), + scr_arr_get_f64(a, (double)inline_cap) == 100 && + scr_arr_states_end(a) == (uint8_t *)(a->data + a->cap) + a->cap, "growth moves inline slots to their own allocation"); scr_arr_release(a); ScrArr *flags = scr_arr_new_small(SCR_ELEM_BOOL); - check(inline_slots(flags) && flags->present == (uint8_t *)flags->data + flags->cap, + check(inline_slots(flags) && scr_arr_states_end(flags) == (uint8_t *)flags->data + 2 * (size_t)flags->cap, "boolean inline slots are one byte each"); scr_arr_push_bool(flags, true); scr_arr_set_bool(flags, 3, true); @@ -1887,6 +1889,10 @@ static void test_dense_slots(void) { /* shift and splice(0, n) advance dense storage into head room instead of * moving the tail; unshift reuses that room, growth gives it back, and a * drained array starts over at the front of its allocation. */ +/* SCR_ARR_HEAD_ROOM (scr_array.c): the array has vacated slots in front of + * `data`, and the slot just before `data` holds their count. */ +static bool head_room(const ScrArr *a) { return (a->spare & 2u) != 0; } + static void test_head_room(void) { #ifdef SCR_RC_AUDIT long strings0 = scr_str_live_count(); @@ -1899,7 +1905,8 @@ static void test_head_room(void) { check_f64(scr_arr_shift_f64(q), 0, "shift answers the first element"); check(q->data == block + 1 && q->cap == cap - 1 && q->len == 63, "shift advances the storage instead of moving the tail"); - check(q->present == (uint8_t *)(block + cap) + 1, "states keep following the whole value area"); + check(scr_arr_states_end(q) == (uint8_t *)(block + cap) + cap - 1 && head_room(q) && q->data[-1] == 1, + "the remaining states stay at the end of the allocation"); check(q->filled == 63, "shift keeps the hole-free prefix"); for (int i = 1; i < 40; i++) scr_arr_shift_f64(q); check(q->data == block + 40 && q->len == 24 && scr_arr_get_f64(q, 0) == 40 && @@ -1974,7 +1981,7 @@ static void test_head_room(void) { bool ordered = rev->len == 1000; for (int i = 0; i < 1000 && ordered; i++) ordered = scr_arr_get_f64(rev, i) == 999 - i; check(ordered, "unshift run keeps reverse order"); - size_t room = (size_t)(rev->present - (uint8_t *)(rev->data + rev->cap)); + size_t room = head_room(rev) ? (size_t)rev->data[-1] : 0; check(room >= 4 && rev->cap >= rev->len, "an unshift run reserves head room"); uint64_t *rblock = rev->data; scr_arr_unshift_f64(rev, -1); @@ -1986,7 +1993,7 @@ static void test_head_room(void) { check(rev->data == rblock - 4 && scr_arr_get_f64(rev, 3) == -1, "unshift many order"); /* Storage swaps (typed views) first drop the head room. */ scr_arr_outline(rev); - check(rev->present == (uint8_t *)(rev->data + rev->cap) && scr_arr_get_f64(rev, 0) == 7 && + check(!head_room(rev) && scr_arr_get_f64(rev, 0) == 7 && scr_arr_get_f64(rev, 1003) == 0, "outline moves the elements to the front of their allocation"); scr_arr_release(rev); @@ -1996,7 +2003,7 @@ static void test_head_room(void) { for (int i = 0; i < 40; i++) scr_arr_push_f64(once, i); size_t once_cap = once->cap; scr_arr_unshift_f64(once, -1); - check(once->cap == once_cap && once->present == (uint8_t *)(once->data + once->cap), + check(once->cap == once_cap && !head_room(once), "a one-off unshift keeps the allocation"); scr_arr_release(once); diff --git a/packages/runtime/test/test_regex.c b/packages/runtime/test/test_regex.c index ae98d627b..6084c5102 100644 --- a/packages/runtime/test/test_regex.c +++ b/packages/runtime/test/test_regex.c @@ -331,7 +331,7 @@ static void test_exec_groups(void) { ScrArr *full = scr_regex_exec(s, &re_opt_group); ScrArr *lean = scr_regex_exec_groups(s, &re_opt_group, 2.0); check(full && lean && full->len == lean->len && lean->len == 2, "exec_groups keeps the capture count"); - check(lean->present[0] == SCR_ARR_UNDEFINED && lean->present[1] == SCR_ARR_UNDEFINED, + check(*scr_arr_state_ptr(lean, 0) == SCR_ARR_UNDEFINED && *scr_arr_state_ptr(lean, 1) == SCR_ARR_UNDEFINED, "exec_groups: unselected and nonparticipating captures are undefined"); check(lean->ext == NULL || lean->ext->metadata == NULL, "exec_groups: no index/input properties"); scr_arr_release(full); @@ -430,7 +430,7 @@ static void test_exec_spans(void) { * subjects, materialized on wide ones. */ ScrStr *ascii = S("xa12"); ScrArr *m = scr_regex_exec_spans(ascii, &re_pair, 6.0, 6.0); - check(m && m->len == 4 && m->present[1] == SCR_ARR_UNDEFINED, + check(m && m->len == 4 && *scr_arr_state_ptr(m, 1) == SCR_ARR_UNDEFINED, "exec_spans: ASCII subjects keep lazy captures as spans"); ScrStr *a = S("a"); ScrStr *twelve = S("12"); @@ -448,7 +448,7 @@ static void test_exec_spans(void) { scr_arr_release(m); ScrStr *wide = S("\xC3\xA9" "a7"); m = scr_regex_exec_spans(wide, &re_pair, 6.0, 6.0); - check(m && m->len == 3 && m->present[2] == SCR_ARR_VALUE && scr_regex_group_number(m, 2) == 7, + check(m && m->len == 3 && *scr_arr_state_ptr(m, 2) == SCR_ARR_VALUE && scr_regex_group_number(m, 2) == 7, "exec_spans: wide subjects materialize lazy captures"); scr_arr_release(m); m = scr_regex_exec_spans(ascii, &re_pair, 38.0, 2.0); diff --git a/scripts/bench-runtime.mjs b/scripts/bench-runtime.mjs index 4e4f2c245..61134fa4c 100644 --- a/scripts/bench-runtime.mjs +++ b/scripts/bench-runtime.mjs @@ -1,6 +1,6 @@ /* Runtime performance A/B harness for compiled programs. * - * Builds every workload in benchmarks/runtime/workloads.json with one or two + * Builds every workload in benchmarks/runtime/workloads/ with one or two * scriptc checkouts, verifies that each executable's output is identical to * Node's (stdout, stderr, exit status), then times interleaved launches and reports * per-workload medians with bootstrap confidence intervals for the @@ -23,6 +23,7 @@ import { tmpdir } from "node:os"; import { dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { parseArgs } from "node:util"; +import { readWorkloads, workloadCategory } from "../benchmarks/runtime/workloads.mjs"; const repoRoot = fileURLToPath(new URL("..", import.meta.url)); const suiteRoot = join(repoRoot, "benchmarks/runtime"); @@ -62,7 +63,7 @@ evenly over those layouts, so verdicts cover code-placement luck instead of one placement. Use it for small codegen changes (e.g. --layouts=4 --runs=16). -Workload names are listed in benchmarks/runtime/workloads.json; for example +Workload names are the file names in benchmarks/runtime/workloads/*.json; for example --workloads=cli-config,http-api runs only the startup and server workloads. --baseline-optimization/--candidate-optimization pick each contender's @@ -88,13 +89,13 @@ assert.ok( "--layouts needs macOS or Linux", ); -const manifest = JSON.parse(readFileSync(join(suiteRoot, "workloads.json"), "utf8")); +const allWorkloads = readWorkloads(suiteRoot); const selected = values.workloads ? new Set(values.workloads.split(",")) : null; -const workloads = manifest.workloads.filter((w) => selected === null || selected.has(w.name)); +const workloads = allWorkloads.filter((w) => selected === null || selected.has(w.name)); if (selected) { for (const name of selected) assert.ok( - manifest.workloads.some((w) => w.name === name), + allWorkloads.some((w) => w.name === name), `unknown workload ${name}`, ); } @@ -488,8 +489,6 @@ const report = { /* Workload kinds: "process" (default) times whole launches; `launches` > 1 * makes each sample the median of that many sequential launches (startup-bound * CLIs). "server" times the measured phase of a load-client session. */ -const categoryOf = (w) => - w.kind === "server" ? "server" : (w.launches ?? 1) > 1 ? "startup" : "cpu"; for (const w of workloads) { assert.ok((w.kind ?? "process") === "process" || w.kind === "server", `${w.name}: unknown kind`); if (w.kind === "server") assert.ok(w.client?.script, `${w.name}: server workloads need a client`); @@ -569,7 +568,7 @@ try { const entry = { name: w.name, args, results: {} }; if (w.kind === "server") entry.kind = "server"; if ((w.launches ?? 1) > 1) entry.launches = w.launches; - entry.category = categoryOf(w); + entry.category = workloadCategory(w); const oracleFailure = server ? serverFailure(oracle) : oracle.timedOut ? "timed out" : ""; if (oracle.status !== 0 || oracleFailure) { entry.error = `node oracle failed (status ${oracle.status}${oracleFailure ? `, ${oracleFailure}` : ""}): ${oracle.stderr.slice(0, 2000)}`; diff --git a/scripts/native-cli-assets.mts b/scripts/native-cli-assets.mts index b3d1436bd..bdecc1da2 100644 --- a/scripts/native-cli-assets.mts +++ b/scripts/native-cli-assets.mts @@ -74,6 +74,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/scripts/test-ts7.mjs b/scripts/test-ts7.mjs index e022c1ff5..5ee1f50fc 100644 --- a/scripts/test-ts7.mjs +++ b/scripts/test-ts7.mjs @@ -30,6 +30,7 @@ const ALLOWED_TYPESCRIPT5_IMPORTS = new Set([ "packages/compiler/src/library/semantic-source.ts", "packages/compiler/test/ts7/harness.ts", "packages/compiler/test/ts7/resolver-parity.test.ts", + "packages/compiler/test/undici-shim-types.test.ts", ]); function sourceFilesUnder(dir) { diff --git a/scripts/zero-cost-gate.mjs b/scripts/zero-cost-gate.mjs index de3c0d0fc..1575fc5ee 100644 --- a/scripts/zero-cost-gate.mjs +++ b/scripts/zero-cost-gate.mjs @@ -27,6 +27,7 @@ import { availableParallelism } from "node:os"; import { dirname, join, relative, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { parseArgs } from "node:util"; +import { readWorkloads } from "../benchmarks/runtime/workloads.mjs"; import { cachegrindCounts, compareArtifact, @@ -173,9 +174,8 @@ function goodCodeSet() { const programs = []; if (sets.has("bench")) { const suite = join(programsRoot, "benchmarks/runtime"); - const manifest = JSON.parse(readFileSync(join(suite, "workloads.json"), "utf8")); const seen = new Set(); - for (const w of manifest.workloads) { + for (const w of readWorkloads(suite)) { const entry = join(suite, w.entry); if (seen.has(entry)) continue; seen.add(entry); 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/6187-js-match-or-empty-array.js b/tests/corpus/6187-js-match-or-empty-array.js index 2cce0e17d..5343f6827 100644 --- a/tests/corpus/6187-js-match-or-empty-array.js +++ b/tests/corpus/6187-js-match-or-empty-array.js @@ -1,8 +1,7 @@ -// @deferred-fences: 1 // `s.match(re) || []` types the empty array as an empty tuple that the // typescript-go API panics to describe (moment.js's configFromStringAndFormat). -// The panic stays with the statements whose lowering asks about that -// type, so the rest of the program compiles and runs. +// The union reads through its widened members, so the whole program +// compiles, including the statements whose lowering asks about that type. /** @param {string} s */ function firstDigit(s) { @@ -18,3 +17,4 @@ function digitCount(s) { const parsers = { firstDigit, digitCount }; console.log(typeof parsers.firstDigit, parsers.digitCount('a1b22')); +console.log(firstDigit('a1b22'), firstDigit('none')); 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/6412-any-valued-dictionaries.ts b/tests/corpus/6412-any-valued-dictionaries.ts new file mode 100644 index 000000000..0cbdc6aad --- /dev/null +++ b/tests/corpus/6412-any-valued-dictionaries.ts @@ -0,0 +1,50 @@ +// Message types whose members are typed with `any`: string-keyed +// dictionaries of `any` values (`{ [key: string]: any }`, a `Dictionary` alias used bare), and `any[]` lists, also optional, nested in +// arrays of records. Values keep whatever the program or JSON stored. +type Dictionary = { [key: string]: T }; + +interface Build { + use: string; + src?: string; + config?: Dictionary; +} +interface Order { + id: string; + builds?: Build[]; + upload: { url: string; fields: { [key: string]: any } }; + tags: any[]; + extra?: any[]; +} + +function tags(): Dictionary { + return { a: 1, b: "x", c: [1, 2] }; +} +async function meta(): Promise { + return { n: 3 }; +} + +async function main(): Promise { + const order: Order = JSON.parse( + '{"id":"o1","builds":[{"use":"node","config":{"zero":true,"n":2}},{"use":"static","src":"a/**"}],' + + '"upload":{"url":"u","fields":{"k":"v","n":2}},"tags":[1,"two",null]}', + ); + console.log(order.id, order.upload.url, String(order.upload.fields.k), order.upload.fields["n"] + 1); + for (const build of order.builds ?? []) { + console.log(build.use, build.src ?? "-", JSON.stringify(build.config ?? null)); + } + console.log(order.tags.length, order.extra === undefined, JSON.stringify(order.tags)); + + const t = tags(); + for (const k of Object.keys(t)) console.log(k, JSON.stringify(t[k])); + t.d = { deep: true }; + console.log(JSON.stringify(t)); + const m = await meta(); + console.log(m.n * 2, JSON.stringify(m)); + + const made: Order = { id: "o2", upload: { url: "w", fields: {} }, tags: [], extra: [0] }; + made.upload.fields.signature = "s"; + made.tags.push({ k: 1 }); + console.log(JSON.stringify(made)); +} +void main(); diff --git a/tests/corpus/6413-sdk-declared-data-class/main.ts b/tests/corpus/6413-sdk-declared-data-class/main.ts new file mode 100644 index 000000000..32509da57 --- /dev/null +++ b/tests/corpus/6413-sdk-declared-data-class/main.ts @@ -0,0 +1,27 @@ +// A types-only SDK package declares the shape of JSON data as a class with +// fields only (`declare class UploadForm { url: string; fields: ... }`, +// with a merged namespace holding the fields interface). The program never +// constructs it: values arrive through JSON.parse or object literals, so +// they compile as records of the declared fields. +import type { UploadForm } from "upload-sdk"; + +interface Order { + id: string; + upload?: UploadForm; + forms: UploadForm[]; +} + +function describe(form: UploadForm | undefined): string { + if (!form) return "none"; + return `${form.url} ${form.fields.Policy} ${form.fields["X-Signature"]} ${form.fields["key"] ?? "-"} ${Object.keys(form.fields).join(",")}`; +} + +const order = JSON.parse( + '{"id":"o1","upload":{"url":"u","fields":{"Policy":"p","X-Signature":"s","key":"k"}},"forms":[]}', +) as Order; +console.log(order.id, describe(order.upload)); +const made: UploadForm = { url: "v", fields: { Policy: "q", "X-Signature": "t" } }; +order.forms.push(made); +made.fields.extra = "e"; +console.log(describe(made), JSON.stringify(order.forms)); +console.log(describe(undefined)); diff --git a/tests/corpus/6413-sdk-declared-data-class/node_modules/upload-sdk/index.d.ts b/tests/corpus/6413-sdk-declared-data-class/node_modules/upload-sdk/index.d.ts new file mode 100644 index 000000000..5c72897ab --- /dev/null +++ b/tests/corpus/6413-sdk-declared-data-class/node_modules/upload-sdk/index.d.ts @@ -0,0 +1,13 @@ +export declare class UploadForm { + /** The URL the form posts to. */ + url: string; + /** The hidden form fields. */ + fields: UploadForm.Fields; +} +export declare namespace UploadForm { + interface Fields { + Policy: string; + "X-Signature": string; + [key: string]: string; + } +} diff --git a/tests/corpus/6413-sdk-declared-data-class/node_modules/upload-sdk/package.json b/tests/corpus/6413-sdk-declared-data-class/node_modules/upload-sdk/package.json new file mode 100644 index 000000000..093d34b91 --- /dev/null +++ b/tests/corpus/6413-sdk-declared-data-class/node_modules/upload-sdk/package.json @@ -0,0 +1 @@ +{ "name": "upload-sdk", "version": "1.0.0", "types": "index.d.ts" } diff --git a/tests/corpus/6414-pick-class-method-views.ts b/tests/corpus/6414-pick-class-method-views.ts new file mode 100644 index 000000000..6bbbbf780 --- /dev/null +++ b/tests/corpus/6414-pick-class-method-views.ts @@ -0,0 +1,83 @@ +// `Pick` and `Omit` over a class, used as structural parameter +// and field types: a view of the instance's own members. Calls through the +// view run the instance's methods on the instance itself, so its state is +// shared with every other reference. +class ApiClient { + private readonly base: string; + calls = 0; + constructor(base: string) { + this.base = base; + } + request(path: string): Promise { + this.calls++; + return Promise.resolve(this.base + path); + } + close(): void { + console.log("closed after", this.calls); + } +} + +interface Context { + client: Pick; + name: string; +} + +async function run(ctx: Context): Promise { + console.log(ctx.name, await ctx.client.request("/x"), await ctx.client.request("/y")); +} +function finish(client: Pick): void { + client.close(); +} +function count(client: Omit): number { + return client.calls; +} + +const client = new ApiClient("https://h"); +void run({ client, name: "ctx" }).then(() => { + finish(client); + console.log(count(client), client.calls); +}); + +// A live view that flows into another structural type keeps the instance: +// reads through the second view see the instance's later writes. +interface Counter { + calls: number; +} +function counted(view: Pick): Counter { + void view.request("/z"); + return view; +} +const second = new ApiClient("https://k"); +const counter = counted(second); +second.calls += 10; +const holder: { counter: Counter } = { counter: counted(second) }; +second.calls = 100; +console.log("counter", counter.calls, holder.counter.calls, second.calls); + +// A view also accepts other classes that have its members. Their instances +// flow through it the same way. +class Meter { + label = "meter"; + reads = 0; + read(): number { + return this.reads; + } +} +class Gauge { + reads = 0; + read(): number { + return this.reads; + } +} +interface Reading { + reads: number; +} +function reading(view: Pick): Reading { + console.log("read", view.read()); + return view; +} +const gauge = new Gauge(); +const result = reading(gauge); +gauge.reads = 50; +const meter = new Meter(); +console.log("reading", result.reads, reading(meter).reads, meter.label); diff --git a/tests/corpus/6419-untyped-async-package/main.ts b/tests/corpus/6419-untyped-async-package/main.ts new file mode 100644 index 000000000..9ec10f7c5 --- /dev/null +++ b/tests/corpus/6419-untyped-async-package/main.ts @@ -0,0 +1,38 @@ +// Untyped async methods of a package, reached through the package's own +// untyped callers and prototype reflection, and overridden by a typed +// subclass. An async generator method called from the package answers its +// generator object. +import { Pool, Schema, limiter, listIssues, prototypeNames, validateAll } from "token-pool"; + +class CountingPool extends Pool { + acquired = 0; + async acquire(): Promise { + await super.acquire(); + this.acquired++; + } +} + +async function main(): Promise { + const pool = new CountingPool(1); + await pool.acquire(); + const waiting = pool.acquire(); + console.log("waiting", pool.nrWaiting()); + pool.release("t"); + await waiting; + console.log("acquired", pool.acquired, pool.nrWaiting()); + const base = new Pool(1); + console.log("token", await base.acquire()); + const run = limiter(1); + console.log(await Promise.all([run(async () => 1), run(async () => 2)])); + const schema = new Schema((value) => typeof value === "number"); + console.log(await validateAll(schema, [1, "x", 3])); + console.log(await listIssues(schema, ["p", 5, "q"])); + console.log(await schema.parseAsync(4)); + try { + await schema.parseAsync("y"); + } catch (e) { + console.log("rejected", e instanceof Error ? e.message : String(e)); + } + console.log(prototypeNames()); +} +main(); diff --git a/tests/corpus/6419-untyped-async-package/node_modules/token-pool/index.d.ts b/tests/corpus/6419-untyped-async-package/node_modules/token-pool/index.d.ts new file mode 100644 index 000000000..ed0c82579 --- /dev/null +++ b/tests/corpus/6419-untyped-async-package/node_modules/token-pool/index.d.ts @@ -0,0 +1,17 @@ +export declare class Pool { + constructor(size: number); + tryAcquire(): any; + acquire(): Promise; + release(token?: any): void; + nrWaiting(): number; +} +export declare function limiter(size: number): (work: () => Promise) => Promise; +export declare class Schema { + constructor(check: (value: unknown) => boolean); + safeParseAsync(data: unknown): Promise; + parseAsync(data: unknown): Promise; + issues(values: unknown[]): AsyncGenerator; +} +export declare function validateAll(schema: Schema, values: unknown[]): Promise; +export declare function listIssues(schema: Schema, values: unknown[]): Promise; +export declare function prototypeNames(): string; diff --git a/tests/corpus/6419-untyped-async-package/node_modules/token-pool/index.js b/tests/corpus/6419-untyped-async-package/node_modules/token-pool/index.js new file mode 100644 index 000000000..04935ff29 --- /dev/null +++ b/tests/corpus/6419-untyped-async-package/node_modules/token-pool/index.js @@ -0,0 +1,123 @@ +"use strict"; +// An untyped token pool and schema in the shape of a semaphore and a +// validation library: async methods whose results come from storage that +// passes through untyped helpers. +Object.defineProperty(exports, "__esModule", { value: true }); +function moveItems(src, srcIndex, dst, dstIndex, len) { + for (let j = 0; j < len; ++j) { + dst[j + dstIndex] = src[j + srcIndex]; + src[j + srcIndex] = void 0; + } +} +class Queue { + constructor() { + this._length = 0; + this.arr = []; + } + push(item) { + if (this._length > 64) moveItems(this.arr, 1, this.arr, 0, this._length - 1); + this.arr[this._length] = item; + this._length = this._length + 1; + } + pop() { + const length = this._length; + if (length === 0) return void 0; + const ret = this.arr[length - 1]; + this.arr[length - 1] = void 0; + this._length = length - 1; + return ret; + } + shift() { + const length = this._length; + if (length === 0) return void 0; + const ret = this.arr[0]; + for (let i = 1; i < length; i++) this.arr[i - 1] = this.arr[i]; + this.arr[length - 1] = void 0; + this._length = length - 1; + return ret; + } + get length() { + return this._length; + } +} +class Pool { + constructor(size) { + this.free = new Queue(); + this.waiting = new Queue(); + for (let i = 0; i < size; i++) this.free.push("token" + i); + } + tryAcquire() { + return this.free.pop(); + } + async acquire() { + const token = this.tryAcquire(); + if (token !== void 0) return token; + return new Promise((resolve) => { + this.waiting.push(resolve); + }); + } + release(token) { + const next = this.waiting.shift(); + if (next) next(token); + else this.free.push(token); + } + nrWaiting() { + return this.waiting.length; + } +} +exports.Pool = Pool; +function limiter(size) { + const pool = new Pool(size); + return async function limited(work) { + const token = await pool.acquire(); + try { + return await work(); + } finally { + pool.release(token); + } + }; +} +exports.limiter = limiter; +class Schema { + constructor(check) { + this.check = check; + this.results = new Queue(); + } + async safeParseAsync(data) { + await null; + this.results.push(this.check(data) ? { success: true, data } : { success: false, issue: "invalid " + data }); + return this.results.pop(); + } + async *issues(values) { + for (const value of values) { + const result = await this.safeParseAsync(value); + if (!result.success) yield result.issue; + } + } + async parseAsync(data) { + const result = await this.safeParseAsync(data); + if (result.success) return result.data; + throw new Error(result.issue); + } +} +exports.Schema = Schema; +async function validateAll(schema, values) { + const out = []; + for (const value of values) { + const result = await schema.safeParseAsync(value); + out.push(result.success ? "ok " + result.data : result.issue); + } + return out.join(", "); +} +exports.validateAll = validateAll; +async function listIssues(schema, values) { + const out = []; + const it = schema.issues(values); + for (let step = await it.next(); !step.done; step = await it.next()) out.push(step.value); + return out.join(", "); +} +exports.listIssues = listIssues; +function prototypeNames() { + return typeof Schema.prototype.parseAsync + " " + typeof Schema.prototype.safeParseAsync; +} +exports.prototypeNames = prototypeNames; diff --git a/tests/corpus/6419-untyped-async-package/node_modules/token-pool/package.json b/tests/corpus/6419-untyped-async-package/node_modules/token-pool/package.json new file mode 100644 index 000000000..994de07b2 --- /dev/null +++ b/tests/corpus/6419-untyped-async-package/node_modules/token-pool/package.json @@ -0,0 +1 @@ +{ "name": "token-pool", "version": "1.0.0", "main": "index.js", "types": "index.d.ts" } diff --git a/tests/corpus/6440-cycle-class-late-use/log.ts b/tests/corpus/6440-cycle-class-late-use/log.ts new file mode 100644 index 000000000..6e3e0e052 --- /dev/null +++ b/tests/corpus/6440-cycle-class-late-use/log.ts @@ -0,0 +1,17 @@ +import { prefix } from "./stats.ts"; + +export class Sink { + private readonly tag: string; + constructor(tag: string) { + this.tag = tag; + } + write(line: string): void { + console.log(`${prefix()}[${this.tag}] ${line}`); + } +} + +function makeLogger(): { make: (tag: string) => Sink } { + return { make: (tag: string) => new Sink(tag) }; +} + +export const logger = makeLogger(); diff --git a/tests/corpus/6440-cycle-class-late-use/main.ts b/tests/corpus/6440-cycle-class-late-use/main.ts new file mode 100644 index 000000000..e16d3f1f0 --- /dev/null +++ b/tests/corpus/6440-cycle-class-late-use/main.ts @@ -0,0 +1,7 @@ +// Two modules that import each other, where one declares a class the other +// reaches only after both finished initializing: Node runs it normally. +import { logger } from "./log.ts"; +import { report } from "./stats.ts"; + +logger.make("main").write("hello"); +report(); diff --git a/tests/corpus/6440-cycle-class-late-use/stats.ts b/tests/corpus/6440-cycle-class-late-use/stats.ts new file mode 100644 index 000000000..d2a496d83 --- /dev/null +++ b/tests/corpus/6440-cycle-class-late-use/stats.ts @@ -0,0 +1,10 @@ +import { logger, Sink } from "./log.ts"; + +export function prefix(): string { + return "stats "; +} + +export function report(): void { + const sink = logger.make("report"); + sink.write(String(sink instanceof Sink)); +} diff --git a/tests/corpus/6441-cycle-class-early-access/log.ts b/tests/corpus/6441-cycle-class-early-access/log.ts new file mode 100644 index 000000000..072a8ccca --- /dev/null +++ b/tests/corpus/6441-cycle-class-early-access/log.ts @@ -0,0 +1,23 @@ +import { probe } from "./stats.ts"; + +class Base { + static count = 1; + static hello(): string { + return "hello"; + } +} + +export class Sink extends Base { + readonly tag: string; + constructor(tag: string) { + super(); + this.tag = tag; + } + static describe(): string { + return "sink"; + } +} + +export const limit = 3; + +export const probed = probe(); diff --git a/tests/corpus/6441-cycle-class-early-access/main.ts b/tests/corpus/6441-cycle-class-early-access/main.ts new file mode 100644 index 000000000..5a87865cc --- /dev/null +++ b/tests/corpus/6441-cycle-class-early-access/main.ts @@ -0,0 +1,25 @@ +// A class of an import cycle reached before its module body ran: Node +// throws the temporal-dead-zone ReferenceError at the access, and the +// same class works once its module finished. +import { Sink, probed } from "./log.ts"; +import { + early, + earlyMethod, + earlyInherited, + earlyInheritedField, + earlyName, + earlyRenamedName, + earlyRenamed, + earlyCap, +} from "./stats.ts"; + +console.log("early:", early); +console.log("early method:", earlyMethod); +console.log("early inherited:", earlyInherited); +console.log("early inherited field:", earlyInheritedField); +console.log("early name:", earlyName); +console.log("early renamed name:", earlyRenamedName); +console.log("early renamed:", earlyRenamed); +console.log("early renamed constant:", earlyCap); +console.log("probed:", probed); +console.log("late:", new Sink("late").tag, Sink.describe(), Sink.hello(), Sink.count, Sink.name); diff --git a/tests/corpus/6441-cycle-class-early-access/stats.ts b/tests/corpus/6441-cycle-class-early-access/stats.ts new file mode 100644 index 000000000..f41fa8c17 --- /dev/null +++ b/tests/corpus/6441-cycle-class-early-access/stats.ts @@ -0,0 +1,29 @@ +import { Sink, Sink as Renamed, limit as cap } from "./log.ts"; + +function attempt(run: () => string): string { + try { + return run(); + } catch (error) { + return `${(error as Error).name}: ${(error as Error).message}`; + } +} + +// Each early access throws Node's ReferenceError, naming the binding the +// access spells: construction, a static method read as a value, static +// members inherited from a base class, the class's name, the class and a +// constant through renamed imports. +export const early = attempt(() => new Sink("early").tag); +export const earlyMethod = attempt(() => { + const describe = Sink.describe; + return typeof describe; +}); +export const earlyInherited = attempt(() => Sink.hello()); +export const earlyInheritedField = attempt(() => String(Renamed.count)); +export const earlyName = attempt(() => Sink.name); +export const earlyRenamedName = attempt(() => Renamed.name); +export const earlyRenamed = attempt(() => new Renamed("renamed").tag); +export const earlyCap = attempt(() => String(cap)); + +export function probe(): string { + return `${new Sink("probe").tag} ${Sink.describe()} ${Sink.hello()} ${Sink.count} ${Sink.name} ${new Renamed("probe").tag} ${cap}`; +} diff --git a/tests/corpus/6442-cycle-class-factory/dogs.ts b/tests/corpus/6442-cycle-class-factory/dogs.ts new file mode 100644 index 000000000..9fcb38181 --- /dev/null +++ b/tests/corpus/6442-cycle-class-factory/dogs.ts @@ -0,0 +1,9 @@ +import { Pet } from "./pets.ts"; + +export function describe(name: string): string { + return `pet ${name}`; +} + +export function makeDog(name: string): Pet { + return new Pet(name); +} diff --git a/tests/corpus/6442-cycle-class-factory/main.ts b/tests/corpus/6442-cycle-class-factory/main.ts new file mode 100644 index 000000000..f86d8b3eb --- /dev/null +++ b/tests/corpus/6442-cycle-class-factory/main.ts @@ -0,0 +1,7 @@ +// An import cycle whose class is constructed in the other module only from +// functions that run after both modules settled. +import { makeDog } from "./dogs.ts"; +import { Pet } from "./pets.ts"; + +const ace = makeDog("ace"); +console.log(makeDog("rex").speak(), ace instanceof Pet); diff --git a/tests/corpus/6442-cycle-class-factory/pets.ts b/tests/corpus/6442-cycle-class-factory/pets.ts new file mode 100644 index 000000000..671486e00 --- /dev/null +++ b/tests/corpus/6442-cycle-class-factory/pets.ts @@ -0,0 +1,11 @@ +import { describe } from "./dogs.ts"; + +export class Pet { + readonly name: string; + constructor(name: string) { + this.name = name; + } + speak(): string { + return describe(this.name); + } +} diff --git a/tests/corpus/6452-dns-lookup-runtime-options.ts b/tests/corpus/6452-dns-lookup-runtime-options.ts new file mode 100644 index 000000000..85f8e84de --- /dev/null +++ b/tests/corpus/6452-dns-lookup-runtime-options.ts @@ -0,0 +1,65 @@ +// A custom lookup function over Node's lookup-option union passes its +// options to dns.lookup through a cast, so the callback's address is a +// string or, with `all: true`, an address list, whatever the overload the +// checker picked says. Array.isArray tests what arrived. The same function +// serves as net.connect's lookup, which Node calls with `all: true`. +import * as dns from "node:dns"; +import * as net from "node:net"; + +type LookupCallback = ( + err: NodeJS.ErrnoException | null, + address: string | dns.LookupAddress[], + family?: number, +) => void; + +function createSafeLookup(): ( + hostname: string, + opts: dns.LookupOneOptions | dns.LookupAllOptions | number | undefined, + callback: LookupCallback, +) => void { + return function safeLookup(hostname, opts, callback) { + dns.lookup(hostname, opts as dns.LookupOneOptions, (err, address, family) => { + if (err) { + callback(err, address as string, family); + return; + } + if (Array.isArray(address)) { + const v4: dns.LookupAddress[] = []; + for (const entry of address) { + if (entry.address === "10.0.0.1") { + callback(new Error("blocked"), "", 0); + return; + } + if (entry.family === 4) v4.push(entry); + } + callback(null, v4); + return; + } + callback(null, address as string, family); + }); + }; +} + +const lookup = createSafeLookup(); +const show = (address: string | dns.LookupAddress[]): string => + typeof address === "string" ? address : JSON.stringify(address); + +lookup("localhost", { family: 4 }, (err, address, family) => { + console.log("one", err, show(address), family); + lookup("localhost", { family: 4, all: false }, (err2, address2, family2) => { + console.log("all false", err2, show(address2), family2); + lookup("localhost", { all: true }, (err3, addresses) => { + console.log("all true", err3, show(addresses)); + lookup("127.0.0.1", 4, (err4, address4, family4) => { + console.log("family", err4, show(address4), family4); + const server = net.createServer((socket) => socket.end("hello")); + server.listen(0, "127.0.0.1", () => { + const port = (server.address() as net.AddressInfo).port; + const socket = net.connect({ host: "localhost", port, lookup: createSafeLookup() }); + socket.on("data", (chunk: Buffer) => console.log("net", chunk.toString())); + socket.on("close", () => server.close()); + }); + }); + }); + }); +}); diff --git a/tests/corpus/6453-dns-lookup-runtime-callbacks.ts b/tests/corpus/6453-dns-lookup-runtime-callbacks.ts new file mode 100644 index 000000000..43678ec38 --- /dev/null +++ b/tests/corpus/6453-dns-lookup-runtime-callbacks.ts @@ -0,0 +1,39 @@ +// dns.lookup callbacks whose options are known only at run time bind their +// parameters as checked-dynamic values. They keep the forms their +// declarations spell: an async callback returns its promise, a defaulted +// parameter takes its default when Node passes fewer arguments (no family +// with `all: true`), and a rest parameter packs the remaining arguments. +import * as dns from "node:dns"; + +type Options = dns.LookupOneOptions | dns.LookupAllOptions | number | undefined; + +function lookupAsync(hostname: string, opts: Options): void { + dns.lookup(hostname, opts as dns.LookupOneOptions, async (err, address) => { + await Promise.resolve(); + console.log("async", err, Array.isArray(address) ? `${address.length} addresses` : address); + }); +} + +function lookupDefault(hostname: string, opts: Options): void { + dns.lookup(hostname, opts as dns.LookupOneOptions, (err, address, family = -1) => { + console.log("default", err, Array.isArray(address) ? `${address.length} addresses` : address, family); + }); +} + +function lookupRest(hostname: string, opts: Options): void { + dns.lookup(hostname, opts as dns.LookupOneOptions, (err, ...rest) => { + console.log("rest", err, rest.length, JSON.stringify(rest)); + }); +} + +function lookupAsyncFunction(hostname: string, opts: Options): void { + dns.lookup(hostname, opts as dns.LookupOneOptions, async function (err, address, family = 0) { + console.log("async function", err, address, family); + return family; + }); +} + +lookupAsync("127.0.0.1", { all: true }); +lookupDefault("127.0.0.1", { all: true }); +lookupRest("127.0.0.1", { family: 4 }); +lookupAsyncFunction("127.0.0.1", 4); diff --git a/tests/corpus/6476-in-literal-key-index-records.ts b/tests/corpus/6476-in-literal-key-index-records.ts new file mode 100644 index 000000000..5866c3ed1 --- /dev/null +++ b/tests/corpus/6476-in-literal-key-index-records.ts @@ -0,0 +1,18 @@ +// `"k" in record` with a literal key over an index-signature record (a +// match's named groups, a dictionary) answers the record's own keys. +function source(header: string): string | undefined { + const result = /^x-meta-source: (?dpl_.*)/m.exec(header); + if (result?.groups && "id" in result.groups) { + return result.groups.id; + } + return undefined; +} +console.log(source("x-meta-source: dpl_123\nother: 1"), source("nothing")); +const both = /(?\w+)=(?\w*)/.exec("mode=fast"); +if (both?.groups) { + console.log("key" in both.groups, "value" in both.groups, "other" in both.groups); +} +const dict: Record = { a: 1 }; +dict["late"] = 2; +delete dict["a"]; +console.log("a" in dict, "late" in dict, "missing" in dict, "toString" in dict); diff --git a/tests/corpus/6477-fresh-array-spread-insertion.ts b/tests/corpus/6477-fresh-array-spread-insertion.ts new file mode 100644 index 000000000..4ce4f87d9 --- /dev/null +++ b/tests/corpus/6477-fresh-array-spread-insertion.ts @@ -0,0 +1,19 @@ +// Spreading a fresh array literal whose elements are typed by their own +// literal (an omitted member reads `undefined`) into an array of the +// declared element type converts the elements; nothing else can reach +// them. +type Result = { message: string; file: { path: string } | undefined; noisy: boolean }; +const results: Result[] = [{ message: "first", file: { path: "/a" }, noisy: false }]; +const spam = results.length > 0 ? [{ message: "spam", file: undefined, noisy: true }] : []; +const more: Result[] = [{ message: "m", file: undefined, noisy: false }]; +results.unshift(...spam, ...more); +const tail = results.length > 5 ? [] : [{ message: "tail", file: undefined, noisy: false }]; +results.push(...tail); +console.log(results.map((r) => `${r.message}:${r.file?.path ?? "-"}:${r.noisy}`).join(" ")); +console.log(JSON.stringify(results[1])); +// A const declared in the loop body is a new array on every iteration. +for (let i = 0; i < 2; i++) { + const step = [{ message: `step${i}`, file: undefined, noisy: false }]; + results.push(...step); +} +console.log(results.map((r) => r.message).join(" "), results[results.length - 1] === results[results.length - 2]); diff --git a/tests/corpus/6478-any-annotated-dictionary.ts b/tests/corpus/6478-any-annotated-dictionary.ts new file mode 100644 index 000000000..6a66f0662 --- /dev/null +++ b/tests/corpus/6478-any-annotated-dictionary.ts @@ -0,0 +1,24 @@ +// A binding annotated with a type the checker reads as `any`, initialized +// with an empty object literal, is a dictionary the code fills by key. +class Ref { + digest: string; + constructor(digest: string) { + this.digest = digest; + } +} +class Blob { + data: string; + constructor(data: string) { + this.data = data; + } +} +function collect(entries: { name: string; sha?: string }[]): unknown { + const files: any = {}; + for (const entry of entries) { + files[entry.name] = entry.sha ? new Ref(`sha:${entry.sha}`) : new Blob(""); + } + files["count"] = entries.length; + return files; +} +const out = collect([{ name: "a.txt", sha: "abc" }, { name: "dir" }]) as Record; +console.log(Object.keys(out), out["a.txt"] instanceof Ref, out["dir"] instanceof Blob, out["count"]); diff --git a/tests/corpus/6481-set-add-chaining.ts b/tests/corpus/6481-set-add-chaining.ts new file mode 100644 index 000000000..a77039510 --- /dev/null +++ b/tests/corpus/6481-set-add-chaining.ts @@ -0,0 +1,56 @@ +// Set.prototype.add returns the Set, so its result can be chained, +// compared, stored and returned. Results nobody reads keep the plain +// statement form. +class Item { + name: string; + constructor(name: string) { + this.name = name; + } +} + +const seen = new Set(); +seen.add("a").add("b").add("a"); +console.log(seen.size, [...seen].join(",")); +console.log(seen.add("c") === seen, seen.has("c")); + +const nodes = new Set(); +const first = new Item("first"); +const memo = new Item("memo"); +nodes.add(first).add(memo); +console.log(nodes.size, nodes.has(first), nodes.has(memo)); + +function collect(values: number[]): Set { + const out = new Set(); + for (const v of values) out.add(v); + return out.add(-1); +} +console.log([...collect([3, 1, 3, 2])].join(" ")); + +const stored = seen.add("d"); +console.log(stored === seen, stored.size); + +let calls = 0; +function target(): Set { + calls++; + return seen; +} +target().add("e").add("f"); +console.log(calls, seen.size); + +const flag = seen.size > 3; +const picked = flag ? seen.add("g") : new Set(); +console.log(picked === seen, seen.has("g")); + +[1, 2, 2, 5].forEach((n) => nodes.add(new Item(`n${n}`))); +console.log(nodes.size); + +// An arrow whose expression body is the add() returns the Set. +const tags = new Set(); +const tag = (t: string) => tags.add(t); +console.log(tag("x").size, tag("y") === tags); +const typedTag = (t: string): Set => tags.add(t); +console.log(typedTag("z").has("z")); +const results = ["p", "q"].map((t) => tags.add(t)); +console.log(results.length, results[1] === tags, tags.size); +["r", "s"].forEach((t) => tags.add(t)); +console.log([...tags].join(",")); diff --git a/tests/corpus/6492-runtime-key-destructuring.ts b/tests/corpus/6492-runtime-key-destructuring.ts new file mode 100644 index 000000000..4ce9cb928 --- /dev/null +++ b/tests/corpus/6492-runtime-key-destructuring.ts @@ -0,0 +1,75 @@ +// Object destructuring with a runtime computed key (`const { [name]: v } = +// source`): an index-signature record reads its entry (undefined when +// absent, where a default applies), and a key whose type is a union of +// literal field names reads the field it names. Keys evaluate once, in +// element order. +const weights: Record = { a: 1, b: 2 }; +function pick(k: string): string { + const { [k]: w } = weights; + return `${k}=${w}`; +} +console.log(pick("a"), pick("b"), pick("z")); + +function withDefault(k: string): number { + const { [k]: w = -1 } = weights; + return w; +} +console.log(withDefault("a"), withDefault("missing")); + +type Side = "left" | "right"; +const pair = { left: 10, right: 20, label: "p" }; +function side(s: Side): number { + const { [s]: v, label } = pair; + return v + label.length; +} +console.log(side("left"), side("right")); + +let trace = ""; +function key(k: string): string { + trace += `key:${k};`; + return k; +} +const { [key("a")]: first, [key("b")]: second, [key("c")]: third = 0 } = weights; +console.log(first, second, third, trace); + +// Parameters and nested patterns take the same path. +const labels: { [code: string]: string } = { ok: "fine", err: "broken" }; +function describe(code: string, { [code]: text }: { [code: string]: string }): string { + return `${code}:${text ?? "unknown"}`; +} +console.log(describe("ok", labels), describe("err", labels), describe("wat", labels)); +const config = { limits: { cpu: 2, memory: 512 } as Record }; +for (const resource of ["cpu", "memory", "disk"]) { + const { + limits: { [resource]: limit = 0 }, + } = config; + console.log(resource, limit); +} + +// A module-scope binding from a missing index-signature entry holds +// undefined. +const missingKey = process.argv.length > 99 ? "a" : "missing"; +const { [missingKey]: missingWeight } = weights; +console.log(missingWeight, missingWeight === undefined); + +// The same in a nested module-scope pattern, with and without a default +// for the nested source. +const nestedConfig = { limits: { cpu: 2 } as Record }; +const { + limits: { [missingKey]: nestedLimit }, +} = nestedConfig; +const { + limits: { [missingKey]: defaultedLimit } = {}, +} = nestedConfig; +console.log(nestedLimit, defaultedLimit === undefined); + +// Parameter patterns return a missing entry as undefined. +function pickWeight(name: string, { [name]: weight }: Record) { + return weight; +} +const pickArrow = (name: string, { [name]: weight }: Record) => weight; +function pickNested(name: string, { limits: { [name]: limit } }: { limits: Record }) { + return limit; +} +console.log(pickWeight("a", weights), pickWeight(missingKey, weights)); +console.log(pickArrow(missingKey, weights) === undefined, pickNested(missingKey, nestedConfig)); diff --git a/tests/corpus/6493-runtime-key-destructuring.cjs b/tests/corpus/6493-runtime-key-destructuring.cjs new file mode 100644 index 000000000..484e94c48 --- /dev/null +++ b/tests/corpus/6493-runtime-key-destructuring.cjs @@ -0,0 +1,60 @@ +"use strict"; +// Runtime computed keys in object patterns over JavaScript values: the +// live property is read, undefined when absent, including nested patterns +// over a typed module object (`const { signals: { [name]: n } } = os.constants`). +const os = require("os"); +const table = [ + { name: "SIGINT", number: 2 }, + { name: "SIGTERM", number: 15 }, + { name: "SIGNOPE", number: 99 }, +]; +function resolve({ name, number: fallback }) { + const { + signals: { [name]: constant }, + } = os.constants; + const supported = constant !== undefined; + return `${name}:${supported}:${supported ? constant : fallback}`; +} +console.log(table.map(resolve).join(" ")); + +function lookup(source, field) { + const { [field]: value = "none", [`${field}Count`]: count } = source; + return `${field}=${value}/${count}`; +} +const record = { color: "red", colorCount: 3, size: 4 }; +console.log(lookup(record, "color"), lookup(record, "size"), lookup(record, "shape")); + +// A nullish source throws before a runtime first key is evaluated. +let keyCalls = 0; +function keyOf(name) { + keyCalls++; + return name; +} +const nothing = () => JSON.parse("null"); +const flag = process.argv.length > 99; +for (const attempt of [ + () => { + const { [keyOf("a")]: v } = nothing(); + return v; + }, + () => { + const { [keyOf("a")]: v = 1 } = flag ? nothing() : undefined; + return v; + }, + () => { + const { a: { [keyOf("b")]: v } } = JSON.parse('{"a":null}'); + return v; + }, +]) { + try { + attempt(); + } catch (e) { + console.log(e instanceof TypeError, keyCalls); + } +} +try { + const { [keyOf("a")]: v } = nothing(); + console.log(v); +} catch (e) { + console.log(e.message, keyCalls); +} diff --git a/tests/corpus/6494-default-export-class-members/lib/date.js b/tests/corpus/6494-default-export-class-members/lib/date.js new file mode 100644 index 000000000..0c4a426f7 --- /dev/null +++ b/tests/corpus/6494-default-export-class-members/lib/date.js @@ -0,0 +1,8 @@ +// A class whose base scriptc cannot extend yet: the default export object +// below narrows this member away, and only reads of it would fence. +export class StampDate extends Date { + constructor(text) { + super(text); + this.label = "stamp:" + text; + } +} diff --git a/tests/corpus/6494-default-export-class-members/lib/error.js b/tests/corpus/6494-default-export-class-members/lib/error.js new file mode 100644 index 000000000..7f2ac5744 --- /dev/null +++ b/tests/corpus/6494-default-export-class-members/lib/error.js @@ -0,0 +1,7 @@ +export class ParseError extends Error { + constructor(message, line) { + super(message); + this.name = "ParseError"; + this.line = line; + } +} diff --git a/tests/corpus/6494-default-export-class-members/lib/index.js b/tests/corpus/6494-default-export-class-members/lib/index.js new file mode 100644 index 000000000..66935fac6 --- /dev/null +++ b/tests/corpus/6494-default-export-class-members/lib/index.js @@ -0,0 +1,14 @@ +import { StampDate } from "./date.js"; +import { ParseError } from "./error.js"; +function parse(text) { + if (text === "") throw new ParseError("empty document", 1); + const [key, value] = text.split("=").map((part) => part.trim()); + return { key, value }; +} +function stringify(value) { + return JSON.stringify(value); +} +// A default export object of functions and class values (the shorthand +// spelling of `{ ParseError: ParseError }`). +export default { parse, stringify, StampDate, ParseError }; +export { parse, stringify, StampDate, ParseError }; diff --git a/tests/corpus/6494-default-export-class-members/main.ts b/tests/corpus/6494-default-export-class-members/main.ts new file mode 100644 index 000000000..1548e6415 --- /dev/null +++ b/tests/corpus/6494-default-export-class-members/main.ts @@ -0,0 +1,15 @@ +// A JavaScript module's default export object holding functions and class +// values builds at module initialization, and its members work through +// both the default object and the named exports. +import lib, { parse, ParseError } from "./lib/index.js"; + +console.log(JSON.stringify(parse("a = 1"))); +console.log(lib.stringify(lib.parse("b = 2"))); +try { + lib.parse(""); +} catch (e) { + if (e instanceof ParseError) console.log(e.name, e.message, e.line); +} +const Ctor = lib.ParseError; +const made = new Ctor("built through the default object", 7); +console.log(made instanceof ParseError, made.message, made.line, Ctor === ParseError); diff --git a/tests/corpus/6494-default-export-class-members/package.json b/tests/corpus/6494-default-export-class-members/package.json new file mode 100644 index 000000000..ca00e63c9 --- /dev/null +++ b/tests/corpus/6494-default-export-class-members/package.json @@ -0,0 +1,5 @@ +{ + "name": "default-export-class-members-corpus", + "private": true, + "type": "module" +} diff --git a/tests/corpus/6495-generic-method-overloads.ts b/tests/corpus/6495-generic-method-overloads.ts new file mode 100644 index 000000000..34bcfa712 --- /dev/null +++ b/tests/corpus/6495-generic-method-overloads.ts @@ -0,0 +1,54 @@ +// Calls that resolve to OVERLOAD signatures of a generic method or +// function: the implementation monomorphizes under what the overload +// proves for its type parameters (a parameter the overload never mentions +// is unknown), directly and through a `Pick` view of the class, whose +// live receiver keeps the instance's state. `T | Promise` callbacks +// bind T from either arm. +interface Reply { + code: number; + text: string; +} +type Mapper = (reply: Reply) => T | Promise; + +class Store { + loads = 0; + async load(key: string, map: Mapper): Promise; + async load(key: string): Promise; + async load(key: string, map?: Mapper): Promise { + this.loads++; + const reply: Reply = { code: 200, text: key }; + if (map === undefined) return reply; + return await map(reply); + } +} + +interface Services { + store: Pick; +} + +function pick(items: T[], index: number): T; +function pick(items: T[]): T; +function pick(items: T[], index = 0): T { + return items[index]!; +} + +async function settle(reply: Reply, map?: Mapper): Promise { + return map ? await map(reply) : reply; +} + +async function main(): Promise { + const store = new Store(); + const plain = await store.load("alpha"); + console.log(plain.code, plain.text); + const length = await store.load("beta", (reply) => reply.text.length); + console.log(length * 2); + const services: Services = { store }; + const viaView = await services.store.load("gamma"); + const upper = await services.store.load("delta", async (reply) => reply.text.toUpperCase()); + console.log(viaView.text, upper, store.loads); + console.log(pick(["x", "y"]), pick([1, 2, 3], 2)); + console.log(JSON.stringify(await settle({ code: 1, text: "s" }))); + console.log(await settle({ code: 2, text: "tt" }, (reply) => reply.code + reply.text.length)); +} + +main(); diff --git a/tests/diagnostics/overload-fences.ts b/tests/corpus/6496-overload-signatures.ts similarity index 66% rename from tests/diagnostics/overload-fences.ts rename to tests/corpus/6496-overload-signatures.ts index 0cf1f7e15..5902d8c19 100644 --- a/tests/diagnostics/overload-fences.ts +++ b/tests/corpus/6496-overload-signatures.ts @@ -1,7 +1,7 @@ -// The overload fences that REMAIN now that overload signatures lower: -// generic overload calls (the per-instantiation body would be typed -// against a signature it was never checked under), and the multi- -// signature type surfacing where a binding needs it spelled. +// Overload signatures: a generic overload call monomorphizes the +// implementation under the bindings the overload proves (a parameter the +// overload omits is optional in the implementation), and an immutable +// alias of an overloaded declaration projects onto the implementation. function twice(x: T): T[]; function twice(x: T, n: number): T[]; function twice(x: T, n?: number): T[] { diff --git a/tests/corpus/6497-generic-overload-bindings.ts b/tests/corpus/6497-generic-overload-bindings.ts new file mode 100644 index 000000000..0d33acaae --- /dev/null +++ b/tests/corpus/6497-generic-overload-bindings.ts @@ -0,0 +1,47 @@ +// Bindings of generic overload calls: explicit type arguments name the +// overload's type parameters, which can be declared in another order than +// the implementation's, and keyof-constrained parameters bound to +// different literals through an overload read their own fields. +function tag(value: A, label: B): string; +function tag(value: A, label?: B): string { + return `${typeof value}:${typeof label}:${String(value)}${String(label)}`; +} +console.log(tag(5, "x")); +console.log(tag("y", true)); + +class Labels { + prefix = "#"; + render(value: A, label: B): string; + render(value: A, label?: B): string { + return `${this.prefix}${typeof value}/${typeof label}`; + } +} +console.log(new Labels().render("z", 7)); + +interface Row { + a: number; + b: number; + name: string; +} +function read(row: T, key: K): T[K]; +function read(row: T, key: K, fallback: T[K]): T[K]; +function read(row: T, key: K, fallback?: T[K]): T[K] { + const value = row[key]; + return value === undefined ? fallback! : value; +} +const row: Row = { a: 1, b: 2, name: "r" }; +console.log(read(row, "a"), read(row, "b"), read(row, "name")); +console.log(read(row, "b", 0) + read(row, "a", 0)); + +// An overload call whose implementation returns an indexed read that +// misses returns undefined, as the implementation's own calls do. +function at(items: T[], index: number): T; +function at(items: T[], index: number, label: string): T; +function at(items: T[], index: number, label?: string): T { + if (label !== undefined) console.log(label); + return items[index]; +} +console.log(at([1, 2, 3], 1), at([1, 2, 3], 5)); +console.log(at(["a", "b"], 0, "strings"), at(["a", "b"], 9, "strings")); +const missing = at([{ id: 1 }], 3); +console.log(missing === undefined, typeof missing); diff --git a/tests/corpus/6520-match-or-empty-widened.ts b/tests/corpus/6520-match-or-empty-widened.ts new file mode 100644 index 000000000..ffb6d49ab --- /dev/null +++ b/tests/corpus/6520-match-or-empty-widened.ts @@ -0,0 +1,15 @@ +// `s.match(re) ?? []` and `s.match(re) || []`: typescript-go types the +// empty literal by the tuple-like match result and cannot serialize the +// union's members (an upstream API panic); the widened union describes +// the same values, so the binding is an ordinary string array and the +// statement compiles instead of reporting the checker crash. +function scripts(text: string): string[] { + const matches = text.match(/(['"]?)([\S^'"]+\.js)\1/g) ?? []; + return matches.map((match) => match.replace(/^['"]|['"]$/g, "")).slice(0, 5); +} +console.log(scripts(`RUN node a.js 'b.js' "c.js"`), scripts("no scripts here")); + +const words = "alpha beta".match(/\w+/g) || []; +console.log(words.length, words.join("+")); +const none = "".match(/x/) || []; +console.log(none.length); diff --git a/tests/corpus/6521-tuple-spread-into-array.ts b/tests/corpus/6521-tuple-spread-into-array.ts new file mode 100644 index 000000000..64112f7ba --- /dev/null +++ b/tests/corpus/6521-tuple-spread-into-array.ts @@ -0,0 +1,12 @@ +// Spreading a fixed tuple into an array literal copies its positions in +// order into the literal's element type. +const SCRIPT_FILES: [string, string, string, string, string] = ["a.ts", "a.mts", "a.cts", "a.js", "a.mjs"]; +const DATA_FILES = ["a.json"]; +const files = [...SCRIPT_FILES, ...DATA_FILES]; +console.log(files.length, files.join(",")); +const PAIR = ["x", "y"] as const; +const more: string[] = [...PAIR, "z", ...SCRIPT_FILES.slice(0, 1)]; +console.log(more); +const numbers: [number, number, number] = [1, 2, 3]; +const widened: (number | string)[] = [...numbers, "end"]; +console.log(widened); diff --git a/tests/corpus/6522-stats-arrays.ts b/tests/corpus/6522-stats-arrays.ts new file mode 100644 index 000000000..61818df3d --- /dev/null +++ b/tests/corpus/6522-stats-arrays.ts @@ -0,0 +1,31 @@ +// fs.Stats values in arrays: Promise.all over stat() promises answers a +// Stats[] whose elements reduce, map and index like any array. +import { mkdirSync, rmSync, writeFileSync } from "node:fs"; +import { readdir, stat } from "node:fs/promises"; +import * as path from "node:path"; +import { tmpdir } from "node:os"; + +async function packBytes(dir: string): Promise { + try { + const entries = await readdir(dir, { withFileTypes: true }); + const packs = entries.filter((entry) => entry.isFile() && entry.name.endsWith(".pack")); + const stats = await Promise.all(packs.map((entry) => stat(path.join(dir, entry.name)))); + return stats.reduce((total, s) => total + s.size, 0); + } catch { + return undefined; + } +} + +async function main(): Promise { + const dir = path.join(tmpdir(), `scriptc-6522-${process.pid}`); + mkdirSync(dir, { recursive: true }); + writeFileSync(path.join(dir, "a.pack"), "12345"); + writeFileSync(path.join(dir, "b.pack"), "123"); + writeFileSync(path.join(dir, "c.idx"), "1"); + console.log(await packBytes(dir), await packBytes(path.join(dir, "missing"))); + const all = await Promise.all(["a.pack", "c.idx"].map((name) => stat(path.join(dir, name)))); + console.log(all.length, all.map((s) => s.isFile()).join(","), all[1]!.size); + rmSync(dir, { recursive: true, force: true }); +} + +main(); diff --git a/tests/corpus/6523-then-primitive-constructors.ts b/tests/corpus/6523-then-primitive-constructors.ts new file mode 100644 index 000000000..da0df6f19 --- /dev/null +++ b/tests/corpus/6523-then-primitive-constructors.ts @@ -0,0 +1,19 @@ +// The ambient String, Number and Boolean as `.then` handlers over a +// number or boolean settled value convert it, as calling them would. +async function flag(value: boolean): Promise { + return value; +} + +async function main(): Promise { + const a = await flag(true).then(String); + const b = await flag(false) + .then(String) + .catch(() => "unknown"); + const n = await flag(true).then(Number); + const s = await Promise.resolve(1.5).then(String); + const t = await Promise.resolve(0).then(Boolean); + const u = await Promise.resolve(-0).then(String); + console.log(a, b, n + 1, s, t, u); +} + +main(); diff --git a/tests/diagnostics/checker-panic-tuple.ts b/tests/corpus/6524-match-or-empty-tuple.ts similarity index 57% rename from tests/diagnostics/checker-panic-tuple.ts rename to tests/corpus/6524-match-or-empty-tuple.ts index cf59bc1f0..6ab2906aa 100644 --- a/tests/diagnostics/checker-panic-tuple.ts +++ b/tests/corpus/6524-match-or-empty-tuple.ts @@ -1,6 +1,6 @@ // `''.match(/x/) || []` drives tsgo into the TupleType interface-conversion // panic (upstream signature 01/13) whenever the union's members are -// serialized: the union stays unmappable, so the statement reports its own -// refusals — a failed compile, never a crashed CLI. +// serialized; the widened union's members are readable, so the binding is +// the match array (the empty tuple is one of its values) and compiles. const a = ''.match(/x/) || []; console.log(a.length); diff --git a/tests/corpus/6525-error-subclass-fields.ts b/tests/corpus/6525-error-subclass-fields.ts new file mode 100644 index 000000000..06164baf8 --- /dev/null +++ b/tests/corpus/6525-error-subclass-fields.ts @@ -0,0 +1,55 @@ +// Inspecting an Error-typed value whose runtime class declares fields +// prints the stackless error base, with Node's constructor-name header +// (`[Bailable [Error]: m]`), followed by the own properties — on one line +// or broken across lines by the same width rule as any object, and as +// `[Name]` beyond the depth budget. A `cause` follows the fields: `cause:` +// when assigned, `[cause]:` from the constructor's options, and a cause +// prints its own cause. (The stack is cleared: compiled errors carry none.) +import * as util from "node:util"; +class Bailable extends Error { + bail = false; +} +class Detailed extends Bailable { + attempts: number[] = []; + label = "x"; +} +function clean(e: E): E { + e.stack = ""; + return e; +} +function show(error: Error): void { + console.log(util.format(error)); + console.log(util.format("%s", error)); + console.log(util.inspect({ wrapped: { deeper: { error } } })); +} +const b = clean(new Bailable("download failed")); +b.bail = true; +show(b); +show(clean(new Error("plain"))); +const d = clean(new Detailed("y".repeat(70))); +d.attempts.push(1, 2); +show(d); +class FetchFailedError extends Error { + status = 0; +} +const f = clean(new FetchFailedError("not found")); +f.status = 404; +show(f); +class Plain extends Error {} +show(clean(new Plain("field-less beside fielded subclasses"))); +class Retried extends Error { + retries = 2; + constructor(message: string, cause?: unknown) { + super(message, { cause }); + } +} +show(clean(new Retried("with a cause", "from the options"))); +show(clean(new Retried("with an undefined cause"))); +const assigned = clean(new Bailable("assigned")); +assigned.cause = { code: 1 }; +show(assigned); +const root = new Error("root"); +root.stack = ""; +const middle: Error = new TypeError("middle", { cause: root }); +middle.stack = ""; +show(clean(new Retried("nested causes", middle))); diff --git a/tests/corpus/6526-error-subclass-cycles.ts b/tests/corpus/6526-error-subclass-cycles.ts new file mode 100644 index 000000000..8d76d02cd --- /dev/null +++ b/tests/corpus/6526-error-subclass-cycles.ts @@ -0,0 +1,48 @@ +// Inspecting Error subclasses whose fields hold the same class or one of +// its bases: a chain renders each link, and a cycle renders as Node's +// circular reference (`` and `[Circular *1]`), also when the cycle +// runs through a base-typed field of a subclass or through an object's +// Error-typed field. (The stack is cleared: compiled errors carry none.) +import * as util from "node:util"; +class Chain extends Error { + next: Chain | undefined = undefined; +} +class Tagged extends Chain { + tag = "t"; +} +function clean(e: E): E { + e.stack = ""; + return e; +} +function show(e: Error): void { + console.log(util.inspect(e)); +} + +const tail = clean(new Chain("tail")); +const head = clean(new Chain("head")); +head.next = tail; +show(head); +console.log(util.inspect({ wrapped: { head } })); + +const loop = clean(new Chain("loop")); +loop.next = loop; +show(loop); + +const a = clean(new Tagged("a")); +const b = clean(new Chain("b")); +a.next = b; +b.next = a; +show(a); +show(b); +show(clean(new Error("plain"))); + +class Holder { + err: Error | undefined = undefined; +} +class Held extends Error { + holder = new Holder(); +} +const held = clean(new Held("held")); +held.holder.err = held; +console.log(util.inspect(held.holder)); +show(held); diff --git a/tests/corpus/6530-error-subclass-header.ts b/tests/corpus/6530-error-subclass-header.ts new file mode 100644 index 000000000..9a33d5eb4 --- /dev/null +++ b/tests/corpus/6530-error-subclass-header.ts @@ -0,0 +1,80 @@ +// Inspecting an instance of a program-declared Error subclass names its +// constructor the way Node's improveStack does: `[Plain [Error]: m]`, or +// `[FetchFailedError: m]` when the constructor name already contains the +// error's name; runtime error classes keep their own header. Own `cause` +// properties (declared, assigned in the constructor or later) and a +// declared `name` follow Node's rendering, as does a cause's own +// `[cause]` and an AggregateError cause's `[errors]`, and beyond the depth +// limit an instance with own properties renders as `[Constructor]`. (The +// stack is cleared: compiled errors carry none.) +import * as util from "node:util"; + +class Plain extends Error {} +class FetchFailedError extends Error {} +class Deeper extends Plain {} +class Caused extends Error { + override cause: unknown = "declared"; +} +class Assigned extends Error { + constructor(message?: string) { + super(message); + this.cause = { at: "constructor" }; + } +} +class Renamed extends Error { + override name = "RenamedError"; +} +class Typed extends TypeError {} +class RangeLimitError extends RangeError { + constructor(message: string) { + super(message); + this.cause = 3; + } +} + +function clean(error: Error): Error { + error.stack = ""; + return error; +} + +function show(error: Error): void { + console.log(util.format(error), "|", util.format("%s", error)); + console.log(util.inspect({ nested: error })); +} + +show(clean(new Plain("p"))); +show(clean(new FetchFailedError("f"))); +show(clean(new Deeper("d"))); +show(clean(new Caused("c"))); +show(clean(new Assigned("a"))); +show(clean(new Renamed("r"))); +show(clean(new Error("plain"))); +show(clean(new TypeError("type"))); +show(clean(new Typed("typed"))); +show(clean(new RangeLimitError("range"))); +const typed: Plain = new Deeper("typed as the base"); +typed.stack = ""; +console.log(util.inspect(typed)); +const listed = [new Plain("in an array")]; +listed[0]!.stack = ""; +console.log(util.inspect(listed)); +const later = clean(new Plain("later")); +later.cause = "assigned later"; +console.log(util.inspect(later)); +const multiline = clean(new Plain("first\nsecond")); +console.log(util.inspect({ multiline })); +const deep = [clean(new Assigned("beyond depth")), clean(new Assigned()), clean(new Plain())]; +for (const error of deep) console.log(util.inspect({ a: { b: error } }, { depth: 1 })); +const inner = new Error("inner", { cause: "deep" }); +inner.stack = ""; +console.log(util.inspect(inner)); +const wrapped = clean(new Plain("outer")); +wrapped.cause = inner; +console.log(util.inspect(wrapped)); +const first = new Error("first"); +first.stack = ""; +const aggregate: Error = new AggregateError([first], "all failed", { cause: "why" }); +aggregate.stack = ""; +const holder = clean(new Plain("holder")); +holder.cause = aggregate; +console.log(util.inspect(holder, { depth: 4 })); diff --git a/tests/corpus/6550-npm-boolean-bitwise/main.ts b/tests/corpus/6550-npm-boolean-bitwise/main.ts new file mode 100644 index 000000000..a08502343 --- /dev/null +++ b/tests/corpus/6550-npm-boolean-bitwise/main.ts @@ -0,0 +1,15 @@ +// Bitwise operators over booleans in a JavaScript dependency convert them +// with ToNumber, true to 1 and false to 0, as Node does (async-sema's +// `isFn(pauseFn) ^ isFn(resumeFn)` option check). +import { Sema, bits } from 'token-sema'; + +const plain = new Sema(3); +const paired = new Sema(2, { pauseFn: () => {}, resumeFn: () => {} }); +console.log(plain.capacity, plain.pausable, paired.capacity, paired.pausable); +try { + new Sema(1, { pauseFn: () => {} }); +} catch (error) { + console.log((error as Error).message); +} +console.log(bits([1, 0, 3])); +console.log(bits([])); diff --git a/tests/corpus/6550-npm-boolean-bitwise/node_modules/token-sema/index.d.ts b/tests/corpus/6550-npm-boolean-bitwise/node_modules/token-sema/index.d.ts new file mode 100644 index 000000000..de028a027 --- /dev/null +++ b/tests/corpus/6550-npm-boolean-bitwise/node_modules/token-sema/index.d.ts @@ -0,0 +1,6 @@ +export declare class Sema { + constructor(capacity: number, options?: { pauseFn?: () => void; resumeFn?: () => void }); + readonly capacity: number; + readonly pausable: number; +} +export declare function bits(values: number[]): string; diff --git a/tests/corpus/6550-npm-boolean-bitwise/node_modules/token-sema/index.js b/tests/corpus/6550-npm-boolean-bitwise/node_modules/token-sema/index.js new file mode 100644 index 000000000..1efaf449c --- /dev/null +++ b/tests/corpus/6550-npm-boolean-bitwise/node_modules/token-sema/index.js @@ -0,0 +1,35 @@ +// The option check from async-sema (MIT): pauseFn and resumeFn must be given +// together, tested with `^` over two booleans. Bitwise operators convert +// booleans with ToNumber. +'use strict'; +function isFn(x) { + return typeof x == 'function'; +} +class Sema { + constructor(capacity, { pauseFn, resumeFn } = {}) { + if (isFn(pauseFn) ^ isFn(resumeFn)) throw new Error('pauseFn and resumeFn must be both set for pausing'); + this.capacity = capacity; + this.pausable = isFn(pauseFn) & isFn(resumeFn); + } +} +function bits(values) { + var yes = values.length > 0; + var no = values.length < 0; + var flags = 0; + var count = 0; + for (var v of values) { + flags = flags | (v > 0); + count = count + ((v > 1) & 1); + } + // The right operand assigns the variable the left one reads; the left + // operand reads it first. + var seen = values.length > 0; + var toggled = seen ^ (seen = !seen); + var shifted = seen << (seen = true); + return [ + yes ^ no, yes ^ yes, yes & no, yes | no, no | no, yes & 3, 2 | yes, + yes << 3, yes >> 0, no >>> 0, -1 >>> yes, 5 ^ yes, no ^ 7, + typeof (yes ^ no), flags, count, toggled, shifted, seen, + ].join(' '); +} +module.exports = { Sema: Sema, bits: bits }; diff --git a/tests/corpus/6550-npm-boolean-bitwise/node_modules/token-sema/package.json b/tests/corpus/6550-npm-boolean-bitwise/node_modules/token-sema/package.json new file mode 100644 index 000000000..7d74609a7 --- /dev/null +++ b/tests/corpus/6550-npm-boolean-bitwise/node_modules/token-sema/package.json @@ -0,0 +1 @@ +{ "name": "token-sema", "version": "1.0.0", "main": "index.js", "types": "index.d.ts" } diff --git a/tests/corpus/6560-npm-js-capability-probes/main.ts b/tests/corpus/6560-npm-js-capability-probes/main.ts new file mode 100644 index 000000000..e9eacee8e --- /dev/null +++ b/tests/corpus/6560-npm-js-capability-probes/main.ts @@ -0,0 +1,14 @@ +// JavaScript dependency forms from bundled zip packages, which run as Node +// runs them: +// - a var table replaced by an Int32Array view of itself (buffer-crc32); +// - function declarations that redefine themselves on the first call, and +// Buffer.from over an array with an undefined encoding (yazl); +// - the deprecated `new Buffer(...)` fallback both keep, and the Buffer +// methods called on the untyped buffers they return. +import { crc32, localHeader, signature, legacy } from 'zip-helpers'; + +console.log(crc32('hello'), crc32(Buffer.from([1, 2, 3])), crc32('world', 17), crc32(4)); +console.log(Array.from(localHeader('a.txt', 9)).join(',')); +console.log(Array.from(localHeader('b', 1)).join(',')); +console.log(Array.from(signature()).join(',')); +console.log(legacy()); diff --git a/tests/corpus/6560-npm-js-capability-probes/node_modules/zip-helpers/index.d.ts b/tests/corpus/6560-npm-js-capability-probes/node_modules/zip-helpers/index.d.ts new file mode 100644 index 000000000..0b51fe366 --- /dev/null +++ b/tests/corpus/6560-npm-js-capability-probes/node_modules/zip-helpers/index.d.ts @@ -0,0 +1,4 @@ +export declare function crc32(input: Uint8Array | string | number, previous?: number): number; +export declare function localHeader(name: string, size: number): Uint8Array; +export declare function signature(): Uint8Array; +export declare function legacy(): string; diff --git a/tests/corpus/6560-npm-js-capability-probes/node_modules/zip-helpers/index.js b/tests/corpus/6560-npm-js-capability-probes/node_modules/zip-helpers/index.js new file mode 100644 index 000000000..15fc0a0c4 --- /dev/null +++ b/tests/corpus/6560-npm-js-capability-probes/node_modules/zip-helpers/index.js @@ -0,0 +1,125 @@ +// Small parts of buffer-crc32 (MIT) and yazl (MIT), in their published +// shapes: a var table replaced by an Int32Array view of itself, and +// function declarations that replace themselves with the implementation +// that works on the first call. +'use strict'; +var Buffer2 = require('buffer').Buffer; +var CRC_TABLE = [ + 0, 1996959894, 3993919788, 2567524794, 124634137, 1886057615, 3915621685, 2657392035, + 249268274, 2044508324, 3772115230, 2547177864, 162941995, 2125561021, 3887607047, 2428444049, + 498536548, 1789927666, 4089016648, 2227061214, 450548861, 1843258603, 4107580753, 2211677639, + 325883990, 1684777152, 4251122042, 2321926636, 335633487, 1661365465, 4195302755, 2366115317, + 997073096, 1281953886, 3579855332, 2724688242, 1006888145, 1258607687, 3524101629, 2768942443, + 901097722, 1119000684, 3686517206, 2898065728, 853044451, 1172266101, 3705015759, 2882616665, + 651767980, 1373503546, 3369554304, 3218104598, 565507253, 1454621731, 3485111705, 3099436303, + 671266974, 1594198024, 3322730930, 2970347812, 795835527, 1483230225, 3244367275, 3060149565, + 1994146192, 31158534, 2563907772, 4023717930, 1907459465, 112637215, 2680153253, 3904427059, + 2013776290, 251722036, 2517215374, 3775830040, 2137656763, 141376813, 2439277719, 3865271297, + 1802195444, 476864866, 2238001368, 4066508878, 1812370925, 453092731, 2181625025, 4111451223, + 1706088902, 314042704, 2344532202, 4240017532, 1658658271, 366619977, 2362670323, 4224994405, + 1303535960, 984961486, 2747007092, 3569037538, 1256170817, 1037604311, 2765210733, 3554079995, + 1131014506, 879679996, 2909243462, 3663771856, 1141124467, 855842277, 2852801631, 3708648649, + 1342533948, 654459306, 3188396048, 3373015174, 1466479909, 544179635, 3110523913, 3462522015, + 1591671054, 702138776, 2966460450, 3352799412, 1504918807, 783551873, 3082640443, 3233442989, + 3988292384, 2596254646, 62317068, 1957810842, 3939845945, 2647816111, 81470997, 1943803523, + 3814918930, 2489596804, 225274430, 2053790376, 3826175755, 2466906013, 167816743, 2097651377, + 4027552580, 2265490386, 503444072, 1762050814, 4150417245, 2154129355, 426522225, 1852507879, + 4275313526, 2312317920, 282753626, 1742555852, 4189708143, 2394877945, 397917763, 1622183637, + 3604390888, 2714866558, 953729732, 1340076626, 3518719985, 2797360999, 1068828381, 1219638859, + 3624741850, 2936675148, 906185462, 1090812512, 3747672003, 2825379669, 829329135, 1181335161, + 3412177804, 3160834842, 628085408, 1382605366, 3423369109, 3138078467, 570562233, 1426400815, + 3317316542, 2998733608, 733239954, 1555261956, 3268935591, 3050360625, 752459403, 1541320221, + 2607071920, 3965973030, 1969922972, 40735498, 2617837225, 3943577151, 1913087877, 83908371, + 2512341634, 3803740692, 2075208622, 213261112, 2463272603, 3855990285, 2094854071, 198958881, + 2262029012, 4057260610, 1759359992, 534414190, 2176718541, 4139329115, 1873836001, 414664567, + 2282248934, 4279200368, 1711684554, 285281116, 2405801727, 4167216745, 1634467795, 376229701, + 2685067896, 3608007406, 1308918612, 956543938, 2808555105, 3495958263, 1231636301, 1047427035, + 2932959818, 3654703836, 1088359270, 936918000, 2847714899, 3736837829, 1202900863, 817233897, + 3183342108, 3401237130, 1404277552, 615818150, 3134207493, 3453421203, 1423857449, 601450431, + 3009837614, 3294710456, 1567103746, 711928724, 3020668471, 3272380065, 1510334235, 755167117 +]; +typeof Int32Array < 'u' && (CRC_TABLE = new Int32Array(CRC_TABLE)); + +function ensureBuffer(input) { + if (Buffer2.isBuffer(input)) return input; + var hasNewBufferAPI = typeof Buffer2.alloc == 'function' && typeof Buffer2.from == 'function'; + if (typeof input == 'number') return hasNewBufferAPI ? Buffer2.alloc(input) : new Buffer2(input); + if (typeof input == 'string') return hasNewBufferAPI ? Buffer2.from(input) : new Buffer2(input); + throw new Error('input must be buffer, number, or string, received ' + typeof input); +} + +function crc32(buf, previous) { + buf = ensureBuffer(buf); + for (var crc = ~~previous ^ -1, n = 0; n < buf.length; n++) crc = CRC_TABLE[(crc ^ buf[n]) & 255] ^ (crc >>> 8); + return (crc ^ -1) >>> 0; +} + +function bufferFrom(something, encoding) { + bufferFrom = modern; + try { + return bufferFrom(something, encoding); + } catch { + return (bufferFrom = legacy), bufferFrom(something, encoding); + } + function modern(something2, encoding2) { + return Buffer.from(something2, encoding2); + } + function legacy(something2, encoding2) { + return new Buffer(something2, encoding2); + } +} + +function bufferAlloc(size) { + bufferAlloc = modern; + try { + return bufferAlloc(size); + } catch { + return (bufferAlloc = legacy), bufferAlloc(size); + } + function modern(size2) { + return Buffer.alloc(size2); + } + function legacy(size2) { + return new Buffer(size2); + } +} + +function localHeader(name, size) { + var nameBuffer = bufferFrom(name, 'utf-8'); + var fixed = bufferAlloc(8); + fixed.writeUInt32LE(0x04034b50, 0); + fixed.writeUInt16LE(nameBuffer.length, 4); + fixed.writeUInt16LE(size, 6); + return Buffer.concat([fixed, nameBuffer]); +} + +var SIGNATURE = bufferFrom([0x50, 0x4b, 0x05, 0x06], undefined); + +function legacy() { + var sized = new Buffer2(3); + var text = new Buffer2('hi', 'utf8'); + var array = new Buffer2([1, 2, 255]); + var out = [sized.length, sized[0], text.toString(), array.readUInt16BE(1), array.readInt8(2)]; + try { + new Buffer2(2, 'utf8'); + } catch (error) { + out.push(error.code); + } + try { + array.writeUInt8(300, 0); + } catch (error) { + out.push(error.code); + } + try { + array.readUInt8('0'); + } catch (error) { + out.push(error.code); + } + return out.join(' '); +} + +function signature() { + return SIGNATURE; +} + +module.exports = { crc32: crc32, localHeader: localHeader, signature: signature, legacy: legacy }; diff --git a/tests/corpus/6560-npm-js-capability-probes/node_modules/zip-helpers/package.json b/tests/corpus/6560-npm-js-capability-probes/node_modules/zip-helpers/package.json new file mode 100644 index 000000000..2192addcf --- /dev/null +++ b/tests/corpus/6560-npm-js-capability-probes/node_modules/zip-helpers/package.json @@ -0,0 +1 @@ +{ "name": "zip-helpers", "version": "1.0.0", "main": "index.js", "types": "index.d.ts" } diff --git a/tests/corpus/6565-rebindable-function-import-cycle/main.ts b/tests/corpus/6565-rebindable-function-import-cycle/main.ts new file mode 100644 index 000000000..ff505d978 --- /dev/null +++ b/tests/corpus/6565-rebindable-function-import-cycle/main.ts @@ -0,0 +1,5 @@ +// A package whose import cycle calls a reassigned function declaration +// while the cycle initializes. +import { report } from "cycle-format"; + +for (const line of report()) console.log(line); diff --git a/tests/corpus/6565-rebindable-function-import-cycle/node_modules/cycle-format/dep.js b/tests/corpus/6565-rebindable-function-import-cycle/node_modules/cycle-format/dep.js new file mode 100644 index 000000000..730e7b34e --- /dev/null +++ b/tests/corpus/6565-rebindable-function-import-cycle/node_modules/cycle-format/dep.js @@ -0,0 +1,8 @@ +import { format } from "./index.js"; + +// Runs before index.js's body: format is the hoisted declaration. +export const early = format("a"); + +export function describe(value) { + return `${typeof format} ${format(value)}`; +} diff --git a/tests/corpus/6565-rebindable-function-import-cycle/node_modules/cycle-format/index.d.ts b/tests/corpus/6565-rebindable-function-import-cycle/node_modules/cycle-format/index.d.ts new file mode 100644 index 000000000..55a683bb7 --- /dev/null +++ b/tests/corpus/6565-rebindable-function-import-cycle/node_modules/cycle-format/index.d.ts @@ -0,0 +1,2 @@ +export declare function format(value: string): string; +export declare const report: () => string[]; diff --git a/tests/corpus/6565-rebindable-function-import-cycle/node_modules/cycle-format/index.js b/tests/corpus/6565-rebindable-function-import-cycle/node_modules/cycle-format/index.js new file mode 100644 index 000000000..c96fadcad --- /dev/null +++ b/tests/corpus/6565-rebindable-function-import-cycle/node_modules/cycle-format/index.js @@ -0,0 +1,18 @@ +// A reassigned function declaration of a module in an import cycle: the +// dependency calls it while the cycle initializes, before this module's +// body runs, and gets the hoisted declaration; later calls see the +// replacement. +import { early, describe } from "./dep.js"; + +export function format(value) { + format = function formatUpper(text) { + return `[${String(text).toUpperCase()}]`; + }; + return `<${value}>`; +} + +export const report = () => [ + "early: " + early, + "now: " + format("b") + " " + format("c"), + "dep: " + describe("d"), +]; diff --git a/tests/corpus/6565-rebindable-function-import-cycle/node_modules/cycle-format/package.json b/tests/corpus/6565-rebindable-function-import-cycle/node_modules/cycle-format/package.json new file mode 100644 index 000000000..becff6bc6 --- /dev/null +++ b/tests/corpus/6565-rebindable-function-import-cycle/node_modules/cycle-format/package.json @@ -0,0 +1 @@ +{ "name": "cycle-format", "version": "1.0.0", "type": "module", "types": "./index.d.ts", "exports": { ".": { "types": "./index.d.ts", "default": "./index.js" } } } diff --git a/tests/corpus/6570-npm-builtin-function-bind/main.ts b/tests/corpus/6570-npm-builtin-function-bind/main.ts new file mode 100644 index 000000000..f04dc2053 --- /dev/null +++ b/tests/corpus/6570-npm-builtin-function-bind/main.ts @@ -0,0 +1,26 @@ +// Function.prototype.bind, call and apply on builtin static functions in +// JavaScript dependencies. The vendored truncate-utf8-bytes 1.0.2 (WTFPL) +// binds Buffer.byteLength at load time and calls the bound function per +// character; bound-builtins binds Buffer, Math, String and JSON statics. +// fs.realpath 1.0.0 (ISC) and value-forms read builtin functions as values: +// fs.realpathSync stored and compared, Buffer.alloc aliased, and a String +// method read for a feature probe. +import truncate from 'truncate-utf8-bytes'; +import { report, badLength } from 'bound-builtins'; +import realpath from 'fs.realpath'; +import { report as valueForms, more as moreValueForms } from 'value-forms'; + +console.log(truncate('hello world', 5), truncate('héllo', 2), truncate('héllo', 3)); +console.log(JSON.stringify(truncate('a😀b', 3)), JSON.stringify(truncate('a😀b', 5)), truncate('short', 99)); +try { + (truncate as unknown as (value: unknown, n: number) => string)(42, 1); +} catch (error) { + console.log((error as Error).message); +} +for (const value of report()) console.log(value); +console.log(badLength()); + +console.log(realpath.realpathSync('/'), realpath.sync('/')); +realpath('/', (error, resolved) => console.log('async', error, resolved)); +for (const value of valueForms()) console.log(value); +for (const value of moreValueForms()) console.log(value); diff --git a/tests/corpus/6570-npm-builtin-function-bind/node_modules/bound-builtins/index.d.ts b/tests/corpus/6570-npm-builtin-function-bind/node_modules/bound-builtins/index.d.ts new file mode 100644 index 000000000..9810c8bfc --- /dev/null +++ b/tests/corpus/6570-npm-builtin-function-bind/node_modules/bound-builtins/index.d.ts @@ -0,0 +1,2 @@ +export declare function report(): unknown[]; +export declare function badLength(): string; diff --git a/tests/corpus/6570-npm-builtin-function-bind/node_modules/bound-builtins/index.js b/tests/corpus/6570-npm-builtin-function-bind/node_modules/bound-builtins/index.js new file mode 100644 index 000000000..7cc668957 --- /dev/null +++ b/tests/corpus/6570-npm-builtin-function-bind/node_modules/bound-builtins/index.js @@ -0,0 +1,42 @@ +// Function.prototype.bind, call and apply on builtin static functions in +// JavaScript: Buffer, Math, String and JSON. +'use strict'; +var byteLength = Buffer.byteLength.bind(Buffer); +var max = Math.max.bind(null, 10); +var fromCodes = String.fromCharCode.bind(String, 104); +var stringify = JSON.stringify.bind(JSON); +var parse = JSON.parse.bind(JSON); +var compare = Buffer.compare.bind(Buffer); + +exports.report = function report() { + return [ + byteLength('héllo'), + byteLength('héllo', 'latin1'), + byteLength('abcd', 'hex'), + byteLength('héllo', 'not-an-encoding'), + byteLength(''), + byteLength(Buffer.from([1, 2, 3])), + max(3, 42), + max(), + Math.min.call(null, 4, 2), + Math.abs.apply(null, [-7]), + fromCodes(105, 33), + String.fromCharCode.apply(null, [111, 107]), + stringify({ a: [1, 2] }), + stringify({ b: 1 }, null, 2), + compare(Buffer.from('a'), Buffer.from('b')), + parse('{"n":1}').n, + JSON.stringify(parse('[1,2]', function (key, value) { + return typeof value === 'number' ? value * 10 : value; + })), + ]; +}; + +exports.badLength = function badLength() { + try { + byteLength(42); + return 'no error'; + } catch (error) { + return error.code + ' ' + error.name; + } +}; diff --git a/tests/corpus/6570-npm-builtin-function-bind/node_modules/bound-builtins/package.json b/tests/corpus/6570-npm-builtin-function-bind/node_modules/bound-builtins/package.json new file mode 100644 index 000000000..dd7b59369 --- /dev/null +++ b/tests/corpus/6570-npm-builtin-function-bind/node_modules/bound-builtins/package.json @@ -0,0 +1 @@ +{ "name": "bound-builtins", "version": "1.0.0", "main": "index.js", "types": "index.d.ts" } diff --git a/tests/corpus/6570-npm-builtin-function-bind/node_modules/fs.realpath/LICENSE b/tests/corpus/6570-npm-builtin-function-bind/node_modules/fs.realpath/LICENSE new file mode 100644 index 000000000..5bd884c25 --- /dev/null +++ b/tests/corpus/6570-npm-builtin-function-bind/node_modules/fs.realpath/LICENSE @@ -0,0 +1,43 @@ +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +---- + +This library bundles a version of the `fs.realpath` and `fs.realpathSync` +methods from Node.js v0.10 under the terms of the Node.js MIT license. + +Node's license follows, also included at the header of `old.js` which contains +the licensed code: + + Copyright Joyent, Inc. and other Node 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/corpus/6570-npm-builtin-function-bind/node_modules/fs.realpath/index.d.ts b/tests/corpus/6570-npm-builtin-function-bind/node_modules/fs.realpath/index.d.ts new file mode 100644 index 000000000..308c19e6c --- /dev/null +++ b/tests/corpus/6570-npm-builtin-function-bind/node_modules/fs.realpath/index.d.ts @@ -0,0 +1,6 @@ +declare function realpath(p: string, cb: (error: Error | null, resolved?: string) => void): void; +declare namespace realpath { + function realpathSync(p: string, cache?: object): string; + function sync(p: string, cache?: object): string; +} +export = realpath; diff --git a/tests/corpus/6570-npm-builtin-function-bind/node_modules/fs.realpath/index.js b/tests/corpus/6570-npm-builtin-function-bind/node_modules/fs.realpath/index.js new file mode 100644 index 000000000..b09c7c7e6 --- /dev/null +++ b/tests/corpus/6570-npm-builtin-function-bind/node_modules/fs.realpath/index.js @@ -0,0 +1,66 @@ +module.exports = realpath +realpath.realpath = realpath +realpath.sync = realpathSync +realpath.realpathSync = realpathSync +realpath.monkeypatch = monkeypatch +realpath.unmonkeypatch = unmonkeypatch + +var fs = require('fs') +var origRealpath = fs.realpath +var origRealpathSync = fs.realpathSync + +var version = process.version +var ok = /^v[0-5]\./.test(version) +var old = require('./old.js') + +function newError (er) { + return er && er.syscall === 'realpath' && ( + er.code === 'ELOOP' || + er.code === 'ENOMEM' || + er.code === 'ENAMETOOLONG' + ) +} + +function realpath (p, cache, cb) { + if (ok) { + return origRealpath(p, cache, cb) + } + + if (typeof cache === 'function') { + cb = cache + cache = null + } + origRealpath(p, cache, function (er, result) { + if (newError(er)) { + old.realpath(p, cache, cb) + } else { + cb(er, result) + } + }) +} + +function realpathSync (p, cache) { + if (ok) { + return origRealpathSync(p, cache) + } + + try { + return origRealpathSync(p, cache) + } catch (er) { + if (newError(er)) { + return old.realpathSync(p, cache) + } else { + throw er + } + } +} + +function monkeypatch () { + fs.realpath = realpath + fs.realpathSync = realpathSync +} + +function unmonkeypatch () { + fs.realpath = origRealpath + fs.realpathSync = origRealpathSync +} diff --git a/tests/corpus/6570-npm-builtin-function-bind/node_modules/fs.realpath/old.js b/tests/corpus/6570-npm-builtin-function-bind/node_modules/fs.realpath/old.js new file mode 100644 index 000000000..b40305e73 --- /dev/null +++ b/tests/corpus/6570-npm-builtin-function-bind/node_modules/fs.realpath/old.js @@ -0,0 +1,303 @@ +// Copyright Joyent, Inc. and other Node 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. + +var pathModule = require('path'); +var isWindows = process.platform === 'win32'; +var fs = require('fs'); + +// JavaScript implementation of realpath, ported from node pre-v6 + +var DEBUG = process.env.NODE_DEBUG && /fs/.test(process.env.NODE_DEBUG); + +function rethrow() { + // Only enable in debug mode. A backtrace uses ~1000 bytes of heap space and + // is fairly slow to generate. + var callback; + if (DEBUG) { + var backtrace = new Error; + callback = debugCallback; + } else + callback = missingCallback; + + return callback; + + function debugCallback(err) { + if (err) { + backtrace.message = err.message; + err = backtrace; + missingCallback(err); + } + } + + function missingCallback(err) { + if (err) { + if (process.throwDeprecation) + throw err; // Forgot a callback but don't know where? Use NODE_DEBUG=fs + else if (!process.noDeprecation) { + var msg = 'fs: missing callback ' + (err.stack || err.message); + if (process.traceDeprecation) + console.trace(msg); + else + console.error(msg); + } + } + } +} + +function maybeCallback(cb) { + return typeof cb === 'function' ? cb : rethrow(); +} + +var normalize = pathModule.normalize; + +// Regexp that finds the next partion of a (partial) path +// result is [base_with_slash, base], e.g. ['somedir/', 'somedir'] +if (isWindows) { + var nextPartRe = /(.*?)(?:[\/\\]+|$)/g; +} else { + var nextPartRe = /(.*?)(?:[\/]+|$)/g; +} + +// Regex to find the device root, including trailing slash. E.g. 'c:\\'. +if (isWindows) { + var splitRootRe = /^(?:[a-zA-Z]:|[\\\/]{2}[^\\\/]+[\\\/][^\\\/]+)?[\\\/]*/; +} else { + var splitRootRe = /^[\/]*/; +} + +exports.realpathSync = function realpathSync(p, cache) { + // make p is absolute + p = pathModule.resolve(p); + + if (cache && Object.prototype.hasOwnProperty.call(cache, p)) { + return cache[p]; + } + + var original = p, + seenLinks = {}, + knownHard = {}; + + // current character position in p + var pos; + // the partial path so far, including a trailing slash if any + var current; + // the partial path without a trailing slash (except when pointing at a root) + var base; + // the partial path scanned in the previous round, with slash + var previous; + + start(); + + function start() { + // Skip over roots + var m = splitRootRe.exec(p); + pos = m[0].length; + current = m[0]; + base = m[0]; + previous = ''; + + // On windows, check that the root exists. On unix there is no need. + if (isWindows && !knownHard[base]) { + fs.lstatSync(base); + knownHard[base] = true; + } + } + + // walk down the path, swapping out linked pathparts for their real + // values + // NB: p.length changes. + while (pos < p.length) { + // find the next part + nextPartRe.lastIndex = pos; + var result = nextPartRe.exec(p); + previous = current; + current += result[0]; + base = previous + result[1]; + pos = nextPartRe.lastIndex; + + // continue if not a symlink + if (knownHard[base] || (cache && cache[base] === base)) { + continue; + } + + var resolvedLink; + if (cache && Object.prototype.hasOwnProperty.call(cache, base)) { + // some known symbolic link. no need to stat again. + resolvedLink = cache[base]; + } else { + var stat = fs.lstatSync(base); + if (!stat.isSymbolicLink()) { + knownHard[base] = true; + if (cache) cache[base] = base; + continue; + } + + // read the link if it wasn't read before + // dev/ino always return 0 on windows, so skip the check. + var linkTarget = null; + if (!isWindows) { + var id = stat.dev.toString(32) + ':' + stat.ino.toString(32); + if (seenLinks.hasOwnProperty(id)) { + linkTarget = seenLinks[id]; + } + } + if (linkTarget === null) { + fs.statSync(base); + linkTarget = fs.readlinkSync(base); + } + resolvedLink = pathModule.resolve(previous, linkTarget); + // track this, if given a cache. + if (cache) cache[base] = resolvedLink; + if (!isWindows) seenLinks[id] = linkTarget; + } + + // resolve the link, then start over + p = pathModule.resolve(resolvedLink, p.slice(pos)); + start(); + } + + if (cache) cache[original] = p; + + return p; +}; + + +exports.realpath = function realpath(p, cache, cb) { + if (typeof cb !== 'function') { + cb = maybeCallback(cache); + cache = null; + } + + // make p is absolute + p = pathModule.resolve(p); + + if (cache && Object.prototype.hasOwnProperty.call(cache, p)) { + return process.nextTick(cb.bind(null, null, cache[p])); + } + + var original = p, + seenLinks = {}, + knownHard = {}; + + // current character position in p + var pos; + // the partial path so far, including a trailing slash if any + var current; + // the partial path without a trailing slash (except when pointing at a root) + var base; + // the partial path scanned in the previous round, with slash + var previous; + + start(); + + function start() { + // Skip over roots + var m = splitRootRe.exec(p); + pos = m[0].length; + current = m[0]; + base = m[0]; + previous = ''; + + // On windows, check that the root exists. On unix there is no need. + if (isWindows && !knownHard[base]) { + fs.lstat(base, function(err) { + if (err) return cb(err); + knownHard[base] = true; + LOOP(); + }); + } else { + process.nextTick(LOOP); + } + } + + // walk down the path, swapping out linked pathparts for their real + // values + function LOOP() { + // stop if scanned past end of path + if (pos >= p.length) { + if (cache) cache[original] = p; + return cb(null, p); + } + + // find the next part + nextPartRe.lastIndex = pos; + var result = nextPartRe.exec(p); + previous = current; + current += result[0]; + base = previous + result[1]; + pos = nextPartRe.lastIndex; + + // continue if not a symlink + if (knownHard[base] || (cache && cache[base] === base)) { + return process.nextTick(LOOP); + } + + if (cache && Object.prototype.hasOwnProperty.call(cache, base)) { + // known symbolic link. no need to stat again. + return gotResolvedLink(cache[base]); + } + + return fs.lstat(base, gotStat); + } + + function gotStat(err, stat) { + if (err) return cb(err); + + // if not a symlink, skip to the next path part + if (!stat.isSymbolicLink()) { + knownHard[base] = true; + if (cache) cache[base] = base; + return process.nextTick(LOOP); + } + + // stat & read the link if not read before + // call gotTarget as soon as the link target is known + // dev/ino always return 0 on windows, so skip the check. + if (!isWindows) { + var id = stat.dev.toString(32) + ':' + stat.ino.toString(32); + if (seenLinks.hasOwnProperty(id)) { + return gotTarget(null, seenLinks[id], base); + } + } + fs.stat(base, function(err) { + if (err) return cb(err); + + fs.readlink(base, function(err, target) { + if (!isWindows) seenLinks[id] = target; + gotTarget(err, target); + }); + }); + } + + function gotTarget(err, target, base) { + if (err) return cb(err); + + var resolvedLink = pathModule.resolve(previous, target); + if (cache) cache[base] = resolvedLink; + gotResolvedLink(resolvedLink); + } + + function gotResolvedLink(resolvedLink) { + // resolve the link, then start over + p = pathModule.resolve(resolvedLink, p.slice(pos)); + start(); + } +}; diff --git a/tests/corpus/6570-npm-builtin-function-bind/node_modules/fs.realpath/package.json b/tests/corpus/6570-npm-builtin-function-bind/node_modules/fs.realpath/package.json new file mode 100644 index 000000000..9adf9cdf3 --- /dev/null +++ b/tests/corpus/6570-npm-builtin-function-bind/node_modules/fs.realpath/package.json @@ -0,0 +1 @@ +{ "name": "fs.realpath", "version": "1.0.0", "main": "index.js", "types": "index.d.ts", "license": "ISC" } diff --git a/tests/corpus/6570-npm-builtin-function-bind/node_modules/truncate-utf8-bytes/index.d.ts b/tests/corpus/6570-npm-builtin-function-bind/node_modules/truncate-utf8-bytes/index.d.ts new file mode 100644 index 000000000..2d051a0b8 --- /dev/null +++ b/tests/corpus/6570-npm-builtin-function-bind/node_modules/truncate-utf8-bytes/index.d.ts @@ -0,0 +1,2 @@ +declare function truncate(string: string, byteLength: number): string; +export = truncate; diff --git a/tests/corpus/6570-npm-builtin-function-bind/node_modules/truncate-utf8-bytes/index.js b/tests/corpus/6570-npm-builtin-function-bind/node_modules/truncate-utf8-bytes/index.js new file mode 100644 index 000000000..39e899c37 --- /dev/null +++ b/tests/corpus/6570-npm-builtin-function-bind/node_modules/truncate-utf8-bytes/index.js @@ -0,0 +1,5 @@ +'use strict'; + +var truncate = require("./lib/truncate"); +var getLength = Buffer.byteLength.bind(Buffer); +module.exports = truncate.bind(null, getLength); diff --git a/tests/corpus/6570-npm-builtin-function-bind/node_modules/truncate-utf8-bytes/lib/truncate.js b/tests/corpus/6570-npm-builtin-function-bind/node_modules/truncate-utf8-bytes/lib/truncate.js new file mode 100644 index 000000000..7837a4e11 --- /dev/null +++ b/tests/corpus/6570-npm-builtin-function-bind/node_modules/truncate-utf8-bytes/lib/truncate.js @@ -0,0 +1,42 @@ +'use strict'; + +function isHighSurrogate(codePoint) { + return codePoint >= 0xd800 && codePoint <= 0xdbff; +} + +function isLowSurrogate(codePoint) { + return codePoint >= 0xdc00 && codePoint <= 0xdfff; +} + +// Truncate string by size in bytes +module.exports = function truncate(getLength, string, byteLength) { + if (typeof string !== "string") { + throw new Error("Input must be string"); + } + + var charLength = string.length; + var curByteLength = 0; + var codePoint; + var segment; + + for (var i = 0; i < charLength; i += 1) { + codePoint = string.charCodeAt(i); + segment = string[i]; + + if (isHighSurrogate(codePoint) && isLowSurrogate(string.charCodeAt(i + 1))) { + i += 1; + segment += string[i]; + } + + curByteLength += getLength(segment); + + if (curByteLength === byteLength) { + return string.slice(0, i + 1); + } + else if (curByteLength > byteLength) { + return string.slice(0, i - segment.length + 1); + } + } + + return string; +}; diff --git a/tests/corpus/6570-npm-builtin-function-bind/node_modules/truncate-utf8-bytes/package.json b/tests/corpus/6570-npm-builtin-function-bind/node_modules/truncate-utf8-bytes/package.json new file mode 100644 index 000000000..05cd0181a --- /dev/null +++ b/tests/corpus/6570-npm-builtin-function-bind/node_modules/truncate-utf8-bytes/package.json @@ -0,0 +1,8 @@ +{ + "name": "truncate-utf8-bytes", + "version": "1.0.2", + "description": "Truncate string to given length in bytes", + "main": "index.js", + "types": "index.d.ts", + "license": "WTFPL" +} diff --git a/tests/corpus/6570-npm-builtin-function-bind/node_modules/value-forms/index.d.ts b/tests/corpus/6570-npm-builtin-function-bind/node_modules/value-forms/index.d.ts new file mode 100644 index 000000000..d2d676c10 --- /dev/null +++ b/tests/corpus/6570-npm-builtin-function-bind/node_modules/value-forms/index.d.ts @@ -0,0 +1,2 @@ +export declare function report(): unknown[]; +export declare function more(): string[]; diff --git a/tests/corpus/6570-npm-builtin-function-bind/node_modules/value-forms/index.js b/tests/corpus/6570-npm-builtin-function-bind/node_modules/value-forms/index.js new file mode 100644 index 000000000..8144f66b9 --- /dev/null +++ b/tests/corpus/6570-npm-builtin-function-bind/node_modules/value-forms/index.js @@ -0,0 +1,74 @@ +// Builtin functions read as values: tar-stream's `var alloc = Buffer.alloc`, +// fs.realpathSync stored and compared, and smol-toml's `''.isWellFormed` +// feature probe. +'use strict'; +var fs = require('fs'); +var alloc = Buffer.alloc; +var origRealpathSync = fs.realpathSync; +var HAS_WELLFORMED = !!''.isWellFormed; + +exports.report = function report() { + var header = alloc(8); + var filled = alloc(5, 'ab'); + var numbered = alloc(3, 7); + var out = []; + out.push(header.length, header[0], filled.toString(), numbered[2], typeof alloc, alloc.length); + try { + alloc('8'); + } catch (error) { + out.push(error.code); + } + out.push(origRealpathSync('/'), origRealpathSync === fs.realpathSync, typeof fs.realpathSync.native); + out.push(Buffer.isBuffer(origRealpathSync('/', 'buffer')), origRealpathSync('/', { encoding: 'hex' })); + out.push(fs.realpathSync.native('/')); + try { + origRealpathSync('/definitely/missing/path'); + } catch (error) { + out.push(error.code); + } + out.push(HAS_WELLFORMED, 'ok'.isWellFormed(), typeof ''.toUpperCase); + return out; +}; + +// The stored alloc with each fill form, and the stored realpathSync and +// realpathSync.native with file URLs. +var realpathNative = fs.realpathSync.native; +var url = require('url'); +exports.more = function more() { + var out = []; + function attempt(label, fn) { + try { + var value = fn(); + out.push(label + ' ' + (Buffer.isBuffer(value) ? value.toString('hex') : value)); + } catch (error) { + out.push(label + ' ' + error.name + ' ' + error.code + ' ' + error.message); + } + } + attempt('hex', function () { return alloc(4, 'ab', 'hex'); }); + attempt('base64', function () { return alloc(4, 'YQ==', 'base64'); }); + attempt('latin1', function () { return alloc(3, '\u00e9', 'latin1'); }); + attempt('upper case', function () { return alloc(4, 'ab', 'UTF8'); }); + attempt('null encoding', function () { return alloc(4, 'ab', null); }); + attempt('buffer pattern', function () { return alloc(5, Buffer.from([1, 2, 3])); }); + attempt('uint16 pattern', function () { return alloc(4, new Uint16Array([0x0102])); }); + attempt('null fill', function () { return alloc(3, null); }); + attempt('true fill', function () { return alloc(3, true); }); + attempt('object fill', function () { return alloc(3, { valueOf: function () { return 258; } }); }); + attempt('negative fill', function () { return alloc(2, -1); }); + attempt('empty string', function () { return alloc(2, ''); }); + attempt('empty buffer', function () { return alloc(2, Buffer.alloc(0)); }); + attempt('no bytes', function () { return alloc(2, 'zz', 'hex'); }); + attempt('unknown encoding', function () { return alloc(2, 'ab', 'nope'); }); + attempt('number encoding', function () { return alloc(2, 'ab', 5); }); + attempt('empty size', function () { return alloc(0, 'ab', 'nope').length; }); + // Through call, as a wrapper forwarding its arguments passes them. + var root = url.pathToFileURL('/'); + attempt('url', function () { return origRealpathSync.call(null, root); }); + attempt('native url', function () { return realpathNative.call(null, root); }); + attempt('http url', function () { return origRealpathSync.call(null, new URL('http://example.com/')); }); + attempt('native http url', function () { return realpathNative.call(null, new URL('http://example.com/')); }); + attempt('encoded slash', function () { return origRealpathSync.call(null, new URL('file:///a%2Fb')); }); + attempt('native number', function () { return realpathNative.call(null, 5); }); + attempt('native bytes', function () { return realpathNative.call(null, new Uint8Array(Buffer.from('/'))); }); + return out; +}; diff --git a/tests/corpus/6570-npm-builtin-function-bind/node_modules/value-forms/package.json b/tests/corpus/6570-npm-builtin-function-bind/node_modules/value-forms/package.json new file mode 100644 index 000000000..6f96b3124 --- /dev/null +++ b/tests/corpus/6570-npm-builtin-function-bind/node_modules/value-forms/package.json @@ -0,0 +1 @@ +{ "name": "value-forms", "version": "1.0.0", "main": "index.js", "types": "index.d.ts" } diff --git a/tests/corpus/6570-require-mixed-declarations.cjs b/tests/corpus/6570-require-mixed-declarations.cjs new file mode 100644 index 000000000..3d3e2c2fc --- /dev/null +++ b/tests/corpus/6570-require-mixed-declarations.cjs @@ -0,0 +1,46 @@ +// A require declarator beside other declarators in one var statement, the +// shape bundlers emit for a module's top level. The require binding has no +// storage of its own, so every read resolves to the module: construction in +// a class constructor, a class expression extending it, and calls through +// the other builtin bindings of the same statement. +var EventEmitter = require("events"), util = require("util"), path = require("path"), Base = class extends EventEmitter {}; + +class Pool { + constructor(size) { + this.size = size; + this.events = new EventEmitter; + this.events.on("grow", (by) => { + this.size += by; + console.log("grow", by, util.format("%d -> %d", this.size - by, this.size)); + }); + } +} +const pool = new Pool(2); +pool.events.emit("grow", 3); +console.log("pool", pool.size, pool.events instanceof EventEmitter, pool.events.listenerCount("grow")); + +var Worker = class { + constructor(name) { + this.name = name; + this.bus = new Base; + this.bus.on("done", (file) => console.log("done", this.name, path.basename(file))); + } +}; +const worker = new Worker("w1"); +worker.bus.emit("done", "/tmp/out/report.txt"); +console.log("worker", worker.bus instanceof Base, worker.bus instanceof EventEmitter, typeof Base); + +// A builtin require in a nested block is alias plumbing too. +if (pool.size > 0) { + var Emitter = require("node:events"), label = "nested"; +} +class Relay { + constructor() { + this.out = new Emitter(); + this.out.once("ping", (n) => console.log(label, "ping", n)); + } +} +const relay = new Relay(); +relay.out.emit("ping", 1); +relay.out.emit("ping", 2); +console.log("relay", relay.out.listenerCount("ping"), Emitter === EventEmitter); diff --git a/tests/corpus/6592-promise-armed-method-values/main.ts b/tests/corpus/6592-promise-armed-method-values/main.ts new file mode 100644 index 000000000..379c56ef9 --- /dev/null +++ b/tests/corpus/6592-promise-armed-method-values/main.ts @@ -0,0 +1,27 @@ +// Parse steps that answer either a status record or a promise of one keep +// their method values: reads through untyped code, detached calls, and the +// per-instance rebinding of the base constructor all see the function. +import { map, number, object, record, set, stepDetached, stepKinds } from "status-schema"; +async function main(): Promise { + const shaped = object({ count: number(), size: number() }); + const counts = record(number()); + const byName = map(number()); + const seen = set(number()); + console.log(stepKinds(shaped), "|", stepKinds(counts), "|", stepKinds(byName), "|", stepKinds(seen)); + console.log(shaped.safeParse({ count: 1, size: 2 }).success, shaped.safeParse({ count: "x", size: 2 }).success); + console.log(JSON.stringify(shaped.parse({ count: 3, size: 4 }))); + console.log(JSON.stringify(await shaped.parseAsync({ count: 5, size: 6 })), (await shaped.spa(7)).success); + console.log(counts.safeParse({ a: 1, b: 2 }).success, (await counts.safeParseAsync({ a: "x" })).success); + console.log(byName.safeParse(new Map([["a", 1]])).success, (await byName.safeParseAsync(new Map([["b", "x"]]))).success); + console.log(seen.safeParse(new Set([1, 2])).success, (await seen.spa(new Set([3]))).success); + console.log(await stepDetached(shaped, { count: 1, size: 1 }), "|", await stepDetached(counts, { a: "x" })); + console.log(await stepDetached(byName, new Map([["c", 2]])), "|", await stepDetached(seen, [1])); + try { + await counts.parseAsync(null); + } catch (e) { + console.log("rejected", e instanceof Error ? e.message : String(e)); + } + const nested = object({ inner: record(number()), tags: set(number()) }); + console.log(JSON.stringify(await nested.parseAsync({ inner: { q: 1 }, tags: new Set([1]) }))); +} +main(); diff --git a/tests/corpus/6592-promise-armed-method-values/node_modules/status-schema/index.d.ts b/tests/corpus/6592-promise-armed-method-values/node_modules/status-schema/index.d.ts new file mode 100644 index 000000000..6607bf0c7 --- /dev/null +++ b/tests/corpus/6592-promise-armed-method-values/node_modules/status-schema/index.d.ts @@ -0,0 +1,18 @@ +export interface SafeResult { + success: boolean; + data?: unknown; +} +export declare class Schema { + safeParse(data: unknown): SafeResult; + parse(data: unknown): unknown; + safeParseAsync(data: unknown): Promise; + spa(data: unknown): Promise; + parseAsync(data: unknown): Promise; +} +export declare function number(): Schema; +export declare function object(shape: { [key: string]: Schema }): Schema; +export declare function record(valueType: Schema): Schema; +export declare function map(valueType: Schema): Schema; +export declare function set(valueType: Schema): Schema; +export declare function stepKinds(schema: Schema): string; +export declare function stepDetached(schema: Schema, data: unknown): Promise; diff --git a/tests/corpus/6592-promise-armed-method-values/node_modules/status-schema/index.js b/tests/corpus/6592-promise-armed-method-values/node_modules/status-schema/index.js new file mode 100644 index 000000000..4fb5ae460 --- /dev/null +++ b/tests/corpus/6592-promise-armed-method-values/node_modules/status-schema/index.js @@ -0,0 +1,118 @@ +// A schema hierarchy in the shape of a validation library's: each parse +// step answers a status record or, in async mode, a promise of one built +// by a static async merge helper. The base constructor rebinds its +// methods per instance, async ones included. +export const INVALID = Object.freeze({ status: "aborted" }); +export class Status { + static async mergeAsync(pairs) { + const settled = []; + for (const pair of pairs) { + settled.push({ key: pair.key, value: await pair.value }); + } + return Status.mergeSync(settled); + } + static mergeSync(pairs) { + const value = {}; + for (const pair of pairs) { + if (pair.value.status === "aborted") return INVALID; + value[pair.key] = pair.value.value; + } + return { status: "valid", value }; + } +} +export class Schema { + constructor(def) { + this.spa = this.safeParseAsync; + this._def = def; + this.parse = this.parse.bind(this); + this.safeParse = this.safeParse.bind(this); + this.parseAsync = this.parseAsync.bind(this); + this.safeParseAsync = this.safeParseAsync.bind(this); + this.spa = this.spa.bind(this); + } + safeParse(data) { + const result = this._parse({ data, async: false }); + if (result instanceof Promise) throw new Error("async parse step in safeParse"); + return result.status === "valid" ? { success: true, data: result.value } : { success: false }; + } + parse(data) { + const result = this.safeParse(data); + if (result.success) return result.data; + throw new Error(this._def.name + ": invalid input"); + } + async safeParseAsync(data) { + const result = await this._parse({ data, async: true }); + return result.status === "valid" ? { success: true, data: result.value } : { success: false }; + } + async parseAsync(data) { + const result = await this.safeParseAsync(data); + if (result.success) return result.data; + throw new Error(this._def.name + ": invalid input"); + } +} +export class NumberSchema extends Schema { + _parse(input) { + return typeof input.data === "number" ? { status: "valid", value: input.data } : INVALID; + } +} +export class ObjectSchema extends Schema { + _parse(input) { + if (input.data === null || typeof input.data !== "object") return INVALID; + const pairs = []; + for (const key of Object.keys(this._def.shape)) { + pairs.push({ key, value: this._def.shape[key]._parse({ data: input.data[key], async: input.async }) }); + } + return input.async ? Status.mergeAsync(pairs) : Status.mergeSync(pairs); + } +} +export class RecordSchema extends Schema { + _parse(input) { + if (input.data === null || typeof input.data !== "object") return INVALID; + const pairs = []; + for (const key in input.data) { + pairs.push({ key, value: this._def.valueType._parse({ data: input.data[key], async: input.async }) }); + } + return input.async ? Status.mergeAsync(pairs) : Status.mergeSync(pairs); + } +} +export class MapSchema extends Schema { + _parse(input) { + if (!(input.data instanceof Map)) return INVALID; + const finish = () => { + const out = new Map(); + for (const [k, v] of input.data) { + if (this._def.valueType._parse({ data: v, async: false }).status !== "valid") return INVALID; + out.set(k, v); + } + return { status: "valid", value: out }; + }; + return input.async ? Promise.resolve().then(finish) : finish(); + } +} +export class SetSchema extends Schema { + _parse(input) { + if (!(input.data instanceof Set)) return INVALID; + const finish = () => { + const out = new Set(); + for (const v of input.data) { + if (this._def.valueType._parse({ data: v, async: false }).status !== "valid") return INVALID; + out.add(v); + } + return { status: "valid", value: out }; + }; + return input.async ? Promise.resolve().then(finish) : finish(); + } +} +export const number = () => new NumberSchema({ name: "number" }); +export const object = (shape) => new ObjectSchema({ name: "object", shape }); +export const record = (valueType) => new RecordSchema({ name: "record", valueType }); +export const map = (valueType) => new MapSchema({ name: "map", valueType }); +export const set = (valueType) => new SetSchema({ name: "set", valueType }); +// Untyped readers of the parse step's method value. +export const stepKinds = (schema) => typeof schema._parse + " " + typeof Object.getPrototypeOf(schema)._parse; +export const stepDetached = async (schema, data) => { + const step = schema._parse; + const sync = step.call(schema, { data, async: false }); + const later = await step.call(schema, { data, async: true }); + return sync.status + " " + later.status; +}; diff --git a/tests/corpus/6592-promise-armed-method-values/node_modules/status-schema/package.json b/tests/corpus/6592-promise-armed-method-values/node_modules/status-schema/package.json new file mode 100644 index 000000000..1160994b1 --- /dev/null +++ b/tests/corpus/6592-promise-armed-method-values/node_modules/status-schema/package.json @@ -0,0 +1 @@ +{ "name": "status-schema", "version": "1.0.0", "type": "module", "main": "index.js", "types": "index.d.ts" } diff --git a/tests/corpus/array-length-limits.ts b/tests/corpus/array-length-limits.ts new file mode 100644 index 000000000..d98d8e311 --- /dev/null +++ b/tests/corpus/array-length-limits.ts @@ -0,0 +1,94 @@ +// Array lengths and indices at the 2^32 - 1 limit, sparse writes past the +// dense capacity, holes and boolean arrays: the length and capacity fit in +// 32 bits, and the dense states follow the value slots. + +function report(label: string, run: () => unknown): void { + try { + console.log(label, run()); + } catch (e) { + console.log(label, e instanceof Error ? `${e.name}: ${e.message}` : e); + } +} + +const big: string[] = []; +big.length = 4294967295; +console.log("max length", big.length); +report("length 2^32", () => { + const a: number[] = []; + a.length = 4294967296; + return a.length; +}); +report("length -1", () => { + const a: number[] = []; + a.length = -1; + return a.length; +}); + +const edge: string[] = []; +edge[4294967294] = "last"; +console.log("max index", edge.length, edge[4294967294]); + +// Sparse writes beyond the dense capacity, then densifying appends. +const sparse: number[] = [1, 2, 3]; +sparse[1000] = 4; +sparse[50] = 5; +console.log("sparse", sparse.length, sparse[1000], sparse[50], 999 in sparse, sparse.indexOf(5)); +for (let i = 3; i < 60; i++) if (!(i in sparse)) sparse[i] = i; +console.log("filled", sparse.slice(0, 8).join(","), sparse[49], sparse[50], sparse.lastIndexOf(4)); +sparse.length = 10; +console.log("truncated", sparse.length, sparse.join(","), 50 in sparse); + +// Holes and their states across growth and splice. +const holes: (string | undefined)[] = ["a", , "c", undefined] as (string | undefined)[]; +console.log("holes", holes.length, 1 in holes, 3 in holes, holes.indexOf(undefined)); +holes.push("e", "f", "g", "h", "i"); +holes.splice(1, 2, "B"); +console.log("spliced", holes.length, JSON.stringify(holes), 1 in holes); +const copy = holes.slice(0, 3); +console.log("slice", copy.length, copy.join("|")); + +// Boolean arrays keep one byte per element; their states follow them. +const flags: boolean[] = []; +for (let i = 0; i < 40; i++) flags.push(i % 3 === 0); +flags[45] = true; +console.log("bools", flags.length, flags.filter((f) => f).length, 42 in flags, flags.indexOf(true, 40)); +flags.length = 5; +flags.unshift(false); +console.log("bools after", flags.join(","), flags.length); + +// Many one-element arrays (the common case the 48-byte header targets). +const ones: number[][] = []; +for (let i = 0; i < 1000; i++) ones.push([i]); +let total = 0; +for (const one of ones) total += one[0]! + one.length; +console.log("ones", ones.length, total, ones[999]!.concat(ones[0]!).join(",")); + +// Tagged templates hand out an immortal strings array. +function tag(strings: TemplateStringsArray, ...values: number[]): string { + return `${strings.length}:${strings.join("_")}:${values.join(",")}`; +} +console.log("tagged", tag`a${1}b${2}c`, tag`only`); + +// Small arrays filled past their inline slots, of numbers and of class +// instances that can form cycles. +class Node { + readonly id: number; + readonly next: Node[] = []; + constructor(id: number) { + this.id = id; + } +} +const grown: number[][] = []; +for (let n = 0; n < 10; n++) { + const a: number[] = []; + for (let i = 0; i < n; i++) a.push(i * n); + grown.push(a); +} +console.log("grown", grown.map((a) => a.join("-")).join(" ")); +const root = new Node(0); +for (let i = 1; i <= 9; i++) { + const child = new Node(i); + child.next.push(root); + root.next.push(child); + console.log("node", i, root.next.length, root.next.map((c) => c.id).join(""), child.next[0]!.id); +} diff --git a/tests/corpus/class-id-instances-as-streams.ts b/tests/corpus/class-id-instances-as-streams.ts new file mode 100644 index 000000000..813e54ed2 --- /dev/null +++ b/tests/corpus/class-id-instances-as-streams.ts @@ -0,0 +1,39 @@ +// Instances of a class hierarchy, held as untyped values, where the stream +// adapters probe for a native stream: pipe, pipeline, readline, and +// instanceof checks against the stream classes. Their first field is a +// number, which sits where a stream's vtable word would be. +import { Duplex, PassThrough, Readable, Transform, Writable, pipeline } from "node:stream"; +import { createInterface } from "node:readline"; + +class Shape { + size = 3.5; + area(): number { + return 0; + } +} +class Square extends Shape { + override area(): number { + return this.size * this.size; + } +} + +function attempt(label: string, run: () => unknown): void { + try { + run(); + console.log(label, "ok"); + } catch (error) { + console.log(label, "throws", error instanceof Error); + } +} + +const shape: any = new Square(); +try { + Readable.from(["a"]).pipe(shape); +} catch (error) { + console.log("pipe", (error as Error).name); +} +attempt("pipeline", () => pipeline(shape, new PassThrough(), () => {})); +attempt("readline-input", () => createInterface({ input: shape })); +console.log("instanceof", shape instanceof Readable, shape instanceof Writable, shape instanceof Duplex); +console.log("instanceof-transform", shape instanceof Transform, shape instanceof PassThrough); +console.log("area", (shape as Square).area()); diff --git a/tests/corpus/fs-extra-package/main.ts b/tests/corpus/fs-extra-package/main.ts new file mode 100644 index 000000000..1c91a7000 --- /dev/null +++ b/tests/corpus/fs-extra-package/main.ts @@ -0,0 +1,182 @@ +// fs-extra compiles from scriptc's built-in implementation; Node runs the +// real package. node_modules vendors fs-extra 9.0.0 with its dependencies +// (graceful-fs, jsonfile, universalify, at-least-node) and @types/fs-extra +// 9.0.13, unmodified (MIT). The program reads fs-extra through its default +// import, a namespace import and named imports: fs's re-exported members in +// promise and callback form, fs.promises, streams, and the extras (mkdirs, +// copy, remove, pathExists, the JSON helpers, outputFile, emptyDir, move), +// the modes of binary and JSON writes, and the sync copy and move checks. +import fs from "fs-extra"; +import * as fsx from "fs-extra"; +// Named imports use the names Node detects in fs-extra's CommonJS source. +import { copy, outputFile, pathExists, remove } from "fs-extra"; +import * as os from "node:os"; +import * as path from "node:path"; + +function shown(root: string, text: string): string { + return text.split(root).join(""); +} + +async function readStream(file: string): Promise { + return new Promise((resolve, reject) => { + let text = ""; + const stream = fs.createReadStream(file, { encoding: "utf8" }); + stream.on("data", (chunk) => { + text += String(chunk); + }); + stream.on("end", () => resolve(text)); + stream.on("error", reject); + }); +} + +async function main(): Promise { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "scriptc-fs-extra-")); + const file = path.join(root, "a.txt"); + + await fs.writeFile(file, "hello\n"); + await fs.appendFile(file, "world\n"); + console.log(JSON.stringify(await fs.readFile(file, "utf8"))); + console.log((await fs.readFile(file)).length, fs.existsSync(file), fs.existsSync(root + "-missing")); + console.log((await fs.stat(file)).size, (await fs.lstat(file)).isFile(), fs.statSync(root).isDirectory()); + await fs.chmod(file, 0o640); + console.log(((await fs.stat(file)).mode & 0o777).toString(8)); + + const link = path.join(root, "link.txt"); + await fs.symlink(file, link); + console.log(shown(root, await fs.readlink(link)), (await fs.lstat(link)).isSymbolicLink()); + + await fs.ensureDir(path.join(root, "deep", "er")); + await fs.mkdirp(path.join(root, "made", "by", "mkdirp")); + await fs.writeFile(path.join(root, "deep", "er", "leaf.txt"), "leaf"); + console.log((await fs.readdir(root)).sort().join(",")); + + const data = path.join(root, "data.json"); + await fs.writeJson(data, { a: 1, list: [true, null, "x"] }, { spaces: 2 }); + console.log(JSON.stringify(await fs.readFile(data, "utf8"))); + console.log(JSON.stringify(await fs.readJson(data))); + await fs.outputJson(path.join(root, "out", "nested", "o.json"), { ok: true }); + console.log(JSON.stringify(fs.readJSONSync(path.join(root, "out", "nested", "o.json")))); + await outputFile(path.join(root, "out2", "z.txt"), "zed"); + console.log(await fs.readFile(path.join(root, "out2", "z.txt"), "utf8")); + console.log(fs.readJsonSync(file, { throws: false })); + fs.writeFileSync(path.join(root, "bom.json"), '\uFEFF{"bom":true}'); + console.log(JSON.stringify(fs.readJsonSync(path.join(root, "bom.json")))); + + await fs.copy(path.join(root, "deep"), path.join(root, "copied")); + await copy(file, path.join(root, "copied", "a-copy.txt")); + await fsx.copy(file, path.join(root, "copied", "b-copy.txt")); + console.log((await fs.readdir(path.join(root, "copied"))).sort().join(",")); + console.log(await fs.readFile(path.join(root, "copied", "er", "leaf.txt"), "utf8")); + console.log(await pathExists(path.join(root, "copied", "er")), await fs.pathExists(root + "-missing")); + try { + await fs.copy(path.join(root, "deep"), path.join(root, "deep", "er", "inner")); + } catch (err) { + console.log(shown(root, (err as Error).message)); + } + try { + await fs.copy(file, file); + } catch (err) { + console.log((err as Error).message); + } + + await fs.move(path.join(root, "out2"), path.join(root, "moved")); + console.log(await fs.pathExists(path.join(root, "out2")), await fs.pathExists(path.join(root, "moved", "z.txt"))); + try { + await fs.move(path.join(root, "moved"), path.join(root, "copied")); + } catch (err) { + console.log((err as Error).message); + } + await fs.emptyDir(path.join(root, "copied")); + console.log((await fs.readdir(path.join(root, "copied"))).length); + + // Modes of binary and JSON writes, and the sync copy and move checks. + await fs.writeFile(path.join(root, "bytes.bin"), Buffer.from([1, 2, 3]), { mode: 0o600 }); + await fs.outputFile(path.join(root, "out3", "bytes.bin"), new Uint8Array([4, 5]), { mode: 0o600 }); + fs.writeJsonSync(path.join(root, "private.json"), { a: 1 }, { mode: 0o600 }); + fs.outputJsonSync(path.join(root, "out4", "private.json"), { b: 2 }, { mode: 0o600 }); + for (const name of ["bytes.bin", "out3/bytes.bin", "private.json", "out4/private.json"]) + console.log(name, (fs.statSync(path.join(root, name)).mode & 0o777).toString(8)); + const selfLink = path.join(root, "self-link"); + fs.symlinkSync(path.join(root, "deep"), selfLink); + try { + fs.copySync(path.join(root, "deep"), path.join(selfLink, "inner")); + } catch (err) { + console.log(shown(root, (err as Error).message)); + } + try { + fs.moveSync(path.join(root, "deep"), path.join(selfLink, "inner")); + } catch (err) { + console.log(shown(root, (err as Error).message)); + } + try { + fs.moveSync(file, file, { overwrite: true }); + } catch (err) { + console.log((err as Error).message); + } + console.log(fs.existsSync(file), fs.existsSync(path.join(root, "deep", "inner"))); + + // A symlink source over an existing destination entry: a regular file is + // kept and the copy fails with EEXIST; a destination symlink is replaced. + const linkSrc = path.join(root, "link-src"); + fs.mkdirSync(linkSrc); + fs.symlinkSync(file, path.join(linkSrc, "entry")); + for (const sync of [true, false]) { + const linkDest = path.join(root, sync ? "link-dest-sync" : "link-dest-async"); + fs.mkdirSync(linkDest); + fs.writeFileSync(path.join(linkDest, "entry"), "kept"); + try { + if (sync) fs.copySync(linkSrc, linkDest, { overwrite: false }); + else await fs.copy(linkSrc, linkDest, { overwrite: false }); + } catch (err) { + console.log((err as NodeJS.ErrnoException).code, shown(root, (err as Error).message)); + } + const entry = path.join(linkDest, "entry"); + console.log(fs.readFileSync(entry, "utf8"), fs.lstatSync(entry).isSymbolicLink()); + } + const relinkDest = path.join(root, "relink-dest"); + fs.mkdirSync(relinkDest); + fs.symlinkSync(data, path.join(relinkDest, "entry")); + fs.copySync(linkSrc, relinkDest); + console.log(shown(root, fs.readlinkSync(path.join(relinkDest, "entry")))); + + // A destination directory symlink back into the source tree: each entry + // is checked against its destination, so the copy stops there instead + // of replacing the source's own file. + const loopSrc = path.join(root, "loop-src"); + fs.mkdirSync(path.join(loopSrc, "sub"), { recursive: true }); + fs.writeFileSync(path.join(loopSrc, "sub", "keep.txt"), "source"); + const loopDest = path.join(root, "loop-dest"); + fs.mkdirSync(loopDest); + fs.symlinkSync(path.join(loopSrc, "sub"), path.join(loopDest, "sub")); + for (const sync of [true, false]) { + try { + if (sync) fs.copySync(loopSrc, loopDest); + else await fs.copy(loopSrc, loopDest); + } catch (err) { + console.log(shown(root, (err as Error).message)); + } + console.log(fs.readFileSync(path.join(loopSrc, "sub", "keep.txt"), "utf8")); + } + + console.log(await readStream(file)); + console.log((await fs.promises.readFile(file, "utf8")).length); + console.log(fs.readFileSync(file, "utf8").length); + + try { + await fs.readFile(path.join(root, "missing.txt"), "utf8"); + } catch (err) { + console.log((err as NodeJS.ErrnoException).code); + } + + fs.readFile(file, "utf8", (err, text) => { + console.log("callback", err === null, text.length); + fs.ensureDir(path.join(root, "from-callback"), (dirErr) => { + console.log("callback ensureDir", dirErr === null, fs.existsSync(path.join(root, "from-callback"))); + void remove(root).then(async () => { + console.log("removed", await fs.pathExists(root)); + }); + }); + }); +} + +main(); diff --git a/tests/corpus/fs-extra-package/node_modules/@types/fs-extra/LICENSE b/tests/corpus/fs-extra-package/node_modules/@types/fs-extra/LICENSE new file mode 100755 index 000000000..9e841e7a2 --- /dev/null +++ b/tests/corpus/fs-extra-package/node_modules/@types/fs-extra/LICENSE @@ -0,0 +1,21 @@ + MIT License + + Copyright (c) Microsoft Corporation. + + 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/fs-extra-package/node_modules/@types/fs-extra/index.d.ts b/tests/corpus/fs-extra-package/node_modules/@types/fs-extra/index.d.ts new file mode 100755 index 000000000..4daa56482 --- /dev/null +++ b/tests/corpus/fs-extra-package/node_modules/@types/fs-extra/index.d.ts @@ -0,0 +1,563 @@ +// Type definitions for fs-extra 9.0 +// Project: https://github.com/jprichardson/node-fs-extra +// Definitions by: Alan Agius , +// midknight41 , +// Brendan Forster , +// Mees van Dijk , +// Justin Rockwood , +// Sang Dang , +// Florian Keller +// Piotr Błażejewicz +// Tiger Oakes +// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped +// Minimum TypeScript Version: 3.9 + +/// + +import * as fs from 'fs'; +import Stats = fs.Stats; +import PathLike = fs.PathLike; + +export * from 'fs'; + +export function copy(src: string, dest: string, options?: CopyOptions): Promise; +export function copy(src: string, dest: string, callback: (err: Error) => void): void; +export function copy(src: string, dest: string, options: CopyOptions, callback: (err: Error) => void): void; +export function copySync(src: string, dest: string, options?: CopyOptionsSync): void; + +export function copyFile(src: string, dest: string, flags?: number): Promise; +export function copyFile(src: string, dest: string, callback: (err: Error) => void): void; +export function copyFile(src: string, dest: string, flags: number, callback: (err: Error) => void): void; + +export function move(src: string, dest: string, options?: MoveOptions): Promise; +export function move(src: string, dest: string, callback: (err: Error) => void): void; +export function move(src: string, dest: string, options: MoveOptions, callback: (err: Error) => void): void; +export function moveSync(src: string, dest: string, options?: MoveOptions): void; + +export function createFile(file: string): Promise; +export function createFile(file: string, callback: (err: Error) => void): void; +export function createFileSync(file: string): void; + +export function createSymlink(src: string, dest: string, type: SymlinkType): Promise; +export function createSymlink(src: string, dest: string, type: SymlinkType, callback?: (err: Error) => void): void; +export function createSymlinkSync(src: string, dest: string, type: SymlinkType): void; + +export function ensureDir(path: string, options?: EnsureOptions | number): Promise; +export function ensureDir(path: string, callback?: (err: Error) => void): void; +export function ensureDir(path: string, options?: EnsureOptions | number, callback?: (err: Error) => void): void; +export function ensureDirSync(path: string, options?: EnsureOptions | number): void; + +export function mkdirs(dir: string): Promise; +export function mkdirs(dir: string, callback: (err: Error) => void): void; +export function mkdirp(dir: string): Promise; +export function mkdirp(dir: string, callback: (err: Error) => void): void; +export function mkdirsSync(dir: string): void; +export function mkdirpSync(dir: string): void; + +export function outputFile( + file: string, + data: any, + options?: WriteFileOptions | BufferEncoding | string, +): Promise; +export function outputFile(file: string, data: any, callback: (err: Error) => void): void; +export function outputFile( + file: string, + data: any, + options: WriteFileOptions | string, + callback: (err: Error) => void, +): void; +export function outputFileSync(file: string, data: any, options?: WriteFileOptions | BufferEncoding | string): void; + +export function readJson(file: string, options?: ReadOptions | BufferEncoding | string): Promise; +export function readJson(file: string, callback: (err: Error, jsonObject: any) => void): void; +export function readJson( + file: string, + options: ReadOptions | BufferEncoding | string, + callback: (err: Error, jsonObject: any) => void, +): void; +export function readJSON(file: string, options?: ReadOptions | BufferEncoding | string): Promise; +export function readJSON(file: string, callback: (err: Error, jsonObject: any) => void): void; +export function readJSON( + file: string, + options: ReadOptions | BufferEncoding | string, + callback: (err: Error, jsonObject: any) => void, +): void; + +export function readJsonSync(file: string, options?: ReadOptions | BufferEncoding | string): any; +export function readJSONSync(file: string, options?: ReadOptions | BufferEncoding | string): any; + +export function remove(dir: string, callback: (err: Error) => void): void; +export function remove(dir: string, callback?: (err: Error) => void): Promise; +export function removeSync(dir: string): void; + +export function outputJSON(file: string, data: any, options?: WriteOptions | BufferEncoding | string): Promise; +export function outputJSON( + file: string, + data: any, + options: WriteOptions | BufferEncoding | string, + callback: (err: Error) => void, +): void; +export function outputJSON(file: string, data: any, callback: (err: Error) => void): void; +export function outputJson(file: string, data: any, options?: WriteOptions | BufferEncoding | string): Promise; +export function outputJson( + file: string, + data: any, + options: WriteOptions | BufferEncoding | string, + callback: (err: Error) => void, +): void; +export function outputJson(file: string, data: any, callback: (err: Error) => void): void; +export function outputJsonSync(file: string, data: any, options?: WriteOptions | BufferEncoding | string): void; +export function outputJSONSync(file: string, data: any, options?: WriteOptions | BufferEncoding | string): void; + +export function writeJSON(file: string, object: any, options?: WriteOptions | BufferEncoding | string): Promise; +export function writeJSON(file: string, object: any, callback: (err: Error) => void): void; +export function writeJSON( + file: string, + object: any, + options: WriteOptions | BufferEncoding | string, + callback: (err: Error) => void, +): void; +export function writeJson(file: string, object: any, options?: WriteOptions | BufferEncoding | string): Promise; +export function writeJson(file: string, object: any, callback: (err: Error) => void): void; +export function writeJson( + file: string, + object: any, + options: WriteOptions | BufferEncoding | string, + callback: (err: Error) => void, +): void; + +export function writeJsonSync(file: string, object: any, options?: WriteOptions | BufferEncoding | string): void; +export function writeJSONSync(file: string, object: any, options?: WriteOptions | BufferEncoding | string): void; + +export function ensureFile(path: string): Promise; +export function ensureFile(path: string, callback: (err: Error) => void): void; +export function ensureFileSync(path: string): void; + +export function ensureLink(src: string, dest: string): Promise; +export function ensureLink(src: string, dest: string, callback: (err: Error) => void): void; +// alias for ensureLink +export const createLink: typeof ensureLink; +export function ensureLinkSync(src: string, dest: string): void; +// aliased as +export const createLinkSync: typeof ensureLinkSync; + +export function ensureSymlink(src: string, dest: string, type?: SymlinkType): Promise; +export function ensureSymlink(src: string, dest: string, type: SymlinkType, callback: (err: Error) => void): void; +export function ensureSymlink(src: string, dest: string, callback: (err: Error) => void): void; +export function ensureSymlinkSync(src: string, dest: string, type?: SymlinkType): void; + +export function emptyDir(path: string): Promise; +export function emptyDir(path: string, callback: (err: Error) => void): void; +export const emptydir: typeof emptyDir; + +export function emptyDirSync(path: string): void; +export const emptydirSync: typeof emptyDirSync; + +export function pathExists(path: string): Promise; +export function pathExists(path: string, callback: (err: Error, exists: boolean) => void): void; +export function pathExistsSync(path: string): boolean; + +// fs async methods +// copied from https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/node/v6/index.d.ts + +export function access(path: PathLike, callback: (err: NodeJS.ErrnoException) => void): void; +export function access(path: PathLike, mode: number, callback: (err: NodeJS.ErrnoException) => void): void; +export function access(path: PathLike, mode?: number): Promise; + +export function appendFile( + file: PathLike | number, + data: any, + options: { + encoding?: BufferEncoding | string | undefined; + mode?: number | string | undefined; + flag?: string | undefined; + }, + callback: (err: NodeJS.ErrnoException) => void, +): void; +export function appendFile(file: PathLike | number, data: any, callback: (err: NodeJS.ErrnoException) => void): void; +export function appendFile( + file: PathLike | number, + data: any, + options?: + | { + encoding?: BufferEncoding | string | undefined; + mode?: number | string | undefined; + flag?: string | undefined; + } + | BufferEncoding + | string, +): Promise; + +export function chmod(path: PathLike, mode: Mode, callback: (err: NodeJS.ErrnoException) => void): void; +export function chmod(path: PathLike, mode: Mode): Promise; + +export function chown(path: PathLike, uid: number, gid: number): Promise; +export function chown(path: PathLike, uid: number, gid: number, callback: (err: NodeJS.ErrnoException) => void): void; + +export function close(fd: number, callback: (err: NodeJS.ErrnoException) => void): void; +export function close(fd: number): Promise; + +export function fchmod(fd: number, mode: Mode, callback: (err: NodeJS.ErrnoException) => void): void; +export function fchmod(fd: number, mode: Mode): Promise; + +export function fchown(fd: number, uid: number, gid: number, callback: (err: NodeJS.ErrnoException) => void): void; +export function fchown(fd: number, uid: number, gid: number): Promise; + +export function fdatasync(fd: number, callback: () => void): void; +export function fdatasync(fd: number): Promise; + +export function fstat(fd: number, callback: (err: NodeJS.ErrnoException, stats: Stats) => any): void; +export function fstat(fd: number): Promise; + +export function fsync(fd: number, callback: (err: NodeJS.ErrnoException) => void): void; +export function fsync(fd: number): Promise; + +export function ftruncate(fd: number, callback: (err: NodeJS.ErrnoException) => void): void; +export function ftruncate(fd: number, len: number, callback: (err: NodeJS.ErrnoException) => void): void; +export function ftruncate(fd: number, len?: number): Promise; + +export function futimes(fd: number, atime: number, mtime: number, callback: (err: NodeJS.ErrnoException) => void): void; +export function futimes(fd: number, atime: Date, mtime: Date, callback: (err: NodeJS.ErrnoException) => void): void; +export function futimes(fd: number, atime: number, mtime: number): Promise; +export function futimes(fd: number, atime: Date, mtime: Date): Promise; + +export function lchown(path: PathLike, uid: number, gid: number, callback: (err: NodeJS.ErrnoException) => void): void; +export function lchown(path: PathLike, uid: number, gid: number): Promise; + +export function link(existingPath: PathLike, newPath: PathLike, callback: (err: NodeJS.ErrnoException) => void): void; +export function link(existingPath: PathLike, newPath: PathLike): Promise; + +export function lstat(path: PathLike, callback: (err: NodeJS.ErrnoException, stats: Stats) => any): void; +export function lstat(path: PathLike): Promise; + +/** + * Asynchronous mkdir - creates the directory specified in {path}. Parameter {mode} defaults to 0777. + * + * @param callback No arguments other than a possible exception are given to the completion callback. + */ +export function mkdir(path: PathLike, callback: (err: NodeJS.ErrnoException) => void): void; +/** + * Asynchronous mkdir - creates the directory specified in {path}. Parameter {mode} defaults to 0777. + * + * @param callback No arguments other than a possible exception are given to the completion callback. + */ +export function mkdir( + path: PathLike, + options: Mode | fs.MakeDirectoryOptions | null, + callback: (err: NodeJS.ErrnoException) => void, +): void; +export function mkdir(path: PathLike, options?: Mode | fs.MakeDirectoryOptions | null): Promise; +export function mkdirSync(path: PathLike, options?: Mode | fs.MakeDirectoryOptions | null): void; + +export function open( + path: PathLike, + flags: string | number, + callback: (err: NodeJS.ErrnoException, fd: number) => void, +): void; +export function open( + path: PathLike, + flags: string | number, + mode: Mode, + callback: (err: NodeJS.ErrnoException, fd: number) => void, +): void; +export function open(path: PathLike, flags: string | number, mode?: Mode | null): Promise; + +export function opendir(path: string, cb: (err: NodeJS.ErrnoException | null, dir: fs.Dir) => void): void; +export function opendir( + path: string, + options: fs.OpenDirOptions, + cb: (err: NodeJS.ErrnoException | null, dir: fs.Dir) => void, +): void; +export function opendir(path: string, options?: fs.OpenDirOptions): Promise; + +export function read( + fd: number, + buffer: TBuffer, + offset: number, + length: number, + position: number | null, + callback: (err: NodeJS.ErrnoException, bytesRead: number, buffer: TBuffer) => void, +): void; +export function read( + fd: number, + buffer: TBuffer, + offset: number, + length: number, + position: number | null, +): Promise<{ bytesRead: number; buffer: TBuffer }>; + +export function readFile(file: PathLike | number, callback: (err: NodeJS.ErrnoException, data: Buffer) => void): void; +export function readFile( + file: PathLike | number, + encoding: BufferEncoding | string, + callback: (err: NodeJS.ErrnoException, data: string) => void, +): void; +export function readFile( + file: PathLike | number, + options: { flag?: string | undefined } | { encoding: BufferEncoding | string; flag?: string | undefined }, + callback: (err: NodeJS.ErrnoException, data: Buffer) => void, +): void; +export function readFile( + file: PathLike | number, + options: { flag?: string | undefined } | { encoding: BufferEncoding | string; flag?: string | undefined }, +): Promise; +// tslint:disable-next-line:unified-signatures +export function readFile(file: PathLike | number, encoding: BufferEncoding | string): Promise; +export function readFile(file: PathLike | number): Promise; + +export function readdir(path: PathLike, callback: (err: NodeJS.ErrnoException, files: string[]) => void): void; +export function readdir( + path: PathLike, + options: 'buffer' | { encoding: 'buffer'; withFileTypes?: false | undefined }, +): Promise; +export function readdir( + path: PathLike, + options?: + | { encoding: BufferEncoding | string | null; withFileTypes?: false | undefined } + | BufferEncoding + | string + | null, +): Promise; +export function readdir( + path: PathLike, + options?: { encoding?: BufferEncoding | string | null | undefined; withFileTypes?: false | undefined }, +): Promise; +export function readdir( + path: PathLike, + options: { encoding?: BufferEncoding | string | null | undefined; withFileTypes: true }, +): Promise; + +export function readlink(path: PathLike, callback: (err: NodeJS.ErrnoException, linkString: string) => any): void; +export function readlink(path: PathLike): Promise; + +export function realpath(path: PathLike, callback: (err: NodeJS.ErrnoException, resolvedPath: string) => any): void; +export function realpath( + path: PathLike, + cache: { [path: string]: string }, + callback: (err: NodeJS.ErrnoException, resolvedPath: string) => any, +): void; +export function realpath(path: PathLike, cache?: { [path: string]: string }): Promise; + +/* tslint:disable:unified-signatures */ +export namespace realpath { + const native: { + (path: PathLike, options: { encoding: 'buffer' } | 'buffer'): Promise; + ( + path: PathLike, + options: { encoding: BufferEncoding | string | null } | BufferEncoding | string | undefined | null, + ): Promise; + (path: PathLike, options: { encoding: BufferEncoding | string | null } | string | undefined | null): Promise< + string | Buffer + >; + (path: PathLike): Promise; + } & typeof fs.realpath.native; +} +/* tslint:enable:unified-signatures */ + +export function rename(oldPath: PathLike, newPath: PathLike, callback: (err: NodeJS.ErrnoException) => void): void; +export function rename(oldPath: PathLike, newPath: PathLike): Promise; + +/** + * Asynchronously removes files and directories (modeled on the standard POSIX + * `rm` utility). + * + * Only available in node >= v14.14.0 + */ +export function rm( + path: PathLike, + options?: { + force?: boolean | undefined; + maxRetries?: number | undefined; + recursive?: boolean | undefined; + retryDelay?: number | undefined; + }, +): Promise; + +/** + * Asynchronous rmdir - removes the directory specified in {path} + * + * @param callback No arguments other than a possible exception are given to the completion callback. + */ +export function rmdir(path: PathLike, callback: (err: NodeJS.ErrnoException) => void): void; +export function rmdir(path: PathLike, options?: fs.RmDirOptions): Promise; + +export function stat(path: PathLike, callback: (err: NodeJS.ErrnoException, stats: Stats) => any): void; +export function stat(path: PathLike): Promise; + +export function symlink( + target: PathLike, + path: PathLike, + type: SymlinkType | undefined, + callback: (err: NodeJS.ErrnoException) => void, +): void; +export function symlink(target: PathLike, path: PathLike, callback: (err: NodeJS.ErrnoException) => void): void; +export function symlink(target: PathLike, path: PathLike, type?: SymlinkType): Promise; + +export function truncate(path: PathLike, callback: (err: NodeJS.ErrnoException) => void): void; +export function truncate(path: PathLike, len: number, callback: (err: NodeJS.ErrnoException) => void): void; +export function truncate(path: PathLike, len?: number): Promise; + +/** + * Asynchronous unlink - deletes the file specified in {path} + * + * @param callback No arguments other than a possible exception are given to the completion callback. + */ +export function unlink(path: PathLike, callback: (err: NodeJS.ErrnoException) => void): void; +export function unlink(path: PathLike): Promise; + +export function utimes( + path: PathLike, + atime: number, + mtime: number, + callback: (err: NodeJS.ErrnoException) => void, +): void; +export function utimes(path: PathLike, atime: Date, mtime: Date, callback: (err: NodeJS.ErrnoException) => void): void; +export function utimes(path: PathLike, atime: number, mtime: number): Promise; +export function utimes(path: PathLike, atime: Date, mtime: Date): Promise; + +export function write( + fd: number, + buffer: TBuffer, + offset: number, + length: number, + position: number | null, + callback: (err: NodeJS.ErrnoException, written: number, buffer: TBuffer) => void, +): void; +export function write( + fd: number, + buffer: TBuffer, + offset: number, + length: number, + callback: (err: NodeJS.ErrnoException, written: number, buffer: TBuffer) => void, +): void; +export function write( + fd: number, + data: any, + callback: (err: NodeJS.ErrnoException, written: number, str: string) => void, +): void; +export function write( + fd: number, + data: any, + offset: number, + callback: (err: NodeJS.ErrnoException, written: number, str: string) => void, +): void; +export function write( + fd: number, + data: any, + offset: number, + encoding: BufferEncoding | string, + callback: (err: NodeJS.ErrnoException, written: number, str: string) => void, +): void; +export function write( + fd: number, + buffer: TBuffer, + offset?: number, + length?: number, + position?: number | null, +): Promise<{ bytesWritten: number; buffer: TBuffer }>; +export function write( + fd: number, + data: any, + offset?: number, + encoding?: BufferEncoding | string, +): Promise<{ bytesWritten: number; buffer: string }>; + +export function writeFile(file: PathLike | number, data: any, callback: (err: NodeJS.ErrnoException) => void): void; +export function writeFile( + file: PathLike | number, + data: any, + options?: WriteFileOptions | BufferEncoding | string, +): Promise; +export function writeFile( + file: PathLike | number, + data: any, + options: WriteFileOptions | BufferEncoding | string, + callback: (err: NodeJS.ErrnoException) => void, +): void; + +export function writev( + fd: number, + buffers: NodeJS.ArrayBufferView[], + position: number, + cb: (err: NodeJS.ErrnoException | null, bytesWritten: number, buffers: NodeJS.ArrayBufferView[]) => void, +): void; +export function writev( + fd: number, + buffers: NodeJS.ArrayBufferView[], + cb: (err: NodeJS.ErrnoException | null, bytesWritten: number, buffers: NodeJS.ArrayBufferView[]) => void, +): void; +export function writev(fd: number, buffers: NodeJS.ArrayBufferView[], position?: number): Promise; + +/** + * Asynchronous mkdtemp - Creates a unique temporary directory. Generates six random characters to be appended behind a required prefix to create a unique temporary directory. + * + * @param callback The created folder path is passed as a string to the callback's second parameter. + */ +export function mkdtemp(prefix: string): Promise; +export function mkdtemp(prefix: string, callback: (err: NodeJS.ErrnoException, folder: string) => void): void; + +export interface PathEntry { + path: string; + stats: Stats; +} + +export interface PathEntryStream { + read(): PathEntry | null; +} + +export type CopyFilterSync = (src: string, dest: string) => boolean; +export type CopyFilterAsync = (src: string, dest: string) => Promise; + +export type SymlinkType = 'dir' | 'file' | 'junction'; + +export type Mode = string | number; + +export type ArrayBufferView = NodeJS.TypedArray | DataView; + +export interface CopyOptions { + dereference?: boolean | undefined; + overwrite?: boolean | undefined; + preserveTimestamps?: boolean | undefined; + errorOnExist?: boolean | undefined; + filter?: CopyFilterSync | CopyFilterAsync | undefined; + recursive?: boolean | undefined; +} + +export interface CopyOptionsSync extends CopyOptions { + filter?: CopyFilterSync | undefined; +} + +export interface EnsureOptions { + mode?: number | undefined; +} + +export interface MoveOptions { + overwrite?: boolean | undefined; + limit?: number | undefined; +} + +export interface ReadOptions { + throws?: boolean | undefined; + fs?: object | undefined; + reviver?: any; + encoding?: BufferEncoding | string | undefined; + flag?: string | undefined; +} + +export interface WriteFileOptions { + encoding?: BufferEncoding | string | null | undefined; + flag?: string | undefined; + mode?: number | undefined; +} + +export interface WriteOptions extends WriteFileOptions { + fs?: object | undefined; + replacer?: any; + spaces?: number | string | undefined; + EOL?: string | undefined; +} + +export interface WritevResult { + bytesWritten: number; + buffers: ArrayBufferView[]; +} diff --git a/tests/corpus/fs-extra-package/node_modules/@types/fs-extra/package.json b/tests/corpus/fs-extra-package/node_modules/@types/fs-extra/package.json new file mode 100755 index 000000000..0dc104089 --- /dev/null +++ b/tests/corpus/fs-extra-package/node_modules/@types/fs-extra/package.json @@ -0,0 +1,67 @@ +{ + "name": "@types/fs-extra", + "version": "9.0.13", + "description": "TypeScript definitions for fs-extra", + "homepage": "https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/fs-extra", + "license": "MIT", + "contributors": [ + { + "name": "Alan Agius", + "url": "https://github.com/alan-agius4", + "githubUsername": "alan-agius4" + }, + { + "name": "midknight41", + "url": "https://github.com/midknight41", + "githubUsername": "midknight41" + }, + { + "name": "Brendan Forster", + "url": "https://github.com/shiftkey", + "githubUsername": "shiftkey" + }, + { + "name": "Mees van Dijk", + "url": "https://github.com/mees-", + "githubUsername": "mees-" + }, + { + "name": "Justin Rockwood", + "url": "https://github.com/jrockwood", + "githubUsername": "jrockwood" + }, + { + "name": "Sang Dang", + "url": "https://github.com/sangdth", + "githubUsername": "sangdth" + }, + { + "name": "Florian Keller", + "url": "https://github.com/ffflorian", + "githubUsername": "ffflorian" + }, + { + "name": "Piotr Błażejewicz", + "url": "https://github.com/peterblazejewicz", + "githubUsername": "peterblazejewicz" + }, + { + "name": "Tiger Oakes", + "url": "https://github.com/NotWoods", + "githubUsername": "NotWoods" + } + ], + "main": "", + "types": "index.d.ts", + "repository": { + "type": "git", + "url": "https://github.com/DefinitelyTyped/DefinitelyTyped.git", + "directory": "types/fs-extra" + }, + "scripts": {}, + "dependencies": { + "@types/node": "*" + }, + "typesPublisherContentHash": "75e10d16a18d936a50e54414024604aa9484e8345b37c2027921869f254523ee", + "typeScriptVersion": "3.9" +} \ No newline at end of file diff --git a/tests/corpus/fs-extra-package/node_modules/at-least-node/LICENSE b/tests/corpus/fs-extra-package/node_modules/at-least-node/LICENSE new file mode 100644 index 000000000..5e29ccc5e --- /dev/null +++ b/tests/corpus/fs-extra-package/node_modules/at-least-node/LICENSE @@ -0,0 +1,6 @@ +The ISC License +Copyright (c) 2020 Ryan Zimmerman + +Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/tests/corpus/fs-extra-package/node_modules/at-least-node/index.js b/tests/corpus/fs-extra-package/node_modules/at-least-node/index.js new file mode 100644 index 000000000..974a2fa85 --- /dev/null +++ b/tests/corpus/fs-extra-package/node_modules/at-least-node/index.js @@ -0,0 +1,5 @@ +module.exports = r => { + const n = process.versions.node.split('.').map(x => parseInt(x, 10)) + r = r.split('.').map(x => parseInt(x, 10)) + return n[0] > r[0] || (n[0] === r[0] && (n[1] > r[1] || (n[1] === r[1] && n[2] >= r[2]))) +} diff --git a/tests/corpus/fs-extra-package/node_modules/at-least-node/package.json b/tests/corpus/fs-extra-package/node_modules/at-least-node/package.json new file mode 100644 index 000000000..2c13641f9 --- /dev/null +++ b/tests/corpus/fs-extra-package/node_modules/at-least-node/package.json @@ -0,0 +1,32 @@ +{ + "name": "at-least-node", + "version": "1.0.0", + "description": "Lightweight Node.js version sniffing/comparison", + "keywords": [ + "semver", + "feature" + ], + "homepage": "https://github.com/RyanZim/at-least-node#readme", + "bugs": { + "url": "https://github.com/RyanZim/at-least-node/issues" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/RyanZim/at-least-node.git" + }, + "license": "ISC", + "author": "Ryan Zimmerman ", + "main": "index.js", + "files": [], + "scripts": { + "test": "ava" + }, + "devDependencies": { + "ava": "^3.1.0", + "rewire": "^4.0.1", + "semver": "^7.1.2" + }, + "engines": { + "node": ">= 4.0.0" + } +} diff --git a/tests/corpus/fs-extra-package/node_modules/fs-extra/LICENSE b/tests/corpus/fs-extra-package/node_modules/fs-extra/LICENSE new file mode 100644 index 000000000..93546dfb7 --- /dev/null +++ b/tests/corpus/fs-extra-package/node_modules/fs-extra/LICENSE @@ -0,0 +1,15 @@ +(The MIT License) + +Copyright (c) 2011-2017 JP Richardson + +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/fs-extra-package/node_modules/fs-extra/lib/copy-sync/copy-sync.js b/tests/corpus/fs-extra-package/node_modules/fs-extra/lib/copy-sync/copy-sync.js new file mode 100644 index 000000000..31f06e441 --- /dev/null +++ b/tests/corpus/fs-extra-package/node_modules/fs-extra/lib/copy-sync/copy-sync.js @@ -0,0 +1,166 @@ +'use strict' + +const fs = require('graceful-fs') +const path = require('path') +const mkdirsSync = require('../mkdirs').mkdirsSync +const utimesMillisSync = require('../util/utimes').utimesMillisSync +const stat = require('../util/stat') + +function copySync (src, dest, opts) { + if (typeof opts === 'function') { + opts = { filter: opts } + } + + opts = opts || {} + opts.clobber = 'clobber' in opts ? !!opts.clobber : true // default to true for now + opts.overwrite = 'overwrite' in opts ? !!opts.overwrite : opts.clobber // overwrite falls back to clobber + + // Warn about using preserveTimestamps on 32-bit node + if (opts.preserveTimestamps && process.arch === 'ia32') { + console.warn(`fs-extra: Using the preserveTimestamps option in 32-bit node is not recommended;\n + see https://github.com/jprichardson/node-fs-extra/issues/269`) + } + + const { srcStat, destStat } = stat.checkPathsSync(src, dest, 'copy') + stat.checkParentPathsSync(src, srcStat, dest, 'copy') + return handleFilterAndCopy(destStat, src, dest, opts) +} + +function handleFilterAndCopy (destStat, src, dest, opts) { + if (opts.filter && !opts.filter(src, dest)) return + const destParent = path.dirname(dest) + if (!fs.existsSync(destParent)) mkdirsSync(destParent) + return startCopy(destStat, src, dest, opts) +} + +function startCopy (destStat, src, dest, opts) { + if (opts.filter && !opts.filter(src, dest)) return + return getStats(destStat, src, dest, opts) +} + +function getStats (destStat, src, dest, opts) { + const statSync = opts.dereference ? fs.statSync : fs.lstatSync + const srcStat = statSync(src) + + if (srcStat.isDirectory()) return onDir(srcStat, destStat, src, dest, opts) + else if (srcStat.isFile() || + srcStat.isCharacterDevice() || + srcStat.isBlockDevice()) return onFile(srcStat, destStat, src, dest, opts) + else if (srcStat.isSymbolicLink()) return onLink(destStat, src, dest, opts) +} + +function onFile (srcStat, destStat, src, dest, opts) { + if (!destStat) return copyFile(srcStat, src, dest, opts) + return mayCopyFile(srcStat, src, dest, opts) +} + +function mayCopyFile (srcStat, src, dest, opts) { + if (opts.overwrite) { + fs.unlinkSync(dest) + return copyFile(srcStat, src, dest, opts) + } else if (opts.errorOnExist) { + throw new Error(`'${dest}' already exists`) + } +} + +function copyFile (srcStat, src, dest, opts) { + fs.copyFileSync(src, dest) + if (opts.preserveTimestamps) handleTimestamps(srcStat.mode, src, dest) + return setDestMode(dest, srcStat.mode) +} + +function handleTimestamps (srcMode, src, dest) { + // Make sure the file is writable before setting the timestamp + // otherwise open fails with EPERM when invoked with 'r+' + // (through utimes call) + if (fileIsNotWritable(srcMode)) makeFileWritable(dest, srcMode) + return setDestTimestamps(src, dest) +} + +function fileIsNotWritable (srcMode) { + return (srcMode & 0o200) === 0 +} + +function makeFileWritable (dest, srcMode) { + return setDestMode(dest, srcMode | 0o200) +} + +function setDestMode (dest, srcMode) { + return fs.chmodSync(dest, srcMode) +} + +function setDestTimestamps (src, dest) { + // The initial srcStat.atime cannot be trusted + // because it is modified by the read(2) system call + // (See https://nodejs.org/api/fs.html#fs_stat_time_values) + const updatedSrcStat = fs.statSync(src) + return utimesMillisSync(dest, updatedSrcStat.atime, updatedSrcStat.mtime) +} + +function onDir (srcStat, destStat, src, dest, opts) { + if (!destStat) return mkDirAndCopy(srcStat.mode, src, dest, opts) + if (destStat && !destStat.isDirectory()) { + throw new Error(`Cannot overwrite non-directory '${dest}' with directory '${src}'.`) + } + return copyDir(src, dest, opts) +} + +function mkDirAndCopy (srcMode, src, dest, opts) { + fs.mkdirSync(dest) + copyDir(src, dest, opts) + return setDestMode(dest, srcMode) +} + +function copyDir (src, dest, opts) { + fs.readdirSync(src).forEach(item => copyDirItem(item, src, dest, opts)) +} + +function copyDirItem (item, src, dest, opts) { + const srcItem = path.join(src, item) + const destItem = path.join(dest, item) + const { destStat } = stat.checkPathsSync(srcItem, destItem, 'copy') + return startCopy(destStat, srcItem, destItem, opts) +} + +function onLink (destStat, src, dest, opts) { + let resolvedSrc = fs.readlinkSync(src) + if (opts.dereference) { + resolvedSrc = path.resolve(process.cwd(), resolvedSrc) + } + + if (!destStat) { + return fs.symlinkSync(resolvedSrc, dest) + } else { + let resolvedDest + try { + resolvedDest = fs.readlinkSync(dest) + } catch (err) { + // dest exists and is a regular file or directory, + // Windows may throw UNKNOWN error. If dest already exists, + // fs throws error anyway, so no need to guard against it here. + if (err.code === 'EINVAL' || err.code === 'UNKNOWN') return fs.symlinkSync(resolvedSrc, dest) + throw err + } + if (opts.dereference) { + resolvedDest = path.resolve(process.cwd(), resolvedDest) + } + if (stat.isSrcSubdir(resolvedSrc, resolvedDest)) { + throw new Error(`Cannot copy '${resolvedSrc}' to a subdirectory of itself, '${resolvedDest}'.`) + } + + // prevent copy if src is a subdir of dest since unlinking + // dest in this case would result in removing src contents + // and therefore a broken symlink would be created. + if (fs.statSync(dest).isDirectory() && stat.isSrcSubdir(resolvedDest, resolvedSrc)) { + throw new Error(`Cannot overwrite '${resolvedDest}' with '${resolvedSrc}'.`) + } + return copyLink(resolvedSrc, dest) + } +} + +function copyLink (resolvedSrc, dest) { + fs.unlinkSync(dest) + return fs.symlinkSync(resolvedSrc, dest) +} + +module.exports = copySync diff --git a/tests/corpus/fs-extra-package/node_modules/fs-extra/lib/copy-sync/index.js b/tests/corpus/fs-extra-package/node_modules/fs-extra/lib/copy-sync/index.js new file mode 100644 index 000000000..65945aedb --- /dev/null +++ b/tests/corpus/fs-extra-package/node_modules/fs-extra/lib/copy-sync/index.js @@ -0,0 +1,5 @@ +'use strict' + +module.exports = { + copySync: require('./copy-sync') +} diff --git a/tests/corpus/fs-extra-package/node_modules/fs-extra/lib/copy/copy.js b/tests/corpus/fs-extra-package/node_modules/fs-extra/lib/copy/copy.js new file mode 100644 index 000000000..328f1025f --- /dev/null +++ b/tests/corpus/fs-extra-package/node_modules/fs-extra/lib/copy/copy.js @@ -0,0 +1,232 @@ +'use strict' + +const fs = require('graceful-fs') +const path = require('path') +const mkdirs = require('../mkdirs').mkdirs +const pathExists = require('../path-exists').pathExists +const utimesMillis = require('../util/utimes').utimesMillis +const stat = require('../util/stat') + +function copy (src, dest, opts, cb) { + if (typeof opts === 'function' && !cb) { + cb = opts + opts = {} + } else if (typeof opts === 'function') { + opts = { filter: opts } + } + + cb = cb || function () {} + opts = opts || {} + + opts.clobber = 'clobber' in opts ? !!opts.clobber : true // default to true for now + opts.overwrite = 'overwrite' in opts ? !!opts.overwrite : opts.clobber // overwrite falls back to clobber + + // Warn about using preserveTimestamps on 32-bit node + if (opts.preserveTimestamps && process.arch === 'ia32') { + console.warn(`fs-extra: Using the preserveTimestamps option in 32-bit node is not recommended;\n + see https://github.com/jprichardson/node-fs-extra/issues/269`) + } + + stat.checkPaths(src, dest, 'copy', (err, stats) => { + if (err) return cb(err) + const { srcStat, destStat } = stats + stat.checkParentPaths(src, srcStat, dest, 'copy', err => { + if (err) return cb(err) + if (opts.filter) return handleFilter(checkParentDir, destStat, src, dest, opts, cb) + return checkParentDir(destStat, src, dest, opts, cb) + }) + }) +} + +function checkParentDir (destStat, src, dest, opts, cb) { + const destParent = path.dirname(dest) + pathExists(destParent, (err, dirExists) => { + if (err) return cb(err) + if (dirExists) return startCopy(destStat, src, dest, opts, cb) + mkdirs(destParent, err => { + if (err) return cb(err) + return startCopy(destStat, src, dest, opts, cb) + }) + }) +} + +function handleFilter (onInclude, destStat, src, dest, opts, cb) { + Promise.resolve(opts.filter(src, dest)).then(include => { + if (include) return onInclude(destStat, src, dest, opts, cb) + return cb() + }, error => cb(error)) +} + +function startCopy (destStat, src, dest, opts, cb) { + if (opts.filter) return handleFilter(getStats, destStat, src, dest, opts, cb) + return getStats(destStat, src, dest, opts, cb) +} + +function getStats (destStat, src, dest, opts, cb) { + const stat = opts.dereference ? fs.stat : fs.lstat + stat(src, (err, srcStat) => { + if (err) return cb(err) + + if (srcStat.isDirectory()) return onDir(srcStat, destStat, src, dest, opts, cb) + else if (srcStat.isFile() || + srcStat.isCharacterDevice() || + srcStat.isBlockDevice()) return onFile(srcStat, destStat, src, dest, opts, cb) + else if (srcStat.isSymbolicLink()) return onLink(destStat, src, dest, opts, cb) + }) +} + +function onFile (srcStat, destStat, src, dest, opts, cb) { + if (!destStat) return copyFile(srcStat, src, dest, opts, cb) + return mayCopyFile(srcStat, src, dest, opts, cb) +} + +function mayCopyFile (srcStat, src, dest, opts, cb) { + if (opts.overwrite) { + fs.unlink(dest, err => { + if (err) return cb(err) + return copyFile(srcStat, src, dest, opts, cb) + }) + } else if (opts.errorOnExist) { + return cb(new Error(`'${dest}' already exists`)) + } else return cb() +} + +function copyFile (srcStat, src, dest, opts, cb) { + fs.copyFile(src, dest, err => { + if (err) return cb(err) + if (opts.preserveTimestamps) return handleTimestampsAndMode(srcStat.mode, src, dest, cb) + return setDestMode(dest, srcStat.mode, cb) + }) +} + +function handleTimestampsAndMode (srcMode, src, dest, cb) { + // Make sure the file is writable before setting the timestamp + // otherwise open fails with EPERM when invoked with 'r+' + // (through utimes call) + if (fileIsNotWritable(srcMode)) { + return makeFileWritable(dest, srcMode, err => { + if (err) return cb(err) + return setDestTimestampsAndMode(srcMode, src, dest, cb) + }) + } + return setDestTimestampsAndMode(srcMode, src, dest, cb) +} + +function fileIsNotWritable (srcMode) { + return (srcMode & 0o200) === 0 +} + +function makeFileWritable (dest, srcMode, cb) { + return setDestMode(dest, srcMode | 0o200, cb) +} + +function setDestTimestampsAndMode (srcMode, src, dest, cb) { + setDestTimestamps(src, dest, err => { + if (err) return cb(err) + return setDestMode(dest, srcMode, cb) + }) +} + +function setDestMode (dest, srcMode, cb) { + return fs.chmod(dest, srcMode, cb) +} + +function setDestTimestamps (src, dest, cb) { + // The initial srcStat.atime cannot be trusted + // because it is modified by the read(2) system call + // (See https://nodejs.org/api/fs.html#fs_stat_time_values) + fs.stat(src, (err, updatedSrcStat) => { + if (err) return cb(err) + return utimesMillis(dest, updatedSrcStat.atime, updatedSrcStat.mtime, cb) + }) +} + +function onDir (srcStat, destStat, src, dest, opts, cb) { + if (!destStat) return mkDirAndCopy(srcStat.mode, src, dest, opts, cb) + if (destStat && !destStat.isDirectory()) { + return cb(new Error(`Cannot overwrite non-directory '${dest}' with directory '${src}'.`)) + } + return copyDir(src, dest, opts, cb) +} + +function mkDirAndCopy (srcMode, src, dest, opts, cb) { + fs.mkdir(dest, err => { + if (err) return cb(err) + copyDir(src, dest, opts, err => { + if (err) return cb(err) + return setDestMode(dest, srcMode, cb) + }) + }) +} + +function copyDir (src, dest, opts, cb) { + fs.readdir(src, (err, items) => { + if (err) return cb(err) + return copyDirItems(items, src, dest, opts, cb) + }) +} + +function copyDirItems (items, src, dest, opts, cb) { + const item = items.pop() + if (!item) return cb() + return copyDirItem(items, item, src, dest, opts, cb) +} + +function copyDirItem (items, item, src, dest, opts, cb) { + const srcItem = path.join(src, item) + const destItem = path.join(dest, item) + stat.checkPaths(srcItem, destItem, 'copy', (err, stats) => { + if (err) return cb(err) + const { destStat } = stats + startCopy(destStat, srcItem, destItem, opts, err => { + if (err) return cb(err) + return copyDirItems(items, src, dest, opts, cb) + }) + }) +} + +function onLink (destStat, src, dest, opts, cb) { + fs.readlink(src, (err, resolvedSrc) => { + if (err) return cb(err) + if (opts.dereference) { + resolvedSrc = path.resolve(process.cwd(), resolvedSrc) + } + + if (!destStat) { + return fs.symlink(resolvedSrc, dest, cb) + } else { + fs.readlink(dest, (err, resolvedDest) => { + if (err) { + // dest exists and is a regular file or directory, + // Windows may throw UNKNOWN error. If dest already exists, + // fs throws error anyway, so no need to guard against it here. + if (err.code === 'EINVAL' || err.code === 'UNKNOWN') return fs.symlink(resolvedSrc, dest, cb) + return cb(err) + } + if (opts.dereference) { + resolvedDest = path.resolve(process.cwd(), resolvedDest) + } + if (stat.isSrcSubdir(resolvedSrc, resolvedDest)) { + return cb(new Error(`Cannot copy '${resolvedSrc}' to a subdirectory of itself, '${resolvedDest}'.`)) + } + + // do not copy if src is a subdir of dest since unlinking + // dest in this case would result in removing src contents + // and therefore a broken symlink would be created. + if (destStat.isDirectory() && stat.isSrcSubdir(resolvedDest, resolvedSrc)) { + return cb(new Error(`Cannot overwrite '${resolvedDest}' with '${resolvedSrc}'.`)) + } + return copyLink(resolvedSrc, dest, cb) + }) + } + }) +} + +function copyLink (resolvedSrc, dest, cb) { + fs.unlink(dest, err => { + if (err) return cb(err) + return fs.symlink(resolvedSrc, dest, cb) + }) +} + +module.exports = copy diff --git a/tests/corpus/fs-extra-package/node_modules/fs-extra/lib/copy/index.js b/tests/corpus/fs-extra-package/node_modules/fs-extra/lib/copy/index.js new file mode 100644 index 000000000..b7e4f7f8d --- /dev/null +++ b/tests/corpus/fs-extra-package/node_modules/fs-extra/lib/copy/index.js @@ -0,0 +1,6 @@ +'use strict' + +const u = require('universalify').fromCallback +module.exports = { + copy: u(require('./copy')) +} diff --git a/tests/corpus/fs-extra-package/node_modules/fs-extra/lib/empty/index.js b/tests/corpus/fs-extra-package/node_modules/fs-extra/lib/empty/index.js new file mode 100644 index 000000000..90fb46991 --- /dev/null +++ b/tests/corpus/fs-extra-package/node_modules/fs-extra/lib/empty/index.js @@ -0,0 +1,48 @@ +'use strict' + +const u = require('universalify').fromCallback +const fs = require('graceful-fs') +const path = require('path') +const mkdir = require('../mkdirs') +const remove = require('../remove') + +const emptyDir = u(function emptyDir (dir, callback) { + callback = callback || function () {} + fs.readdir(dir, (err, items) => { + if (err) return mkdir.mkdirs(dir, callback) + + items = items.map(item => path.join(dir, item)) + + deleteItem() + + function deleteItem () { + const item = items.pop() + if (!item) return callback() + remove.remove(item, err => { + if (err) return callback(err) + deleteItem() + }) + } + }) +}) + +function emptyDirSync (dir) { + let items + try { + items = fs.readdirSync(dir) + } catch { + return mkdir.mkdirsSync(dir) + } + + items.forEach(item => { + item = path.join(dir, item) + remove.removeSync(item) + }) +} + +module.exports = { + emptyDirSync, + emptydirSync: emptyDirSync, + emptyDir, + emptydir: emptyDir +} diff --git a/tests/corpus/fs-extra-package/node_modules/fs-extra/lib/ensure/file.js b/tests/corpus/fs-extra-package/node_modules/fs-extra/lib/ensure/file.js new file mode 100644 index 000000000..15cc473c8 --- /dev/null +++ b/tests/corpus/fs-extra-package/node_modules/fs-extra/lib/ensure/file.js @@ -0,0 +1,69 @@ +'use strict' + +const u = require('universalify').fromCallback +const path = require('path') +const fs = require('graceful-fs') +const mkdir = require('../mkdirs') + +function createFile (file, callback) { + function makeFile () { + fs.writeFile(file, '', err => { + if (err) return callback(err) + callback() + }) + } + + fs.stat(file, (err, stats) => { // eslint-disable-line handle-callback-err + if (!err && stats.isFile()) return callback() + const dir = path.dirname(file) + fs.stat(dir, (err, stats) => { + if (err) { + // if the directory doesn't exist, make it + if (err.code === 'ENOENT') { + return mkdir.mkdirs(dir, err => { + if (err) return callback(err) + makeFile() + }) + } + return callback(err) + } + + if (stats.isDirectory()) makeFile() + else { + // parent is not a directory + // This is just to cause an internal ENOTDIR error to be thrown + fs.readdir(dir, err => { + if (err) return callback(err) + }) + } + }) + }) +} + +function createFileSync (file) { + let stats + try { + stats = fs.statSync(file) + } catch {} + if (stats && stats.isFile()) return + + const dir = path.dirname(file) + try { + if (!fs.statSync(dir).isDirectory()) { + // parent is not a directory + // This is just to cause an internal ENOTDIR error to be thrown + fs.readdirSync(dir) + } + } catch (err) { + // If the stat call above failed because the directory doesn't exist, create it + if (err && err.code === 'ENOENT') mkdir.mkdirsSync(dir) + else throw err + } + + fs.writeFileSync(file, '') +} + +module.exports = { + createFile: u(createFile), + createFileSync +} diff --git a/tests/corpus/fs-extra-package/node_modules/fs-extra/lib/ensure/index.js b/tests/corpus/fs-extra-package/node_modules/fs-extra/lib/ensure/index.js new file mode 100644 index 000000000..c1f67b718 --- /dev/null +++ b/tests/corpus/fs-extra-package/node_modules/fs-extra/lib/ensure/index.js @@ -0,0 +1,23 @@ +'use strict' + +const file = require('./file') +const link = require('./link') +const symlink = require('./symlink') + +module.exports = { + // file + createFile: file.createFile, + createFileSync: file.createFileSync, + ensureFile: file.createFile, + ensureFileSync: file.createFileSync, + // link + createLink: link.createLink, + createLinkSync: link.createLinkSync, + ensureLink: link.createLink, + ensureLinkSync: link.createLinkSync, + // symlink + createSymlink: symlink.createSymlink, + createSymlinkSync: symlink.createSymlinkSync, + ensureSymlink: symlink.createSymlink, + ensureSymlinkSync: symlink.createSymlinkSync +} diff --git a/tests/corpus/fs-extra-package/node_modules/fs-extra/lib/ensure/link.js b/tests/corpus/fs-extra-package/node_modules/fs-extra/lib/ensure/link.js new file mode 100644 index 000000000..2cd419626 --- /dev/null +++ b/tests/corpus/fs-extra-package/node_modules/fs-extra/lib/ensure/link.js @@ -0,0 +1,61 @@ +'use strict' + +const u = require('universalify').fromCallback +const path = require('path') +const fs = require('graceful-fs') +const mkdir = require('../mkdirs') +const pathExists = require('../path-exists').pathExists + +function createLink (srcpath, dstpath, callback) { + function makeLink (srcpath, dstpath) { + fs.link(srcpath, dstpath, err => { + if (err) return callback(err) + callback(null) + }) + } + + pathExists(dstpath, (err, destinationExists) => { + if (err) return callback(err) + if (destinationExists) return callback(null) + fs.lstat(srcpath, (err) => { + if (err) { + err.message = err.message.replace('lstat', 'ensureLink') + return callback(err) + } + + const dir = path.dirname(dstpath) + pathExists(dir, (err, dirExists) => { + if (err) return callback(err) + if (dirExists) return makeLink(srcpath, dstpath) + mkdir.mkdirs(dir, err => { + if (err) return callback(err) + makeLink(srcpath, dstpath) + }) + }) + }) + }) +} + +function createLinkSync (srcpath, dstpath) { + const destinationExists = fs.existsSync(dstpath) + if (destinationExists) return undefined + + try { + fs.lstatSync(srcpath) + } catch (err) { + err.message = err.message.replace('lstat', 'ensureLink') + throw err + } + + const dir = path.dirname(dstpath) + const dirExists = fs.existsSync(dir) + if (dirExists) return fs.linkSync(srcpath, dstpath) + mkdir.mkdirsSync(dir) + + return fs.linkSync(srcpath, dstpath) +} + +module.exports = { + createLink: u(createLink), + createLinkSync +} diff --git a/tests/corpus/fs-extra-package/node_modules/fs-extra/lib/ensure/symlink-paths.js b/tests/corpus/fs-extra-package/node_modules/fs-extra/lib/ensure/symlink-paths.js new file mode 100644 index 000000000..33cd76000 --- /dev/null +++ b/tests/corpus/fs-extra-package/node_modules/fs-extra/lib/ensure/symlink-paths.js @@ -0,0 +1,99 @@ +'use strict' + +const path = require('path') +const fs = require('graceful-fs') +const pathExists = require('../path-exists').pathExists + +/** + * Function that returns two types of paths, one relative to symlink, and one + * relative to the current working directory. Checks if path is absolute or + * relative. If the path is relative, this function checks if the path is + * relative to symlink or relative to current working directory. This is an + * initiative to find a smarter `srcpath` to supply when building symlinks. + * This allows you to determine which path to use out of one of three possible + * types of source paths. The first is an absolute path. This is detected by + * `path.isAbsolute()`. When an absolute path is provided, it is checked to + * see if it exists. If it does it's used, if not an error is returned + * (callback)/ thrown (sync). The other two options for `srcpath` are a + * relative url. By default Node's `fs.symlink` works by creating a symlink + * using `dstpath` and expects the `srcpath` to be relative to the newly + * created symlink. If you provide a `srcpath` that does not exist on the file + * system it results in a broken symlink. To minimize this, the function + * checks to see if the 'relative to symlink' source file exists, and if it + * does it will use it. If it does not, it checks if there's a file that + * exists that is relative to the current working directory, if does its used. + * This preserves the expectations of the original fs.symlink spec and adds + * the ability to pass in `relative to current working direcotry` paths. + */ + +function symlinkPaths (srcpath, dstpath, callback) { + if (path.isAbsolute(srcpath)) { + return fs.lstat(srcpath, (err) => { + if (err) { + err.message = err.message.replace('lstat', 'ensureSymlink') + return callback(err) + } + return callback(null, { + toCwd: srcpath, + toDst: srcpath + }) + }) + } else { + const dstdir = path.dirname(dstpath) + const relativeToDst = path.join(dstdir, srcpath) + return pathExists(relativeToDst, (err, exists) => { + if (err) return callback(err) + if (exists) { + return callback(null, { + toCwd: relativeToDst, + toDst: srcpath + }) + } else { + return fs.lstat(srcpath, (err) => { + if (err) { + err.message = err.message.replace('lstat', 'ensureSymlink') + return callback(err) + } + return callback(null, { + toCwd: srcpath, + toDst: path.relative(dstdir, srcpath) + }) + }) + } + }) + } +} + +function symlinkPathsSync (srcpath, dstpath) { + let exists + if (path.isAbsolute(srcpath)) { + exists = fs.existsSync(srcpath) + if (!exists) throw new Error('absolute srcpath does not exist') + return { + toCwd: srcpath, + toDst: srcpath + } + } else { + const dstdir = path.dirname(dstpath) + const relativeToDst = path.join(dstdir, srcpath) + exists = fs.existsSync(relativeToDst) + if (exists) { + return { + toCwd: relativeToDst, + toDst: srcpath + } + } else { + exists = fs.existsSync(srcpath) + if (!exists) throw new Error('relative srcpath does not exist') + return { + toCwd: srcpath, + toDst: path.relative(dstdir, srcpath) + } + } + } +} + +module.exports = { + symlinkPaths, + symlinkPathsSync +} diff --git a/tests/corpus/fs-extra-package/node_modules/fs-extra/lib/ensure/symlink-type.js b/tests/corpus/fs-extra-package/node_modules/fs-extra/lib/ensure/symlink-type.js new file mode 100644 index 000000000..42dc0ce75 --- /dev/null +++ b/tests/corpus/fs-extra-package/node_modules/fs-extra/lib/ensure/symlink-type.js @@ -0,0 +1,31 @@ +'use strict' + +const fs = require('graceful-fs') + +function symlinkType (srcpath, type, callback) { + callback = (typeof type === 'function') ? type : callback + type = (typeof type === 'function') ? false : type + if (type) return callback(null, type) + fs.lstat(srcpath, (err, stats) => { + if (err) return callback(null, 'file') + type = (stats && stats.isDirectory()) ? 'dir' : 'file' + callback(null, type) + }) +} + +function symlinkTypeSync (srcpath, type) { + let stats + + if (type) return type + try { + stats = fs.lstatSync(srcpath) + } catch { + return 'file' + } + return (stats && stats.isDirectory()) ? 'dir' : 'file' +} + +module.exports = { + symlinkType, + symlinkTypeSync +} diff --git a/tests/corpus/fs-extra-package/node_modules/fs-extra/lib/ensure/symlink.js b/tests/corpus/fs-extra-package/node_modules/fs-extra/lib/ensure/symlink.js new file mode 100644 index 000000000..fe68b7997 --- /dev/null +++ b/tests/corpus/fs-extra-package/node_modules/fs-extra/lib/ensure/symlink.js @@ -0,0 +1,63 @@ +'use strict' + +const u = require('universalify').fromCallback +const path = require('path') +const fs = require('graceful-fs') +const _mkdirs = require('../mkdirs') +const mkdirs = _mkdirs.mkdirs +const mkdirsSync = _mkdirs.mkdirsSync + +const _symlinkPaths = require('./symlink-paths') +const symlinkPaths = _symlinkPaths.symlinkPaths +const symlinkPathsSync = _symlinkPaths.symlinkPathsSync + +const _symlinkType = require('./symlink-type') +const symlinkType = _symlinkType.symlinkType +const symlinkTypeSync = _symlinkType.symlinkTypeSync + +const pathExists = require('../path-exists').pathExists + +function createSymlink (srcpath, dstpath, type, callback) { + callback = (typeof type === 'function') ? type : callback + type = (typeof type === 'function') ? false : type + + pathExists(dstpath, (err, destinationExists) => { + if (err) return callback(err) + if (destinationExists) return callback(null) + symlinkPaths(srcpath, dstpath, (err, relative) => { + if (err) return callback(err) + srcpath = relative.toDst + symlinkType(relative.toCwd, type, (err, type) => { + if (err) return callback(err) + const dir = path.dirname(dstpath) + pathExists(dir, (err, dirExists) => { + if (err) return callback(err) + if (dirExists) return fs.symlink(srcpath, dstpath, type, callback) + mkdirs(dir, err => { + if (err) return callback(err) + fs.symlink(srcpath, dstpath, type, callback) + }) + }) + }) + }) + }) +} + +function createSymlinkSync (srcpath, dstpath, type) { + const destinationExists = fs.existsSync(dstpath) + if (destinationExists) return undefined + + const relative = symlinkPathsSync(srcpath, dstpath) + srcpath = relative.toDst + type = symlinkTypeSync(relative.toCwd, type) + const dir = path.dirname(dstpath) + const exists = fs.existsSync(dir) + if (exists) return fs.symlinkSync(srcpath, dstpath, type) + mkdirsSync(dir) + return fs.symlinkSync(srcpath, dstpath, type) +} + +module.exports = { + createSymlink: u(createSymlink), + createSymlinkSync +} diff --git a/tests/corpus/fs-extra-package/node_modules/fs-extra/lib/fs/index.js b/tests/corpus/fs-extra-package/node_modules/fs-extra/lib/fs/index.js new file mode 100644 index 000000000..101aae9b6 --- /dev/null +++ b/tests/corpus/fs-extra-package/node_modules/fs-extra/lib/fs/index.js @@ -0,0 +1,128 @@ +'use strict' +// This is adapted from https://github.com/normalize/mz +// Copyright (c) 2014-2016 Jonathan Ong me@jongleberry.com and Contributors +const u = require('universalify').fromCallback +const fs = require('graceful-fs') + +const api = [ + 'access', + 'appendFile', + 'chmod', + 'chown', + 'close', + 'copyFile', + 'fchmod', + 'fchown', + 'fdatasync', + 'fstat', + 'fsync', + 'ftruncate', + 'futimes', + 'lchmod', + 'lchown', + 'link', + 'lstat', + 'mkdir', + 'mkdtemp', + 'open', + 'opendir', + 'readdir', + 'readFile', + 'readlink', + 'realpath', + 'rename', + 'rmdir', + 'stat', + 'symlink', + 'truncate', + 'unlink', + 'utimes', + 'writeFile' +].filter(key => { + // Some commands are not available on some systems. Ex: + // fs.opendir was added in Node.js v12.12.0 + // fs.lchown is not available on at least some Linux + return typeof fs[key] === 'function' +}) + +// Export all keys: +Object.keys(fs).forEach(key => { + if (key === 'promises') { + // fs.promises is a getter property that triggers ExperimentalWarning + // Don't re-export it here, the getter is defined in "lib/index.js" + return + } + exports[key] = fs[key] +}) + +// Universalify async methods: +api.forEach(method => { + exports[method] = u(fs[method]) +}) + +// We differ from mz/fs in that we still ship the old, broken, fs.exists() +// since we are a drop-in replacement for the native module +exports.exists = function (filename, callback) { + if (typeof callback === 'function') { + return fs.exists(filename, callback) + } + return new Promise(resolve => { + return fs.exists(filename, resolve) + }) +} + +// fs.read(), fs.write(), & fs.writev() need special treatment due to multiple callback args + +exports.read = function (fd, buffer, offset, length, position, callback) { + if (typeof callback === 'function') { + return fs.read(fd, buffer, offset, length, position, callback) + } + return new Promise((resolve, reject) => { + fs.read(fd, buffer, offset, length, position, (err, bytesRead, buffer) => { + if (err) return reject(err) + resolve({ bytesRead, buffer }) + }) + }) +} + +// Function signature can be +// fs.write(fd, buffer[, offset[, length[, position]]], callback) +// OR +// fs.write(fd, string[, position[, encoding]], callback) +// We need to handle both cases, so we use ...args +exports.write = function (fd, buffer, ...args) { + if (typeof args[args.length - 1] === 'function') { + return fs.write(fd, buffer, ...args) + } + + return new Promise((resolve, reject) => { + fs.write(fd, buffer, ...args, (err, bytesWritten, buffer) => { + if (err) return reject(err) + resolve({ bytesWritten, buffer }) + }) + }) +} + +// fs.writev only available in Node v12.9.0+ +if (typeof fs.writev === 'function') { + // Function signature is + // s.writev(fd, buffers[, position], callback) + // We need to handle the optional arg, so we use ...args + exports.writev = function (fd, buffers, ...args) { + if (typeof args[args.length - 1] === 'function') { + return fs.writev(fd, buffers, ...args) + } + + return new Promise((resolve, reject) => { + fs.writev(fd, buffers, ...args, (err, bytesWritten, buffers) => { + if (err) return reject(err) + resolve({ bytesWritten, buffers }) + }) + }) + } +} + +// fs.realpath.native only available in Node v9.2+ +if (typeof fs.realpath.native === 'function') { + exports.realpath.native = u(fs.realpath.native) +} diff --git a/tests/corpus/fs-extra-package/node_modules/fs-extra/lib/index.js b/tests/corpus/fs-extra-package/node_modules/fs-extra/lib/index.js new file mode 100644 index 000000000..d9468e699 --- /dev/null +++ b/tests/corpus/fs-extra-package/node_modules/fs-extra/lib/index.js @@ -0,0 +1,27 @@ +'use strict' + +module.exports = { + // Export promiseified graceful-fs: + ...require('./fs'), + // Export extra methods: + ...require('./copy-sync'), + ...require('./copy'), + ...require('./empty'), + ...require('./ensure'), + ...require('./json'), + ...require('./mkdirs'), + ...require('./move-sync'), + ...require('./move'), + ...require('./output'), + ...require('./path-exists'), + ...require('./remove') +} + +// Export fs.promises as a getter property so that we don't trigger +// ExperimentalWarning before fs.promises is actually accessed. +const fs = require('fs') +if (Object.getOwnPropertyDescriptor(fs, 'promises')) { + Object.defineProperty(module.exports, 'promises', { + get () { return fs.promises } + }) +} diff --git a/tests/corpus/fs-extra-package/node_modules/fs-extra/lib/json/index.js b/tests/corpus/fs-extra-package/node_modules/fs-extra/lib/json/index.js new file mode 100644 index 000000000..900126ad0 --- /dev/null +++ b/tests/corpus/fs-extra-package/node_modules/fs-extra/lib/json/index.js @@ -0,0 +1,16 @@ +'use strict' + +const u = require('universalify').fromPromise +const jsonFile = require('./jsonfile') + +jsonFile.outputJson = u(require('./output-json')) +jsonFile.outputJsonSync = require('./output-json-sync') +// aliases +jsonFile.outputJSON = jsonFile.outputJson +jsonFile.outputJSONSync = jsonFile.outputJsonSync +jsonFile.writeJSON = jsonFile.writeJson +jsonFile.writeJSONSync = jsonFile.writeJsonSync +jsonFile.readJSON = jsonFile.readJson +jsonFile.readJSONSync = jsonFile.readJsonSync + +module.exports = jsonFile diff --git a/tests/corpus/fs-extra-package/node_modules/fs-extra/lib/json/jsonfile.js b/tests/corpus/fs-extra-package/node_modules/fs-extra/lib/json/jsonfile.js new file mode 100644 index 000000000..f11d34d62 --- /dev/null +++ b/tests/corpus/fs-extra-package/node_modules/fs-extra/lib/json/jsonfile.js @@ -0,0 +1,11 @@ +'use strict' + +const jsonFile = require('jsonfile') + +module.exports = { + // jsonfile exports + readJson: jsonFile.readFile, + readJsonSync: jsonFile.readFileSync, + writeJson: jsonFile.writeFile, + writeJsonSync: jsonFile.writeFileSync +} diff --git a/tests/corpus/fs-extra-package/node_modules/fs-extra/lib/json/output-json-sync.js b/tests/corpus/fs-extra-package/node_modules/fs-extra/lib/json/output-json-sync.js new file mode 100644 index 000000000..f76b4744c --- /dev/null +++ b/tests/corpus/fs-extra-package/node_modules/fs-extra/lib/json/output-json-sync.js @@ -0,0 +1,12 @@ +'use strict' + +const { stringify } = require('jsonfile/utils') +const { outputFileSync } = require('../output') + +function outputJsonSync (file, data, options) { + const str = stringify(data, options) + + outputFileSync(file, str, options) +} + +module.exports = outputJsonSync diff --git a/tests/corpus/fs-extra-package/node_modules/fs-extra/lib/json/output-json.js b/tests/corpus/fs-extra-package/node_modules/fs-extra/lib/json/output-json.js new file mode 100644 index 000000000..0fc668977 --- /dev/null +++ b/tests/corpus/fs-extra-package/node_modules/fs-extra/lib/json/output-json.js @@ -0,0 +1,12 @@ +'use strict' + +const { stringify } = require('jsonfile/utils') +const { outputFile } = require('../output') + +async function outputJson (file, data, options = {}) { + const str = stringify(data, options) + + await outputFile(file, str, options) +} + +module.exports = outputJson diff --git a/tests/corpus/fs-extra-package/node_modules/fs-extra/lib/mkdirs/index.js b/tests/corpus/fs-extra-package/node_modules/fs-extra/lib/mkdirs/index.js new file mode 100644 index 000000000..9edecee08 --- /dev/null +++ b/tests/corpus/fs-extra-package/node_modules/fs-extra/lib/mkdirs/index.js @@ -0,0 +1,14 @@ +'use strict' +const u = require('universalify').fromPromise +const { makeDir: _makeDir, makeDirSync } = require('./make-dir') +const makeDir = u(_makeDir) + +module.exports = { + mkdirs: makeDir, + mkdirsSync: makeDirSync, + // alias + mkdirp: makeDir, + mkdirpSync: makeDirSync, + ensureDir: makeDir, + ensureDirSync: makeDirSync +} diff --git a/tests/corpus/fs-extra-package/node_modules/fs-extra/lib/mkdirs/make-dir.js b/tests/corpus/fs-extra-package/node_modules/fs-extra/lib/mkdirs/make-dir.js new file mode 100644 index 000000000..f17ecffbc --- /dev/null +++ b/tests/corpus/fs-extra-package/node_modules/fs-extra/lib/mkdirs/make-dir.js @@ -0,0 +1,142 @@ +// Adapted from https://github.com/sindresorhus/make-dir +// Copyright (c) Sindre Sorhus (sindresorhus.com) +// 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. +'use strict' +const fs = require('../fs') +const path = require('path') +const atLeastNode = require('at-least-node') + +const useNativeRecursiveOption = atLeastNode('10.12.0') + +// https://github.com/nodejs/node/issues/8987 +// https://github.com/libuv/libuv/pull/1088 +const checkPath = pth => { + if (process.platform === 'win32') { + const pathHasInvalidWinCharacters = /[<>:"|?*]/.test(pth.replace(path.parse(pth).root, '')) + + if (pathHasInvalidWinCharacters) { + const error = new Error(`Path contains invalid characters: ${pth}`) + error.code = 'EINVAL' + throw error + } + } +} + +const processOptions = options => { + // Must be defined here so we get fresh process.umask() + const defaults = { mode: 0o777 & (~process.umask()) } + if (typeof options === 'number') options = { mode: options } + return { ...defaults, ...options } +} + +const permissionError = pth => { + // This replicates the exception of `fs.mkdir` with native the + // `recusive` option when run on an invalid drive under Windows. + const error = new Error(`operation not permitted, mkdir '${pth}'`) + error.code = 'EPERM' + error.errno = -4048 + error.path = pth + error.syscall = 'mkdir' + return error +} + +module.exports.makeDir = async (input, options) => { + checkPath(input) + options = processOptions(options) + + if (useNativeRecursiveOption) { + const pth = path.resolve(input) + + return fs.mkdir(pth, { + mode: options.mode, + recursive: true + }) + } + + const make = async pth => { + try { + await fs.mkdir(pth, options.mode) + } catch (error) { + if (error.code === 'EPERM') { + throw error + } + + if (error.code === 'ENOENT') { + if (path.dirname(pth) === pth) { + throw permissionError(pth) + } + + if (error.message.includes('null bytes')) { + throw error + } + + await make(path.dirname(pth)) + return make(pth) + } + + try { + const stats = await fs.stat(pth) + if (!stats.isDirectory()) { + // This error is never exposed to the user + // it is caught below, and the original error is thrown + throw new Error('The path is not a directory') + } + } catch { + throw error + } + } + } + + return make(path.resolve(input)) +} + +module.exports.makeDirSync = (input, options) => { + checkPath(input) + options = processOptions(options) + + if (useNativeRecursiveOption) { + const pth = path.resolve(input) + + return fs.mkdirSync(pth, { + mode: options.mode, + recursive: true + }) + } + + const make = pth => { + try { + fs.mkdirSync(pth, options.mode) + } catch (error) { + if (error.code === 'EPERM') { + throw error + } + + if (error.code === 'ENOENT') { + if (path.dirname(pth) === pth) { + throw permissionError(pth) + } + + if (error.message.includes('null bytes')) { + throw error + } + + make(path.dirname(pth)) + return make(pth) + } + + try { + if (!fs.statSync(pth).isDirectory()) { + // This error is never exposed to the user + // it is caught below, and the original error is thrown + throw new Error('The path is not a directory') + } + } catch { + throw error + } + } + } + + return make(path.resolve(input)) +} diff --git a/tests/corpus/fs-extra-package/node_modules/fs-extra/lib/move-sync/index.js b/tests/corpus/fs-extra-package/node_modules/fs-extra/lib/move-sync/index.js new file mode 100644 index 000000000..af90b06b4 --- /dev/null +++ b/tests/corpus/fs-extra-package/node_modules/fs-extra/lib/move-sync/index.js @@ -0,0 +1,5 @@ +'use strict' + +module.exports = { + moveSync: require('./move-sync') +} diff --git a/tests/corpus/fs-extra-package/node_modules/fs-extra/lib/move-sync/move-sync.js b/tests/corpus/fs-extra-package/node_modules/fs-extra/lib/move-sync/move-sync.js new file mode 100644 index 000000000..20f910cc2 --- /dev/null +++ b/tests/corpus/fs-extra-package/node_modules/fs-extra/lib/move-sync/move-sync.js @@ -0,0 +1,47 @@ +'use strict' + +const fs = require('graceful-fs') +const path = require('path') +const copySync = require('../copy-sync').copySync +const removeSync = require('../remove').removeSync +const mkdirpSync = require('../mkdirs').mkdirpSync +const stat = require('../util/stat') + +function moveSync (src, dest, opts) { + opts = opts || {} + const overwrite = opts.overwrite || opts.clobber || false + + const { srcStat } = stat.checkPathsSync(src, dest, 'move') + stat.checkParentPathsSync(src, srcStat, dest, 'move') + mkdirpSync(path.dirname(dest)) + return doRename(src, dest, overwrite) +} + +function doRename (src, dest, overwrite) { + if (overwrite) { + removeSync(dest) + return rename(src, dest, overwrite) + } + if (fs.existsSync(dest)) throw new Error('dest already exists.') + return rename(src, dest, overwrite) +} + +function rename (src, dest, overwrite) { + try { + fs.renameSync(src, dest) + } catch (err) { + if (err.code !== 'EXDEV') throw err + return moveAcrossDevice(src, dest, overwrite) + } +} + +function moveAcrossDevice (src, dest, overwrite) { + const opts = { + overwrite, + errorOnExist: true + } + copySync(src, dest, opts) + return removeSync(src) +} + +module.exports = moveSync diff --git a/tests/corpus/fs-extra-package/node_modules/fs-extra/lib/move/index.js b/tests/corpus/fs-extra-package/node_modules/fs-extra/lib/move/index.js new file mode 100644 index 000000000..3785345bd --- /dev/null +++ b/tests/corpus/fs-extra-package/node_modules/fs-extra/lib/move/index.js @@ -0,0 +1,6 @@ +'use strict' + +const u = require('universalify').fromCallback +module.exports = { + move: u(require('./move')) +} diff --git a/tests/corpus/fs-extra-package/node_modules/fs-extra/lib/move/move.js b/tests/corpus/fs-extra-package/node_modules/fs-extra/lib/move/move.js new file mode 100644 index 000000000..fa3ea618a --- /dev/null +++ b/tests/corpus/fs-extra-package/node_modules/fs-extra/lib/move/move.js @@ -0,0 +1,65 @@ +'use strict' + +const fs = require('graceful-fs') +const path = require('path') +const copy = require('../copy').copy +const remove = require('../remove').remove +const mkdirp = require('../mkdirs').mkdirp +const pathExists = require('../path-exists').pathExists +const stat = require('../util/stat') + +function move (src, dest, opts, cb) { + if (typeof opts === 'function') { + cb = opts + opts = {} + } + + const overwrite = opts.overwrite || opts.clobber || false + + stat.checkPaths(src, dest, 'move', (err, stats) => { + if (err) return cb(err) + const { srcStat } = stats + stat.checkParentPaths(src, srcStat, dest, 'move', err => { + if (err) return cb(err) + mkdirp(path.dirname(dest), err => { + if (err) return cb(err) + return doRename(src, dest, overwrite, cb) + }) + }) + }) +} + +function doRename (src, dest, overwrite, cb) { + if (overwrite) { + return remove(dest, err => { + if (err) return cb(err) + return rename(src, dest, overwrite, cb) + }) + } + pathExists(dest, (err, destExists) => { + if (err) return cb(err) + if (destExists) return cb(new Error('dest already exists.')) + return rename(src, dest, overwrite, cb) + }) +} + +function rename (src, dest, overwrite, cb) { + fs.rename(src, dest, err => { + if (!err) return cb() + if (err.code !== 'EXDEV') return cb(err) + return moveAcrossDevice(src, dest, overwrite, cb) + }) +} + +function moveAcrossDevice (src, dest, overwrite, cb) { + const opts = { + overwrite, + errorOnExist: true + } + copy(src, dest, opts, err => { + if (err) return cb(err) + return remove(src, cb) + }) +} + +module.exports = move diff --git a/tests/corpus/fs-extra-package/node_modules/fs-extra/lib/output/index.js b/tests/corpus/fs-extra-package/node_modules/fs-extra/lib/output/index.js new file mode 100644 index 000000000..92297ca3a --- /dev/null +++ b/tests/corpus/fs-extra-package/node_modules/fs-extra/lib/output/index.js @@ -0,0 +1,40 @@ +'use strict' + +const u = require('universalify').fromCallback +const fs = require('graceful-fs') +const path = require('path') +const mkdir = require('../mkdirs') +const pathExists = require('../path-exists').pathExists + +function outputFile (file, data, encoding, callback) { + if (typeof encoding === 'function') { + callback = encoding + encoding = 'utf8' + } + + const dir = path.dirname(file) + pathExists(dir, (err, itDoes) => { + if (err) return callback(err) + if (itDoes) return fs.writeFile(file, data, encoding, callback) + + mkdir.mkdirs(dir, err => { + if (err) return callback(err) + + fs.writeFile(file, data, encoding, callback) + }) + }) +} + +function outputFileSync (file, ...args) { + const dir = path.dirname(file) + if (fs.existsSync(dir)) { + return fs.writeFileSync(file, ...args) + } + mkdir.mkdirsSync(dir) + fs.writeFileSync(file, ...args) +} + +module.exports = { + outputFile: u(outputFile), + outputFileSync +} diff --git a/tests/corpus/fs-extra-package/node_modules/fs-extra/lib/path-exists/index.js b/tests/corpus/fs-extra-package/node_modules/fs-extra/lib/path-exists/index.js new file mode 100644 index 000000000..ddd9bc718 --- /dev/null +++ b/tests/corpus/fs-extra-package/node_modules/fs-extra/lib/path-exists/index.js @@ -0,0 +1,12 @@ +'use strict' +const u = require('universalify').fromPromise +const fs = require('../fs') + +function pathExists (path) { + return fs.access(path).then(() => true).catch(() => false) +} + +module.exports = { + pathExists: u(pathExists), + pathExistsSync: fs.existsSync +} diff --git a/tests/corpus/fs-extra-package/node_modules/fs-extra/lib/remove/index.js b/tests/corpus/fs-extra-package/node_modules/fs-extra/lib/remove/index.js new file mode 100644 index 000000000..cee534007 --- /dev/null +++ b/tests/corpus/fs-extra-package/node_modules/fs-extra/lib/remove/index.js @@ -0,0 +1,9 @@ +'use strict' + +const u = require('universalify').fromCallback +const rimraf = require('./rimraf') + +module.exports = { + remove: u(rimraf), + removeSync: rimraf.sync +} diff --git a/tests/corpus/fs-extra-package/node_modules/fs-extra/lib/remove/rimraf.js b/tests/corpus/fs-extra-package/node_modules/fs-extra/lib/remove/rimraf.js new file mode 100644 index 000000000..1e44c1288 --- /dev/null +++ b/tests/corpus/fs-extra-package/node_modules/fs-extra/lib/remove/rimraf.js @@ -0,0 +1,314 @@ +'use strict' + +const fs = require('graceful-fs') +const path = require('path') +const assert = require('assert') + +const isWindows = (process.platform === 'win32') + +function defaults (options) { + const methods = [ + 'unlink', + 'chmod', + 'stat', + 'lstat', + 'rmdir', + 'readdir' + ] + methods.forEach(m => { + options[m] = options[m] || fs[m] + m = m + 'Sync' + options[m] = options[m] || fs[m] + }) + + options.maxBusyTries = options.maxBusyTries || 3 +} + +function rimraf (p, options, cb) { + let busyTries = 0 + + if (typeof options === 'function') { + cb = options + options = {} + } + + assert(p, 'rimraf: missing path') + assert.strictEqual(typeof p, 'string', 'rimraf: path should be a string') + assert.strictEqual(typeof cb, 'function', 'rimraf: callback function required') + assert(options, 'rimraf: invalid options argument provided') + assert.strictEqual(typeof options, 'object', 'rimraf: options should be object') + + defaults(options) + + rimraf_(p, options, function CB (er) { + if (er) { + if ((er.code === 'EBUSY' || er.code === 'ENOTEMPTY' || er.code === 'EPERM') && + busyTries < options.maxBusyTries) { + busyTries++ + const time = busyTries * 100 + // try again, with the same exact callback as this one. + return setTimeout(() => rimraf_(p, options, CB), time) + } + + // already gone + if (er.code === 'ENOENT') er = null + } + + cb(er) + }) +} + +// Two possible strategies. +// 1. Assume it's a file. unlink it, then do the dir stuff on EPERM or EISDIR +// 2. Assume it's a directory. readdir, then do the file stuff on ENOTDIR +// +// Both result in an extra syscall when you guess wrong. However, there +// are likely far more normal files in the world than directories. This +// is based on the assumption that a the average number of files per +// directory is >= 1. +// +// If anyone ever complains about this, then I guess the strategy could +// be made configurable somehow. But until then, YAGNI. +function rimraf_ (p, options, cb) { + assert(p) + assert(options) + assert(typeof cb === 'function') + + // sunos lets the root user unlink directories, which is... weird. + // so we have to lstat here and make sure it's not a dir. + options.lstat(p, (er, st) => { + if (er && er.code === 'ENOENT') { + return cb(null) + } + + // Windows can EPERM on stat. Life is suffering. + if (er && er.code === 'EPERM' && isWindows) { + return fixWinEPERM(p, options, er, cb) + } + + if (st && st.isDirectory()) { + return rmdir(p, options, er, cb) + } + + options.unlink(p, er => { + if (er) { + if (er.code === 'ENOENT') { + return cb(null) + } + if (er.code === 'EPERM') { + return (isWindows) + ? fixWinEPERM(p, options, er, cb) + : rmdir(p, options, er, cb) + } + if (er.code === 'EISDIR') { + return rmdir(p, options, er, cb) + } + } + return cb(er) + }) + }) +} + +function fixWinEPERM (p, options, er, cb) { + assert(p) + assert(options) + assert(typeof cb === 'function') + if (er) { + assert(er instanceof Error) + } + + options.chmod(p, 0o666, er2 => { + if (er2) { + cb(er2.code === 'ENOENT' ? null : er) + } else { + options.stat(p, (er3, stats) => { + if (er3) { + cb(er3.code === 'ENOENT' ? null : er) + } else if (stats.isDirectory()) { + rmdir(p, options, er, cb) + } else { + options.unlink(p, cb) + } + }) + } + }) +} + +function fixWinEPERMSync (p, options, er) { + let stats + + assert(p) + assert(options) + if (er) { + assert(er instanceof Error) + } + + try { + options.chmodSync(p, 0o666) + } catch (er2) { + if (er2.code === 'ENOENT') { + return + } else { + throw er + } + } + + try { + stats = options.statSync(p) + } catch (er3) { + if (er3.code === 'ENOENT') { + return + } else { + throw er + } + } + + if (stats.isDirectory()) { + rmdirSync(p, options, er) + } else { + options.unlinkSync(p) + } +} + +function rmdir (p, options, originalEr, cb) { + assert(p) + assert(options) + if (originalEr) { + assert(originalEr instanceof Error) + } + assert(typeof cb === 'function') + + // try to rmdir first, and only readdir on ENOTEMPTY or EEXIST (SunOS) + // if we guessed wrong, and it's not a directory, then + // raise the original error. + options.rmdir(p, er => { + if (er && (er.code === 'ENOTEMPTY' || er.code === 'EEXIST' || er.code === 'EPERM')) { + rmkids(p, options, cb) + } else if (er && er.code === 'ENOTDIR') { + cb(originalEr) + } else { + cb(er) + } + }) +} + +function rmkids (p, options, cb) { + assert(p) + assert(options) + assert(typeof cb === 'function') + + options.readdir(p, (er, files) => { + if (er) return cb(er) + + let n = files.length + let errState + + if (n === 0) return options.rmdir(p, cb) + + files.forEach(f => { + rimraf(path.join(p, f), options, er => { + if (errState) { + return + } + if (er) return cb(errState = er) + if (--n === 0) { + options.rmdir(p, cb) + } + }) + }) + }) +} + +// this looks simpler, and is strictly *faster*, but will +// tie up the JavaScript thread and fail on excessively +// deep directory trees. +function rimrafSync (p, options) { + let st + + options = options || {} + defaults(options) + + assert(p, 'rimraf: missing path') + assert.strictEqual(typeof p, 'string', 'rimraf: path should be a string') + assert(options, 'rimraf: missing options') + assert.strictEqual(typeof options, 'object', 'rimraf: options should be object') + + try { + st = options.lstatSync(p) + } catch (er) { + if (er.code === 'ENOENT') { + return + } + + // Windows can EPERM on stat. Life is suffering. + if (er.code === 'EPERM' && isWindows) { + fixWinEPERMSync(p, options, er) + } + } + + try { + // sunos lets the root user unlink directories, which is... weird. + if (st && st.isDirectory()) { + rmdirSync(p, options, null) + } else { + options.unlinkSync(p) + } + } catch (er) { + if (er.code === 'ENOENT') { + return + } else if (er.code === 'EPERM') { + return isWindows ? fixWinEPERMSync(p, options, er) : rmdirSync(p, options, er) + } else if (er.code !== 'EISDIR') { + throw er + } + rmdirSync(p, options, er) + } +} + +function rmdirSync (p, options, originalEr) { + assert(p) + assert(options) + if (originalEr) { + assert(originalEr instanceof Error) + } + + try { + options.rmdirSync(p) + } catch (er) { + if (er.code === 'ENOTDIR') { + throw originalEr + } else if (er.code === 'ENOTEMPTY' || er.code === 'EEXIST' || er.code === 'EPERM') { + rmkidsSync(p, options) + } else if (er.code !== 'ENOENT') { + throw er + } + } +} + +function rmkidsSync (p, options) { + assert(p) + assert(options) + options.readdirSync(p).forEach(f => rimrafSync(path.join(p, f), options)) + + if (isWindows) { + // We only end up here once we got ENOTEMPTY at least once, and + // at this point, we are guaranteed to have removed all the kids. + // So, we know that it won't be ENOENT or ENOTDIR or anything else. + // try really hard to delete stuff on windows, because it has a + // PROFOUNDLY annoying habit of not closing handles promptly when + // files are deleted, resulting in spurious ENOTEMPTY errors. + const startTime = Date.now() + do { + try { + const ret = options.rmdirSync(p, options) + return ret + } catch {} + } while (Date.now() - startTime < 500) // give up after 500ms + } else { + const ret = options.rmdirSync(p, options) + return ret + } +} + +module.exports = rimraf +rimraf.sync = rimrafSync diff --git a/tests/corpus/fs-extra-package/node_modules/fs-extra/lib/util/stat.js b/tests/corpus/fs-extra-package/node_modules/fs-extra/lib/util/stat.js new file mode 100644 index 000000000..0b1c1b096 --- /dev/null +++ b/tests/corpus/fs-extra-package/node_modules/fs-extra/lib/util/stat.js @@ -0,0 +1,139 @@ +'use strict' + +const fs = require('../fs') +const path = require('path') +const util = require('util') +const atLeastNode = require('at-least-node') + +const nodeSupportsBigInt = atLeastNode('10.5.0') +const stat = (file) => nodeSupportsBigInt ? fs.stat(file, { bigint: true }) : fs.stat(file) +const statSync = (file) => nodeSupportsBigInt ? fs.statSync(file, { bigint: true }) : fs.statSync(file) + +function getStats (src, dest) { + return Promise.all([ + stat(src), + stat(dest).catch(err => { + if (err.code === 'ENOENT') return null + throw err + }) + ]).then(([srcStat, destStat]) => ({ srcStat, destStat })) +} + +function getStatsSync (src, dest) { + let destStat + const srcStat = statSync(src) + try { + destStat = statSync(dest) + } catch (err) { + if (err.code === 'ENOENT') return { srcStat, destStat: null } + throw err + } + return { srcStat, destStat } +} + +function checkPaths (src, dest, funcName, cb) { + util.callbackify(getStats)(src, dest, (err, stats) => { + if (err) return cb(err) + const { srcStat, destStat } = stats + if (destStat && areIdentical(srcStat, destStat)) { + return cb(new Error('Source and destination must not be the same.')) + } + if (srcStat.isDirectory() && isSrcSubdir(src, dest)) { + return cb(new Error(errMsg(src, dest, funcName))) + } + return cb(null, { srcStat, destStat }) + }) +} + +function checkPathsSync (src, dest, funcName) { + const { srcStat, destStat } = getStatsSync(src, dest) + if (destStat && areIdentical(srcStat, destStat)) { + throw new Error('Source and destination must not be the same.') + } + if (srcStat.isDirectory() && isSrcSubdir(src, dest)) { + throw new Error(errMsg(src, dest, funcName)) + } + return { srcStat, destStat } +} + +// recursively check if dest parent is a subdirectory of src. +// It works for all file types including symlinks since it +// checks the src and dest inodes. It starts from the deepest +// parent and stops once it reaches the src parent or the root path. +function checkParentPaths (src, srcStat, dest, funcName, cb) { + const srcParent = path.resolve(path.dirname(src)) + const destParent = path.resolve(path.dirname(dest)) + if (destParent === srcParent || destParent === path.parse(destParent).root) return cb() + const callback = (err, destStat) => { + if (err) { + if (err.code === 'ENOENT') return cb() + return cb(err) + } + if (areIdentical(srcStat, destStat)) { + return cb(new Error(errMsg(src, dest, funcName))) + } + return checkParentPaths(src, srcStat, destParent, funcName, cb) + } + if (nodeSupportsBigInt) fs.stat(destParent, { bigint: true }, callback) + else fs.stat(destParent, callback) +} + +function checkParentPathsSync (src, srcStat, dest, funcName) { + const srcParent = path.resolve(path.dirname(src)) + const destParent = path.resolve(path.dirname(dest)) + if (destParent === srcParent || destParent === path.parse(destParent).root) return + let destStat + try { + destStat = statSync(destParent) + } catch (err) { + if (err.code === 'ENOENT') return + throw err + } + if (areIdentical(srcStat, destStat)) { + throw new Error(errMsg(src, dest, funcName)) + } + return checkParentPathsSync(src, srcStat, destParent, funcName) +} + +function areIdentical (srcStat, destStat) { + if (destStat.ino && destStat.dev && destStat.ino === srcStat.ino && destStat.dev === srcStat.dev) { + if (nodeSupportsBigInt || destStat.ino < Number.MAX_SAFE_INTEGER) { + // definitive answer + return true + } + // Use additional heuristics if we can't use 'bigint'. + // Different 'ino' could be represented the same if they are >= Number.MAX_SAFE_INTEGER + // See issue 657 + if (destStat.size === srcStat.size && + destStat.mode === srcStat.mode && + destStat.nlink === srcStat.nlink && + destStat.atimeMs === srcStat.atimeMs && + destStat.mtimeMs === srcStat.mtimeMs && + destStat.ctimeMs === srcStat.ctimeMs && + destStat.birthtimeMs === srcStat.birthtimeMs) { + // heuristic answer + return true + } + } + return false +} + +// return true if dest is a subdir of src, otherwise false. +// It only checks the path strings. +function isSrcSubdir (src, dest) { + const srcArr = path.resolve(src).split(path.sep).filter(i => i) + const destArr = path.resolve(dest).split(path.sep).filter(i => i) + return srcArr.reduce((acc, cur, i) => acc && destArr[i] === cur, true) +} + +function errMsg (src, dest, funcName) { + return `Cannot ${funcName} '${src}' to a subdirectory of itself, '${dest}'.` +} + +module.exports = { + checkPaths, + checkPathsSync, + checkParentPaths, + checkParentPathsSync, + isSrcSubdir +} diff --git a/tests/corpus/fs-extra-package/node_modules/fs-extra/lib/util/utimes.js b/tests/corpus/fs-extra-package/node_modules/fs-extra/lib/util/utimes.js new file mode 100644 index 000000000..75395deff --- /dev/null +++ b/tests/corpus/fs-extra-package/node_modules/fs-extra/lib/util/utimes.js @@ -0,0 +1,26 @@ +'use strict' + +const fs = require('graceful-fs') + +function utimesMillis (path, atime, mtime, callback) { + // if (!HAS_MILLIS_RES) return fs.utimes(path, atime, mtime, callback) + fs.open(path, 'r+', (err, fd) => { + if (err) return callback(err) + fs.futimes(fd, atime, mtime, futimesErr => { + fs.close(fd, closeErr => { + if (callback) callback(futimesErr || closeErr) + }) + }) + }) +} + +function utimesMillisSync (path, atime, mtime) { + const fd = fs.openSync(path, 'r+') + fs.futimesSync(fd, atime, mtime) + return fs.closeSync(fd) +} + +module.exports = { + utimesMillis, + utimesMillisSync +} diff --git a/tests/corpus/fs-extra-package/node_modules/fs-extra/package.json b/tests/corpus/fs-extra-package/node_modules/fs-extra/package.json new file mode 100644 index 000000000..1f251f183 --- /dev/null +++ b/tests/corpus/fs-extra-package/node_modules/fs-extra/package.json @@ -0,0 +1,70 @@ +{ + "name": "fs-extra", + "version": "9.0.0", + "description": "fs-extra contains methods that aren't included in the vanilla Node.js fs package. Such as mkdir -p, cp -r, and rm -rf.", + "engines": { + "node": ">=10" + }, + "homepage": "https://github.com/jprichardson/node-fs-extra", + "repository": { + "type": "git", + "url": "https://github.com/jprichardson/node-fs-extra" + }, + "keywords": [ + "fs", + "file", + "file system", + "copy", + "directory", + "extra", + "mkdirp", + "mkdir", + "mkdirs", + "recursive", + "json", + "read", + "write", + "extra", + "delete", + "remove", + "touch", + "create", + "text", + "output", + "move", + "promise" + ], + "author": "JP Richardson ", + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^1.0.0" + }, + "devDependencies": { + "coveralls": "^3.0.0", + "klaw": "^2.1.1", + "klaw-sync": "^3.0.2", + "minimist": "^1.1.1", + "mocha": "^5.0.5", + "nyc": "^15.0.0", + "proxyquire": "^2.0.1", + "read-dir-files": "^0.1.1", + "standard": "^14.1.0" + }, + "main": "./lib/index.js", + "files": [ + "lib/", + "!lib/**/__tests__/" + ], + "scripts": { + "full-ci": "npm run lint && npm run coverage", + "coverage": "nyc -r lcovonly npm run unit", + "coveralls": "coveralls < coverage/lcov.info", + "lint": "standard", + "test-find": "find ./lib/**/__tests__ -name *.test.js | xargs mocha", + "test": "npm run lint && npm run unit", + "unit": "node test.js" + } +} diff --git a/tests/corpus/fs-extra-package/node_modules/graceful-fs/LICENSE b/tests/corpus/fs-extra-package/node_modules/graceful-fs/LICENSE new file mode 100644 index 000000000..e906a25ac --- /dev/null +++ b/tests/corpus/fs-extra-package/node_modules/graceful-fs/LICENSE @@ -0,0 +1,15 @@ +The ISC License + +Copyright (c) 2011-2022 Isaac Z. Schlueter, Ben Noordhuis, and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/tests/corpus/fs-extra-package/node_modules/graceful-fs/clone.js b/tests/corpus/fs-extra-package/node_modules/graceful-fs/clone.js new file mode 100644 index 000000000..dff3cc8c5 --- /dev/null +++ b/tests/corpus/fs-extra-package/node_modules/graceful-fs/clone.js @@ -0,0 +1,23 @@ +'use strict' + +module.exports = clone + +var getPrototypeOf = Object.getPrototypeOf || function (obj) { + return obj.__proto__ +} + +function clone (obj) { + if (obj === null || typeof obj !== 'object') + return obj + + if (obj instanceof Object) + var copy = { __proto__: getPrototypeOf(obj) } + else + var copy = Object.create(null) + + Object.getOwnPropertyNames(obj).forEach(function (key) { + Object.defineProperty(copy, key, Object.getOwnPropertyDescriptor(obj, key)) + }) + + return copy +} diff --git a/tests/corpus/fs-extra-package/node_modules/graceful-fs/graceful-fs.js b/tests/corpus/fs-extra-package/node_modules/graceful-fs/graceful-fs.js new file mode 100644 index 000000000..8d5b89e4f --- /dev/null +++ b/tests/corpus/fs-extra-package/node_modules/graceful-fs/graceful-fs.js @@ -0,0 +1,448 @@ +var fs = require('fs') +var polyfills = require('./polyfills.js') +var legacy = require('./legacy-streams.js') +var clone = require('./clone.js') + +var util = require('util') + +/* istanbul ignore next - node 0.x polyfill */ +var gracefulQueue +var previousSymbol + +/* istanbul ignore else - node 0.x polyfill */ +if (typeof Symbol === 'function' && typeof Symbol.for === 'function') { + gracefulQueue = Symbol.for('graceful-fs.queue') + // This is used in testing by future versions + previousSymbol = Symbol.for('graceful-fs.previous') +} else { + gracefulQueue = '___graceful-fs.queue' + previousSymbol = '___graceful-fs.previous' +} + +function noop () {} + +function publishQueue(context, queue) { + Object.defineProperty(context, gracefulQueue, { + get: function() { + return queue + } + }) +} + +var debug = noop +if (util.debuglog) + debug = util.debuglog('gfs4') +else if (/\bgfs4\b/i.test(process.env.NODE_DEBUG || '')) + debug = function() { + var m = util.format.apply(util, arguments) + m = 'GFS4: ' + m.split(/\n/).join('\nGFS4: ') + console.error(m) + } + +// Once time initialization +if (!fs[gracefulQueue]) { + // This queue can be shared by multiple loaded instances + var queue = global[gracefulQueue] || [] + publishQueue(fs, queue) + + // Patch fs.close/closeSync to shared queue version, because we need + // to retry() whenever a close happens *anywhere* in the program. + // This is essential when multiple graceful-fs instances are + // in play at the same time. + fs.close = (function (fs$close) { + function close (fd, cb) { + return fs$close.call(fs, fd, function (err) { + // This function uses the graceful-fs shared queue + if (!err) { + resetQueue() + } + + if (typeof cb === 'function') + cb.apply(this, arguments) + }) + } + + Object.defineProperty(close, previousSymbol, { + value: fs$close + }) + return close + })(fs.close) + + fs.closeSync = (function (fs$closeSync) { + function closeSync (fd) { + // This function uses the graceful-fs shared queue + fs$closeSync.apply(fs, arguments) + resetQueue() + } + + Object.defineProperty(closeSync, previousSymbol, { + value: fs$closeSync + }) + return closeSync + })(fs.closeSync) + + if (/\bgfs4\b/i.test(process.env.NODE_DEBUG || '')) { + process.on('exit', function() { + debug(fs[gracefulQueue]) + require('assert').equal(fs[gracefulQueue].length, 0) + }) + } +} + +if (!global[gracefulQueue]) { + publishQueue(global, fs[gracefulQueue]); +} + +module.exports = patch(clone(fs)) +if (process.env.TEST_GRACEFUL_FS_GLOBAL_PATCH && !fs.__patched) { + module.exports = patch(fs) + fs.__patched = true; +} + +function patch (fs) { + // Everything that references the open() function needs to be in here + polyfills(fs) + fs.gracefulify = patch + + fs.createReadStream = createReadStream + fs.createWriteStream = createWriteStream + var fs$readFile = fs.readFile + fs.readFile = readFile + function readFile (path, options, cb) { + if (typeof options === 'function') + cb = options, options = null + + return go$readFile(path, options, cb) + + function go$readFile (path, options, cb, startTime) { + return fs$readFile(path, options, function (err) { + if (err && (err.code === 'EMFILE' || err.code === 'ENFILE')) + enqueue([go$readFile, [path, options, cb], err, startTime || Date.now(), Date.now()]) + else { + if (typeof cb === 'function') + cb.apply(this, arguments) + } + }) + } + } + + var fs$writeFile = fs.writeFile + fs.writeFile = writeFile + function writeFile (path, data, options, cb) { + if (typeof options === 'function') + cb = options, options = null + + return go$writeFile(path, data, options, cb) + + function go$writeFile (path, data, options, cb, startTime) { + return fs$writeFile(path, data, options, function (err) { + if (err && (err.code === 'EMFILE' || err.code === 'ENFILE')) + enqueue([go$writeFile, [path, data, options, cb], err, startTime || Date.now(), Date.now()]) + else { + if (typeof cb === 'function') + cb.apply(this, arguments) + } + }) + } + } + + var fs$appendFile = fs.appendFile + if (fs$appendFile) + fs.appendFile = appendFile + function appendFile (path, data, options, cb) { + if (typeof options === 'function') + cb = options, options = null + + return go$appendFile(path, data, options, cb) + + function go$appendFile (path, data, options, cb, startTime) { + return fs$appendFile(path, data, options, function (err) { + if (err && (err.code === 'EMFILE' || err.code === 'ENFILE')) + enqueue([go$appendFile, [path, data, options, cb], err, startTime || Date.now(), Date.now()]) + else { + if (typeof cb === 'function') + cb.apply(this, arguments) + } + }) + } + } + + var fs$copyFile = fs.copyFile + if (fs$copyFile) + fs.copyFile = copyFile + function copyFile (src, dest, flags, cb) { + if (typeof flags === 'function') { + cb = flags + flags = 0 + } + return go$copyFile(src, dest, flags, cb) + + function go$copyFile (src, dest, flags, cb, startTime) { + return fs$copyFile(src, dest, flags, function (err) { + if (err && (err.code === 'EMFILE' || err.code === 'ENFILE')) + enqueue([go$copyFile, [src, dest, flags, cb], err, startTime || Date.now(), Date.now()]) + else { + if (typeof cb === 'function') + cb.apply(this, arguments) + } + }) + } + } + + var fs$readdir = fs.readdir + fs.readdir = readdir + var noReaddirOptionVersions = /^v[0-5]\./ + function readdir (path, options, cb) { + if (typeof options === 'function') + cb = options, options = null + + var go$readdir = noReaddirOptionVersions.test(process.version) + ? function go$readdir (path, options, cb, startTime) { + return fs$readdir(path, fs$readdirCallback( + path, options, cb, startTime + )) + } + : function go$readdir (path, options, cb, startTime) { + return fs$readdir(path, options, fs$readdirCallback( + path, options, cb, startTime + )) + } + + return go$readdir(path, options, cb) + + function fs$readdirCallback (path, options, cb, startTime) { + return function (err, files) { + if (err && (err.code === 'EMFILE' || err.code === 'ENFILE')) + enqueue([ + go$readdir, + [path, options, cb], + err, + startTime || Date.now(), + Date.now() + ]) + else { + if (files && files.sort) + files.sort() + + if (typeof cb === 'function') + cb.call(this, err, files) + } + } + } + } + + if (process.version.substr(0, 4) === 'v0.8') { + var legStreams = legacy(fs) + ReadStream = legStreams.ReadStream + WriteStream = legStreams.WriteStream + } + + var fs$ReadStream = fs.ReadStream + if (fs$ReadStream) { + ReadStream.prototype = Object.create(fs$ReadStream.prototype) + ReadStream.prototype.open = ReadStream$open + } + + var fs$WriteStream = fs.WriteStream + if (fs$WriteStream) { + WriteStream.prototype = Object.create(fs$WriteStream.prototype) + WriteStream.prototype.open = WriteStream$open + } + + Object.defineProperty(fs, 'ReadStream', { + get: function () { + return ReadStream + }, + set: function (val) { + ReadStream = val + }, + enumerable: true, + configurable: true + }) + Object.defineProperty(fs, 'WriteStream', { + get: function () { + return WriteStream + }, + set: function (val) { + WriteStream = val + }, + enumerable: true, + configurable: true + }) + + // legacy names + var FileReadStream = ReadStream + Object.defineProperty(fs, 'FileReadStream', { + get: function () { + return FileReadStream + }, + set: function (val) { + FileReadStream = val + }, + enumerable: true, + configurable: true + }) + var FileWriteStream = WriteStream + Object.defineProperty(fs, 'FileWriteStream', { + get: function () { + return FileWriteStream + }, + set: function (val) { + FileWriteStream = val + }, + enumerable: true, + configurable: true + }) + + function ReadStream (path, options) { + if (this instanceof ReadStream) + return fs$ReadStream.apply(this, arguments), this + else + return ReadStream.apply(Object.create(ReadStream.prototype), arguments) + } + + function ReadStream$open () { + var that = this + open(that.path, that.flags, that.mode, function (err, fd) { + if (err) { + if (that.autoClose) + that.destroy() + + that.emit('error', err) + } else { + that.fd = fd + that.emit('open', fd) + that.read() + } + }) + } + + function WriteStream (path, options) { + if (this instanceof WriteStream) + return fs$WriteStream.apply(this, arguments), this + else + return WriteStream.apply(Object.create(WriteStream.prototype), arguments) + } + + function WriteStream$open () { + var that = this + open(that.path, that.flags, that.mode, function (err, fd) { + if (err) { + that.destroy() + that.emit('error', err) + } else { + that.fd = fd + that.emit('open', fd) + } + }) + } + + function createReadStream (path, options) { + return new fs.ReadStream(path, options) + } + + function createWriteStream (path, options) { + return new fs.WriteStream(path, options) + } + + var fs$open = fs.open + fs.open = open + function open (path, flags, mode, cb) { + if (typeof mode === 'function') + cb = mode, mode = null + + return go$open(path, flags, mode, cb) + + function go$open (path, flags, mode, cb, startTime) { + return fs$open(path, flags, mode, function (err, fd) { + if (err && (err.code === 'EMFILE' || err.code === 'ENFILE')) + enqueue([go$open, [path, flags, mode, cb], err, startTime || Date.now(), Date.now()]) + else { + if (typeof cb === 'function') + cb.apply(this, arguments) + } + }) + } + } + + return fs +} + +function enqueue (elem) { + debug('ENQUEUE', elem[0].name, elem[1]) + fs[gracefulQueue].push(elem) + retry() +} + +// keep track of the timeout between retry() calls +var retryTimer + +// reset the startTime and lastTime to now +// this resets the start of the 60 second overall timeout as well as the +// delay between attempts so that we'll retry these jobs sooner +function resetQueue () { + var now = Date.now() + for (var i = 0; i < fs[gracefulQueue].length; ++i) { + // entries that are only a length of 2 are from an older version, don't + // bother modifying those since they'll be retried anyway. + if (fs[gracefulQueue][i].length > 2) { + fs[gracefulQueue][i][3] = now // startTime + fs[gracefulQueue][i][4] = now // lastTime + } + } + // call retry to make sure we're actively processing the queue + retry() +} + +function retry () { + // clear the timer and remove it to help prevent unintended concurrency + clearTimeout(retryTimer) + retryTimer = undefined + + if (fs[gracefulQueue].length === 0) + return + + var elem = fs[gracefulQueue].shift() + var fn = elem[0] + var args = elem[1] + // these items may be unset if they were added by an older graceful-fs + var err = elem[2] + var startTime = elem[3] + var lastTime = elem[4] + + // if we don't have a startTime we have no way of knowing if we've waited + // long enough, so go ahead and retry this item now + if (startTime === undefined) { + debug('RETRY', fn.name, args) + fn.apply(null, args) + } else if (Date.now() - startTime >= 60000) { + // it's been more than 60 seconds total, bail now + debug('TIMEOUT', fn.name, args) + var cb = args.pop() + if (typeof cb === 'function') + cb.call(null, err) + } else { + // the amount of time between the last attempt and right now + var sinceAttempt = Date.now() - lastTime + // the amount of time between when we first tried, and when we last tried + // rounded up to at least 1 + var sinceStart = Math.max(lastTime - startTime, 1) + // backoff. wait longer than the total time we've been retrying, but only + // up to a maximum of 100ms + var desiredDelay = Math.min(sinceStart * 1.2, 100) + // it's been long enough since the last retry, do it again + if (sinceAttempt >= desiredDelay) { + debug('RETRY', fn.name, args) + fn.apply(null, args.concat([startTime])) + } else { + // if we can't do this job yet, push it to the end of the queue + // and let the next iteration check again + fs[gracefulQueue].push(elem) + } + } + + // schedule our next run if one isn't already scheduled + if (retryTimer === undefined) { + retryTimer = setTimeout(retry, 0) + } +} diff --git a/tests/corpus/fs-extra-package/node_modules/graceful-fs/legacy-streams.js b/tests/corpus/fs-extra-package/node_modules/graceful-fs/legacy-streams.js new file mode 100644 index 000000000..d617b50fc --- /dev/null +++ b/tests/corpus/fs-extra-package/node_modules/graceful-fs/legacy-streams.js @@ -0,0 +1,118 @@ +var Stream = require('stream').Stream + +module.exports = legacy + +function legacy (fs) { + return { + ReadStream: ReadStream, + WriteStream: WriteStream + } + + function ReadStream (path, options) { + if (!(this instanceof ReadStream)) return new ReadStream(path, options); + + Stream.call(this); + + var self = this; + + this.path = path; + this.fd = null; + this.readable = true; + this.paused = false; + + this.flags = 'r'; + this.mode = 438; /*=0666*/ + this.bufferSize = 64 * 1024; + + options = options || {}; + + // Mixin options into this + var keys = Object.keys(options); + for (var index = 0, length = keys.length; index < length; index++) { + var key = keys[index]; + this[key] = options[key]; + } + + if (this.encoding) this.setEncoding(this.encoding); + + if (this.start !== undefined) { + if ('number' !== typeof this.start) { + throw TypeError('start must be a Number'); + } + if (this.end === undefined) { + this.end = Infinity; + } else if ('number' !== typeof this.end) { + throw TypeError('end must be a Number'); + } + + if (this.start > this.end) { + throw new Error('start must be <= end'); + } + + this.pos = this.start; + } + + if (this.fd !== null) { + process.nextTick(function() { + self._read(); + }); + return; + } + + fs.open(this.path, this.flags, this.mode, function (err, fd) { + if (err) { + self.emit('error', err); + self.readable = false; + return; + } + + self.fd = fd; + self.emit('open', fd); + self._read(); + }) + } + + function WriteStream (path, options) { + if (!(this instanceof WriteStream)) return new WriteStream(path, options); + + Stream.call(this); + + this.path = path; + this.fd = null; + this.writable = true; + + this.flags = 'w'; + this.encoding = 'binary'; + this.mode = 438; /*=0666*/ + this.bytesWritten = 0; + + options = options || {}; + + // Mixin options into this + var keys = Object.keys(options); + for (var index = 0, length = keys.length; index < length; index++) { + var key = keys[index]; + this[key] = options[key]; + } + + if (this.start !== undefined) { + if ('number' !== typeof this.start) { + throw TypeError('start must be a Number'); + } + if (this.start < 0) { + throw new Error('start must be >= zero'); + } + + this.pos = this.start; + } + + this.busy = false; + this._queue = []; + + if (this.fd === null) { + this._open = fs.open; + this._queue.push([this._open, this.path, this.flags, this.mode, undefined]); + this.flush(); + } + } +} diff --git a/tests/corpus/fs-extra-package/node_modules/graceful-fs/package.json b/tests/corpus/fs-extra-package/node_modules/graceful-fs/package.json new file mode 100644 index 000000000..87babf024 --- /dev/null +++ b/tests/corpus/fs-extra-package/node_modules/graceful-fs/package.json @@ -0,0 +1,53 @@ +{ + "name": "graceful-fs", + "description": "A drop-in replacement for fs, making various improvements.", + "version": "4.2.11", + "repository": { + "type": "git", + "url": "https://github.com/isaacs/node-graceful-fs" + }, + "main": "graceful-fs.js", + "directories": { + "test": "test" + }, + "scripts": { + "preversion": "npm test", + "postversion": "npm publish", + "postpublish": "git push origin --follow-tags", + "test": "nyc --silent node test.js | tap -c -", + "posttest": "nyc report" + }, + "keywords": [ + "fs", + "module", + "reading", + "retry", + "retries", + "queue", + "error", + "errors", + "handling", + "EMFILE", + "EAGAIN", + "EINVAL", + "EPERM", + "EACCESS" + ], + "license": "ISC", + "devDependencies": { + "import-fresh": "^2.0.0", + "mkdirp": "^0.5.0", + "rimraf": "^2.2.8", + "tap": "^16.3.4" + }, + "files": [ + "fs.js", + "graceful-fs.js", + "legacy-streams.js", + "polyfills.js", + "clone.js" + ], + "tap": { + "reporter": "classic" + } +} diff --git a/tests/corpus/fs-extra-package/node_modules/graceful-fs/polyfills.js b/tests/corpus/fs-extra-package/node_modules/graceful-fs/polyfills.js new file mode 100644 index 000000000..453f1a9e7 --- /dev/null +++ b/tests/corpus/fs-extra-package/node_modules/graceful-fs/polyfills.js @@ -0,0 +1,355 @@ +var constants = require('constants') + +var origCwd = process.cwd +var cwd = null + +var platform = process.env.GRACEFUL_FS_PLATFORM || process.platform + +process.cwd = function() { + if (!cwd) + cwd = origCwd.call(process) + return cwd +} +try { + process.cwd() +} catch (er) {} + +// This check is needed until node.js 12 is required +if (typeof process.chdir === 'function') { + var chdir = process.chdir + process.chdir = function (d) { + cwd = null + chdir.call(process, d) + } + if (Object.setPrototypeOf) Object.setPrototypeOf(process.chdir, chdir) +} + +module.exports = patch + +function patch (fs) { + // (re-)implement some things that are known busted or missing. + + // lchmod, broken prior to 0.6.2 + // back-port the fix here. + if (constants.hasOwnProperty('O_SYMLINK') && + process.version.match(/^v0\.6\.[0-2]|^v0\.5\./)) { + patchLchmod(fs) + } + + // lutimes implementation, or no-op + if (!fs.lutimes) { + patchLutimes(fs) + } + + // https://github.com/isaacs/node-graceful-fs/issues/4 + // Chown should not fail on einval or eperm if non-root. + // It should not fail on enosys ever, as this just indicates + // that a fs doesn't support the intended operation. + + fs.chown = chownFix(fs.chown) + fs.fchown = chownFix(fs.fchown) + fs.lchown = chownFix(fs.lchown) + + fs.chmod = chmodFix(fs.chmod) + fs.fchmod = chmodFix(fs.fchmod) + fs.lchmod = chmodFix(fs.lchmod) + + fs.chownSync = chownFixSync(fs.chownSync) + fs.fchownSync = chownFixSync(fs.fchownSync) + fs.lchownSync = chownFixSync(fs.lchownSync) + + fs.chmodSync = chmodFixSync(fs.chmodSync) + fs.fchmodSync = chmodFixSync(fs.fchmodSync) + fs.lchmodSync = chmodFixSync(fs.lchmodSync) + + fs.stat = statFix(fs.stat) + fs.fstat = statFix(fs.fstat) + fs.lstat = statFix(fs.lstat) + + fs.statSync = statFixSync(fs.statSync) + fs.fstatSync = statFixSync(fs.fstatSync) + fs.lstatSync = statFixSync(fs.lstatSync) + + // if lchmod/lchown do not exist, then make them no-ops + if (fs.chmod && !fs.lchmod) { + fs.lchmod = function (path, mode, cb) { + if (cb) process.nextTick(cb) + } + fs.lchmodSync = function () {} + } + if (fs.chown && !fs.lchown) { + fs.lchown = function (path, uid, gid, cb) { + if (cb) process.nextTick(cb) + } + fs.lchownSync = function () {} + } + + // on Windows, A/V software can lock the directory, causing this + // to fail with an EACCES or EPERM if the directory contains newly + // created files. Try again on failure, for up to 60 seconds. + + // Set the timeout this long because some Windows Anti-Virus, such as Parity + // bit9, may lock files for up to a minute, causing npm package install + // failures. Also, take care to yield the scheduler. Windows scheduling gives + // CPU to a busy looping process, which can cause the program causing the lock + // contention to be starved of CPU by node, so the contention doesn't resolve. + if (platform === "win32") { + fs.rename = typeof fs.rename !== 'function' ? fs.rename + : (function (fs$rename) { + function rename (from, to, cb) { + var start = Date.now() + var backoff = 0; + fs$rename(from, to, function CB (er) { + if (er + && (er.code === "EACCES" || er.code === "EPERM" || er.code === "EBUSY") + && Date.now() - start < 60000) { + setTimeout(function() { + fs.stat(to, function (stater, st) { + if (stater && stater.code === "ENOENT") + fs$rename(from, to, CB); + else + cb(er) + }) + }, backoff) + if (backoff < 100) + backoff += 10; + return; + } + if (cb) cb(er) + }) + } + if (Object.setPrototypeOf) Object.setPrototypeOf(rename, fs$rename) + return rename + })(fs.rename) + } + + // if read() returns EAGAIN, then just try it again. + fs.read = typeof fs.read !== 'function' ? fs.read + : (function (fs$read) { + function read (fd, buffer, offset, length, position, callback_) { + var callback + if (callback_ && typeof callback_ === 'function') { + var eagCounter = 0 + callback = function (er, _, __) { + if (er && er.code === 'EAGAIN' && eagCounter < 10) { + eagCounter ++ + return fs$read.call(fs, fd, buffer, offset, length, position, callback) + } + callback_.apply(this, arguments) + } + } + return fs$read.call(fs, fd, buffer, offset, length, position, callback) + } + + // This ensures `util.promisify` works as it does for native `fs.read`. + if (Object.setPrototypeOf) Object.setPrototypeOf(read, fs$read) + return read + })(fs.read) + + fs.readSync = typeof fs.readSync !== 'function' ? fs.readSync + : (function (fs$readSync) { return function (fd, buffer, offset, length, position) { + var eagCounter = 0 + while (true) { + try { + return fs$readSync.call(fs, fd, buffer, offset, length, position) + } catch (er) { + if (er.code === 'EAGAIN' && eagCounter < 10) { + eagCounter ++ + continue + } + throw er + } + } + }})(fs.readSync) + + function patchLchmod (fs) { + fs.lchmod = function (path, mode, callback) { + fs.open( path + , constants.O_WRONLY | constants.O_SYMLINK + , mode + , function (err, fd) { + if (err) { + if (callback) callback(err) + return + } + // prefer to return the chmod error, if one occurs, + // but still try to close, and report closing errors if they occur. + fs.fchmod(fd, mode, function (err) { + fs.close(fd, function(err2) { + if (callback) callback(err || err2) + }) + }) + }) + } + + fs.lchmodSync = function (path, mode) { + var fd = fs.openSync(path, constants.O_WRONLY | constants.O_SYMLINK, mode) + + // prefer to return the chmod error, if one occurs, + // but still try to close, and report closing errors if they occur. + var threw = true + var ret + try { + ret = fs.fchmodSync(fd, mode) + threw = false + } finally { + if (threw) { + try { + fs.closeSync(fd) + } catch (er) {} + } else { + fs.closeSync(fd) + } + } + return ret + } + } + + function patchLutimes (fs) { + if (constants.hasOwnProperty("O_SYMLINK") && fs.futimes) { + fs.lutimes = function (path, at, mt, cb) { + fs.open(path, constants.O_SYMLINK, function (er, fd) { + if (er) { + if (cb) cb(er) + return + } + fs.futimes(fd, at, mt, function (er) { + fs.close(fd, function (er2) { + if (cb) cb(er || er2) + }) + }) + }) + } + + fs.lutimesSync = function (path, at, mt) { + var fd = fs.openSync(path, constants.O_SYMLINK) + var ret + var threw = true + try { + ret = fs.futimesSync(fd, at, mt) + threw = false + } finally { + if (threw) { + try { + fs.closeSync(fd) + } catch (er) {} + } else { + fs.closeSync(fd) + } + } + return ret + } + + } else if (fs.futimes) { + fs.lutimes = function (_a, _b, _c, cb) { if (cb) process.nextTick(cb) } + fs.lutimesSync = function () {} + } + } + + function chmodFix (orig) { + if (!orig) return orig + return function (target, mode, cb) { + return orig.call(fs, target, mode, function (er) { + if (chownErOk(er)) er = null + if (cb) cb.apply(this, arguments) + }) + } + } + + function chmodFixSync (orig) { + if (!orig) return orig + return function (target, mode) { + try { + return orig.call(fs, target, mode) + } catch (er) { + if (!chownErOk(er)) throw er + } + } + } + + + function chownFix (orig) { + if (!orig) return orig + return function (target, uid, gid, cb) { + return orig.call(fs, target, uid, gid, function (er) { + if (chownErOk(er)) er = null + if (cb) cb.apply(this, arguments) + }) + } + } + + function chownFixSync (orig) { + if (!orig) return orig + return function (target, uid, gid) { + try { + return orig.call(fs, target, uid, gid) + } catch (er) { + if (!chownErOk(er)) throw er + } + } + } + + function statFix (orig) { + if (!orig) return orig + // Older versions of Node erroneously returned signed integers for + // uid + gid. + return function (target, options, cb) { + if (typeof options === 'function') { + cb = options + options = null + } + function callback (er, stats) { + if (stats) { + if (stats.uid < 0) stats.uid += 0x100000000 + if (stats.gid < 0) stats.gid += 0x100000000 + } + if (cb) cb.apply(this, arguments) + } + return options ? orig.call(fs, target, options, callback) + : orig.call(fs, target, callback) + } + } + + function statFixSync (orig) { + if (!orig) return orig + // Older versions of Node erroneously returned signed integers for + // uid + gid. + return function (target, options) { + var stats = options ? orig.call(fs, target, options) + : orig.call(fs, target) + if (stats) { + if (stats.uid < 0) stats.uid += 0x100000000 + if (stats.gid < 0) stats.gid += 0x100000000 + } + return stats; + } + } + + // ENOSYS means that the fs doesn't support the op. Just ignore + // that, because it doesn't matter. + // + // if there's no getuid, or if getuid() is something other + // than 0, and the error is EINVAL or EPERM, then just ignore + // it. + // + // This specific case is a silent failure in cp, install, tar, + // and most other unix tools that manage permissions. + // + // When running as root, or if other types of errors are + // encountered, then it's strict. + function chownErOk (er) { + if (!er) + return true + + if (er.code === "ENOSYS") + return true + + var nonroot = !process.getuid || process.getuid() !== 0 + if (nonroot) { + if (er.code === "EINVAL" || er.code === "EPERM") + return true + } + + return false + } +} diff --git a/tests/corpus/fs-extra-package/node_modules/jsonfile/LICENSE b/tests/corpus/fs-extra-package/node_modules/jsonfile/LICENSE new file mode 100644 index 000000000..cb7e807b9 --- /dev/null +++ b/tests/corpus/fs-extra-package/node_modules/jsonfile/LICENSE @@ -0,0 +1,15 @@ +(The MIT License) + +Copyright (c) 2012-2015, JP Richardson + +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/fs-extra-package/node_modules/jsonfile/index.js b/tests/corpus/fs-extra-package/node_modules/jsonfile/index.js new file mode 100644 index 000000000..0582868f1 --- /dev/null +++ b/tests/corpus/fs-extra-package/node_modules/jsonfile/index.js @@ -0,0 +1,88 @@ +let _fs +try { + _fs = require('graceful-fs') +} catch (_) { + _fs = require('fs') +} +const universalify = require('universalify') +const { stringify, stripBom } = require('./utils') + +async function _readFile (file, options = {}) { + if (typeof options === 'string') { + options = { encoding: options } + } + + const fs = options.fs || _fs + + const shouldThrow = 'throws' in options ? options.throws : true + + let data = await universalify.fromCallback(fs.readFile)(file, options) + + data = stripBom(data) + + let obj + try { + obj = JSON.parse(data, options ? options.reviver : null) + } catch (err) { + if (shouldThrow) { + err.message = `${file}: ${err.message}` + throw err + } else { + return null + } + } + + return obj +} + +const readFile = universalify.fromPromise(_readFile) + +function readFileSync (file, options = {}) { + if (typeof options === 'string') { + options = { encoding: options } + } + + const fs = options.fs || _fs + + const shouldThrow = 'throws' in options ? options.throws : true + + try { + let content = fs.readFileSync(file, options) + content = stripBom(content) + return JSON.parse(content, options.reviver) + } catch (err) { + if (shouldThrow) { + err.message = `${file}: ${err.message}` + throw err + } else { + return null + } + } +} + +async function _writeFile (file, obj, options = {}) { + const fs = options.fs || _fs + + const str = stringify(obj, options) + + await universalify.fromCallback(fs.writeFile)(file, str, options) +} + +const writeFile = universalify.fromPromise(_writeFile) + +function writeFileSync (file, obj, options = {}) { + const fs = options.fs || _fs + + const str = stringify(obj, options) + // not sure if fs.writeFileSync returns anything, but just in case + return fs.writeFileSync(file, str, options) +} + +const jsonfile = { + readFile, + readFileSync, + writeFile, + writeFileSync +} + +module.exports = jsonfile diff --git a/tests/corpus/fs-extra-package/node_modules/jsonfile/node_modules/universalify/LICENSE b/tests/corpus/fs-extra-package/node_modules/jsonfile/node_modules/universalify/LICENSE new file mode 100644 index 000000000..514e84e64 --- /dev/null +++ b/tests/corpus/fs-extra-package/node_modules/jsonfile/node_modules/universalify/LICENSE @@ -0,0 +1,20 @@ +(The MIT License) + +Copyright (c) 2017, Ryan Zimmerman + +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/fs-extra-package/node_modules/jsonfile/node_modules/universalify/index.js b/tests/corpus/fs-extra-package/node_modules/jsonfile/node_modules/universalify/index.js new file mode 100644 index 000000000..233beac75 --- /dev/null +++ b/tests/corpus/fs-extra-package/node_modules/jsonfile/node_modules/universalify/index.js @@ -0,0 +1,24 @@ +'use strict' + +exports.fromCallback = function (fn) { + return Object.defineProperty(function (...args) { + if (typeof args[args.length - 1] === 'function') fn.apply(this, args) + else { + return new Promise((resolve, reject) => { + args.push((err, res) => (err != null) ? reject(err) : resolve(res)) + fn.apply(this, args) + }) + } + }, 'name', { value: fn.name }) +} + +exports.fromPromise = function (fn) { + return Object.defineProperty(function (...args) { + const cb = args[args.length - 1] + if (typeof cb !== 'function') return fn.apply(this, args) + else { + args.pop() + fn.apply(this, args).then(r => cb(null, r), cb) + } + }, 'name', { value: fn.name }) +} diff --git a/tests/corpus/fs-extra-package/node_modules/jsonfile/node_modules/universalify/package.json b/tests/corpus/fs-extra-package/node_modules/jsonfile/node_modules/universalify/package.json new file mode 100644 index 000000000..d60fccb50 --- /dev/null +++ b/tests/corpus/fs-extra-package/node_modules/jsonfile/node_modules/universalify/package.json @@ -0,0 +1,34 @@ +{ + "name": "universalify", + "version": "2.0.1", + "description": "Make a callback- or promise-based function support both promises and callbacks.", + "keywords": [ + "callback", + "native", + "promise" + ], + "homepage": "https://github.com/RyanZim/universalify#readme", + "bugs": "https://github.com/RyanZim/universalify/issues", + "license": "MIT", + "author": "Ryan Zimmerman ", + "files": [ + "index.js" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/RyanZim/universalify.git" + }, + "scripts": { + "test": "standard && nyc --reporter text --reporter lcovonly tape test/*.js | colortape" + }, + "devDependencies": { + "colortape": "^0.1.2", + "coveralls": "^3.0.1", + "nyc": "^15.0.0", + "standard": "^14.3.1", + "tape": "^5.0.1" + }, + "engines": { + "node": ">= 10.0.0" + } +} diff --git a/tests/corpus/fs-extra-package/node_modules/jsonfile/package.json b/tests/corpus/fs-extra-package/node_modules/jsonfile/package.json new file mode 100644 index 000000000..4d01eb1d7 --- /dev/null +++ b/tests/corpus/fs-extra-package/node_modules/jsonfile/package.json @@ -0,0 +1,40 @@ +{ + "name": "jsonfile", + "version": "6.1.0", + "description": "Easily read/write JSON files.", + "repository": { + "type": "git", + "url": "git@github.com:jprichardson/node-jsonfile.git" + }, + "keywords": [ + "read", + "write", + "file", + "json", + "fs", + "fs-extra" + ], + "author": "JP Richardson ", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + }, + "devDependencies": { + "mocha": "^8.2.0", + "rimraf": "^2.4.0", + "standard": "^16.0.1" + }, + "main": "index.js", + "files": [ + "index.js", + "utils.js" + ], + "scripts": { + "lint": "standard", + "test": "npm run lint && npm run unit", + "unit": "mocha" + } +} diff --git a/tests/corpus/fs-extra-package/node_modules/jsonfile/utils.js b/tests/corpus/fs-extra-package/node_modules/jsonfile/utils.js new file mode 100644 index 000000000..b5ff48e5e --- /dev/null +++ b/tests/corpus/fs-extra-package/node_modules/jsonfile/utils.js @@ -0,0 +1,14 @@ +function stringify (obj, { EOL = '\n', finalEOL = true, replacer = null, spaces } = {}) { + const EOF = finalEOL ? EOL : '' + const str = JSON.stringify(obj, replacer, spaces) + + return str.replace(/\n/g, EOL) + EOF +} + +function stripBom (content) { + // we do this because JSON.parse would convert it to a utf8 string if encoding wasn't specified + if (Buffer.isBuffer(content)) content = content.toString('utf8') + return content.replace(/^\uFEFF/, '') +} + +module.exports = { stringify, stripBom } diff --git a/tests/corpus/fs-extra-package/node_modules/universalify/LICENSE b/tests/corpus/fs-extra-package/node_modules/universalify/LICENSE new file mode 100644 index 000000000..514e84e64 --- /dev/null +++ b/tests/corpus/fs-extra-package/node_modules/universalify/LICENSE @@ -0,0 +1,20 @@ +(The MIT License) + +Copyright (c) 2017, Ryan Zimmerman + +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/fs-extra-package/node_modules/universalify/index.js b/tests/corpus/fs-extra-package/node_modules/universalify/index.js new file mode 100644 index 000000000..e7b5712ea --- /dev/null +++ b/tests/corpus/fs-extra-package/node_modules/universalify/index.js @@ -0,0 +1,23 @@ +'use strict' + +exports.fromCallback = function (fn) { + return Object.defineProperty(function (...args) { + if (typeof args[args.length - 1] === 'function') fn.apply(this, args) + else { + return new Promise((resolve, reject) => { + fn.apply( + this, + args.concat([(err, res) => err ? reject(err) : resolve(res)]) + ) + }) + } + }, 'name', { value: fn.name }) +} + +exports.fromPromise = function (fn) { + return Object.defineProperty(function (...args) { + const cb = args[args.length - 1] + if (typeof cb !== 'function') return fn.apply(this, args) + else fn.apply(this, args.slice(0, -1)).then(r => cb(null, r), cb) + }, 'name', { value: fn.name }) +} diff --git a/tests/corpus/fs-extra-package/node_modules/universalify/package.json b/tests/corpus/fs-extra-package/node_modules/universalify/package.json new file mode 100644 index 000000000..4ac07daf3 --- /dev/null +++ b/tests/corpus/fs-extra-package/node_modules/universalify/package.json @@ -0,0 +1,34 @@ +{ + "name": "universalify", + "version": "1.0.0", + "description": "Make a callback- or promise-based function support both promises and callbacks.", + "keywords": [ + "callback", + "native", + "promise" + ], + "homepage": "https://github.com/RyanZim/universalify#readme", + "bugs": "https://github.com/RyanZim/universalify/issues", + "license": "MIT", + "author": "Ryan Zimmerman ", + "files": [ + "index.js" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/RyanZim/universalify.git" + }, + "scripts": { + "test": "standard && nyc tape test/*.js | colortape" + }, + "devDependencies": { + "colortape": "^0.1.2", + "coveralls": "^3.0.1", + "nyc": "^15.0.0", + "standard": "^14.3.1", + "tape": "^4.6.3" + }, + "engines": { + "node": ">= 10.0.0" + } +} diff --git a/tests/corpus/global-detection-typeof.cjs b/tests/corpus/global-detection-typeof.cjs new file mode 100644 index 000000000..971652ee9 --- /dev/null +++ b/tests/corpus/global-detection-typeof.cjs @@ -0,0 +1,28 @@ +// Host detection in CommonJS: typeof on globals nothing declares (window, +// self, document) answers "undefined" without throwing, esbuild's minified +// `typeof x < "u"` test included, and the untaken arm of a detection chain +// is code Node never runs. The global object's members read as the +// globals themselves. +"use strict"; +console.log(typeof window, typeof self, typeof document, typeof global, typeof globalThis); +console.log(typeof window < "u", typeof global < "u", typeof self !== "undefined"); + +var OurUint8Array = + (typeof global < "u" ? global : typeof window < "u" ? window : typeof self < "u" ? self : {}) + .Uint8Array || function () {}; +console.log(OurUint8Array === Uint8Array, new OurUint8Array(3).length); + +var isBrowser = typeof window !== "undefined" && typeof window.document !== "undefined"; +console.log(isBrowser ? "browser" : "node"); +if (typeof window !== "undefined") { + console.log(window.location.href); +} else { + console.log("no window"); +} +var where = typeof self < "u" ? self.location : typeof process === "object" ? process.platform.length > 0 : null; +console.log(where); + +console.log(globalThis.Uint8Array === Uint8Array, global.Buffer === Buffer); +console.log(typeof global.setTimeout, typeof globalThis.queueMicrotask, typeof globalThis.Uint8Array); +var U = global.Uint8Array || function () {}; +console.log(new U(2).length); diff --git a/tests/corpus/json-parse-fallback-extra-keys.ts b/tests/corpus/json-parse-fallback-extra-keys.ts new file mode 100644 index 000000000..7eba7b2a8 --- /dev/null +++ b/tests/corpus/json-parse-fallback-extra-keys.ts @@ -0,0 +1,37 @@ +// A failed checked cast keeps every member of the parsed object: extra +// members stay visible to Object.keys, JSON.stringify, for...in and +// util.inspect, in the source object's key order. +interface Point { + x: number; + y: number; +} + +function show(text: string): void { + const p = JSON.parse(text) as Point; + console.log(Object.keys(p).join(",")); + console.log(Object.values(p).length, Object.entries(p).length); + console.log(JSON.stringify(p)); + console.log(JSON.stringify(p, null, 2)); + const seen: string[] = []; + for (const k in p) seen.push(k); + console.log(seen.join("|")); + console.log(p); + console.log("has z:", "z" in p, Object.hasOwn(p, "z")); +} + +show('{"x":1,"y":2}'); +show('{"z":3,"x":"1","y":2,"label":{"text":"hi","size":[1,2]}}'); +show('{"x":1}'); +show('{"b":1,"a":2,"x":null,"y":[]}'); + +// Spread copies every member of the failed value, in source order. +function copyOf(text: string): void { + const p = JSON.parse(text) as Point; + const copy = { ...p, y: 10 }; + console.log(copy, Object.keys(copy).join(",")); + const pair = [p.x, p.y]; + console.log(pair, JSON.stringify({ point: p, at: [p.x] })); +} + +copyOf('{"x":1,"y":2}'); +copyOf('{"w":0,"x":"1","extra":true}'); diff --git a/tests/corpus/json-parse-fallback-flows.ts b/tests/corpus/json-parse-fallback-flows.ts new file mode 100644 index 000000000..82232959f --- /dev/null +++ b/tests/corpus/json-parse-fallback-flows.ts @@ -0,0 +1,57 @@ +// Checked-cast fallback across declaration forms and control flow: an +// annotated declaration from an untyped value, `unknown as T`, a site in a +// loop whose iterations alternate between fitting and failing values, +// closures and callbacks over the failed value, and a value that fits a +// typed parameter once it reaches it. +interface Job { + name: string; + retries: number; + steps: string[]; +} + +function retriesOf(n: number): string { + return n === 1 ? "1 retry" : `${n} retries`; +} + +function annotated(text: string): void { + const job: Job = JSON.parse(text); + console.log("annotated", job.name, job.retries, job.steps?.length); +} + +function fromUnknown(value: unknown): void { + const job = value as Job; + console.log("unknown", job.name ?? "(unnamed)", typeof job.retries); +} + +function loop(texts: string[]): void { + for (const text of texts) { + const job = JSON.parse(text) as Job; + const label = job.name + ":" + job.retries; + console.log("loop", label, typeof job.retries === "number" ? retriesOf(job.retries) : "n/a"); + } +} + +async function later(text: string): Promise { + const job = JSON.parse(text) as Job; + const steps = job.steps ?? []; + await new Promise((resolve) => setTimeout(resolve, 1)); + steps.forEach((step, i) => console.log("step", i, step)); + const upper = steps.map((s) => String(s).toUpperCase()); + console.log("upper", upper.join(" ")); + const describeJob = (): string => `${job.name} (${job.retries})`; + console.log("closure", describeJob()); +} + +annotated('{"name":"a","retries":1,"steps":["x"]}'); +annotated('{"name":"b","retries":"2"}'); +fromUnknown(JSON.parse('{"name":"c","retries":3,"steps":[]}')); +fromUnknown(JSON.parse('{"retries":"3"}')); +loop([ + '{"name":"d","retries":1,"steps":[]}', + '{"name":"e","retries":"x","steps":[]}', + '{"name":"f","retries":2,"steps":[]}', + '{"name":"g"}', +]); +later('{"name":"h","retries":0,"steps":["s1","s2"]}').then(() => + later('{"name":"i","retries":"0","steps":["s1",2]}'), +); diff --git a/tests/corpus/json-parse-fallback-missing.ts b/tests/corpus/json-parse-fallback-missing.ts new file mode 100644 index 000000000..b84f8af2f --- /dev/null +++ b/tests/corpus/json-parse-fallback-missing.ts @@ -0,0 +1,36 @@ +// A `JSON.parse(text) as T` whose value lacks required members keeps the +// parsed value: missing members read undefined, as in Node. +interface Address { + city: string; + zip: string; +} +interface User { + id: number; + name: string; + admin: boolean; + address: Address; + tags: string[]; +} + +function describe(text: string): void { + const user = JSON.parse(text) as User; + console.log("id", user.id, typeof user.id); + console.log("name", user.name, user.name === undefined); + console.log("admin", user.admin ? "yes" : "no", !user.admin); + console.log("city", user.address?.city ?? "(no city)"); + console.log("zip?", "zip" in (user.address ?? {})); + console.log("tags", user.tags?.length ?? 0); + if (!user.address) console.log("no address"); + else console.log("address", user.address.city, user.address.zip); + try { + console.log("street", user.address.city.length); + } catch (e) { + console.log("read threw", e instanceof TypeError, (e as Error).message); + } + console.log(`label: ${user.name}#${user.id}`); +} + +describe('{"id":1,"name":"ada","admin":true,"address":{"city":"London","zip":"N1"},"tags":["a"]}'); +describe('{"id":2}'); +describe('{"name":"bob","address":{"city":"Paris"}}'); +describe("{}"); diff --git a/tests/corpus/json-parse-fallback-nested.ts b/tests/corpus/json-parse-fallback-nested.ts new file mode 100644 index 000000000..96abddaf9 --- /dev/null +++ b/tests/corpus/json-parse-fallback-nested.ts @@ -0,0 +1,42 @@ +// Nested records and arrays of a value that failed its checked cast keep +// their identity: reads, writes and comparisons reach the same objects. +interface Line { + sku: string; + qty: number; +} +interface Cart { + id: string; + lines: Line[]; + owner: { name: string; email: string }; +} + +function lineTotal(lines: Line[]): number { + let n = 0; + for (const line of lines) n += line.qty; + return n; +} + +function inspectCart(text: string): void { + const cart = JSON.parse(text) as Cart; + const lines = cart.lines; + console.log("same lines:", lines === cart.lines, cart.owner === cart.owner); + lines.push({ sku: "added", qty: 1 }); + console.log("count:", cart.lines.length, lines.length); + const first = lines[0]; + first.qty = first.qty + 10; + console.log("first:", cart.lines[0].sku, cart.lines[0].qty); + console.log("skus:", lines.map((l) => l.sku).join(",")); + console.log("big:", lines.filter((l) => l.qty > 5).length); + console.log("found:", lines.find((l) => l.sku === "b")?.qty ?? "none"); + console.log("some string qty:", lines.some((l) => typeof l.qty === "string")); + console.log("owner:", cart.owner?.name ?? "(anonymous)", cart.owner?.email); + console.log(JSON.stringify(cart)); + // A typed function receives the lines when they fit its parameter. + if (lines.every((l) => typeof l.qty === "number" && typeof l.sku === "string")) + console.log("total:", lineTotal(lines)); + else console.log("total: skipped"); +} + +inspectCart('{"id":"c1","lines":[{"sku":"a","qty":1},{"sku":"b","qty":2}],"owner":{"name":"n","email":"e"}}'); +inspectCart('{"id":"c2","lines":[{"sku":"a","qty":1},{"sku":"b","qty":2}]}'); +inspectCart('{"id":3,"lines":[{"sku":"a","qty":"1"}],"owner":{"name":"n"}}'); diff --git a/tests/corpus/json-parse-fallback-partial.ts b/tests/corpus/json-parse-fallback-partial.ts new file mode 100644 index 000000000..ec3e2c769 --- /dev/null +++ b/tests/corpus/json-parse-fallback-partial.ts @@ -0,0 +1,59 @@ +// `Partial` and optional members: a request whose nested required +// members are missing is accepted the way Node accepts it (the listener +// pattern: the binding is assigned inside try, read after it). +interface Deployment { + id: string; + name: string; + billing: { plan: string }; +} +interface Order { + deployment: Deployment; + owner: { id: string }; +} +interface ListenerRequest { + buildOrder: Order; + traceCarrier?: Record; +} + +const delivered: string[] = []; + +async function readBody(body: string): Promise { + return body; +} + +async function handle(body: string): Promise { + const text = await readBody(body); + let request: Partial; + try { + request = JSON.parse(text) as Partial; + } catch { + console.log("400 invalid JSON body"); + return 400; + } + const buildOrder = request.buildOrder; + const deploymentId = buildOrder?.deployment?.id; + if (!deploymentId) { + console.log("400 missing buildOrder.deployment.id"); + return 400; + } + delivered.push(deploymentId); + console.log("200 accepted", deploymentId, Object.keys(request).join(",")); + console.log("carrier:", request.traceCarrier?.traceparent ?? "(none)"); + try { + console.log("plan:", buildOrder?.deployment.billing.plan); + } catch (e) { + console.log("plan threw:", (e as Error).message); + } + return 200; +} + +async function main(): Promise { + await handle('{"buildOrder":{"deployment":{"id":"dpl_1","name":"n","billing":{"plan":"pro"}},"owner":{"id":"o"}}}'); + await handle('{"buildOrder":{"deployment":{"id":"dpl_2"}},"traceCarrier":{"traceparent":"00-ab"}}'); + await handle('{"buildOrder":{"deployment":{}}}'); + await handle('{"traceCarrier":{}}'); + await handle("not json"); + console.log(delivered.join(",")); +} + +main(); diff --git a/tests/corpus/json-parse-fallback-wrong-types.ts b/tests/corpus/json-parse-fallback-wrong-types.ts new file mode 100644 index 000000000..06ed79255 --- /dev/null +++ b/tests/corpus/json-parse-fallback-wrong-types.ts @@ -0,0 +1,33 @@ +// Members whose JSON type differs from the declared one keep their real +// values and JavaScript semantics after a failed checked cast. +interface Reading { + sensor: string; + value: number; + ok: boolean | number; + samples: number[]; +} + +function report(text: string): string { + const r = JSON.parse(text) as Reading; + const doubled = r.value * 2; + const sum = r.value + 1; + console.log(r.sensor, typeof r.sensor, r.value, typeof r.value, doubled, sum); + console.log("ok:", r.ok, typeof r.ok, r.ok === true, r.ok == 1); + console.log("cmp:", r.value > 10, r.value < 10, r.value === 5); + console.log("samples:", Array.isArray(r.samples), r.samples.length); + let total = 0; + for (const s of r.samples) total = total + s; + console.log("total:", total, typeof total); + try { + const parts = r.samples.map((s) => typeof s); + console.log("kinds:", parts.join(",")); + } catch (e) { + console.log("map threw:", (e as Error).message); + } + return `${r.sensor}=${r.value}`; +} + +console.log(report('{"sensor":"t1","value":5,"ok":true,"samples":[1,2,3]}')); +console.log(report('{"sensor":7,"value":"5","ok":1,"samples":[1,"2",3]}')); +console.log(report('{"sensor":"t3","value":null,"ok":"yes","samples":"abc"}')); +console.log(report('{"sensor":["x"],"value":{"n":1},"ok":false,"samples":[]}')); diff --git a/tests/corpus/readable-stream-3/main.cjs b/tests/corpus/readable-stream-3/main.cjs new file mode 100644 index 000000000..2576e1339 --- /dev/null +++ b/tests/corpus/readable-stream-3/main.cjs @@ -0,0 +1,86 @@ +// readable-stream 3.x compiles as Node's builtin stream module, a +// documented divergence: Node runs the package's own JavaScript streams (a +// copy of Node 10's), and the package's READABLE_STREAM=disable branch, +// which would hand out the builtin classes, throws on Node 24. This program +// keeps to behavior both share: piping through transforms, pipeline and +// finished callbacks, and error propagation, one step at a time. +// node_modules vendors readable-stream 3.6.2 and its dependencies, +// unmodified (MIT). +"use strict"; +const rs = require("readable-stream"); + +console.log( + typeof rs.Readable, + typeof rs.Writable, + typeof rs.Transform, + typeof rs.PassThrough, + typeof rs.pipeline, + typeof rs.finished, +); + +function piped(next) { + const seen = []; + const source = new rs.Readable({ read: function () {} }); + const upper = new rs.Transform({ + transform: function (chunk, encoding, callback) { + callback(null, String(chunk).toUpperCase()); + }, + }); + const through = new rs.PassThrough(); + const sink = new rs.Writable({ + write: function (chunk, encoding, callback) { + seen.push(String(chunk)); + callback(); + }, + }); + rs.pipeline(source, upper, through, sink, function (err) { + console.log("pipeline", err ? err.message : "ok", seen.join("|")); + next(); + }); + ["a", "bc", "def"].forEach(function (w) { + source.push(w); + }); + source.push(null); +} + +function failed(next) { + const source = new rs.Readable({ read: function () {} }); + const broken = new rs.Transform({ + transform: function (chunk, encoding, callback) { + callback(new Error("boom on " + String(chunk))); + }, + }); + const sink = new rs.Writable({ + write: function (chunk, encoding, callback) { + callback(); + }, + }); + rs.pipeline(source, broken, sink, function (err) { + console.log("pipeline", err ? err.message : "ok"); + next(); + }); + source.push("x"); + source.push(null); +} + +function ended(next) { + const sink = new rs.Writable({ + write: function (chunk, encoding, callback) { + callback(); + }, + }); + rs.finished(sink, function (err) { + console.log("finished", err ? err.message : "ok"); + next(); + }); + sink.end("last"); +} + +piped(function () { + failed(function () { + ended(function () { + console.log("done"); + }); + }); +}); +console.log("sync done"); diff --git a/tests/corpus/readable-stream-3/node_modules/inherits/LICENSE b/tests/corpus/readable-stream-3/node_modules/inherits/LICENSE new file mode 100644 index 000000000..dea3013d6 --- /dev/null +++ b/tests/corpus/readable-stream-3/node_modules/inherits/LICENSE @@ -0,0 +1,16 @@ +The ISC License + +Copyright (c) Isaac Z. Schlueter + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. + diff --git a/tests/corpus/readable-stream-3/node_modules/inherits/README.md b/tests/corpus/readable-stream-3/node_modules/inherits/README.md new file mode 100644 index 000000000..b1c566585 --- /dev/null +++ b/tests/corpus/readable-stream-3/node_modules/inherits/README.md @@ -0,0 +1,42 @@ +Browser-friendly inheritance fully compatible with standard node.js +[inherits](http://nodejs.org/api/util.html#util_util_inherits_constructor_superconstructor). + +This package exports standard `inherits` from node.js `util` module in +node environment, but also provides alternative browser-friendly +implementation through [browser +field](https://gist.github.com/shtylman/4339901). Alternative +implementation is a literal copy of standard one located in standalone +module to avoid requiring of `util`. It also has a shim for old +browsers with no `Object.create` support. + +While keeping you sure you are using standard `inherits` +implementation in node.js environment, it allows bundlers such as +[browserify](https://github.com/substack/node-browserify) to not +include full `util` package to your client code if all you need is +just `inherits` function. It worth, because browser shim for `util` +package is large and `inherits` is often the single function you need +from it. + +It's recommended to use this package instead of +`require('util').inherits` for any code that has chances to be used +not only in node.js but in browser too. + +## usage + +```js +var inherits = require('inherits'); +// then use exactly as the standard one +``` + +## note on version ~1.0 + +Version ~1.0 had completely different motivation and is not compatible +neither with 2.0 nor with standard node.js `inherits`. + +If you are using version ~1.0 and planning to switch to ~2.0, be +careful: + +* new version uses `super_` instead of `super` for referencing + superclass +* new version overwrites current prototype while old one preserves any + existing fields on it diff --git a/tests/corpus/readable-stream-3/node_modules/inherits/inherits.js b/tests/corpus/readable-stream-3/node_modules/inherits/inherits.js new file mode 100644 index 000000000..f71f2d932 --- /dev/null +++ b/tests/corpus/readable-stream-3/node_modules/inherits/inherits.js @@ -0,0 +1,9 @@ +try { + var util = require('util'); + /* istanbul ignore next */ + if (typeof util.inherits !== 'function') throw ''; + module.exports = util.inherits; +} catch (e) { + /* istanbul ignore next */ + module.exports = require('./inherits_browser.js'); +} diff --git a/tests/corpus/readable-stream-3/node_modules/inherits/inherits_browser.js b/tests/corpus/readable-stream-3/node_modules/inherits/inherits_browser.js new file mode 100644 index 000000000..86bbb3dc2 --- /dev/null +++ b/tests/corpus/readable-stream-3/node_modules/inherits/inherits_browser.js @@ -0,0 +1,27 @@ +if (typeof Object.create === 'function') { + // implementation from standard node.js 'util' module + module.exports = function inherits(ctor, superCtor) { + if (superCtor) { + ctor.super_ = superCtor + ctor.prototype = Object.create(superCtor.prototype, { + constructor: { + value: ctor, + enumerable: false, + writable: true, + configurable: true + } + }) + } + }; +} else { + // old school shim for old browsers + module.exports = function inherits(ctor, superCtor) { + if (superCtor) { + ctor.super_ = superCtor + var TempCtor = function () {} + TempCtor.prototype = superCtor.prototype + ctor.prototype = new TempCtor() + ctor.prototype.constructor = ctor + } + } +} diff --git a/tests/corpus/readable-stream-3/node_modules/inherits/package.json b/tests/corpus/readable-stream-3/node_modules/inherits/package.json new file mode 100644 index 000000000..37b4366b8 --- /dev/null +++ b/tests/corpus/readable-stream-3/node_modules/inherits/package.json @@ -0,0 +1,29 @@ +{ + "name": "inherits", + "description": "Browser-friendly inheritance fully compatible with standard node.js inherits()", + "version": "2.0.4", + "keywords": [ + "inheritance", + "class", + "klass", + "oop", + "object-oriented", + "inherits", + "browser", + "browserify" + ], + "main": "./inherits.js", + "browser": "./inherits_browser.js", + "repository": "git://github.com/isaacs/inherits", + "license": "ISC", + "scripts": { + "test": "tap" + }, + "devDependencies": { + "tap": "^14.2.4" + }, + "files": [ + "inherits.js", + "inherits_browser.js" + ] +} diff --git a/tests/corpus/readable-stream-3/node_modules/readable-stream/CONTRIBUTING.md b/tests/corpus/readable-stream-3/node_modules/readable-stream/CONTRIBUTING.md new file mode 100644 index 000000000..f478d58dc --- /dev/null +++ b/tests/corpus/readable-stream-3/node_modules/readable-stream/CONTRIBUTING.md @@ -0,0 +1,38 @@ +# Developer's Certificate of Origin 1.1 + +By making a contribution to this project, I certify that: + +* (a) The contribution was created in whole or in part by me and I + have the right to submit it under the open source license + indicated in the file; or + +* (b) The contribution is based upon previous work that, to the best + of my knowledge, is covered under an appropriate open source + license and I have the right under that license to submit that + work with modifications, whether created in whole or in part + by me, under the same open source license (unless I am + permitted to submit under a different license), as indicated + in the file; or + +* (c) The contribution was provided directly to me by some other + person who certified (a), (b) or (c) and I have not modified + it. + +* (d) I understand and agree that this project and the contribution + are public and that a record of the contribution (including all + personal information I submit with it, including my sign-off) is + maintained indefinitely and may be redistributed consistent with + this project or the open source license(s) involved. + +## Moderation Policy + +The [Node.js Moderation Policy] applies to this WG. + +## Code of Conduct + +The [Node.js Code of Conduct][] applies to this WG. + +[Node.js Code of Conduct]: +https://github.com/nodejs/node/blob/master/CODE_OF_CONDUCT.md +[Node.js Moderation Policy]: +https://github.com/nodejs/TSC/blob/master/Moderation-Policy.md diff --git a/tests/corpus/readable-stream-3/node_modules/readable-stream/GOVERNANCE.md b/tests/corpus/readable-stream-3/node_modules/readable-stream/GOVERNANCE.md new file mode 100644 index 000000000..16ffb93f2 --- /dev/null +++ b/tests/corpus/readable-stream-3/node_modules/readable-stream/GOVERNANCE.md @@ -0,0 +1,136 @@ +### Streams Working Group + +The Node.js Streams is jointly governed by a Working Group +(WG) +that is responsible for high-level guidance of the project. + +The WG has final authority over this project including: + +* Technical direction +* Project governance and process (including this policy) +* Contribution policy +* GitHub repository hosting +* Conduct guidelines +* Maintaining the list of additional Collaborators + +For the current list of WG members, see the project +[README.md](./README.md#current-project-team-members). + +### Collaborators + +The readable-stream GitHub repository is +maintained by the WG and additional Collaborators who are added by the +WG on an ongoing basis. + +Individuals making significant and valuable contributions are made +Collaborators and given commit-access to the project. These +individuals are identified by the WG and their addition as +Collaborators is discussed during the WG meeting. + +_Note:_ If you make a significant contribution and are not considered +for commit-access log an issue or contact a WG member directly and it +will be brought up in the next WG meeting. + +Modifications of the contents of the readable-stream repository are +made on +a collaborative basis. Anybody with a GitHub account may propose a +modification via pull request and it will be considered by the project +Collaborators. All pull requests must be reviewed and accepted by a +Collaborator with sufficient expertise who is able to take full +responsibility for the change. In the case of pull requests proposed +by an existing Collaborator, an additional Collaborator is required +for sign-off. Consensus should be sought if additional Collaborators +participate and there is disagreement around a particular +modification. See _Consensus Seeking Process_ below for further detail +on the consensus model used for governance. + +Collaborators may opt to elevate significant or controversial +modifications, or modifications that have not found consensus to the +WG for discussion by assigning the ***WG-agenda*** tag to a pull +request or issue. The WG should serve as the final arbiter where +required. + +For the current list of Collaborators, see the project +[README.md](./README.md#members). + +### WG Membership + +WG seats are not time-limited. There is no fixed size of the WG. +However, the expected target is between 6 and 12, to ensure adequate +coverage of important areas of expertise, balanced with the ability to +make decisions efficiently. + +There is no specific set of requirements or qualifications for WG +membership beyond these rules. + +The WG may add additional members to the WG by unanimous consensus. + +A WG member may be removed from the WG by voluntary resignation, or by +unanimous consensus of all other WG members. + +Changes to WG membership should be posted in the agenda, and may be +suggested as any other agenda item (see "WG Meetings" below). + +If an addition or removal is proposed during a meeting, and the full +WG is not in attendance to participate, then the addition or removal +is added to the agenda for the subsequent meeting. This is to ensure +that all members are given the opportunity to participate in all +membership decisions. If a WG member is unable to attend a meeting +where a planned membership decision is being made, then their consent +is assumed. + +No more than 1/3 of the WG members may be affiliated with the same +employer. If removal or resignation of a WG member, or a change of +employment by a WG member, creates a situation where more than 1/3 of +the WG membership shares an employer, then the situation must be +immediately remedied by the resignation or removal of one or more WG +members affiliated with the over-represented employer(s). + +### WG Meetings + +The WG meets occasionally on a Google Hangout On Air. A designated moderator +approved by the WG runs the meeting. Each meeting should be +published to YouTube. + +Items are added to the WG agenda that are considered contentious or +are modifications of governance, contribution policy, WG membership, +or release process. + +The intention of the agenda is not to approve or review all patches; +that should happen continuously on GitHub and be handled by the larger +group of Collaborators. + +Any community member or contributor can ask that something be added to +the next meeting's agenda by logging a GitHub Issue. Any Collaborator, +WG member or the moderator can add the item to the agenda by adding +the ***WG-agenda*** tag to the issue. + +Prior to each WG meeting the moderator will share the Agenda with +members of the WG. WG members can add any items they like to the +agenda at the beginning of each meeting. The moderator and the WG +cannot veto or remove items. + +The WG may invite persons or representatives from certain projects to +participate in a non-voting capacity. + +The moderator is responsible for summarizing the discussion of each +agenda item and sends it as a pull request after the meeting. + +### Consensus Seeking Process + +The WG follows a +[Consensus +Seeking](http://en.wikipedia.org/wiki/Consensus-seeking_decision-making) +decision-making model. + +When an agenda item has appeared to reach a consensus the moderator +will ask "Does anyone object?" as a final call for dissent from the +consensus. + +If an agenda item cannot reach a consensus a WG member can call for +either a closing vote or a vote to table the issue to the next +meeting. The call for a vote must be seconded by a majority of the WG +or else the discussion will continue. Simple majority wins. + +Note that changes to WG membership require a majority consensus. See +"WG Membership" above. diff --git a/tests/corpus/readable-stream-3/node_modules/readable-stream/LICENSE b/tests/corpus/readable-stream-3/node_modules/readable-stream/LICENSE new file mode 100644 index 000000000..2873b3b2e --- /dev/null +++ b/tests/corpus/readable-stream-3/node_modules/readable-stream/LICENSE @@ -0,0 +1,47 @@ +Node.js is licensed for use as follows: + +""" +Copyright Node.js contributors. 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. +""" + +This license applies to parts of Node.js originating from the +https://github.com/joyent/node repository: + +""" +Copyright Joyent, Inc. and other Node contributors. 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. +""" diff --git a/tests/corpus/readable-stream-3/node_modules/readable-stream/README.md b/tests/corpus/readable-stream-3/node_modules/readable-stream/README.md new file mode 100644 index 000000000..19117c1a0 --- /dev/null +++ b/tests/corpus/readable-stream-3/node_modules/readable-stream/README.md @@ -0,0 +1,106 @@ +# readable-stream + +***Node.js core streams for userland*** [![Build Status](https://travis-ci.com/nodejs/readable-stream.svg?branch=master)](https://travis-ci.com/nodejs/readable-stream) + + +[![NPM](https://nodei.co/npm/readable-stream.png?downloads=true&downloadRank=true)](https://nodei.co/npm/readable-stream/) +[![NPM](https://nodei.co/npm-dl/readable-stream.png?&months=6&height=3)](https://nodei.co/npm/readable-stream/) + + +[![Sauce Test Status](https://saucelabs.com/browser-matrix/readabe-stream.svg)](https://saucelabs.com/u/readabe-stream) + +```bash +npm install --save readable-stream +``` + +This package is a mirror of the streams implementations in Node.js. + +Full documentation may be found on the [Node.js website](https://nodejs.org/dist/v10.18.1/docs/api/stream.html). + +If you want to guarantee a stable streams base, regardless of what version of +Node you, or the users of your libraries are using, use **readable-stream** *only* and avoid the *"stream"* module in Node-core, for background see [this blogpost](http://r.va.gg/2014/06/why-i-dont-use-nodes-core-stream-module.html). + +As of version 2.0.0 **readable-stream** uses semantic versioning. + +## Version 3.x.x + +v3.x.x of `readable-stream` is a cut from Node 10. This version supports Node 6, 8, and 10, as well as evergreen browsers, IE 11 and latest Safari. The breaking changes introduced by v3 are composed by the combined breaking changes in [Node v9](https://nodejs.org/en/blog/release/v9.0.0/) and [Node v10](https://nodejs.org/en/blog/release/v10.0.0/), as follows: + +1. Error codes: https://github.com/nodejs/node/pull/13310, + https://github.com/nodejs/node/pull/13291, + https://github.com/nodejs/node/pull/16589, + https://github.com/nodejs/node/pull/15042, + https://github.com/nodejs/node/pull/15665, + https://github.com/nodejs/readable-stream/pull/344 +2. 'readable' have precedence over flowing + https://github.com/nodejs/node/pull/18994 +3. make virtual methods errors consistent + https://github.com/nodejs/node/pull/18813 +4. updated streams error handling + https://github.com/nodejs/node/pull/18438 +5. writable.end should return this. + https://github.com/nodejs/node/pull/18780 +6. readable continues to read when push('') + https://github.com/nodejs/node/pull/18211 +7. add custom inspect to BufferList + https://github.com/nodejs/node/pull/17907 +8. always defer 'readable' with nextTick + https://github.com/nodejs/node/pull/17979 + +## Version 2.x.x +v2.x.x of `readable-stream` is a cut of the stream module from Node 8 (there have been no semver-major changes from Node 4 to 8). This version supports all Node.js versions from 0.8, as well as evergreen browsers and IE 10 & 11. + +### Big Thanks + +Cross-browser Testing Platform and Open Source <3 Provided by [Sauce Labs][sauce] + +# Usage + +You can swap your `require('stream')` with `require('readable-stream')` +without any changes, if you are just using one of the main classes and +functions. + +```js +const { + Readable, + Writable, + Transform, + Duplex, + pipeline, + finished +} = require('readable-stream') +```` + +Note that `require('stream')` will return `Stream`, while +`require('readable-stream')` will return `Readable`. We discourage using +whatever is exported directly, but rather use one of the properties as +shown in the example above. + +# Streams Working Group + +`readable-stream` is maintained by the Streams Working Group, which +oversees the development and maintenance of the Streams API within +Node.js. The responsibilities of the Streams Working Group include: + +* Addressing stream issues on the Node.js issue tracker. +* Authoring and editing stream documentation within the Node.js project. +* Reviewing changes to stream subclasses within the Node.js project. +* Redirecting changes to streams from the Node.js project to this + project. +* Assisting in the implementation of stream providers within Node.js. +* Recommending versions of `readable-stream` to be included in Node.js. +* Messaging about the future of streams to give the community advance + notice of changes. + + +## Team Members + +* **Calvin Metcalf** ([@calvinmetcalf](https://github.com/calvinmetcalf)) <calvin.metcalf@gmail.com> + - Release GPG key: F3EF5F62A87FC27A22E643F714CE4FF5015AA242 +* **Mathias Buus** ([@mafintosh](https://github.com/mafintosh)) <mathiasbuus@gmail.com> +* **Matteo Collina** ([@mcollina](https://github.com/mcollina)) <matteo.collina@gmail.com> + - Release GPG key: 3ABC01543F22DD2239285CDD818674489FBC127E +* **Irina Shestak** ([@lrlna](https://github.com/lrlna)) <shestak.irina@gmail.com> +* **Yoshua Wyuts** ([@yoshuawuyts](https://github.com/yoshuawuyts)) <yoshuawuyts@gmail.com> + +[sauce]: https://saucelabs.com diff --git a/tests/corpus/readable-stream-3/node_modules/readable-stream/errors-browser.js b/tests/corpus/readable-stream-3/node_modules/readable-stream/errors-browser.js new file mode 100644 index 000000000..fb8e73e18 --- /dev/null +++ b/tests/corpus/readable-stream-3/node_modules/readable-stream/errors-browser.js @@ -0,0 +1,127 @@ +'use strict'; + +function _inheritsLoose(subClass, superClass) { subClass.prototype = Object.create(superClass.prototype); subClass.prototype.constructor = subClass; subClass.__proto__ = superClass; } + +var codes = {}; + +function createErrorType(code, message, Base) { + if (!Base) { + Base = Error; + } + + function getMessage(arg1, arg2, arg3) { + if (typeof message === 'string') { + return message; + } else { + return message(arg1, arg2, arg3); + } + } + + var NodeError = + /*#__PURE__*/ + function (_Base) { + _inheritsLoose(NodeError, _Base); + + function NodeError(arg1, arg2, arg3) { + return _Base.call(this, getMessage(arg1, arg2, arg3)) || this; + } + + return NodeError; + }(Base); + + NodeError.prototype.name = Base.name; + NodeError.prototype.code = code; + codes[code] = NodeError; +} // https://github.com/nodejs/node/blob/v10.8.0/lib/internal/errors.js + + +function oneOf(expected, thing) { + if (Array.isArray(expected)) { + var len = expected.length; + expected = expected.map(function (i) { + return String(i); + }); + + if (len > 2) { + return "one of ".concat(thing, " ").concat(expected.slice(0, len - 1).join(', '), ", or ") + expected[len - 1]; + } else if (len === 2) { + return "one of ".concat(thing, " ").concat(expected[0], " or ").concat(expected[1]); + } else { + return "of ".concat(thing, " ").concat(expected[0]); + } + } else { + return "of ".concat(thing, " ").concat(String(expected)); + } +} // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/startsWith + + +function startsWith(str, search, pos) { + return str.substr(!pos || pos < 0 ? 0 : +pos, search.length) === search; +} // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/endsWith + + +function endsWith(str, search, this_len) { + if (this_len === undefined || this_len > str.length) { + this_len = str.length; + } + + return str.substring(this_len - search.length, this_len) === search; +} // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/includes + + +function includes(str, search, start) { + if (typeof start !== 'number') { + start = 0; + } + + if (start + search.length > str.length) { + return false; + } else { + return str.indexOf(search, start) !== -1; + } +} + +createErrorType('ERR_INVALID_OPT_VALUE', function (name, value) { + return 'The value "' + value + '" is invalid for option "' + name + '"'; +}, TypeError); +createErrorType('ERR_INVALID_ARG_TYPE', function (name, expected, actual) { + // determiner: 'must be' or 'must not be' + var determiner; + + if (typeof expected === 'string' && startsWith(expected, 'not ')) { + determiner = 'must not be'; + expected = expected.replace(/^not /, ''); + } else { + determiner = 'must be'; + } + + var msg; + + if (endsWith(name, ' argument')) { + // For cases like 'first argument' + msg = "The ".concat(name, " ").concat(determiner, " ").concat(oneOf(expected, 'type')); + } else { + var type = includes(name, '.') ? 'property' : 'argument'; + msg = "The \"".concat(name, "\" ").concat(type, " ").concat(determiner, " ").concat(oneOf(expected, 'type')); + } + + msg += ". Received type ".concat(typeof actual); + return msg; +}, TypeError); +createErrorType('ERR_STREAM_PUSH_AFTER_EOF', 'stream.push() after EOF'); +createErrorType('ERR_METHOD_NOT_IMPLEMENTED', function (name) { + return 'The ' + name + ' method is not implemented'; +}); +createErrorType('ERR_STREAM_PREMATURE_CLOSE', 'Premature close'); +createErrorType('ERR_STREAM_DESTROYED', function (name) { + return 'Cannot call ' + name + ' after a stream was destroyed'; +}); +createErrorType('ERR_MULTIPLE_CALLBACK', 'Callback called multiple times'); +createErrorType('ERR_STREAM_CANNOT_PIPE', 'Cannot pipe, not readable'); +createErrorType('ERR_STREAM_WRITE_AFTER_END', 'write after end'); +createErrorType('ERR_STREAM_NULL_VALUES', 'May not write null values to stream', TypeError); +createErrorType('ERR_UNKNOWN_ENCODING', function (arg) { + return 'Unknown encoding: ' + arg; +}, TypeError); +createErrorType('ERR_STREAM_UNSHIFT_AFTER_END_EVENT', 'stream.unshift() after end event'); +module.exports.codes = codes; diff --git a/tests/corpus/readable-stream-3/node_modules/readable-stream/errors.js b/tests/corpus/readable-stream-3/node_modules/readable-stream/errors.js new file mode 100644 index 000000000..8471526d6 --- /dev/null +++ b/tests/corpus/readable-stream-3/node_modules/readable-stream/errors.js @@ -0,0 +1,116 @@ +'use strict'; + +const codes = {}; + +function createErrorType(code, message, Base) { + if (!Base) { + Base = Error + } + + function getMessage (arg1, arg2, arg3) { + if (typeof message === 'string') { + return message + } else { + return message(arg1, arg2, arg3) + } + } + + class NodeError extends Base { + constructor (arg1, arg2, arg3) { + super(getMessage(arg1, arg2, arg3)); + } + } + + NodeError.prototype.name = Base.name; + NodeError.prototype.code = code; + + codes[code] = NodeError; +} + +// https://github.com/nodejs/node/blob/v10.8.0/lib/internal/errors.js +function oneOf(expected, thing) { + if (Array.isArray(expected)) { + const len = expected.length; + expected = expected.map((i) => String(i)); + if (len > 2) { + return `one of ${thing} ${expected.slice(0, len - 1).join(', ')}, or ` + + expected[len - 1]; + } else if (len === 2) { + return `one of ${thing} ${expected[0]} or ${expected[1]}`; + } else { + return `of ${thing} ${expected[0]}`; + } + } else { + return `of ${thing} ${String(expected)}`; + } +} + +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/startsWith +function startsWith(str, search, pos) { + return str.substr(!pos || pos < 0 ? 0 : +pos, search.length) === search; +} + +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/endsWith +function endsWith(str, search, this_len) { + if (this_len === undefined || this_len > str.length) { + this_len = str.length; + } + return str.substring(this_len - search.length, this_len) === search; +} + +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/includes +function includes(str, search, start) { + if (typeof start !== 'number') { + start = 0; + } + + if (start + search.length > str.length) { + return false; + } else { + return str.indexOf(search, start) !== -1; + } +} + +createErrorType('ERR_INVALID_OPT_VALUE', function (name, value) { + return 'The value "' + value + '" is invalid for option "' + name + '"' +}, TypeError); +createErrorType('ERR_INVALID_ARG_TYPE', function (name, expected, actual) { + // determiner: 'must be' or 'must not be' + let determiner; + if (typeof expected === 'string' && startsWith(expected, 'not ')) { + determiner = 'must not be'; + expected = expected.replace(/^not /, ''); + } else { + determiner = 'must be'; + } + + let msg; + if (endsWith(name, ' argument')) { + // For cases like 'first argument' + msg = `The ${name} ${determiner} ${oneOf(expected, 'type')}`; + } else { + const type = includes(name, '.') ? 'property' : 'argument'; + msg = `The "${name}" ${type} ${determiner} ${oneOf(expected, 'type')}`; + } + + msg += `. Received type ${typeof actual}`; + return msg; +}, TypeError); +createErrorType('ERR_STREAM_PUSH_AFTER_EOF', 'stream.push() after EOF'); +createErrorType('ERR_METHOD_NOT_IMPLEMENTED', function (name) { + return 'The ' + name + ' method is not implemented' +}); +createErrorType('ERR_STREAM_PREMATURE_CLOSE', 'Premature close'); +createErrorType('ERR_STREAM_DESTROYED', function (name) { + return 'Cannot call ' + name + ' after a stream was destroyed'; +}); +createErrorType('ERR_MULTIPLE_CALLBACK', 'Callback called multiple times'); +createErrorType('ERR_STREAM_CANNOT_PIPE', 'Cannot pipe, not readable'); +createErrorType('ERR_STREAM_WRITE_AFTER_END', 'write after end'); +createErrorType('ERR_STREAM_NULL_VALUES', 'May not write null values to stream', TypeError); +createErrorType('ERR_UNKNOWN_ENCODING', function (arg) { + return 'Unknown encoding: ' + arg +}, TypeError); +createErrorType('ERR_STREAM_UNSHIFT_AFTER_END_EVENT', 'stream.unshift() after end event'); + +module.exports.codes = codes; diff --git a/tests/corpus/readable-stream-3/node_modules/readable-stream/experimentalWarning.js b/tests/corpus/readable-stream-3/node_modules/readable-stream/experimentalWarning.js new file mode 100644 index 000000000..78e841495 --- /dev/null +++ b/tests/corpus/readable-stream-3/node_modules/readable-stream/experimentalWarning.js @@ -0,0 +1,17 @@ +'use strict' + +var experimentalWarnings = new Set(); + +function emitExperimentalWarning(feature) { + if (experimentalWarnings.has(feature)) return; + var msg = feature + ' is an experimental feature. This feature could ' + + 'change at any time'; + experimentalWarnings.add(feature); + process.emitWarning(msg, 'ExperimentalWarning'); +} + +function noop() {} + +module.exports.emitExperimentalWarning = process.emitWarning + ? emitExperimentalWarning + : noop; diff --git a/tests/corpus/readable-stream-3/node_modules/readable-stream/lib/_stream_duplex.js b/tests/corpus/readable-stream-3/node_modules/readable-stream/lib/_stream_duplex.js new file mode 100644 index 000000000..19abfa604 --- /dev/null +++ b/tests/corpus/readable-stream-3/node_modules/readable-stream/lib/_stream_duplex.js @@ -0,0 +1,126 @@ +// Copyright Joyent, Inc. and other Node 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. + +// a duplex stream is just a stream that is both readable and writable. +// Since JS doesn't have multiple prototypal inheritance, this class +// prototypally inherits from Readable, and then parasitically from +// Writable. + +'use strict'; + +/**/ +var objectKeys = Object.keys || function (obj) { + var keys = []; + for (var key in obj) keys.push(key); + return keys; +}; +/**/ + +module.exports = Duplex; +var Readable = require('./_stream_readable'); +var Writable = require('./_stream_writable'); +require('inherits')(Duplex, Readable); +{ + // Allow the keys array to be GC'ed. + var keys = objectKeys(Writable.prototype); + for (var v = 0; v < keys.length; v++) { + var method = keys[v]; + if (!Duplex.prototype[method]) Duplex.prototype[method] = Writable.prototype[method]; + } +} +function Duplex(options) { + if (!(this instanceof Duplex)) return new Duplex(options); + Readable.call(this, options); + Writable.call(this, options); + this.allowHalfOpen = true; + if (options) { + if (options.readable === false) this.readable = false; + if (options.writable === false) this.writable = false; + if (options.allowHalfOpen === false) { + this.allowHalfOpen = false; + this.once('end', onend); + } + } +} +Object.defineProperty(Duplex.prototype, 'writableHighWaterMark', { + // making it explicit this property is not enumerable + // because otherwise some prototype manipulation in + // userland will fail + enumerable: false, + get: function get() { + return this._writableState.highWaterMark; + } +}); +Object.defineProperty(Duplex.prototype, 'writableBuffer', { + // making it explicit this property is not enumerable + // because otherwise some prototype manipulation in + // userland will fail + enumerable: false, + get: function get() { + return this._writableState && this._writableState.getBuffer(); + } +}); +Object.defineProperty(Duplex.prototype, 'writableLength', { + // making it explicit this property is not enumerable + // because otherwise some prototype manipulation in + // userland will fail + enumerable: false, + get: function get() { + return this._writableState.length; + } +}); + +// the no-half-open enforcer +function onend() { + // If the writable side ended, then we're ok. + if (this._writableState.ended) return; + + // no more data can be written. + // But allow more writes to happen in this tick. + process.nextTick(onEndNT, this); +} +function onEndNT(self) { + self.end(); +} +Object.defineProperty(Duplex.prototype, 'destroyed', { + // making it explicit this property is not enumerable + // because otherwise some prototype manipulation in + // userland will fail + enumerable: false, + get: function get() { + if (this._readableState === undefined || this._writableState === undefined) { + return false; + } + return this._readableState.destroyed && this._writableState.destroyed; + }, + set: function set(value) { + // we ignore the value if the stream + // has not been initialized yet + if (this._readableState === undefined || this._writableState === undefined) { + return; + } + + // backward compatibility, the user is explicitly + // managing destroyed + this._readableState.destroyed = value; + this._writableState.destroyed = value; + } +}); \ No newline at end of file diff --git a/tests/corpus/readable-stream-3/node_modules/readable-stream/lib/_stream_passthrough.js b/tests/corpus/readable-stream-3/node_modules/readable-stream/lib/_stream_passthrough.js new file mode 100644 index 000000000..24a6bdde2 --- /dev/null +++ b/tests/corpus/readable-stream-3/node_modules/readable-stream/lib/_stream_passthrough.js @@ -0,0 +1,37 @@ +// Copyright Joyent, Inc. and other Node 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. + +// a passthrough stream. +// basically just the most minimal sort of Transform stream. +// Every written chunk gets output as-is. + +'use strict'; + +module.exports = PassThrough; +var Transform = require('./_stream_transform'); +require('inherits')(PassThrough, Transform); +function PassThrough(options) { + if (!(this instanceof PassThrough)) return new PassThrough(options); + Transform.call(this, options); +} +PassThrough.prototype._transform = function (chunk, encoding, cb) { + cb(null, chunk); +}; \ No newline at end of file diff --git a/tests/corpus/readable-stream-3/node_modules/readable-stream/lib/_stream_readable.js b/tests/corpus/readable-stream-3/node_modules/readable-stream/lib/_stream_readable.js new file mode 100644 index 000000000..df1f608d5 --- /dev/null +++ b/tests/corpus/readable-stream-3/node_modules/readable-stream/lib/_stream_readable.js @@ -0,0 +1,1027 @@ +// Copyright Joyent, Inc. and other Node 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. + +'use strict'; + +module.exports = Readable; + +/**/ +var Duplex; +/**/ + +Readable.ReadableState = ReadableState; + +/**/ +var EE = require('events').EventEmitter; +var EElistenerCount = function EElistenerCount(emitter, type) { + return emitter.listeners(type).length; +}; +/**/ + +/**/ +var Stream = require('./internal/streams/stream'); +/**/ + +var Buffer = require('buffer').Buffer; +var OurUint8Array = (typeof global !== 'undefined' ? global : typeof window !== 'undefined' ? window : typeof self !== 'undefined' ? self : {}).Uint8Array || function () {}; +function _uint8ArrayToBuffer(chunk) { + return Buffer.from(chunk); +} +function _isUint8Array(obj) { + return Buffer.isBuffer(obj) || obj instanceof OurUint8Array; +} + +/**/ +var debugUtil = require('util'); +var debug; +if (debugUtil && debugUtil.debuglog) { + debug = debugUtil.debuglog('stream'); +} else { + debug = function debug() {}; +} +/**/ + +var BufferList = require('./internal/streams/buffer_list'); +var destroyImpl = require('./internal/streams/destroy'); +var _require = require('./internal/streams/state'), + getHighWaterMark = _require.getHighWaterMark; +var _require$codes = require('../errors').codes, + ERR_INVALID_ARG_TYPE = _require$codes.ERR_INVALID_ARG_TYPE, + ERR_STREAM_PUSH_AFTER_EOF = _require$codes.ERR_STREAM_PUSH_AFTER_EOF, + ERR_METHOD_NOT_IMPLEMENTED = _require$codes.ERR_METHOD_NOT_IMPLEMENTED, + ERR_STREAM_UNSHIFT_AFTER_END_EVENT = _require$codes.ERR_STREAM_UNSHIFT_AFTER_END_EVENT; + +// Lazy loaded to improve the startup performance. +var StringDecoder; +var createReadableStreamAsyncIterator; +var from; +require('inherits')(Readable, Stream); +var errorOrDestroy = destroyImpl.errorOrDestroy; +var kProxyEvents = ['error', 'close', 'destroy', 'pause', 'resume']; +function prependListener(emitter, event, fn) { + // Sadly this is not cacheable as some libraries bundle their own + // event emitter implementation with them. + if (typeof emitter.prependListener === 'function') return emitter.prependListener(event, fn); + + // This is a hack to make sure that our error handler is attached before any + // userland ones. NEVER DO THIS. This is here only because this code needs + // to continue to work with older versions of Node.js that do not include + // the prependListener() method. The goal is to eventually remove this hack. + if (!emitter._events || !emitter._events[event]) emitter.on(event, fn);else if (Array.isArray(emitter._events[event])) emitter._events[event].unshift(fn);else emitter._events[event] = [fn, emitter._events[event]]; +} +function ReadableState(options, stream, isDuplex) { + Duplex = Duplex || require('./_stream_duplex'); + options = options || {}; + + // Duplex streams are both readable and writable, but share + // the same options object. + // However, some cases require setting options to different + // values for the readable and the writable sides of the duplex stream. + // These options can be provided separately as readableXXX and writableXXX. + if (typeof isDuplex !== 'boolean') isDuplex = stream instanceof Duplex; + + // object stream flag. Used to make read(n) ignore n and to + // make all the buffer merging and length checks go away + this.objectMode = !!options.objectMode; + if (isDuplex) this.objectMode = this.objectMode || !!options.readableObjectMode; + + // the point at which it stops calling _read() to fill the buffer + // Note: 0 is a valid value, means "don't call _read preemptively ever" + this.highWaterMark = getHighWaterMark(this, options, 'readableHighWaterMark', isDuplex); + + // A linked list is used to store data chunks instead of an array because the + // linked list can remove elements from the beginning faster than + // array.shift() + this.buffer = new BufferList(); + this.length = 0; + this.pipes = null; + this.pipesCount = 0; + this.flowing = null; + this.ended = false; + this.endEmitted = false; + this.reading = false; + + // a flag to be able to tell if the event 'readable'/'data' is emitted + // immediately, or on a later tick. We set this to true at first, because + // any actions that shouldn't happen until "later" should generally also + // not happen before the first read call. + this.sync = true; + + // whenever we return null, then we set a flag to say + // that we're awaiting a 'readable' event emission. + this.needReadable = false; + this.emittedReadable = false; + this.readableListening = false; + this.resumeScheduled = false; + this.paused = true; + + // Should close be emitted on destroy. Defaults to true. + this.emitClose = options.emitClose !== false; + + // Should .destroy() be called after 'end' (and potentially 'finish') + this.autoDestroy = !!options.autoDestroy; + + // has it been destroyed + this.destroyed = false; + + // Crypto is kind of old and crusty. Historically, its default string + // encoding is 'binary' so we have to make this configurable. + // Everything else in the universe uses 'utf8', though. + this.defaultEncoding = options.defaultEncoding || 'utf8'; + + // the number of writers that are awaiting a drain event in .pipe()s + this.awaitDrain = 0; + + // if true, a maybeReadMore has been scheduled + this.readingMore = false; + this.decoder = null; + this.encoding = null; + if (options.encoding) { + if (!StringDecoder) StringDecoder = require('string_decoder/').StringDecoder; + this.decoder = new StringDecoder(options.encoding); + this.encoding = options.encoding; + } +} +function Readable(options) { + Duplex = Duplex || require('./_stream_duplex'); + if (!(this instanceof Readable)) return new Readable(options); + + // Checking for a Stream.Duplex instance is faster here instead of inside + // the ReadableState constructor, at least with V8 6.5 + var isDuplex = this instanceof Duplex; + this._readableState = new ReadableState(options, this, isDuplex); + + // legacy + this.readable = true; + if (options) { + if (typeof options.read === 'function') this._read = options.read; + if (typeof options.destroy === 'function') this._destroy = options.destroy; + } + Stream.call(this); +} +Object.defineProperty(Readable.prototype, 'destroyed', { + // making it explicit this property is not enumerable + // because otherwise some prototype manipulation in + // userland will fail + enumerable: false, + get: function get() { + if (this._readableState === undefined) { + return false; + } + return this._readableState.destroyed; + }, + set: function set(value) { + // we ignore the value if the stream + // has not been initialized yet + if (!this._readableState) { + return; + } + + // backward compatibility, the user is explicitly + // managing destroyed + this._readableState.destroyed = value; + } +}); +Readable.prototype.destroy = destroyImpl.destroy; +Readable.prototype._undestroy = destroyImpl.undestroy; +Readable.prototype._destroy = function (err, cb) { + cb(err); +}; + +// Manually shove something into the read() buffer. +// This returns true if the highWaterMark has not been hit yet, +// similar to how Writable.write() returns true if you should +// write() some more. +Readable.prototype.push = function (chunk, encoding) { + var state = this._readableState; + var skipChunkCheck; + if (!state.objectMode) { + if (typeof chunk === 'string') { + encoding = encoding || state.defaultEncoding; + if (encoding !== state.encoding) { + chunk = Buffer.from(chunk, encoding); + encoding = ''; + } + skipChunkCheck = true; + } + } else { + skipChunkCheck = true; + } + return readableAddChunk(this, chunk, encoding, false, skipChunkCheck); +}; + +// Unshift should *always* be something directly out of read() +Readable.prototype.unshift = function (chunk) { + return readableAddChunk(this, chunk, null, true, false); +}; +function readableAddChunk(stream, chunk, encoding, addToFront, skipChunkCheck) { + debug('readableAddChunk', chunk); + var state = stream._readableState; + if (chunk === null) { + state.reading = false; + onEofChunk(stream, state); + } else { + var er; + if (!skipChunkCheck) er = chunkInvalid(state, chunk); + if (er) { + errorOrDestroy(stream, er); + } else if (state.objectMode || chunk && chunk.length > 0) { + if (typeof chunk !== 'string' && !state.objectMode && Object.getPrototypeOf(chunk) !== Buffer.prototype) { + chunk = _uint8ArrayToBuffer(chunk); + } + if (addToFront) { + if (state.endEmitted) errorOrDestroy(stream, new ERR_STREAM_UNSHIFT_AFTER_END_EVENT());else addChunk(stream, state, chunk, true); + } else if (state.ended) { + errorOrDestroy(stream, new ERR_STREAM_PUSH_AFTER_EOF()); + } else if (state.destroyed) { + return false; + } else { + state.reading = false; + if (state.decoder && !encoding) { + chunk = state.decoder.write(chunk); + if (state.objectMode || chunk.length !== 0) addChunk(stream, state, chunk, false);else maybeReadMore(stream, state); + } else { + addChunk(stream, state, chunk, false); + } + } + } else if (!addToFront) { + state.reading = false; + maybeReadMore(stream, state); + } + } + + // We can push more data if we are below the highWaterMark. + // Also, if we have no data yet, we can stand some more bytes. + // This is to work around cases where hwm=0, such as the repl. + return !state.ended && (state.length < state.highWaterMark || state.length === 0); +} +function addChunk(stream, state, chunk, addToFront) { + if (state.flowing && state.length === 0 && !state.sync) { + state.awaitDrain = 0; + stream.emit('data', chunk); + } else { + // update the buffer info. + state.length += state.objectMode ? 1 : chunk.length; + if (addToFront) state.buffer.unshift(chunk);else state.buffer.push(chunk); + if (state.needReadable) emitReadable(stream); + } + maybeReadMore(stream, state); +} +function chunkInvalid(state, chunk) { + var er; + if (!_isUint8Array(chunk) && typeof chunk !== 'string' && chunk !== undefined && !state.objectMode) { + er = new ERR_INVALID_ARG_TYPE('chunk', ['string', 'Buffer', 'Uint8Array'], chunk); + } + return er; +} +Readable.prototype.isPaused = function () { + return this._readableState.flowing === false; +}; + +// backwards compatibility. +Readable.prototype.setEncoding = function (enc) { + if (!StringDecoder) StringDecoder = require('string_decoder/').StringDecoder; + var decoder = new StringDecoder(enc); + this._readableState.decoder = decoder; + // If setEncoding(null), decoder.encoding equals utf8 + this._readableState.encoding = this._readableState.decoder.encoding; + + // Iterate over current buffer to convert already stored Buffers: + var p = this._readableState.buffer.head; + var content = ''; + while (p !== null) { + content += decoder.write(p.data); + p = p.next; + } + this._readableState.buffer.clear(); + if (content !== '') this._readableState.buffer.push(content); + this._readableState.length = content.length; + return this; +}; + +// Don't raise the hwm > 1GB +var MAX_HWM = 0x40000000; +function computeNewHighWaterMark(n) { + if (n >= MAX_HWM) { + // TODO(ronag): Throw ERR_VALUE_OUT_OF_RANGE. + n = MAX_HWM; + } else { + // Get the next highest power of 2 to prevent increasing hwm excessively in + // tiny amounts + n--; + n |= n >>> 1; + n |= n >>> 2; + n |= n >>> 4; + n |= n >>> 8; + n |= n >>> 16; + n++; + } + return n; +} + +// This function is designed to be inlinable, so please take care when making +// changes to the function body. +function howMuchToRead(n, state) { + if (n <= 0 || state.length === 0 && state.ended) return 0; + if (state.objectMode) return 1; + if (n !== n) { + // Only flow one buffer at a time + if (state.flowing && state.length) return state.buffer.head.data.length;else return state.length; + } + // If we're asking for more than the current hwm, then raise the hwm. + if (n > state.highWaterMark) state.highWaterMark = computeNewHighWaterMark(n); + if (n <= state.length) return n; + // Don't have enough + if (!state.ended) { + state.needReadable = true; + return 0; + } + return state.length; +} + +// you can override either this method, or the async _read(n) below. +Readable.prototype.read = function (n) { + debug('read', n); + n = parseInt(n, 10); + var state = this._readableState; + var nOrig = n; + if (n !== 0) state.emittedReadable = false; + + // if we're doing read(0) to trigger a readable event, but we + // already have a bunch of data in the buffer, then just trigger + // the 'readable' event and move on. + if (n === 0 && state.needReadable && ((state.highWaterMark !== 0 ? state.length >= state.highWaterMark : state.length > 0) || state.ended)) { + debug('read: emitReadable', state.length, state.ended); + if (state.length === 0 && state.ended) endReadable(this);else emitReadable(this); + return null; + } + n = howMuchToRead(n, state); + + // if we've ended, and we're now clear, then finish it up. + if (n === 0 && state.ended) { + if (state.length === 0) endReadable(this); + return null; + } + + // All the actual chunk generation logic needs to be + // *below* the call to _read. The reason is that in certain + // synthetic stream cases, such as passthrough streams, _read + // may be a completely synchronous operation which may change + // the state of the read buffer, providing enough data when + // before there was *not* enough. + // + // So, the steps are: + // 1. Figure out what the state of things will be after we do + // a read from the buffer. + // + // 2. If that resulting state will trigger a _read, then call _read. + // Note that this may be asynchronous, or synchronous. Yes, it is + // deeply ugly to write APIs this way, but that still doesn't mean + // that the Readable class should behave improperly, as streams are + // designed to be sync/async agnostic. + // Take note if the _read call is sync or async (ie, if the read call + // has returned yet), so that we know whether or not it's safe to emit + // 'readable' etc. + // + // 3. Actually pull the requested chunks out of the buffer and return. + + // if we need a readable event, then we need to do some reading. + var doRead = state.needReadable; + debug('need readable', doRead); + + // if we currently have less than the highWaterMark, then also read some + if (state.length === 0 || state.length - n < state.highWaterMark) { + doRead = true; + debug('length less than watermark', doRead); + } + + // however, if we've ended, then there's no point, and if we're already + // reading, then it's unnecessary. + if (state.ended || state.reading) { + doRead = false; + debug('reading or ended', doRead); + } else if (doRead) { + debug('do read'); + state.reading = true; + state.sync = true; + // if the length is currently zero, then we *need* a readable event. + if (state.length === 0) state.needReadable = true; + // call internal read method + this._read(state.highWaterMark); + state.sync = false; + // If _read pushed data synchronously, then `reading` will be false, + // and we need to re-evaluate how much data we can return to the user. + if (!state.reading) n = howMuchToRead(nOrig, state); + } + var ret; + if (n > 0) ret = fromList(n, state);else ret = null; + if (ret === null) { + state.needReadable = state.length <= state.highWaterMark; + n = 0; + } else { + state.length -= n; + state.awaitDrain = 0; + } + if (state.length === 0) { + // If we have nothing in the buffer, then we want to know + // as soon as we *do* get something into the buffer. + if (!state.ended) state.needReadable = true; + + // If we tried to read() past the EOF, then emit end on the next tick. + if (nOrig !== n && state.ended) endReadable(this); + } + if (ret !== null) this.emit('data', ret); + return ret; +}; +function onEofChunk(stream, state) { + debug('onEofChunk'); + if (state.ended) return; + if (state.decoder) { + var chunk = state.decoder.end(); + if (chunk && chunk.length) { + state.buffer.push(chunk); + state.length += state.objectMode ? 1 : chunk.length; + } + } + state.ended = true; + if (state.sync) { + // if we are sync, wait until next tick to emit the data. + // Otherwise we risk emitting data in the flow() + // the readable code triggers during a read() call + emitReadable(stream); + } else { + // emit 'readable' now to make sure it gets picked up. + state.needReadable = false; + if (!state.emittedReadable) { + state.emittedReadable = true; + emitReadable_(stream); + } + } +} + +// Don't emit readable right away in sync mode, because this can trigger +// another read() call => stack overflow. This way, it might trigger +// a nextTick recursion warning, but that's not so bad. +function emitReadable(stream) { + var state = stream._readableState; + debug('emitReadable', state.needReadable, state.emittedReadable); + state.needReadable = false; + if (!state.emittedReadable) { + debug('emitReadable', state.flowing); + state.emittedReadable = true; + process.nextTick(emitReadable_, stream); + } +} +function emitReadable_(stream) { + var state = stream._readableState; + debug('emitReadable_', state.destroyed, state.length, state.ended); + if (!state.destroyed && (state.length || state.ended)) { + stream.emit('readable'); + state.emittedReadable = false; + } + + // The stream needs another readable event if + // 1. It is not flowing, as the flow mechanism will take + // care of it. + // 2. It is not ended. + // 3. It is below the highWaterMark, so we can schedule + // another readable later. + state.needReadable = !state.flowing && !state.ended && state.length <= state.highWaterMark; + flow(stream); +} + +// at this point, the user has presumably seen the 'readable' event, +// and called read() to consume some data. that may have triggered +// in turn another _read(n) call, in which case reading = true if +// it's in progress. +// However, if we're not ended, or reading, and the length < hwm, +// then go ahead and try to read some more preemptively. +function maybeReadMore(stream, state) { + if (!state.readingMore) { + state.readingMore = true; + process.nextTick(maybeReadMore_, stream, state); + } +} +function maybeReadMore_(stream, state) { + // Attempt to read more data if we should. + // + // The conditions for reading more data are (one of): + // - Not enough data buffered (state.length < state.highWaterMark). The loop + // is responsible for filling the buffer with enough data if such data + // is available. If highWaterMark is 0 and we are not in the flowing mode + // we should _not_ attempt to buffer any extra data. We'll get more data + // when the stream consumer calls read() instead. + // - No data in the buffer, and the stream is in flowing mode. In this mode + // the loop below is responsible for ensuring read() is called. Failing to + // call read here would abort the flow and there's no other mechanism for + // continuing the flow if the stream consumer has just subscribed to the + // 'data' event. + // + // In addition to the above conditions to keep reading data, the following + // conditions prevent the data from being read: + // - The stream has ended (state.ended). + // - There is already a pending 'read' operation (state.reading). This is a + // case where the the stream has called the implementation defined _read() + // method, but they are processing the call asynchronously and have _not_ + // called push() with new data. In this case we skip performing more + // read()s. The execution ends in this method again after the _read() ends + // up calling push() with more data. + while (!state.reading && !state.ended && (state.length < state.highWaterMark || state.flowing && state.length === 0)) { + var len = state.length; + debug('maybeReadMore read 0'); + stream.read(0); + if (len === state.length) + // didn't get any data, stop spinning. + break; + } + state.readingMore = false; +} + +// abstract method. to be overridden in specific implementation classes. +// call cb(er, data) where data is <= n in length. +// for virtual (non-string, non-buffer) streams, "length" is somewhat +// arbitrary, and perhaps not very meaningful. +Readable.prototype._read = function (n) { + errorOrDestroy(this, new ERR_METHOD_NOT_IMPLEMENTED('_read()')); +}; +Readable.prototype.pipe = function (dest, pipeOpts) { + var src = this; + var state = this._readableState; + switch (state.pipesCount) { + case 0: + state.pipes = dest; + break; + case 1: + state.pipes = [state.pipes, dest]; + break; + default: + state.pipes.push(dest); + break; + } + state.pipesCount += 1; + debug('pipe count=%d opts=%j', state.pipesCount, pipeOpts); + var doEnd = (!pipeOpts || pipeOpts.end !== false) && dest !== process.stdout && dest !== process.stderr; + var endFn = doEnd ? onend : unpipe; + if (state.endEmitted) process.nextTick(endFn);else src.once('end', endFn); + dest.on('unpipe', onunpipe); + function onunpipe(readable, unpipeInfo) { + debug('onunpipe'); + if (readable === src) { + if (unpipeInfo && unpipeInfo.hasUnpiped === false) { + unpipeInfo.hasUnpiped = true; + cleanup(); + } + } + } + function onend() { + debug('onend'); + dest.end(); + } + + // when the dest drains, it reduces the awaitDrain counter + // on the source. This would be more elegant with a .once() + // handler in flow(), but adding and removing repeatedly is + // too slow. + var ondrain = pipeOnDrain(src); + dest.on('drain', ondrain); + var cleanedUp = false; + function cleanup() { + debug('cleanup'); + // cleanup event handlers once the pipe is broken + dest.removeListener('close', onclose); + dest.removeListener('finish', onfinish); + dest.removeListener('drain', ondrain); + dest.removeListener('error', onerror); + dest.removeListener('unpipe', onunpipe); + src.removeListener('end', onend); + src.removeListener('end', unpipe); + src.removeListener('data', ondata); + cleanedUp = true; + + // if the reader is waiting for a drain event from this + // specific writer, then it would cause it to never start + // flowing again. + // So, if this is awaiting a drain, then we just call it now. + // If we don't know, then assume that we are waiting for one. + if (state.awaitDrain && (!dest._writableState || dest._writableState.needDrain)) ondrain(); + } + src.on('data', ondata); + function ondata(chunk) { + debug('ondata'); + var ret = dest.write(chunk); + debug('dest.write', ret); + if (ret === false) { + // If the user unpiped during `dest.write()`, it is possible + // to get stuck in a permanently paused state if that write + // also returned false. + // => Check whether `dest` is still a piping destination. + if ((state.pipesCount === 1 && state.pipes === dest || state.pipesCount > 1 && indexOf(state.pipes, dest) !== -1) && !cleanedUp) { + debug('false write response, pause', state.awaitDrain); + state.awaitDrain++; + } + src.pause(); + } + } + + // if the dest has an error, then stop piping into it. + // however, don't suppress the throwing behavior for this. + function onerror(er) { + debug('onerror', er); + unpipe(); + dest.removeListener('error', onerror); + if (EElistenerCount(dest, 'error') === 0) errorOrDestroy(dest, er); + } + + // Make sure our error handler is attached before userland ones. + prependListener(dest, 'error', onerror); + + // Both close and finish should trigger unpipe, but only once. + function onclose() { + dest.removeListener('finish', onfinish); + unpipe(); + } + dest.once('close', onclose); + function onfinish() { + debug('onfinish'); + dest.removeListener('close', onclose); + unpipe(); + } + dest.once('finish', onfinish); + function unpipe() { + debug('unpipe'); + src.unpipe(dest); + } + + // tell the dest that it's being piped to + dest.emit('pipe', src); + + // start the flow if it hasn't been started already. + if (!state.flowing) { + debug('pipe resume'); + src.resume(); + } + return dest; +}; +function pipeOnDrain(src) { + return function pipeOnDrainFunctionResult() { + var state = src._readableState; + debug('pipeOnDrain', state.awaitDrain); + if (state.awaitDrain) state.awaitDrain--; + if (state.awaitDrain === 0 && EElistenerCount(src, 'data')) { + state.flowing = true; + flow(src); + } + }; +} +Readable.prototype.unpipe = function (dest) { + var state = this._readableState; + var unpipeInfo = { + hasUnpiped: false + }; + + // if we're not piping anywhere, then do nothing. + if (state.pipesCount === 0) return this; + + // just one destination. most common case. + if (state.pipesCount === 1) { + // passed in one, but it's not the right one. + if (dest && dest !== state.pipes) return this; + if (!dest) dest = state.pipes; + + // got a match. + state.pipes = null; + state.pipesCount = 0; + state.flowing = false; + if (dest) dest.emit('unpipe', this, unpipeInfo); + return this; + } + + // slow case. multiple pipe destinations. + + if (!dest) { + // remove all. + var dests = state.pipes; + var len = state.pipesCount; + state.pipes = null; + state.pipesCount = 0; + state.flowing = false; + for (var i = 0; i < len; i++) dests[i].emit('unpipe', this, { + hasUnpiped: false + }); + return this; + } + + // try to find the right one. + var index = indexOf(state.pipes, dest); + if (index === -1) return this; + state.pipes.splice(index, 1); + state.pipesCount -= 1; + if (state.pipesCount === 1) state.pipes = state.pipes[0]; + dest.emit('unpipe', this, unpipeInfo); + return this; +}; + +// set up data events if they are asked for +// Ensure readable listeners eventually get something +Readable.prototype.on = function (ev, fn) { + var res = Stream.prototype.on.call(this, ev, fn); + var state = this._readableState; + if (ev === 'data') { + // update readableListening so that resume() may be a no-op + // a few lines down. This is needed to support once('readable'). + state.readableListening = this.listenerCount('readable') > 0; + + // Try start flowing on next tick if stream isn't explicitly paused + if (state.flowing !== false) this.resume(); + } else if (ev === 'readable') { + if (!state.endEmitted && !state.readableListening) { + state.readableListening = state.needReadable = true; + state.flowing = false; + state.emittedReadable = false; + debug('on readable', state.length, state.reading); + if (state.length) { + emitReadable(this); + } else if (!state.reading) { + process.nextTick(nReadingNextTick, this); + } + } + } + return res; +}; +Readable.prototype.addListener = Readable.prototype.on; +Readable.prototype.removeListener = function (ev, fn) { + var res = Stream.prototype.removeListener.call(this, ev, fn); + if (ev === 'readable') { + // We need to check if there is someone still listening to + // readable and reset the state. However this needs to happen + // after readable has been emitted but before I/O (nextTick) to + // support once('readable', fn) cycles. This means that calling + // resume within the same tick will have no + // effect. + process.nextTick(updateReadableListening, this); + } + return res; +}; +Readable.prototype.removeAllListeners = function (ev) { + var res = Stream.prototype.removeAllListeners.apply(this, arguments); + if (ev === 'readable' || ev === undefined) { + // We need to check if there is someone still listening to + // readable and reset the state. However this needs to happen + // after readable has been emitted but before I/O (nextTick) to + // support once('readable', fn) cycles. This means that calling + // resume within the same tick will have no + // effect. + process.nextTick(updateReadableListening, this); + } + return res; +}; +function updateReadableListening(self) { + var state = self._readableState; + state.readableListening = self.listenerCount('readable') > 0; + if (state.resumeScheduled && !state.paused) { + // flowing needs to be set to true now, otherwise + // the upcoming resume will not flow. + state.flowing = true; + + // crude way to check if we should resume + } else if (self.listenerCount('data') > 0) { + self.resume(); + } +} +function nReadingNextTick(self) { + debug('readable nexttick read 0'); + self.read(0); +} + +// pause() and resume() are remnants of the legacy readable stream API +// If the user uses them, then switch into old mode. +Readable.prototype.resume = function () { + var state = this._readableState; + if (!state.flowing) { + debug('resume'); + // we flow only if there is no one listening + // for readable, but we still have to call + // resume() + state.flowing = !state.readableListening; + resume(this, state); + } + state.paused = false; + return this; +}; +function resume(stream, state) { + if (!state.resumeScheduled) { + state.resumeScheduled = true; + process.nextTick(resume_, stream, state); + } +} +function resume_(stream, state) { + debug('resume', state.reading); + if (!state.reading) { + stream.read(0); + } + state.resumeScheduled = false; + stream.emit('resume'); + flow(stream); + if (state.flowing && !state.reading) stream.read(0); +} +Readable.prototype.pause = function () { + debug('call pause flowing=%j', this._readableState.flowing); + if (this._readableState.flowing !== false) { + debug('pause'); + this._readableState.flowing = false; + this.emit('pause'); + } + this._readableState.paused = true; + return this; +}; +function flow(stream) { + var state = stream._readableState; + debug('flow', state.flowing); + while (state.flowing && stream.read() !== null); +} + +// wrap an old-style stream as the async data source. +// This is *not* part of the readable stream interface. +// It is an ugly unfortunate mess of history. +Readable.prototype.wrap = function (stream) { + var _this = this; + var state = this._readableState; + var paused = false; + stream.on('end', function () { + debug('wrapped end'); + if (state.decoder && !state.ended) { + var chunk = state.decoder.end(); + if (chunk && chunk.length) _this.push(chunk); + } + _this.push(null); + }); + stream.on('data', function (chunk) { + debug('wrapped data'); + if (state.decoder) chunk = state.decoder.write(chunk); + + // don't skip over falsy values in objectMode + if (state.objectMode && (chunk === null || chunk === undefined)) return;else if (!state.objectMode && (!chunk || !chunk.length)) return; + var ret = _this.push(chunk); + if (!ret) { + paused = true; + stream.pause(); + } + }); + + // proxy all the other methods. + // important when wrapping filters and duplexes. + for (var i in stream) { + if (this[i] === undefined && typeof stream[i] === 'function') { + this[i] = function methodWrap(method) { + return function methodWrapReturnFunction() { + return stream[method].apply(stream, arguments); + }; + }(i); + } + } + + // proxy certain important events. + for (var n = 0; n < kProxyEvents.length; n++) { + stream.on(kProxyEvents[n], this.emit.bind(this, kProxyEvents[n])); + } + + // when we try to consume some more bytes, simply unpause the + // underlying stream. + this._read = function (n) { + debug('wrapped _read', n); + if (paused) { + paused = false; + stream.resume(); + } + }; + return this; +}; +if (typeof Symbol === 'function') { + Readable.prototype[Symbol.asyncIterator] = function () { + if (createReadableStreamAsyncIterator === undefined) { + createReadableStreamAsyncIterator = require('./internal/streams/async_iterator'); + } + return createReadableStreamAsyncIterator(this); + }; +} +Object.defineProperty(Readable.prototype, 'readableHighWaterMark', { + // making it explicit this property is not enumerable + // because otherwise some prototype manipulation in + // userland will fail + enumerable: false, + get: function get() { + return this._readableState.highWaterMark; + } +}); +Object.defineProperty(Readable.prototype, 'readableBuffer', { + // making it explicit this property is not enumerable + // because otherwise some prototype manipulation in + // userland will fail + enumerable: false, + get: function get() { + return this._readableState && this._readableState.buffer; + } +}); +Object.defineProperty(Readable.prototype, 'readableFlowing', { + // making it explicit this property is not enumerable + // because otherwise some prototype manipulation in + // userland will fail + enumerable: false, + get: function get() { + return this._readableState.flowing; + }, + set: function set(state) { + if (this._readableState) { + this._readableState.flowing = state; + } + } +}); + +// exposed for testing purposes only. +Readable._fromList = fromList; +Object.defineProperty(Readable.prototype, 'readableLength', { + // making it explicit this property is not enumerable + // because otherwise some prototype manipulation in + // userland will fail + enumerable: false, + get: function get() { + return this._readableState.length; + } +}); + +// Pluck off n bytes from an array of buffers. +// Length is the combined lengths of all the buffers in the list. +// This function is designed to be inlinable, so please take care when making +// changes to the function body. +function fromList(n, state) { + // nothing buffered + if (state.length === 0) return null; + var ret; + if (state.objectMode) ret = state.buffer.shift();else if (!n || n >= state.length) { + // read it all, truncate the list + if (state.decoder) ret = state.buffer.join('');else if (state.buffer.length === 1) ret = state.buffer.first();else ret = state.buffer.concat(state.length); + state.buffer.clear(); + } else { + // read part of list + ret = state.buffer.consume(n, state.decoder); + } + return ret; +} +function endReadable(stream) { + var state = stream._readableState; + debug('endReadable', state.endEmitted); + if (!state.endEmitted) { + state.ended = true; + process.nextTick(endReadableNT, state, stream); + } +} +function endReadableNT(state, stream) { + debug('endReadableNT', state.endEmitted, state.length); + + // Check that we didn't get one last unshift. + if (!state.endEmitted && state.length === 0) { + state.endEmitted = true; + stream.readable = false; + stream.emit('end'); + if (state.autoDestroy) { + // In case of duplex streams we need a way to detect + // if the writable side is ready for autoDestroy as well + var wState = stream._writableState; + if (!wState || wState.autoDestroy && wState.finished) { + stream.destroy(); + } + } + } +} +if (typeof Symbol === 'function') { + Readable.from = function (iterable, opts) { + if (from === undefined) { + from = require('./internal/streams/from'); + } + return from(Readable, iterable, opts); + }; +} +function indexOf(xs, x) { + for (var i = 0, l = xs.length; i < l; i++) { + if (xs[i] === x) return i; + } + return -1; +} \ No newline at end of file diff --git a/tests/corpus/readable-stream-3/node_modules/readable-stream/lib/_stream_transform.js b/tests/corpus/readable-stream-3/node_modules/readable-stream/lib/_stream_transform.js new file mode 100644 index 000000000..1ccb7157b --- /dev/null +++ b/tests/corpus/readable-stream-3/node_modules/readable-stream/lib/_stream_transform.js @@ -0,0 +1,190 @@ +// Copyright Joyent, Inc. and other Node 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. + +// a transform stream is a readable/writable stream where you do +// something with the data. Sometimes it's called a "filter", +// but that's not a great name for it, since that implies a thing where +// some bits pass through, and others are simply ignored. (That would +// be a valid example of a transform, of course.) +// +// While the output is causally related to the input, it's not a +// necessarily symmetric or synchronous transformation. For example, +// a zlib stream might take multiple plain-text writes(), and then +// emit a single compressed chunk some time in the future. +// +// Here's how this works: +// +// The Transform stream has all the aspects of the readable and writable +// stream classes. When you write(chunk), that calls _write(chunk,cb) +// internally, and returns false if there's a lot of pending writes +// buffered up. When you call read(), that calls _read(n) until +// there's enough pending readable data buffered up. +// +// In a transform stream, the written data is placed in a buffer. When +// _read(n) is called, it transforms the queued up data, calling the +// buffered _write cb's as it consumes chunks. If consuming a single +// written chunk would result in multiple output chunks, then the first +// outputted bit calls the readcb, and subsequent chunks just go into +// the read buffer, and will cause it to emit 'readable' if necessary. +// +// This way, back-pressure is actually determined by the reading side, +// since _read has to be called to start processing a new chunk. However, +// a pathological inflate type of transform can cause excessive buffering +// here. For example, imagine a stream where every byte of input is +// interpreted as an integer from 0-255, and then results in that many +// bytes of output. Writing the 4 bytes {ff,ff,ff,ff} would result in +// 1kb of data being output. In this case, you could write a very small +// amount of input, and end up with a very large amount of output. In +// such a pathological inflating mechanism, there'd be no way to tell +// the system to stop doing the transform. A single 4MB write could +// cause the system to run out of memory. +// +// However, even in such a pathological case, only a single written chunk +// would be consumed, and then the rest would wait (un-transformed) until +// the results of the previous transformed chunk were consumed. + +'use strict'; + +module.exports = Transform; +var _require$codes = require('../errors').codes, + ERR_METHOD_NOT_IMPLEMENTED = _require$codes.ERR_METHOD_NOT_IMPLEMENTED, + ERR_MULTIPLE_CALLBACK = _require$codes.ERR_MULTIPLE_CALLBACK, + ERR_TRANSFORM_ALREADY_TRANSFORMING = _require$codes.ERR_TRANSFORM_ALREADY_TRANSFORMING, + ERR_TRANSFORM_WITH_LENGTH_0 = _require$codes.ERR_TRANSFORM_WITH_LENGTH_0; +var Duplex = require('./_stream_duplex'); +require('inherits')(Transform, Duplex); +function afterTransform(er, data) { + var ts = this._transformState; + ts.transforming = false; + var cb = ts.writecb; + if (cb === null) { + return this.emit('error', new ERR_MULTIPLE_CALLBACK()); + } + ts.writechunk = null; + ts.writecb = null; + if (data != null) + // single equals check for both `null` and `undefined` + this.push(data); + cb(er); + var rs = this._readableState; + rs.reading = false; + if (rs.needReadable || rs.length < rs.highWaterMark) { + this._read(rs.highWaterMark); + } +} +function Transform(options) { + if (!(this instanceof Transform)) return new Transform(options); + Duplex.call(this, options); + this._transformState = { + afterTransform: afterTransform.bind(this), + needTransform: false, + transforming: false, + writecb: null, + writechunk: null, + writeencoding: null + }; + + // start out asking for a readable event once data is transformed. + this._readableState.needReadable = true; + + // we have implemented the _read method, and done the other things + // that Readable wants before the first _read call, so unset the + // sync guard flag. + this._readableState.sync = false; + if (options) { + if (typeof options.transform === 'function') this._transform = options.transform; + if (typeof options.flush === 'function') this._flush = options.flush; + } + + // When the writable side finishes, then flush out anything remaining. + this.on('prefinish', prefinish); +} +function prefinish() { + var _this = this; + if (typeof this._flush === 'function' && !this._readableState.destroyed) { + this._flush(function (er, data) { + done(_this, er, data); + }); + } else { + done(this, null, null); + } +} +Transform.prototype.push = function (chunk, encoding) { + this._transformState.needTransform = false; + return Duplex.prototype.push.call(this, chunk, encoding); +}; + +// This is the part where you do stuff! +// override this function in implementation classes. +// 'chunk' is an input chunk. +// +// Call `push(newChunk)` to pass along transformed output +// to the readable side. You may call 'push' zero or more times. +// +// Call `cb(err)` when you are done with this chunk. If you pass +// an error, then that'll put the hurt on the whole operation. If you +// never call cb(), then you'll never get another chunk. +Transform.prototype._transform = function (chunk, encoding, cb) { + cb(new ERR_METHOD_NOT_IMPLEMENTED('_transform()')); +}; +Transform.prototype._write = function (chunk, encoding, cb) { + var ts = this._transformState; + ts.writecb = cb; + ts.writechunk = chunk; + ts.writeencoding = encoding; + if (!ts.transforming) { + var rs = this._readableState; + if (ts.needTransform || rs.needReadable || rs.length < rs.highWaterMark) this._read(rs.highWaterMark); + } +}; + +// Doesn't matter what the args are here. +// _transform does all the work. +// That we got here means that the readable side wants more data. +Transform.prototype._read = function (n) { + var ts = this._transformState; + if (ts.writechunk !== null && !ts.transforming) { + ts.transforming = true; + this._transform(ts.writechunk, ts.writeencoding, ts.afterTransform); + } else { + // mark that we need a transform, so that any data that comes in + // will get processed, now that we've asked for it. + ts.needTransform = true; + } +}; +Transform.prototype._destroy = function (err, cb) { + Duplex.prototype._destroy.call(this, err, function (err2) { + cb(err2); + }); +}; +function done(stream, er, data) { + if (er) return stream.emit('error', er); + if (data != null) + // single equals check for both `null` and `undefined` + stream.push(data); + + // TODO(BridgeAR): Write a test for these two error cases + // if there's nothing in the write buffer, then that means + // that nothing more will ever be provided + if (stream._writableState.length) throw new ERR_TRANSFORM_WITH_LENGTH_0(); + if (stream._transformState.transforming) throw new ERR_TRANSFORM_ALREADY_TRANSFORMING(); + return stream.push(null); +} \ No newline at end of file diff --git a/tests/corpus/readable-stream-3/node_modules/readable-stream/lib/_stream_writable.js b/tests/corpus/readable-stream-3/node_modules/readable-stream/lib/_stream_writable.js new file mode 100644 index 000000000..292415e23 --- /dev/null +++ b/tests/corpus/readable-stream-3/node_modules/readable-stream/lib/_stream_writable.js @@ -0,0 +1,641 @@ +// Copyright Joyent, Inc. and other Node 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. + +// A bit simpler than readable streams. +// Implement an async ._write(chunk, encoding, cb), and it'll handle all +// the drain event emission and buffering. + +'use strict'; + +module.exports = Writable; + +/* */ +function WriteReq(chunk, encoding, cb) { + this.chunk = chunk; + this.encoding = encoding; + this.callback = cb; + this.next = null; +} + +// It seems a linked list but it is not +// there will be only 2 of these for each stream +function CorkedRequest(state) { + var _this = this; + this.next = null; + this.entry = null; + this.finish = function () { + onCorkedFinish(_this, state); + }; +} +/* */ + +/**/ +var Duplex; +/**/ + +Writable.WritableState = WritableState; + +/**/ +var internalUtil = { + deprecate: require('util-deprecate') +}; +/**/ + +/**/ +var Stream = require('./internal/streams/stream'); +/**/ + +var Buffer = require('buffer').Buffer; +var OurUint8Array = (typeof global !== 'undefined' ? global : typeof window !== 'undefined' ? window : typeof self !== 'undefined' ? self : {}).Uint8Array || function () {}; +function _uint8ArrayToBuffer(chunk) { + return Buffer.from(chunk); +} +function _isUint8Array(obj) { + return Buffer.isBuffer(obj) || obj instanceof OurUint8Array; +} +var destroyImpl = require('./internal/streams/destroy'); +var _require = require('./internal/streams/state'), + getHighWaterMark = _require.getHighWaterMark; +var _require$codes = require('../errors').codes, + ERR_INVALID_ARG_TYPE = _require$codes.ERR_INVALID_ARG_TYPE, + ERR_METHOD_NOT_IMPLEMENTED = _require$codes.ERR_METHOD_NOT_IMPLEMENTED, + ERR_MULTIPLE_CALLBACK = _require$codes.ERR_MULTIPLE_CALLBACK, + ERR_STREAM_CANNOT_PIPE = _require$codes.ERR_STREAM_CANNOT_PIPE, + ERR_STREAM_DESTROYED = _require$codes.ERR_STREAM_DESTROYED, + ERR_STREAM_NULL_VALUES = _require$codes.ERR_STREAM_NULL_VALUES, + ERR_STREAM_WRITE_AFTER_END = _require$codes.ERR_STREAM_WRITE_AFTER_END, + ERR_UNKNOWN_ENCODING = _require$codes.ERR_UNKNOWN_ENCODING; +var errorOrDestroy = destroyImpl.errorOrDestroy; +require('inherits')(Writable, Stream); +function nop() {} +function WritableState(options, stream, isDuplex) { + Duplex = Duplex || require('./_stream_duplex'); + options = options || {}; + + // Duplex streams are both readable and writable, but share + // the same options object. + // However, some cases require setting options to different + // values for the readable and the writable sides of the duplex stream, + // e.g. options.readableObjectMode vs. options.writableObjectMode, etc. + if (typeof isDuplex !== 'boolean') isDuplex = stream instanceof Duplex; + + // object stream flag to indicate whether or not this stream + // contains buffers or objects. + this.objectMode = !!options.objectMode; + if (isDuplex) this.objectMode = this.objectMode || !!options.writableObjectMode; + + // the point at which write() starts returning false + // Note: 0 is a valid value, means that we always return false if + // the entire buffer is not flushed immediately on write() + this.highWaterMark = getHighWaterMark(this, options, 'writableHighWaterMark', isDuplex); + + // if _final has been called + this.finalCalled = false; + + // drain event flag. + this.needDrain = false; + // at the start of calling end() + this.ending = false; + // when end() has been called, and returned + this.ended = false; + // when 'finish' is emitted + this.finished = false; + + // has it been destroyed + this.destroyed = false; + + // should we decode strings into buffers before passing to _write? + // this is here so that some node-core streams can optimize string + // handling at a lower level. + var noDecode = options.decodeStrings === false; + this.decodeStrings = !noDecode; + + // Crypto is kind of old and crusty. Historically, its default string + // encoding is 'binary' so we have to make this configurable. + // Everything else in the universe uses 'utf8', though. + this.defaultEncoding = options.defaultEncoding || 'utf8'; + + // not an actual buffer we keep track of, but a measurement + // of how much we're waiting to get pushed to some underlying + // socket or file. + this.length = 0; + + // a flag to see when we're in the middle of a write. + this.writing = false; + + // when true all writes will be buffered until .uncork() call + this.corked = 0; + + // a flag to be able to tell if the onwrite cb is called immediately, + // or on a later tick. We set this to true at first, because any + // actions that shouldn't happen until "later" should generally also + // not happen before the first write call. + this.sync = true; + + // a flag to know if we're processing previously buffered items, which + // may call the _write() callback in the same tick, so that we don't + // end up in an overlapped onwrite situation. + this.bufferProcessing = false; + + // the callback that's passed to _write(chunk,cb) + this.onwrite = function (er) { + onwrite(stream, er); + }; + + // the callback that the user supplies to write(chunk,encoding,cb) + this.writecb = null; + + // the amount that is being written when _write is called. + this.writelen = 0; + this.bufferedRequest = null; + this.lastBufferedRequest = null; + + // number of pending user-supplied write callbacks + // this must be 0 before 'finish' can be emitted + this.pendingcb = 0; + + // emit prefinish if the only thing we're waiting for is _write cbs + // This is relevant for synchronous Transform streams + this.prefinished = false; + + // True if the error was already emitted and should not be thrown again + this.errorEmitted = false; + + // Should close be emitted on destroy. Defaults to true. + this.emitClose = options.emitClose !== false; + + // Should .destroy() be called after 'finish' (and potentially 'end') + this.autoDestroy = !!options.autoDestroy; + + // count buffered requests + this.bufferedRequestCount = 0; + + // allocate the first CorkedRequest, there is always + // one allocated and free to use, and we maintain at most two + this.corkedRequestsFree = new CorkedRequest(this); +} +WritableState.prototype.getBuffer = function getBuffer() { + var current = this.bufferedRequest; + var out = []; + while (current) { + out.push(current); + current = current.next; + } + return out; +}; +(function () { + try { + Object.defineProperty(WritableState.prototype, 'buffer', { + get: internalUtil.deprecate(function writableStateBufferGetter() { + return this.getBuffer(); + }, '_writableState.buffer is deprecated. Use _writableState.getBuffer ' + 'instead.', 'DEP0003') + }); + } catch (_) {} +})(); + +// Test _writableState for inheritance to account for Duplex streams, +// whose prototype chain only points to Readable. +var realHasInstance; +if (typeof Symbol === 'function' && Symbol.hasInstance && typeof Function.prototype[Symbol.hasInstance] === 'function') { + realHasInstance = Function.prototype[Symbol.hasInstance]; + Object.defineProperty(Writable, Symbol.hasInstance, { + value: function value(object) { + if (realHasInstance.call(this, object)) return true; + if (this !== Writable) return false; + return object && object._writableState instanceof WritableState; + } + }); +} else { + realHasInstance = function realHasInstance(object) { + return object instanceof this; + }; +} +function Writable(options) { + Duplex = Duplex || require('./_stream_duplex'); + + // Writable ctor is applied to Duplexes, too. + // `realHasInstance` is necessary because using plain `instanceof` + // would return false, as no `_writableState` property is attached. + + // Trying to use the custom `instanceof` for Writable here will also break the + // Node.js LazyTransform implementation, which has a non-trivial getter for + // `_writableState` that would lead to infinite recursion. + + // Checking for a Stream.Duplex instance is faster here instead of inside + // the WritableState constructor, at least with V8 6.5 + var isDuplex = this instanceof Duplex; + if (!isDuplex && !realHasInstance.call(Writable, this)) return new Writable(options); + this._writableState = new WritableState(options, this, isDuplex); + + // legacy. + this.writable = true; + if (options) { + if (typeof options.write === 'function') this._write = options.write; + if (typeof options.writev === 'function') this._writev = options.writev; + if (typeof options.destroy === 'function') this._destroy = options.destroy; + if (typeof options.final === 'function') this._final = options.final; + } + Stream.call(this); +} + +// Otherwise people can pipe Writable streams, which is just wrong. +Writable.prototype.pipe = function () { + errorOrDestroy(this, new ERR_STREAM_CANNOT_PIPE()); +}; +function writeAfterEnd(stream, cb) { + var er = new ERR_STREAM_WRITE_AFTER_END(); + // TODO: defer error events consistently everywhere, not just the cb + errorOrDestroy(stream, er); + process.nextTick(cb, er); +} + +// Checks that a user-supplied chunk is valid, especially for the particular +// mode the stream is in. Currently this means that `null` is never accepted +// and undefined/non-string values are only allowed in object mode. +function validChunk(stream, state, chunk, cb) { + var er; + if (chunk === null) { + er = new ERR_STREAM_NULL_VALUES(); + } else if (typeof chunk !== 'string' && !state.objectMode) { + er = new ERR_INVALID_ARG_TYPE('chunk', ['string', 'Buffer'], chunk); + } + if (er) { + errorOrDestroy(stream, er); + process.nextTick(cb, er); + return false; + } + return true; +} +Writable.prototype.write = function (chunk, encoding, cb) { + var state = this._writableState; + var ret = false; + var isBuf = !state.objectMode && _isUint8Array(chunk); + if (isBuf && !Buffer.isBuffer(chunk)) { + chunk = _uint8ArrayToBuffer(chunk); + } + if (typeof encoding === 'function') { + cb = encoding; + encoding = null; + } + if (isBuf) encoding = 'buffer';else if (!encoding) encoding = state.defaultEncoding; + if (typeof cb !== 'function') cb = nop; + if (state.ending) writeAfterEnd(this, cb);else if (isBuf || validChunk(this, state, chunk, cb)) { + state.pendingcb++; + ret = writeOrBuffer(this, state, isBuf, chunk, encoding, cb); + } + return ret; +}; +Writable.prototype.cork = function () { + this._writableState.corked++; +}; +Writable.prototype.uncork = function () { + var state = this._writableState; + if (state.corked) { + state.corked--; + if (!state.writing && !state.corked && !state.bufferProcessing && state.bufferedRequest) clearBuffer(this, state); + } +}; +Writable.prototype.setDefaultEncoding = function setDefaultEncoding(encoding) { + // node::ParseEncoding() requires lower case. + if (typeof encoding === 'string') encoding = encoding.toLowerCase(); + if (!(['hex', 'utf8', 'utf-8', 'ascii', 'binary', 'base64', 'ucs2', 'ucs-2', 'utf16le', 'utf-16le', 'raw'].indexOf((encoding + '').toLowerCase()) > -1)) throw new ERR_UNKNOWN_ENCODING(encoding); + this._writableState.defaultEncoding = encoding; + return this; +}; +Object.defineProperty(Writable.prototype, 'writableBuffer', { + // making it explicit this property is not enumerable + // because otherwise some prototype manipulation in + // userland will fail + enumerable: false, + get: function get() { + return this._writableState && this._writableState.getBuffer(); + } +}); +function decodeChunk(state, chunk, encoding) { + if (!state.objectMode && state.decodeStrings !== false && typeof chunk === 'string') { + chunk = Buffer.from(chunk, encoding); + } + return chunk; +} +Object.defineProperty(Writable.prototype, 'writableHighWaterMark', { + // making it explicit this property is not enumerable + // because otherwise some prototype manipulation in + // userland will fail + enumerable: false, + get: function get() { + return this._writableState.highWaterMark; + } +}); + +// if we're already writing something, then just put this +// in the queue, and wait our turn. Otherwise, call _write +// If we return false, then we need a drain event, so set that flag. +function writeOrBuffer(stream, state, isBuf, chunk, encoding, cb) { + if (!isBuf) { + var newChunk = decodeChunk(state, chunk, encoding); + if (chunk !== newChunk) { + isBuf = true; + encoding = 'buffer'; + chunk = newChunk; + } + } + var len = state.objectMode ? 1 : chunk.length; + state.length += len; + var ret = state.length < state.highWaterMark; + // we must ensure that previous needDrain will not be reset to false. + if (!ret) state.needDrain = true; + if (state.writing || state.corked) { + var last = state.lastBufferedRequest; + state.lastBufferedRequest = { + chunk: chunk, + encoding: encoding, + isBuf: isBuf, + callback: cb, + next: null + }; + if (last) { + last.next = state.lastBufferedRequest; + } else { + state.bufferedRequest = state.lastBufferedRequest; + } + state.bufferedRequestCount += 1; + } else { + doWrite(stream, state, false, len, chunk, encoding, cb); + } + return ret; +} +function doWrite(stream, state, writev, len, chunk, encoding, cb) { + state.writelen = len; + state.writecb = cb; + state.writing = true; + state.sync = true; + if (state.destroyed) state.onwrite(new ERR_STREAM_DESTROYED('write'));else if (writev) stream._writev(chunk, state.onwrite);else stream._write(chunk, encoding, state.onwrite); + state.sync = false; +} +function onwriteError(stream, state, sync, er, cb) { + --state.pendingcb; + if (sync) { + // defer the callback if we are being called synchronously + // to avoid piling up things on the stack + process.nextTick(cb, er); + // this can emit finish, and it will always happen + // after error + process.nextTick(finishMaybe, stream, state); + stream._writableState.errorEmitted = true; + errorOrDestroy(stream, er); + } else { + // the caller expect this to happen before if + // it is async + cb(er); + stream._writableState.errorEmitted = true; + errorOrDestroy(stream, er); + // this can emit finish, but finish must + // always follow error + finishMaybe(stream, state); + } +} +function onwriteStateUpdate(state) { + state.writing = false; + state.writecb = null; + state.length -= state.writelen; + state.writelen = 0; +} +function onwrite(stream, er) { + var state = stream._writableState; + var sync = state.sync; + var cb = state.writecb; + if (typeof cb !== 'function') throw new ERR_MULTIPLE_CALLBACK(); + onwriteStateUpdate(state); + if (er) onwriteError(stream, state, sync, er, cb);else { + // Check if we're actually ready to finish, but don't emit yet + var finished = needFinish(state) || stream.destroyed; + if (!finished && !state.corked && !state.bufferProcessing && state.bufferedRequest) { + clearBuffer(stream, state); + } + if (sync) { + process.nextTick(afterWrite, stream, state, finished, cb); + } else { + afterWrite(stream, state, finished, cb); + } + } +} +function afterWrite(stream, state, finished, cb) { + if (!finished) onwriteDrain(stream, state); + state.pendingcb--; + cb(); + finishMaybe(stream, state); +} + +// Must force callback to be called on nextTick, so that we don't +// emit 'drain' before the write() consumer gets the 'false' return +// value, and has a chance to attach a 'drain' listener. +function onwriteDrain(stream, state) { + if (state.length === 0 && state.needDrain) { + state.needDrain = false; + stream.emit('drain'); + } +} + +// if there's something in the buffer waiting, then process it +function clearBuffer(stream, state) { + state.bufferProcessing = true; + var entry = state.bufferedRequest; + if (stream._writev && entry && entry.next) { + // Fast case, write everything using _writev() + var l = state.bufferedRequestCount; + var buffer = new Array(l); + var holder = state.corkedRequestsFree; + holder.entry = entry; + var count = 0; + var allBuffers = true; + while (entry) { + buffer[count] = entry; + if (!entry.isBuf) allBuffers = false; + entry = entry.next; + count += 1; + } + buffer.allBuffers = allBuffers; + doWrite(stream, state, true, state.length, buffer, '', holder.finish); + + // doWrite is almost always async, defer these to save a bit of time + // as the hot path ends with doWrite + state.pendingcb++; + state.lastBufferedRequest = null; + if (holder.next) { + state.corkedRequestsFree = holder.next; + holder.next = null; + } else { + state.corkedRequestsFree = new CorkedRequest(state); + } + state.bufferedRequestCount = 0; + } else { + // Slow case, write chunks one-by-one + while (entry) { + var chunk = entry.chunk; + var encoding = entry.encoding; + var cb = entry.callback; + var len = state.objectMode ? 1 : chunk.length; + doWrite(stream, state, false, len, chunk, encoding, cb); + entry = entry.next; + state.bufferedRequestCount--; + // if we didn't call the onwrite immediately, then + // it means that we need to wait until it does. + // also, that means that the chunk and cb are currently + // being processed, so move the buffer counter past them. + if (state.writing) { + break; + } + } + if (entry === null) state.lastBufferedRequest = null; + } + state.bufferedRequest = entry; + state.bufferProcessing = false; +} +Writable.prototype._write = function (chunk, encoding, cb) { + cb(new ERR_METHOD_NOT_IMPLEMENTED('_write()')); +}; +Writable.prototype._writev = null; +Writable.prototype.end = function (chunk, encoding, cb) { + var state = this._writableState; + if (typeof chunk === 'function') { + cb = chunk; + chunk = null; + encoding = null; + } else if (typeof encoding === 'function') { + cb = encoding; + encoding = null; + } + if (chunk !== null && chunk !== undefined) this.write(chunk, encoding); + + // .end() fully uncorks + if (state.corked) { + state.corked = 1; + this.uncork(); + } + + // ignore unnecessary end() calls. + if (!state.ending) endWritable(this, state, cb); + return this; +}; +Object.defineProperty(Writable.prototype, 'writableLength', { + // making it explicit this property is not enumerable + // because otherwise some prototype manipulation in + // userland will fail + enumerable: false, + get: function get() { + return this._writableState.length; + } +}); +function needFinish(state) { + return state.ending && state.length === 0 && state.bufferedRequest === null && !state.finished && !state.writing; +} +function callFinal(stream, state) { + stream._final(function (err) { + state.pendingcb--; + if (err) { + errorOrDestroy(stream, err); + } + state.prefinished = true; + stream.emit('prefinish'); + finishMaybe(stream, state); + }); +} +function prefinish(stream, state) { + if (!state.prefinished && !state.finalCalled) { + if (typeof stream._final === 'function' && !state.destroyed) { + state.pendingcb++; + state.finalCalled = true; + process.nextTick(callFinal, stream, state); + } else { + state.prefinished = true; + stream.emit('prefinish'); + } + } +} +function finishMaybe(stream, state) { + var need = needFinish(state); + if (need) { + prefinish(stream, state); + if (state.pendingcb === 0) { + state.finished = true; + stream.emit('finish'); + if (state.autoDestroy) { + // In case of duplex streams we need a way to detect + // if the readable side is ready for autoDestroy as well + var rState = stream._readableState; + if (!rState || rState.autoDestroy && rState.endEmitted) { + stream.destroy(); + } + } + } + } + return need; +} +function endWritable(stream, state, cb) { + state.ending = true; + finishMaybe(stream, state); + if (cb) { + if (state.finished) process.nextTick(cb);else stream.once('finish', cb); + } + state.ended = true; + stream.writable = false; +} +function onCorkedFinish(corkReq, state, err) { + var entry = corkReq.entry; + corkReq.entry = null; + while (entry) { + var cb = entry.callback; + state.pendingcb--; + cb(err); + entry = entry.next; + } + + // reuse the free corkReq. + state.corkedRequestsFree.next = corkReq; +} +Object.defineProperty(Writable.prototype, 'destroyed', { + // making it explicit this property is not enumerable + // because otherwise some prototype manipulation in + // userland will fail + enumerable: false, + get: function get() { + if (this._writableState === undefined) { + return false; + } + return this._writableState.destroyed; + }, + set: function set(value) { + // we ignore the value if the stream + // has not been initialized yet + if (!this._writableState) { + return; + } + + // backward compatibility, the user is explicitly + // managing destroyed + this._writableState.destroyed = value; + } +}); +Writable.prototype.destroy = destroyImpl.destroy; +Writable.prototype._undestroy = destroyImpl.undestroy; +Writable.prototype._destroy = function (err, cb) { + cb(err); +}; \ No newline at end of file diff --git a/tests/corpus/readable-stream-3/node_modules/readable-stream/lib/internal/streams/async_iterator.js b/tests/corpus/readable-stream-3/node_modules/readable-stream/lib/internal/streams/async_iterator.js new file mode 100644 index 000000000..742c5a467 --- /dev/null +++ b/tests/corpus/readable-stream-3/node_modules/readable-stream/lib/internal/streams/async_iterator.js @@ -0,0 +1,180 @@ +'use strict'; + +var _Object$setPrototypeO; +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } +var finished = require('./end-of-stream'); +var kLastResolve = Symbol('lastResolve'); +var kLastReject = Symbol('lastReject'); +var kError = Symbol('error'); +var kEnded = Symbol('ended'); +var kLastPromise = Symbol('lastPromise'); +var kHandlePromise = Symbol('handlePromise'); +var kStream = Symbol('stream'); +function createIterResult(value, done) { + return { + value: value, + done: done + }; +} +function readAndResolve(iter) { + var resolve = iter[kLastResolve]; + if (resolve !== null) { + var data = iter[kStream].read(); + // we defer if data is null + // we can be expecting either 'end' or + // 'error' + if (data !== null) { + iter[kLastPromise] = null; + iter[kLastResolve] = null; + iter[kLastReject] = null; + resolve(createIterResult(data, false)); + } + } +} +function onReadable(iter) { + // we wait for the next tick, because it might + // emit an error with process.nextTick + process.nextTick(readAndResolve, iter); +} +function wrapForNext(lastPromise, iter) { + return function (resolve, reject) { + lastPromise.then(function () { + if (iter[kEnded]) { + resolve(createIterResult(undefined, true)); + return; + } + iter[kHandlePromise](resolve, reject); + }, reject); + }; +} +var AsyncIteratorPrototype = Object.getPrototypeOf(function () {}); +var ReadableStreamAsyncIteratorPrototype = Object.setPrototypeOf((_Object$setPrototypeO = { + get stream() { + return this[kStream]; + }, + next: function next() { + var _this = this; + // if we have detected an error in the meanwhile + // reject straight away + var error = this[kError]; + if (error !== null) { + return Promise.reject(error); + } + if (this[kEnded]) { + return Promise.resolve(createIterResult(undefined, true)); + } + if (this[kStream].destroyed) { + // We need to defer via nextTick because if .destroy(err) is + // called, the error will be emitted via nextTick, and + // we cannot guarantee that there is no error lingering around + // waiting to be emitted. + return new Promise(function (resolve, reject) { + process.nextTick(function () { + if (_this[kError]) { + reject(_this[kError]); + } else { + resolve(createIterResult(undefined, true)); + } + }); + }); + } + + // if we have multiple next() calls + // we will wait for the previous Promise to finish + // this logic is optimized to support for await loops, + // where next() is only called once at a time + var lastPromise = this[kLastPromise]; + var promise; + if (lastPromise) { + promise = new Promise(wrapForNext(lastPromise, this)); + } else { + // fast path needed to support multiple this.push() + // without triggering the next() queue + var data = this[kStream].read(); + if (data !== null) { + return Promise.resolve(createIterResult(data, false)); + } + promise = new Promise(this[kHandlePromise]); + } + this[kLastPromise] = promise; + return promise; + } +}, _defineProperty(_Object$setPrototypeO, Symbol.asyncIterator, function () { + return this; +}), _defineProperty(_Object$setPrototypeO, "return", function _return() { + var _this2 = this; + // destroy(err, cb) is a private API + // we can guarantee we have that here, because we control the + // Readable class this is attached to + return new Promise(function (resolve, reject) { + _this2[kStream].destroy(null, function (err) { + if (err) { + reject(err); + return; + } + resolve(createIterResult(undefined, true)); + }); + }); +}), _Object$setPrototypeO), AsyncIteratorPrototype); +var createReadableStreamAsyncIterator = function createReadableStreamAsyncIterator(stream) { + var _Object$create; + var iterator = Object.create(ReadableStreamAsyncIteratorPrototype, (_Object$create = {}, _defineProperty(_Object$create, kStream, { + value: stream, + writable: true + }), _defineProperty(_Object$create, kLastResolve, { + value: null, + writable: true + }), _defineProperty(_Object$create, kLastReject, { + value: null, + writable: true + }), _defineProperty(_Object$create, kError, { + value: null, + writable: true + }), _defineProperty(_Object$create, kEnded, { + value: stream._readableState.endEmitted, + writable: true + }), _defineProperty(_Object$create, kHandlePromise, { + value: function value(resolve, reject) { + var data = iterator[kStream].read(); + if (data) { + iterator[kLastPromise] = null; + iterator[kLastResolve] = null; + iterator[kLastReject] = null; + resolve(createIterResult(data, false)); + } else { + iterator[kLastResolve] = resolve; + iterator[kLastReject] = reject; + } + }, + writable: true + }), _Object$create)); + iterator[kLastPromise] = null; + finished(stream, function (err) { + if (err && err.code !== 'ERR_STREAM_PREMATURE_CLOSE') { + var reject = iterator[kLastReject]; + // reject if we are waiting for data in the Promise + // returned by next() and store the error + if (reject !== null) { + iterator[kLastPromise] = null; + iterator[kLastResolve] = null; + iterator[kLastReject] = null; + reject(err); + } + iterator[kError] = err; + return; + } + var resolve = iterator[kLastResolve]; + if (resolve !== null) { + iterator[kLastPromise] = null; + iterator[kLastResolve] = null; + iterator[kLastReject] = null; + resolve(createIterResult(undefined, true)); + } + iterator[kEnded] = true; + }); + stream.on('readable', onReadable.bind(null, iterator)); + return iterator; +}; +module.exports = createReadableStreamAsyncIterator; \ No newline at end of file diff --git a/tests/corpus/readable-stream-3/node_modules/readable-stream/lib/internal/streams/buffer_list.js b/tests/corpus/readable-stream-3/node_modules/readable-stream/lib/internal/streams/buffer_list.js new file mode 100644 index 000000000..69bda497d --- /dev/null +++ b/tests/corpus/readable-stream-3/node_modules/readable-stream/lib/internal/streams/buffer_list.js @@ -0,0 +1,183 @@ +'use strict'; + +function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; } +function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; } +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } +function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, _toPropertyKey(descriptor.key), descriptor); } } +function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } +var _require = require('buffer'), + Buffer = _require.Buffer; +var _require2 = require('util'), + inspect = _require2.inspect; +var custom = inspect && inspect.custom || 'inspect'; +function copyBuffer(src, target, offset) { + Buffer.prototype.copy.call(src, target, offset); +} +module.exports = /*#__PURE__*/function () { + function BufferList() { + _classCallCheck(this, BufferList); + this.head = null; + this.tail = null; + this.length = 0; + } + _createClass(BufferList, [{ + key: "push", + value: function push(v) { + var entry = { + data: v, + next: null + }; + if (this.length > 0) this.tail.next = entry;else this.head = entry; + this.tail = entry; + ++this.length; + } + }, { + key: "unshift", + value: function unshift(v) { + var entry = { + data: v, + next: this.head + }; + if (this.length === 0) this.tail = entry; + this.head = entry; + ++this.length; + } + }, { + key: "shift", + value: function shift() { + if (this.length === 0) return; + var ret = this.head.data; + if (this.length === 1) this.head = this.tail = null;else this.head = this.head.next; + --this.length; + return ret; + } + }, { + key: "clear", + value: function clear() { + this.head = this.tail = null; + this.length = 0; + } + }, { + key: "join", + value: function join(s) { + if (this.length === 0) return ''; + var p = this.head; + var ret = '' + p.data; + while (p = p.next) ret += s + p.data; + return ret; + } + }, { + key: "concat", + value: function concat(n) { + if (this.length === 0) return Buffer.alloc(0); + var ret = Buffer.allocUnsafe(n >>> 0); + var p = this.head; + var i = 0; + while (p) { + copyBuffer(p.data, ret, i); + i += p.data.length; + p = p.next; + } + return ret; + } + + // Consumes a specified amount of bytes or characters from the buffered data. + }, { + key: "consume", + value: function consume(n, hasStrings) { + var ret; + if (n < this.head.data.length) { + // `slice` is the same for buffers and strings. + ret = this.head.data.slice(0, n); + this.head.data = this.head.data.slice(n); + } else if (n === this.head.data.length) { + // First chunk is a perfect match. + ret = this.shift(); + } else { + // Result spans more than one buffer. + ret = hasStrings ? this._getString(n) : this._getBuffer(n); + } + return ret; + } + }, { + key: "first", + value: function first() { + return this.head.data; + } + + // Consumes a specified amount of characters from the buffered data. + }, { + key: "_getString", + value: function _getString(n) { + var p = this.head; + var c = 1; + var ret = p.data; + n -= ret.length; + while (p = p.next) { + var str = p.data; + var nb = n > str.length ? str.length : n; + if (nb === str.length) ret += str;else ret += str.slice(0, n); + n -= nb; + if (n === 0) { + if (nb === str.length) { + ++c; + if (p.next) this.head = p.next;else this.head = this.tail = null; + } else { + this.head = p; + p.data = str.slice(nb); + } + break; + } + ++c; + } + this.length -= c; + return ret; + } + + // Consumes a specified amount of bytes from the buffered data. + }, { + key: "_getBuffer", + value: function _getBuffer(n) { + var ret = Buffer.allocUnsafe(n); + var p = this.head; + var c = 1; + p.data.copy(ret); + n -= p.data.length; + while (p = p.next) { + var buf = p.data; + var nb = n > buf.length ? buf.length : n; + buf.copy(ret, ret.length - n, 0, nb); + n -= nb; + if (n === 0) { + if (nb === buf.length) { + ++c; + if (p.next) this.head = p.next;else this.head = this.tail = null; + } else { + this.head = p; + p.data = buf.slice(nb); + } + break; + } + ++c; + } + this.length -= c; + return ret; + } + + // Make sure the linked list only shows the minimal necessary information. + }, { + key: custom, + value: function value(_, options) { + return inspect(this, _objectSpread(_objectSpread({}, options), {}, { + // Only inspect one level. + depth: 0, + // It should not recurse. + customInspect: false + })); + } + }]); + return BufferList; +}(); \ No newline at end of file diff --git a/tests/corpus/readable-stream-3/node_modules/readable-stream/lib/internal/streams/destroy.js b/tests/corpus/readable-stream-3/node_modules/readable-stream/lib/internal/streams/destroy.js new file mode 100644 index 000000000..31a17c4dc --- /dev/null +++ b/tests/corpus/readable-stream-3/node_modules/readable-stream/lib/internal/streams/destroy.js @@ -0,0 +1,96 @@ +'use strict'; + +// undocumented cb() API, needed for core, not for public API +function destroy(err, cb) { + var _this = this; + var readableDestroyed = this._readableState && this._readableState.destroyed; + var writableDestroyed = this._writableState && this._writableState.destroyed; + if (readableDestroyed || writableDestroyed) { + if (cb) { + cb(err); + } else if (err) { + if (!this._writableState) { + process.nextTick(emitErrorNT, this, err); + } else if (!this._writableState.errorEmitted) { + this._writableState.errorEmitted = true; + process.nextTick(emitErrorNT, this, err); + } + } + return this; + } + + // we set destroyed to true before firing error callbacks in order + // to make it re-entrance safe in case destroy() is called within callbacks + + if (this._readableState) { + this._readableState.destroyed = true; + } + + // if this is a duplex stream mark the writable part as destroyed as well + if (this._writableState) { + this._writableState.destroyed = true; + } + this._destroy(err || null, function (err) { + if (!cb && err) { + if (!_this._writableState) { + process.nextTick(emitErrorAndCloseNT, _this, err); + } else if (!_this._writableState.errorEmitted) { + _this._writableState.errorEmitted = true; + process.nextTick(emitErrorAndCloseNT, _this, err); + } else { + process.nextTick(emitCloseNT, _this); + } + } else if (cb) { + process.nextTick(emitCloseNT, _this); + cb(err); + } else { + process.nextTick(emitCloseNT, _this); + } + }); + return this; +} +function emitErrorAndCloseNT(self, err) { + emitErrorNT(self, err); + emitCloseNT(self); +} +function emitCloseNT(self) { + if (self._writableState && !self._writableState.emitClose) return; + if (self._readableState && !self._readableState.emitClose) return; + self.emit('close'); +} +function undestroy() { + if (this._readableState) { + this._readableState.destroyed = false; + this._readableState.reading = false; + this._readableState.ended = false; + this._readableState.endEmitted = false; + } + if (this._writableState) { + this._writableState.destroyed = false; + this._writableState.ended = false; + this._writableState.ending = false; + this._writableState.finalCalled = false; + this._writableState.prefinished = false; + this._writableState.finished = false; + this._writableState.errorEmitted = false; + } +} +function emitErrorNT(self, err) { + self.emit('error', err); +} +function errorOrDestroy(stream, err) { + // We have tests that rely on errors being emitted + // in the same tick, so changing this is semver major. + // For now when you opt-in to autoDestroy we allow + // the error to be emitted nextTick. In a future + // semver major update we should change the default to this. + + var rState = stream._readableState; + var wState = stream._writableState; + if (rState && rState.autoDestroy || wState && wState.autoDestroy) stream.destroy(err);else stream.emit('error', err); +} +module.exports = { + destroy: destroy, + undestroy: undestroy, + errorOrDestroy: errorOrDestroy +}; \ No newline at end of file diff --git a/tests/corpus/readable-stream-3/node_modules/readable-stream/lib/internal/streams/end-of-stream.js b/tests/corpus/readable-stream-3/node_modules/readable-stream/lib/internal/streams/end-of-stream.js new file mode 100644 index 000000000..59c671b5a --- /dev/null +++ b/tests/corpus/readable-stream-3/node_modules/readable-stream/lib/internal/streams/end-of-stream.js @@ -0,0 +1,86 @@ +// Ported from https://github.com/mafintosh/end-of-stream with +// permission from the author, Mathias Buus (@mafintosh). + +'use strict'; + +var ERR_STREAM_PREMATURE_CLOSE = require('../../../errors').codes.ERR_STREAM_PREMATURE_CLOSE; +function once(callback) { + var called = false; + return function () { + if (called) return; + called = true; + for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { + args[_key] = arguments[_key]; + } + callback.apply(this, args); + }; +} +function noop() {} +function isRequest(stream) { + return stream.setHeader && typeof stream.abort === 'function'; +} +function eos(stream, opts, callback) { + if (typeof opts === 'function') return eos(stream, null, opts); + if (!opts) opts = {}; + callback = once(callback || noop); + var readable = opts.readable || opts.readable !== false && stream.readable; + var writable = opts.writable || opts.writable !== false && stream.writable; + var onlegacyfinish = function onlegacyfinish() { + if (!stream.writable) onfinish(); + }; + var writableEnded = stream._writableState && stream._writableState.finished; + var onfinish = function onfinish() { + writable = false; + writableEnded = true; + if (!readable) callback.call(stream); + }; + var readableEnded = stream._readableState && stream._readableState.endEmitted; + var onend = function onend() { + readable = false; + readableEnded = true; + if (!writable) callback.call(stream); + }; + var onerror = function onerror(err) { + callback.call(stream, err); + }; + var onclose = function onclose() { + var err; + if (readable && !readableEnded) { + if (!stream._readableState || !stream._readableState.ended) err = new ERR_STREAM_PREMATURE_CLOSE(); + return callback.call(stream, err); + } + if (writable && !writableEnded) { + if (!stream._writableState || !stream._writableState.ended) err = new ERR_STREAM_PREMATURE_CLOSE(); + return callback.call(stream, err); + } + }; + var onrequest = function onrequest() { + stream.req.on('finish', onfinish); + }; + if (isRequest(stream)) { + stream.on('complete', onfinish); + stream.on('abort', onclose); + if (stream.req) onrequest();else stream.on('request', onrequest); + } else if (writable && !stream._writableState) { + // legacy streams + stream.on('end', onlegacyfinish); + stream.on('close', onlegacyfinish); + } + stream.on('end', onend); + stream.on('finish', onfinish); + if (opts.error !== false) stream.on('error', onerror); + stream.on('close', onclose); + return function () { + stream.removeListener('complete', onfinish); + stream.removeListener('abort', onclose); + stream.removeListener('request', onrequest); + if (stream.req) stream.req.removeListener('finish', onfinish); + stream.removeListener('end', onlegacyfinish); + stream.removeListener('close', onlegacyfinish); + stream.removeListener('finish', onfinish); + stream.removeListener('end', onend); + stream.removeListener('error', onerror); + stream.removeListener('close', onclose); + }; +} +module.exports = eos; \ No newline at end of file diff --git a/tests/corpus/readable-stream-3/node_modules/readable-stream/lib/internal/streams/from-browser.js b/tests/corpus/readable-stream-3/node_modules/readable-stream/lib/internal/streams/from-browser.js new file mode 100644 index 000000000..a4ce56f3c --- /dev/null +++ b/tests/corpus/readable-stream-3/node_modules/readable-stream/lib/internal/streams/from-browser.js @@ -0,0 +1,3 @@ +module.exports = function () { + throw new Error('Readable.from is not available in the browser') +}; diff --git a/tests/corpus/readable-stream-3/node_modules/readable-stream/lib/internal/streams/from.js b/tests/corpus/readable-stream-3/node_modules/readable-stream/lib/internal/streams/from.js new file mode 100644 index 000000000..0a34ee92e --- /dev/null +++ b/tests/corpus/readable-stream-3/node_modules/readable-stream/lib/internal/streams/from.js @@ -0,0 +1,52 @@ +'use strict'; + +function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } } +function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; } +function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; } +function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; } +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } +var ERR_INVALID_ARG_TYPE = require('../../../errors').codes.ERR_INVALID_ARG_TYPE; +function from(Readable, iterable, opts) { + var iterator; + if (iterable && typeof iterable.next === 'function') { + iterator = iterable; + } else if (iterable && iterable[Symbol.asyncIterator]) iterator = iterable[Symbol.asyncIterator]();else if (iterable && iterable[Symbol.iterator]) iterator = iterable[Symbol.iterator]();else throw new ERR_INVALID_ARG_TYPE('iterable', ['Iterable'], iterable); + var readable = new Readable(_objectSpread({ + objectMode: true + }, opts)); + // Reading boolean to protect against _read + // being called before last iteration completion. + var reading = false; + readable._read = function () { + if (!reading) { + reading = true; + next(); + } + }; + function next() { + return _next2.apply(this, arguments); + } + function _next2() { + _next2 = _asyncToGenerator(function* () { + try { + var _yield$iterator$next = yield iterator.next(), + value = _yield$iterator$next.value, + done = _yield$iterator$next.done; + if (done) { + readable.push(null); + } else if (readable.push(yield value)) { + next(); + } else { + reading = false; + } + } catch (err) { + readable.destroy(err); + } + }); + return _next2.apply(this, arguments); + } + return readable; +} +module.exports = from; diff --git a/tests/corpus/readable-stream-3/node_modules/readable-stream/lib/internal/streams/pipeline.js b/tests/corpus/readable-stream-3/node_modules/readable-stream/lib/internal/streams/pipeline.js new file mode 100644 index 000000000..e6f39241f --- /dev/null +++ b/tests/corpus/readable-stream-3/node_modules/readable-stream/lib/internal/streams/pipeline.js @@ -0,0 +1,86 @@ +// Ported from https://github.com/mafintosh/pump with +// permission from the author, Mathias Buus (@mafintosh). + +'use strict'; + +var eos; +function once(callback) { + var called = false; + return function () { + if (called) return; + called = true; + callback.apply(void 0, arguments); + }; +} +var _require$codes = require('../../../errors').codes, + ERR_MISSING_ARGS = _require$codes.ERR_MISSING_ARGS, + ERR_STREAM_DESTROYED = _require$codes.ERR_STREAM_DESTROYED; +function noop(err) { + // Rethrow the error if it exists to avoid swallowing it + if (err) throw err; +} +function isRequest(stream) { + return stream.setHeader && typeof stream.abort === 'function'; +} +function destroyer(stream, reading, writing, callback) { + callback = once(callback); + var closed = false; + stream.on('close', function () { + closed = true; + }); + if (eos === undefined) eos = require('./end-of-stream'); + eos(stream, { + readable: reading, + writable: writing + }, function (err) { + if (err) return callback(err); + closed = true; + callback(); + }); + var destroyed = false; + return function (err) { + if (closed) return; + if (destroyed) return; + destroyed = true; + + // request.destroy just do .end - .abort is what we want + if (isRequest(stream)) return stream.abort(); + if (typeof stream.destroy === 'function') return stream.destroy(); + callback(err || new ERR_STREAM_DESTROYED('pipe')); + }; +} +function call(fn) { + fn(); +} +function pipe(from, to) { + return from.pipe(to); +} +function popCallback(streams) { + if (!streams.length) return noop; + if (typeof streams[streams.length - 1] !== 'function') return noop; + return streams.pop(); +} +function pipeline() { + for (var _len = arguments.length, streams = new Array(_len), _key = 0; _key < _len; _key++) { + streams[_key] = arguments[_key]; + } + var callback = popCallback(streams); + if (Array.isArray(streams[0])) streams = streams[0]; + if (streams.length < 2) { + throw new ERR_MISSING_ARGS('streams'); + } + var error; + var destroys = streams.map(function (stream, i) { + var reading = i < streams.length - 1; + var writing = i > 0; + return destroyer(stream, reading, writing, function (err) { + if (!error) error = err; + if (err) destroys.forEach(call); + if (reading) return; + destroys.forEach(call); + callback(error); + }); + }); + return streams.reduce(pipe); +} +module.exports = pipeline; \ No newline at end of file diff --git a/tests/corpus/readable-stream-3/node_modules/readable-stream/lib/internal/streams/state.js b/tests/corpus/readable-stream-3/node_modules/readable-stream/lib/internal/streams/state.js new file mode 100644 index 000000000..3fbf8927e --- /dev/null +++ b/tests/corpus/readable-stream-3/node_modules/readable-stream/lib/internal/streams/state.js @@ -0,0 +1,22 @@ +'use strict'; + +var ERR_INVALID_OPT_VALUE = require('../../../errors').codes.ERR_INVALID_OPT_VALUE; +function highWaterMarkFrom(options, isDuplex, duplexKey) { + return options.highWaterMark != null ? options.highWaterMark : isDuplex ? options[duplexKey] : null; +} +function getHighWaterMark(state, options, duplexKey, isDuplex) { + var hwm = highWaterMarkFrom(options, isDuplex, duplexKey); + if (hwm != null) { + if (!(isFinite(hwm) && Math.floor(hwm) === hwm) || hwm < 0) { + var name = isDuplex ? duplexKey : 'highWaterMark'; + throw new ERR_INVALID_OPT_VALUE(name, hwm); + } + return Math.floor(hwm); + } + + // Default value + return state.objectMode ? 16 : 16 * 1024; +} +module.exports = { + getHighWaterMark: getHighWaterMark +}; \ No newline at end of file diff --git a/tests/corpus/readable-stream-3/node_modules/readable-stream/lib/internal/streams/stream-browser.js b/tests/corpus/readable-stream-3/node_modules/readable-stream/lib/internal/streams/stream-browser.js new file mode 100644 index 000000000..9332a3fda --- /dev/null +++ b/tests/corpus/readable-stream-3/node_modules/readable-stream/lib/internal/streams/stream-browser.js @@ -0,0 +1 @@ +module.exports = require('events').EventEmitter; diff --git a/tests/corpus/readable-stream-3/node_modules/readable-stream/lib/internal/streams/stream.js b/tests/corpus/readable-stream-3/node_modules/readable-stream/lib/internal/streams/stream.js new file mode 100644 index 000000000..ce2ad5b6e --- /dev/null +++ b/tests/corpus/readable-stream-3/node_modules/readable-stream/lib/internal/streams/stream.js @@ -0,0 +1 @@ +module.exports = require('stream'); diff --git a/tests/corpus/readable-stream-3/node_modules/readable-stream/package.json b/tests/corpus/readable-stream-3/node_modules/readable-stream/package.json new file mode 100644 index 000000000..ade59e71a --- /dev/null +++ b/tests/corpus/readable-stream-3/node_modules/readable-stream/package.json @@ -0,0 +1,68 @@ +{ + "name": "readable-stream", + "version": "3.6.2", + "description": "Streams3, a user-land copy of the stream library from Node.js", + "main": "readable.js", + "engines": { + "node": ">= 6" + }, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "devDependencies": { + "@babel/cli": "^7.2.0", + "@babel/core": "^7.2.0", + "@babel/polyfill": "^7.0.0", + "@babel/preset-env": "^7.2.0", + "airtap": "0.0.9", + "assert": "^1.4.0", + "bl": "^2.0.0", + "deep-strict-equal": "^0.2.0", + "events.once": "^2.0.2", + "glob": "^7.1.2", + "gunzip-maybe": "^1.4.1", + "hyperquest": "^2.1.3", + "lolex": "^2.6.0", + "nyc": "^11.0.0", + "pump": "^3.0.0", + "rimraf": "^2.6.2", + "tap": "^12.0.0", + "tape": "^4.9.0", + "tar-fs": "^1.16.2", + "util-promisify": "^2.1.0" + }, + "scripts": { + "test": "tap -J --no-esm test/parallel/*.js test/ours/*.js", + "ci": "TAP=1 tap --no-esm test/parallel/*.js test/ours/*.js | tee test.tap", + "test-browsers": "airtap --sauce-connect --loopback airtap.local -- test/browser.js", + "test-browser-local": "airtap --open --local -- test/browser.js", + "cover": "nyc npm test", + "report": "nyc report --reporter=lcov", + "update-browser-errors": "babel -o errors-browser.js errors.js" + }, + "repository": { + "type": "git", + "url": "git://github.com/nodejs/readable-stream" + }, + "keywords": [ + "readable", + "stream", + "pipe" + ], + "browser": { + "util": false, + "worker_threads": false, + "./errors": "./errors-browser.js", + "./readable.js": "./readable-browser.js", + "./lib/internal/streams/from.js": "./lib/internal/streams/from-browser.js", + "./lib/internal/streams/stream.js": "./lib/internal/streams/stream-browser.js" + }, + "nyc": { + "include": [ + "lib/**.js" + ] + }, + "license": "MIT" +} diff --git a/tests/corpus/readable-stream-3/node_modules/readable-stream/readable-browser.js b/tests/corpus/readable-stream-3/node_modules/readable-stream/readable-browser.js new file mode 100644 index 000000000..adbf60de8 --- /dev/null +++ b/tests/corpus/readable-stream-3/node_modules/readable-stream/readable-browser.js @@ -0,0 +1,9 @@ +exports = module.exports = require('./lib/_stream_readable.js'); +exports.Stream = exports; +exports.Readable = exports; +exports.Writable = require('./lib/_stream_writable.js'); +exports.Duplex = require('./lib/_stream_duplex.js'); +exports.Transform = require('./lib/_stream_transform.js'); +exports.PassThrough = require('./lib/_stream_passthrough.js'); +exports.finished = require('./lib/internal/streams/end-of-stream.js'); +exports.pipeline = require('./lib/internal/streams/pipeline.js'); diff --git a/tests/corpus/readable-stream-3/node_modules/readable-stream/readable.js b/tests/corpus/readable-stream-3/node_modules/readable-stream/readable.js new file mode 100644 index 000000000..9e0ca120d --- /dev/null +++ b/tests/corpus/readable-stream-3/node_modules/readable-stream/readable.js @@ -0,0 +1,16 @@ +var Stream = require('stream'); +if (process.env.READABLE_STREAM === 'disable' && Stream) { + module.exports = Stream.Readable; + Object.assign(module.exports, Stream); + module.exports.Stream = Stream; +} else { + exports = module.exports = require('./lib/_stream_readable.js'); + exports.Stream = Stream || exports; + exports.Readable = exports; + exports.Writable = require('./lib/_stream_writable.js'); + exports.Duplex = require('./lib/_stream_duplex.js'); + exports.Transform = require('./lib/_stream_transform.js'); + exports.PassThrough = require('./lib/_stream_passthrough.js'); + exports.finished = require('./lib/internal/streams/end-of-stream.js'); + exports.pipeline = require('./lib/internal/streams/pipeline.js'); +} diff --git a/tests/corpus/readable-stream-3/node_modules/safe-buffer/LICENSE b/tests/corpus/readable-stream-3/node_modules/safe-buffer/LICENSE new file mode 100644 index 000000000..0c068ceec --- /dev/null +++ b/tests/corpus/readable-stream-3/node_modules/safe-buffer/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) Feross Aboukhadijeh + +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/readable-stream-3/node_modules/safe-buffer/README.md b/tests/corpus/readable-stream-3/node_modules/safe-buffer/README.md new file mode 100644 index 000000000..e9a81afd0 --- /dev/null +++ b/tests/corpus/readable-stream-3/node_modules/safe-buffer/README.md @@ -0,0 +1,584 @@ +# safe-buffer [![travis][travis-image]][travis-url] [![npm][npm-image]][npm-url] [![downloads][downloads-image]][downloads-url] [![javascript style guide][standard-image]][standard-url] + +[travis-image]: https://img.shields.io/travis/feross/safe-buffer/master.svg +[travis-url]: https://travis-ci.org/feross/safe-buffer +[npm-image]: https://img.shields.io/npm/v/safe-buffer.svg +[npm-url]: https://npmjs.org/package/safe-buffer +[downloads-image]: https://img.shields.io/npm/dm/safe-buffer.svg +[downloads-url]: https://npmjs.org/package/safe-buffer +[standard-image]: https://img.shields.io/badge/code_style-standard-brightgreen.svg +[standard-url]: https://standardjs.com + +#### Safer Node.js Buffer API + +**Use the new Node.js Buffer APIs (`Buffer.from`, `Buffer.alloc`, +`Buffer.allocUnsafe`, `Buffer.allocUnsafeSlow`) in all versions of Node.js.** + +**Uses the built-in implementation when available.** + +## install + +``` +npm install safe-buffer +``` + +## usage + +The goal of this package is to provide a safe replacement for the node.js `Buffer`. + +It's a drop-in replacement for `Buffer`. You can use it by adding one `require` line to +the top of your node.js modules: + +```js +var Buffer = require('safe-buffer').Buffer + +// Existing buffer code will continue to work without issues: + +new Buffer('hey', 'utf8') +new Buffer([1, 2, 3], 'utf8') +new Buffer(obj) +new Buffer(16) // create an uninitialized buffer (potentially unsafe) + +// But you can use these new explicit APIs to make clear what you want: + +Buffer.from('hey', 'utf8') // convert from many types to a Buffer +Buffer.alloc(16) // create a zero-filled buffer (safe) +Buffer.allocUnsafe(16) // create an uninitialized buffer (potentially unsafe) +``` + +## api + +### Class Method: Buffer.from(array) + + +* `array` {Array} + +Allocates a new `Buffer` using an `array` of octets. + +```js +const buf = Buffer.from([0x62,0x75,0x66,0x66,0x65,0x72]); + // creates a new Buffer containing ASCII bytes + // ['b','u','f','f','e','r'] +``` + +A `TypeError` will be thrown if `array` is not an `Array`. + +### Class Method: Buffer.from(arrayBuffer[, byteOffset[, length]]) + + +* `arrayBuffer` {ArrayBuffer} The `.buffer` property of a `TypedArray` or + a `new ArrayBuffer()` +* `byteOffset` {Number} Default: `0` +* `length` {Number} Default: `arrayBuffer.length - byteOffset` + +When passed a reference to the `.buffer` property of a `TypedArray` instance, +the newly created `Buffer` will share the same allocated memory as the +TypedArray. + +```js +const arr = new Uint16Array(2); +arr[0] = 5000; +arr[1] = 4000; + +const buf = Buffer.from(arr.buffer); // shares the memory with arr; + +console.log(buf); + // Prints: + +// changing the TypedArray changes the Buffer also +arr[1] = 6000; + +console.log(buf); + // Prints: +``` + +The optional `byteOffset` and `length` arguments specify a memory range within +the `arrayBuffer` that will be shared by the `Buffer`. + +```js +const ab = new ArrayBuffer(10); +const buf = Buffer.from(ab, 0, 2); +console.log(buf.length); + // Prints: 2 +``` + +A `TypeError` will be thrown if `arrayBuffer` is not an `ArrayBuffer`. + +### Class Method: Buffer.from(buffer) + + +* `buffer` {Buffer} + +Copies the passed `buffer` data onto a new `Buffer` instance. + +```js +const buf1 = Buffer.from('buffer'); +const buf2 = Buffer.from(buf1); + +buf1[0] = 0x61; +console.log(buf1.toString()); + // 'auffer' +console.log(buf2.toString()); + // 'buffer' (copy is not changed) +``` + +A `TypeError` will be thrown if `buffer` is not a `Buffer`. + +### Class Method: Buffer.from(str[, encoding]) + + +* `str` {String} String to encode. +* `encoding` {String} Encoding to use, Default: `'utf8'` + +Creates a new `Buffer` containing the given JavaScript string `str`. If +provided, the `encoding` parameter identifies the character encoding. +If not provided, `encoding` defaults to `'utf8'`. + +```js +const buf1 = Buffer.from('this is a tést'); +console.log(buf1.toString()); + // prints: this is a tést +console.log(buf1.toString('ascii')); + // prints: this is a tC)st + +const buf2 = Buffer.from('7468697320697320612074c3a97374', 'hex'); +console.log(buf2.toString()); + // prints: this is a tést +``` + +A `TypeError` will be thrown if `str` is not a string. + +### Class Method: Buffer.alloc(size[, fill[, encoding]]) + + +* `size` {Number} +* `fill` {Value} Default: `undefined` +* `encoding` {String} Default: `utf8` + +Allocates a new `Buffer` of `size` bytes. If `fill` is `undefined`, the +`Buffer` will be *zero-filled*. + +```js +const buf = Buffer.alloc(5); +console.log(buf); + // +``` + +The `size` must be less than or equal to the value of +`require('buffer').kMaxLength` (on 64-bit architectures, `kMaxLength` is +`(2^31)-1`). Otherwise, a [`RangeError`][] is thrown. A zero-length Buffer will +be created if a `size` less than or equal to 0 is specified. + +If `fill` is specified, the allocated `Buffer` will be initialized by calling +`buf.fill(fill)`. See [`buf.fill()`][] for more information. + +```js +const buf = Buffer.alloc(5, 'a'); +console.log(buf); + // +``` + +If both `fill` and `encoding` are specified, the allocated `Buffer` will be +initialized by calling `buf.fill(fill, encoding)`. For example: + +```js +const buf = Buffer.alloc(11, 'aGVsbG8gd29ybGQ=', 'base64'); +console.log(buf); + // +``` + +Calling `Buffer.alloc(size)` can be significantly slower than the alternative +`Buffer.allocUnsafe(size)` but ensures that the newly created `Buffer` instance +contents will *never contain sensitive data*. + +A `TypeError` will be thrown if `size` is not a number. + +### Class Method: Buffer.allocUnsafe(size) + + +* `size` {Number} + +Allocates a new *non-zero-filled* `Buffer` of `size` bytes. The `size` must +be less than or equal to the value of `require('buffer').kMaxLength` (on 64-bit +architectures, `kMaxLength` is `(2^31)-1`). Otherwise, a [`RangeError`][] is +thrown. A zero-length Buffer will be created if a `size` less than or equal to +0 is specified. + +The underlying memory for `Buffer` instances created in this way is *not +initialized*. The contents of the newly created `Buffer` are unknown and +*may contain sensitive data*. Use [`buf.fill(0)`][] to initialize such +`Buffer` instances to zeroes. + +```js +const buf = Buffer.allocUnsafe(5); +console.log(buf); + // + // (octets will be different, every time) +buf.fill(0); +console.log(buf); + // +``` + +A `TypeError` will be thrown if `size` is not a number. + +Note that the `Buffer` module pre-allocates an internal `Buffer` instance of +size `Buffer.poolSize` that is used as a pool for the fast allocation of new +`Buffer` instances created using `Buffer.allocUnsafe(size)` (and the deprecated +`new Buffer(size)` constructor) only when `size` is less than or equal to +`Buffer.poolSize >> 1` (floor of `Buffer.poolSize` divided by two). The default +value of `Buffer.poolSize` is `8192` but can be modified. + +Use of this pre-allocated internal memory pool is a key difference between +calling `Buffer.alloc(size, fill)` vs. `Buffer.allocUnsafe(size).fill(fill)`. +Specifically, `Buffer.alloc(size, fill)` will *never* use the internal Buffer +pool, while `Buffer.allocUnsafe(size).fill(fill)` *will* use the internal +Buffer pool if `size` is less than or equal to half `Buffer.poolSize`. The +difference is subtle but can be important when an application requires the +additional performance that `Buffer.allocUnsafe(size)` provides. + +### Class Method: Buffer.allocUnsafeSlow(size) + + +* `size` {Number} + +Allocates a new *non-zero-filled* and non-pooled `Buffer` of `size` bytes. The +`size` must be less than or equal to the value of +`require('buffer').kMaxLength` (on 64-bit architectures, `kMaxLength` is +`(2^31)-1`). Otherwise, a [`RangeError`][] is thrown. A zero-length Buffer will +be created if a `size` less than or equal to 0 is specified. + +The underlying memory for `Buffer` instances created in this way is *not +initialized*. The contents of the newly created `Buffer` are unknown and +*may contain sensitive data*. Use [`buf.fill(0)`][] to initialize such +`Buffer` instances to zeroes. + +When using `Buffer.allocUnsafe()` to allocate new `Buffer` instances, +allocations under 4KB are, by default, sliced from a single pre-allocated +`Buffer`. This allows applications to avoid the garbage collection overhead of +creating many individually allocated Buffers. This approach improves both +performance and memory usage by eliminating the need to track and cleanup as +many `Persistent` objects. + +However, in the case where a developer may need to retain a small chunk of +memory from a pool for an indeterminate amount of time, it may be appropriate +to create an un-pooled Buffer instance using `Buffer.allocUnsafeSlow()` then +copy out the relevant bits. + +```js +// need to keep around a few small chunks of memory +const store = []; + +socket.on('readable', () => { + const data = socket.read(); + // allocate for retained data + const sb = Buffer.allocUnsafeSlow(10); + // copy the data into the new allocation + data.copy(sb, 0, 0, 10); + store.push(sb); +}); +``` + +Use of `Buffer.allocUnsafeSlow()` should be used only as a last resort *after* +a developer has observed undue memory retention in their applications. + +A `TypeError` will be thrown if `size` is not a number. + +### All the Rest + +The rest of the `Buffer` API is exactly the same as in node.js. +[See the docs](https://nodejs.org/api/buffer.html). + + +## Related links + +- [Node.js issue: Buffer(number) is unsafe](https://github.com/nodejs/node/issues/4660) +- [Node.js Enhancement Proposal: Buffer.from/Buffer.alloc/Buffer.zalloc/Buffer() soft-deprecate](https://github.com/nodejs/node-eps/pull/4) + +## Why is `Buffer` unsafe? + +Today, the node.js `Buffer` constructor is overloaded to handle many different argument +types like `String`, `Array`, `Object`, `TypedArrayView` (`Uint8Array`, etc.), +`ArrayBuffer`, and also `Number`. + +The API is optimized for convenience: you can throw any type at it, and it will try to do +what you want. + +Because the Buffer constructor is so powerful, you often see code like this: + +```js +// Convert UTF-8 strings to hex +function toHex (str) { + return new Buffer(str).toString('hex') +} +``` + +***But what happens if `toHex` is called with a `Number` argument?*** + +### Remote Memory Disclosure + +If an attacker can make your program call the `Buffer` constructor with a `Number` +argument, then they can make it allocate uninitialized memory from the node.js process. +This could potentially disclose TLS private keys, user data, or database passwords. + +When the `Buffer` constructor is passed a `Number` argument, it returns an +**UNINITIALIZED** block of memory of the specified `size`. When you create a `Buffer` like +this, you **MUST** overwrite the contents before returning it to the user. + +From the [node.js docs](https://nodejs.org/api/buffer.html#buffer_new_buffer_size): + +> `new Buffer(size)` +> +> - `size` Number +> +> The underlying memory for `Buffer` instances created in this way is not initialized. +> **The contents of a newly created `Buffer` are unknown and could contain sensitive +> data.** Use `buf.fill(0)` to initialize a Buffer to zeroes. + +(Emphasis our own.) + +Whenever the programmer intended to create an uninitialized `Buffer` you often see code +like this: + +```js +var buf = new Buffer(16) + +// Immediately overwrite the uninitialized buffer with data from another buffer +for (var i = 0; i < buf.length; i++) { + buf[i] = otherBuf[i] +} +``` + + +### Would this ever be a problem in real code? + +Yes. It's surprisingly common to forget to check the type of your variables in a +dynamically-typed language like JavaScript. + +Usually the consequences of assuming the wrong type is that your program crashes with an +uncaught exception. But the failure mode for forgetting to check the type of arguments to +the `Buffer` constructor is more catastrophic. + +Here's an example of a vulnerable service that takes a JSON payload and converts it to +hex: + +```js +// Take a JSON payload {str: "some string"} and convert it to hex +var server = http.createServer(function (req, res) { + var data = '' + req.setEncoding('utf8') + req.on('data', function (chunk) { + data += chunk + }) + req.on('end', function () { + var body = JSON.parse(data) + res.end(new Buffer(body.str).toString('hex')) + }) +}) + +server.listen(8080) +``` + +In this example, an http client just has to send: + +```json +{ + "str": 1000 +} +``` + +and it will get back 1,000 bytes of uninitialized memory from the server. + +This is a very serious bug. It's similar in severity to the +[the Heartbleed bug](http://heartbleed.com/) that allowed disclosure of OpenSSL process +memory by remote attackers. + + +### Which real-world packages were vulnerable? + +#### [`bittorrent-dht`](https://www.npmjs.com/package/bittorrent-dht) + +[Mathias Buus](https://github.com/mafintosh) and I +([Feross Aboukhadijeh](http://feross.org/)) found this issue in one of our own packages, +[`bittorrent-dht`](https://www.npmjs.com/package/bittorrent-dht). The bug would allow +anyone on the internet to send a series of messages to a user of `bittorrent-dht` and get +them to reveal 20 bytes at a time of uninitialized memory from the node.js process. + +Here's +[the commit](https://github.com/feross/bittorrent-dht/commit/6c7da04025d5633699800a99ec3fbadf70ad35b8) +that fixed it. We released a new fixed version, created a +[Node Security Project disclosure](https://nodesecurity.io/advisories/68), and deprecated all +vulnerable versions on npm so users will get a warning to upgrade to a newer version. + +#### [`ws`](https://www.npmjs.com/package/ws) + +That got us wondering if there were other vulnerable packages. Sure enough, within a short +period of time, we found the same issue in [`ws`](https://www.npmjs.com/package/ws), the +most popular WebSocket implementation in node.js. + +If certain APIs were called with `Number` parameters instead of `String` or `Buffer` as +expected, then uninitialized server memory would be disclosed to the remote peer. + +These were the vulnerable methods: + +```js +socket.send(number) +socket.ping(number) +socket.pong(number) +``` + +Here's a vulnerable socket server with some echo functionality: + +```js +server.on('connection', function (socket) { + socket.on('message', function (message) { + message = JSON.parse(message) + if (message.type === 'echo') { + socket.send(message.data) // send back the user's message + } + }) +}) +``` + +`socket.send(number)` called on the server, will disclose server memory. + +Here's [the release](https://github.com/websockets/ws/releases/tag/1.0.1) where the issue +was fixed, with a more detailed explanation. Props to +[Arnout Kazemier](https://github.com/3rd-Eden) for the quick fix. Here's the +[Node Security Project disclosure](https://nodesecurity.io/advisories/67). + + +### What's the solution? + +It's important that node.js offers a fast way to get memory otherwise performance-critical +applications would needlessly get a lot slower. + +But we need a better way to *signal our intent* as programmers. **When we want +uninitialized memory, we should request it explicitly.** + +Sensitive functionality should not be packed into a developer-friendly API that loosely +accepts many different types. This type of API encourages the lazy practice of passing +variables in without checking the type very carefully. + +#### A new API: `Buffer.allocUnsafe(number)` + +The functionality of creating buffers with uninitialized memory should be part of another +API. We propose `Buffer.allocUnsafe(number)`. This way, it's not part of an API that +frequently gets user input of all sorts of different types passed into it. + +```js +var buf = Buffer.allocUnsafe(16) // careful, uninitialized memory! + +// Immediately overwrite the uninitialized buffer with data from another buffer +for (var i = 0; i < buf.length; i++) { + buf[i] = otherBuf[i] +} +``` + + +### How do we fix node.js core? + +We sent [a PR to node.js core](https://github.com/nodejs/node/pull/4514) (merged as +`semver-major`) which defends against one case: + +```js +var str = 16 +new Buffer(str, 'utf8') +``` + +In this situation, it's implied that the programmer intended the first argument to be a +string, since they passed an encoding as a second argument. Today, node.js will allocate +uninitialized memory in the case of `new Buffer(number, encoding)`, which is probably not +what the programmer intended. + +But this is only a partial solution, since if the programmer does `new Buffer(variable)` +(without an `encoding` parameter) there's no way to know what they intended. If `variable` +is sometimes a number, then uninitialized memory will sometimes be returned. + +### What's the real long-term fix? + +We could deprecate and remove `new Buffer(number)` and use `Buffer.allocUnsafe(number)` when +we need uninitialized memory. But that would break 1000s of packages. + +~~We believe the best solution is to:~~ + +~~1. Change `new Buffer(number)` to return safe, zeroed-out memory~~ + +~~2. Create a new API for creating uninitialized Buffers. We propose: `Buffer.allocUnsafe(number)`~~ + +#### Update + +We now support adding three new APIs: + +- `Buffer.from(value)` - convert from any type to a buffer +- `Buffer.alloc(size)` - create a zero-filled buffer +- `Buffer.allocUnsafe(size)` - create an uninitialized buffer with given size + +This solves the core problem that affected `ws` and `bittorrent-dht` which is +`Buffer(variable)` getting tricked into taking a number argument. + +This way, existing code continues working and the impact on the npm ecosystem will be +minimal. Over time, npm maintainers can migrate performance-critical code to use +`Buffer.allocUnsafe(number)` instead of `new Buffer(number)`. + + +### Conclusion + +We think there's a serious design issue with the `Buffer` API as it exists today. It +promotes insecure software by putting high-risk functionality into a convenient API +with friendly "developer ergonomics". + +This wasn't merely a theoretical exercise because we found the issue in some of the +most popular npm packages. + +Fortunately, there's an easy fix that can be applied today. Use `safe-buffer` in place of +`buffer`. + +```js +var Buffer = require('safe-buffer').Buffer +``` + +Eventually, we hope that node.js core can switch to this new, safer behavior. We believe +the impact on the ecosystem would be minimal since it's not a breaking change. +Well-maintained, popular packages would be updated to use `Buffer.alloc` quickly, while +older, insecure packages would magically become safe from this attack vector. + + +## links + +- [Node.js PR: buffer: throw if both length and enc are passed](https://github.com/nodejs/node/pull/4514) +- [Node Security Project disclosure for `ws`](https://nodesecurity.io/advisories/67) +- [Node Security Project disclosure for`bittorrent-dht`](https://nodesecurity.io/advisories/68) + + +## credit + +The original issues in `bittorrent-dht` +([disclosure](https://nodesecurity.io/advisories/68)) and +`ws` ([disclosure](https://nodesecurity.io/advisories/67)) were discovered by +[Mathias Buus](https://github.com/mafintosh) and +[Feross Aboukhadijeh](http://feross.org/). + +Thanks to [Adam Baldwin](https://github.com/evilpacket) for helping disclose these issues +and for his work running the [Node Security Project](https://nodesecurity.io/). + +Thanks to [John Hiesey](https://github.com/jhiesey) for proofreading this README and +auditing the code. + + +## license + +MIT. Copyright (C) [Feross Aboukhadijeh](http://feross.org) diff --git a/tests/corpus/readable-stream-3/node_modules/safe-buffer/index.d.ts b/tests/corpus/readable-stream-3/node_modules/safe-buffer/index.d.ts new file mode 100644 index 000000000..e9fed809a --- /dev/null +++ b/tests/corpus/readable-stream-3/node_modules/safe-buffer/index.d.ts @@ -0,0 +1,187 @@ +declare module "safe-buffer" { + export class Buffer { + length: number + write(string: string, offset?: number, length?: number, encoding?: string): number; + toString(encoding?: string, start?: number, end?: number): string; + toJSON(): { type: 'Buffer', data: any[] }; + equals(otherBuffer: Buffer): boolean; + compare(otherBuffer: Buffer, targetStart?: number, targetEnd?: number, sourceStart?: number, sourceEnd?: number): number; + copy(targetBuffer: Buffer, targetStart?: number, sourceStart?: number, sourceEnd?: number): number; + slice(start?: number, end?: number): Buffer; + writeUIntLE(value: number, offset: number, byteLength: number, noAssert?: boolean): number; + writeUIntBE(value: number, offset: number, byteLength: number, noAssert?: boolean): number; + writeIntLE(value: number, offset: number, byteLength: number, noAssert?: boolean): number; + writeIntBE(value: number, offset: number, byteLength: number, noAssert?: boolean): number; + readUIntLE(offset: number, byteLength: number, noAssert?: boolean): number; + readUIntBE(offset: number, byteLength: number, noAssert?: boolean): number; + readIntLE(offset: number, byteLength: number, noAssert?: boolean): number; + readIntBE(offset: number, byteLength: number, noAssert?: boolean): number; + readUInt8(offset: number, noAssert?: boolean): number; + readUInt16LE(offset: number, noAssert?: boolean): number; + readUInt16BE(offset: number, noAssert?: boolean): number; + readUInt32LE(offset: number, noAssert?: boolean): number; + readUInt32BE(offset: number, noAssert?: boolean): number; + readInt8(offset: number, noAssert?: boolean): number; + readInt16LE(offset: number, noAssert?: boolean): number; + readInt16BE(offset: number, noAssert?: boolean): number; + readInt32LE(offset: number, noAssert?: boolean): number; + readInt32BE(offset: number, noAssert?: boolean): number; + readFloatLE(offset: number, noAssert?: boolean): number; + readFloatBE(offset: number, noAssert?: boolean): number; + readDoubleLE(offset: number, noAssert?: boolean): number; + readDoubleBE(offset: number, noAssert?: boolean): number; + swap16(): Buffer; + swap32(): Buffer; + swap64(): Buffer; + writeUInt8(value: number, offset: number, noAssert?: boolean): number; + writeUInt16LE(value: number, offset: number, noAssert?: boolean): number; + writeUInt16BE(value: number, offset: number, noAssert?: boolean): number; + writeUInt32LE(value: number, offset: number, noAssert?: boolean): number; + writeUInt32BE(value: number, offset: number, noAssert?: boolean): number; + writeInt8(value: number, offset: number, noAssert?: boolean): number; + writeInt16LE(value: number, offset: number, noAssert?: boolean): number; + writeInt16BE(value: number, offset: number, noAssert?: boolean): number; + writeInt32LE(value: number, offset: number, noAssert?: boolean): number; + writeInt32BE(value: number, offset: number, noAssert?: boolean): number; + writeFloatLE(value: number, offset: number, noAssert?: boolean): number; + writeFloatBE(value: number, offset: number, noAssert?: boolean): number; + writeDoubleLE(value: number, offset: number, noAssert?: boolean): number; + writeDoubleBE(value: number, offset: number, noAssert?: boolean): number; + fill(value: any, offset?: number, end?: number): this; + indexOf(value: string | number | Buffer, byteOffset?: number, encoding?: string): number; + lastIndexOf(value: string | number | Buffer, byteOffset?: number, encoding?: string): number; + includes(value: string | number | Buffer, byteOffset?: number, encoding?: string): boolean; + + /** + * Allocates a new buffer containing the given {str}. + * + * @param str String to store in buffer. + * @param encoding encoding to use, optional. Default is 'utf8' + */ + constructor (str: string, encoding?: string); + /** + * Allocates a new buffer of {size} octets. + * + * @param size count of octets to allocate. + */ + constructor (size: number); + /** + * Allocates a new buffer containing the given {array} of octets. + * + * @param array The octets to store. + */ + constructor (array: Uint8Array); + /** + * Produces a Buffer backed by the same allocated memory as + * the given {ArrayBuffer}. + * + * + * @param arrayBuffer The ArrayBuffer with which to share memory. + */ + constructor (arrayBuffer: ArrayBuffer); + /** + * Allocates a new buffer containing the given {array} of octets. + * + * @param array The octets to store. + */ + constructor (array: any[]); + /** + * Copies the passed {buffer} data onto a new {Buffer} instance. + * + * @param buffer The buffer to copy. + */ + constructor (buffer: Buffer); + prototype: Buffer; + /** + * Allocates a new Buffer using an {array} of octets. + * + * @param array + */ + static from(array: any[]): Buffer; + /** + * When passed a reference to the .buffer property of a TypedArray instance, + * the newly created Buffer will share the same allocated memory as the TypedArray. + * The optional {byteOffset} and {length} arguments specify a memory range + * within the {arrayBuffer} that will be shared by the Buffer. + * + * @param arrayBuffer The .buffer property of a TypedArray or a new ArrayBuffer() + * @param byteOffset + * @param length + */ + static from(arrayBuffer: ArrayBuffer, byteOffset?: number, length?: number): Buffer; + /** + * Copies the passed {buffer} data onto a new Buffer instance. + * + * @param buffer + */ + static from(buffer: Buffer): Buffer; + /** + * Creates a new Buffer containing the given JavaScript string {str}. + * If provided, the {encoding} parameter identifies the character encoding. + * If not provided, {encoding} defaults to 'utf8'. + * + * @param str + */ + static from(str: string, encoding?: string): Buffer; + /** + * Returns true if {obj} is a Buffer + * + * @param obj object to test. + */ + static isBuffer(obj: any): obj is Buffer; + /** + * Returns true if {encoding} is a valid encoding argument. + * Valid string encodings in Node 0.12: 'ascii'|'utf8'|'utf16le'|'ucs2'(alias of 'utf16le')|'base64'|'binary'(deprecated)|'hex' + * + * @param encoding string to test. + */ + static isEncoding(encoding: string): boolean; + /** + * Gives the actual byte length of a string. encoding defaults to 'utf8'. + * This is not the same as String.prototype.length since that returns the number of characters in a string. + * + * @param string string to test. + * @param encoding encoding used to evaluate (defaults to 'utf8') + */ + static byteLength(string: string, encoding?: string): number; + /** + * Returns a buffer which is the result of concatenating all the buffers in the list together. + * + * If the list has no items, or if the totalLength is 0, then it returns a zero-length buffer. + * If the list has exactly one item, then the first item of the list is returned. + * If the list has more than one item, then a new Buffer is created. + * + * @param list An array of Buffer objects to concatenate + * @param totalLength Total length of the buffers when concatenated. + * If totalLength is not provided, it is read from the buffers in the list. However, this adds an additional loop to the function, so it is faster to provide the length explicitly. + */ + static concat(list: Buffer[], totalLength?: number): Buffer; + /** + * The same as buf1.compare(buf2). + */ + static compare(buf1: Buffer, buf2: Buffer): number; + /** + * Allocates a new buffer of {size} octets. + * + * @param size count of octets to allocate. + * @param fill if specified, buffer will be initialized by calling buf.fill(fill). + * If parameter is omitted, buffer will be filled with zeros. + * @param encoding encoding used for call to buf.fill while initalizing + */ + static alloc(size: number, fill?: string | Buffer | number, encoding?: string): Buffer; + /** + * Allocates a new buffer of {size} octets, leaving memory not initialized, so the contents + * of the newly created Buffer are unknown and may contain sensitive data. + * + * @param size count of octets to allocate + */ + static allocUnsafe(size: number): Buffer; + /** + * Allocates a new non-pooled buffer of {size} octets, leaving memory not initialized, so the contents + * of the newly created Buffer are unknown and may contain sensitive data. + * + * @param size count of octets to allocate + */ + static allocUnsafeSlow(size: number): Buffer; + } +} \ No newline at end of file diff --git a/tests/corpus/readable-stream-3/node_modules/safe-buffer/index.js b/tests/corpus/readable-stream-3/node_modules/safe-buffer/index.js new file mode 100644 index 000000000..f8d3ec988 --- /dev/null +++ b/tests/corpus/readable-stream-3/node_modules/safe-buffer/index.js @@ -0,0 +1,65 @@ +/*! safe-buffer. MIT License. Feross Aboukhadijeh */ +/* eslint-disable node/no-deprecated-api */ +var buffer = require('buffer') +var Buffer = buffer.Buffer + +// alternative to using Object.keys for old browsers +function copyProps (src, dst) { + for (var key in src) { + dst[key] = src[key] + } +} +if (Buffer.from && Buffer.alloc && Buffer.allocUnsafe && Buffer.allocUnsafeSlow) { + module.exports = buffer +} else { + // Copy properties from require('buffer') + copyProps(buffer, exports) + exports.Buffer = SafeBuffer +} + +function SafeBuffer (arg, encodingOrOffset, length) { + return Buffer(arg, encodingOrOffset, length) +} + +SafeBuffer.prototype = Object.create(Buffer.prototype) + +// Copy static methods from Buffer +copyProps(Buffer, SafeBuffer) + +SafeBuffer.from = function (arg, encodingOrOffset, length) { + if (typeof arg === 'number') { + throw new TypeError('Argument must not be a number') + } + return Buffer(arg, encodingOrOffset, length) +} + +SafeBuffer.alloc = function (size, fill, encoding) { + if (typeof size !== 'number') { + throw new TypeError('Argument must be a number') + } + var buf = Buffer(size) + if (fill !== undefined) { + if (typeof encoding === 'string') { + buf.fill(fill, encoding) + } else { + buf.fill(fill) + } + } else { + buf.fill(0) + } + return buf +} + +SafeBuffer.allocUnsafe = function (size) { + if (typeof size !== 'number') { + throw new TypeError('Argument must be a number') + } + return Buffer(size) +} + +SafeBuffer.allocUnsafeSlow = function (size) { + if (typeof size !== 'number') { + throw new TypeError('Argument must be a number') + } + return buffer.SlowBuffer(size) +} diff --git a/tests/corpus/readable-stream-3/node_modules/safe-buffer/package.json b/tests/corpus/readable-stream-3/node_modules/safe-buffer/package.json new file mode 100644 index 000000000..f2869e256 --- /dev/null +++ b/tests/corpus/readable-stream-3/node_modules/safe-buffer/package.json @@ -0,0 +1,51 @@ +{ + "name": "safe-buffer", + "description": "Safer Node.js Buffer API", + "version": "5.2.1", + "author": { + "name": "Feross Aboukhadijeh", + "email": "feross@feross.org", + "url": "https://feross.org" + }, + "bugs": { + "url": "https://github.com/feross/safe-buffer/issues" + }, + "devDependencies": { + "standard": "*", + "tape": "^5.0.0" + }, + "homepage": "https://github.com/feross/safe-buffer", + "keywords": [ + "buffer", + "buffer allocate", + "node security", + "safe", + "safe-buffer", + "security", + "uninitialized" + ], + "license": "MIT", + "main": "index.js", + "types": "index.d.ts", + "repository": { + "type": "git", + "url": "git://github.com/feross/safe-buffer.git" + }, + "scripts": { + "test": "standard && tape test/*.js" + }, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] +} diff --git a/tests/corpus/readable-stream-3/node_modules/string_decoder/LICENSE b/tests/corpus/readable-stream-3/node_modules/string_decoder/LICENSE new file mode 100644 index 000000000..778edb207 --- /dev/null +++ b/tests/corpus/readable-stream-3/node_modules/string_decoder/LICENSE @@ -0,0 +1,48 @@ +Node.js is licensed for use as follows: + +""" +Copyright Node.js contributors. 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. +""" + +This license applies to parts of Node.js originating from the +https://github.com/joyent/node repository: + +""" +Copyright Joyent, Inc. and other Node contributors. 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. +""" + diff --git a/tests/corpus/readable-stream-3/node_modules/string_decoder/README.md b/tests/corpus/readable-stream-3/node_modules/string_decoder/README.md new file mode 100644 index 000000000..5fd58315e --- /dev/null +++ b/tests/corpus/readable-stream-3/node_modules/string_decoder/README.md @@ -0,0 +1,47 @@ +# string_decoder + +***Node-core v8.9.4 string_decoder for userland*** + + +[![NPM](https://nodei.co/npm/string_decoder.png?downloads=true&downloadRank=true)](https://nodei.co/npm/string_decoder/) +[![NPM](https://nodei.co/npm-dl/string_decoder.png?&months=6&height=3)](https://nodei.co/npm/string_decoder/) + + +```bash +npm install --save string_decoder +``` + +***Node-core string_decoder for userland*** + +This package is a mirror of the string_decoder implementation in Node-core. + +Full documentation may be found on the [Node.js website](https://nodejs.org/dist/v8.9.4/docs/api/). + +As of version 1.0.0 **string_decoder** uses semantic versioning. + +## Previous versions + +Previous version numbers match the versions found in Node core, e.g. 0.10.24 matches Node 0.10.24, likewise 0.11.10 matches Node 0.11.10. + +## Update + +The *build/* directory contains a build script that will scrape the source from the [nodejs/node](https://github.com/nodejs/node) repo given a specific Node version. + +## Streams Working Group + +`string_decoder` is maintained by the Streams Working Group, which +oversees the development and maintenance of the Streams API within +Node.js. The responsibilities of the Streams Working Group include: + +* Addressing stream issues on the Node.js issue tracker. +* Authoring and editing stream documentation within the Node.js project. +* Reviewing changes to stream subclasses within the Node.js project. +* Redirecting changes to streams from the Node.js project to this + project. +* Assisting in the implementation of stream providers within Node.js. +* Recommending versions of `readable-stream` to be included in Node.js. +* Messaging about the future of streams to give the community advance + notice of changes. + +See [readable-stream](https://github.com/nodejs/readable-stream) for +more details. diff --git a/tests/corpus/readable-stream-3/node_modules/string_decoder/lib/string_decoder.js b/tests/corpus/readable-stream-3/node_modules/string_decoder/lib/string_decoder.js new file mode 100644 index 000000000..2e89e63f7 --- /dev/null +++ b/tests/corpus/readable-stream-3/node_modules/string_decoder/lib/string_decoder.js @@ -0,0 +1,296 @@ +// Copyright Joyent, Inc. and other Node 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. + +'use strict'; + +/**/ + +var Buffer = require('safe-buffer').Buffer; +/**/ + +var isEncoding = Buffer.isEncoding || function (encoding) { + encoding = '' + encoding; + switch (encoding && encoding.toLowerCase()) { + case 'hex':case 'utf8':case 'utf-8':case 'ascii':case 'binary':case 'base64':case 'ucs2':case 'ucs-2':case 'utf16le':case 'utf-16le':case 'raw': + return true; + default: + return false; + } +}; + +function _normalizeEncoding(enc) { + if (!enc) return 'utf8'; + var retried; + while (true) { + switch (enc) { + case 'utf8': + case 'utf-8': + return 'utf8'; + case 'ucs2': + case 'ucs-2': + case 'utf16le': + case 'utf-16le': + return 'utf16le'; + case 'latin1': + case 'binary': + return 'latin1'; + case 'base64': + case 'ascii': + case 'hex': + return enc; + default: + if (retried) return; // undefined + enc = ('' + enc).toLowerCase(); + retried = true; + } + } +}; + +// Do not cache `Buffer.isEncoding` when checking encoding names as some +// modules monkey-patch it to support additional encodings +function normalizeEncoding(enc) { + var nenc = _normalizeEncoding(enc); + if (typeof nenc !== 'string' && (Buffer.isEncoding === isEncoding || !isEncoding(enc))) throw new Error('Unknown encoding: ' + enc); + return nenc || enc; +} + +// StringDecoder provides an interface for efficiently splitting a series of +// buffers into a series of JS strings without breaking apart multi-byte +// characters. +exports.StringDecoder = StringDecoder; +function StringDecoder(encoding) { + this.encoding = normalizeEncoding(encoding); + var nb; + switch (this.encoding) { + case 'utf16le': + this.text = utf16Text; + this.end = utf16End; + nb = 4; + break; + case 'utf8': + this.fillLast = utf8FillLast; + nb = 4; + break; + case 'base64': + this.text = base64Text; + this.end = base64End; + nb = 3; + break; + default: + this.write = simpleWrite; + this.end = simpleEnd; + return; + } + this.lastNeed = 0; + this.lastTotal = 0; + this.lastChar = Buffer.allocUnsafe(nb); +} + +StringDecoder.prototype.write = function (buf) { + if (buf.length === 0) return ''; + var r; + var i; + if (this.lastNeed) { + r = this.fillLast(buf); + if (r === undefined) return ''; + i = this.lastNeed; + this.lastNeed = 0; + } else { + i = 0; + } + if (i < buf.length) return r ? r + this.text(buf, i) : this.text(buf, i); + return r || ''; +}; + +StringDecoder.prototype.end = utf8End; + +// Returns only complete characters in a Buffer +StringDecoder.prototype.text = utf8Text; + +// Attempts to complete a partial non-UTF-8 character using bytes from a Buffer +StringDecoder.prototype.fillLast = function (buf) { + if (this.lastNeed <= buf.length) { + buf.copy(this.lastChar, this.lastTotal - this.lastNeed, 0, this.lastNeed); + return this.lastChar.toString(this.encoding, 0, this.lastTotal); + } + buf.copy(this.lastChar, this.lastTotal - this.lastNeed, 0, buf.length); + this.lastNeed -= buf.length; +}; + +// Checks the type of a UTF-8 byte, whether it's ASCII, a leading byte, or a +// continuation byte. If an invalid byte is detected, -2 is returned. +function utf8CheckByte(byte) { + if (byte <= 0x7F) return 0;else if (byte >> 5 === 0x06) return 2;else if (byte >> 4 === 0x0E) return 3;else if (byte >> 3 === 0x1E) return 4; + return byte >> 6 === 0x02 ? -1 : -2; +} + +// Checks at most 3 bytes at the end of a Buffer in order to detect an +// incomplete multi-byte UTF-8 character. The total number of bytes (2, 3, or 4) +// needed to complete the UTF-8 character (if applicable) are returned. +function utf8CheckIncomplete(self, buf, i) { + var j = buf.length - 1; + if (j < i) return 0; + var nb = utf8CheckByte(buf[j]); + if (nb >= 0) { + if (nb > 0) self.lastNeed = nb - 1; + return nb; + } + if (--j < i || nb === -2) return 0; + nb = utf8CheckByte(buf[j]); + if (nb >= 0) { + if (nb > 0) self.lastNeed = nb - 2; + return nb; + } + if (--j < i || nb === -2) return 0; + nb = utf8CheckByte(buf[j]); + if (nb >= 0) { + if (nb > 0) { + if (nb === 2) nb = 0;else self.lastNeed = nb - 3; + } + return nb; + } + return 0; +} + +// Validates as many continuation bytes for a multi-byte UTF-8 character as +// needed or are available. If we see a non-continuation byte where we expect +// one, we "replace" the validated continuation bytes we've seen so far with +// a single UTF-8 replacement character ('\ufffd'), to match v8's UTF-8 decoding +// behavior. The continuation byte check is included three times in the case +// where all of the continuation bytes for a character exist in the same buffer. +// It is also done this way as a slight performance increase instead of using a +// loop. +function utf8CheckExtraBytes(self, buf, p) { + if ((buf[0] & 0xC0) !== 0x80) { + self.lastNeed = 0; + return '\ufffd'; + } + if (self.lastNeed > 1 && buf.length > 1) { + if ((buf[1] & 0xC0) !== 0x80) { + self.lastNeed = 1; + return '\ufffd'; + } + if (self.lastNeed > 2 && buf.length > 2) { + if ((buf[2] & 0xC0) !== 0x80) { + self.lastNeed = 2; + return '\ufffd'; + } + } + } +} + +// Attempts to complete a multi-byte UTF-8 character using bytes from a Buffer. +function utf8FillLast(buf) { + var p = this.lastTotal - this.lastNeed; + var r = utf8CheckExtraBytes(this, buf, p); + if (r !== undefined) return r; + if (this.lastNeed <= buf.length) { + buf.copy(this.lastChar, p, 0, this.lastNeed); + return this.lastChar.toString(this.encoding, 0, this.lastTotal); + } + buf.copy(this.lastChar, p, 0, buf.length); + this.lastNeed -= buf.length; +} + +// Returns all complete UTF-8 characters in a Buffer. If the Buffer ended on a +// partial character, the character's bytes are buffered until the required +// number of bytes are available. +function utf8Text(buf, i) { + var total = utf8CheckIncomplete(this, buf, i); + if (!this.lastNeed) return buf.toString('utf8', i); + this.lastTotal = total; + var end = buf.length - (total - this.lastNeed); + buf.copy(this.lastChar, 0, end); + return buf.toString('utf8', i, end); +} + +// For UTF-8, a replacement character is added when ending on a partial +// character. +function utf8End(buf) { + var r = buf && buf.length ? this.write(buf) : ''; + if (this.lastNeed) return r + '\ufffd'; + return r; +} + +// UTF-16LE typically needs two bytes per character, but even if we have an even +// number of bytes available, we need to check if we end on a leading/high +// surrogate. In that case, we need to wait for the next two bytes in order to +// decode the last character properly. +function utf16Text(buf, i) { + if ((buf.length - i) % 2 === 0) { + var r = buf.toString('utf16le', i); + if (r) { + var c = r.charCodeAt(r.length - 1); + if (c >= 0xD800 && c <= 0xDBFF) { + this.lastNeed = 2; + this.lastTotal = 4; + this.lastChar[0] = buf[buf.length - 2]; + this.lastChar[1] = buf[buf.length - 1]; + return r.slice(0, -1); + } + } + return r; + } + this.lastNeed = 1; + this.lastTotal = 2; + this.lastChar[0] = buf[buf.length - 1]; + return buf.toString('utf16le', i, buf.length - 1); +} + +// For UTF-16LE we do not explicitly append special replacement characters if we +// end on a partial character, we simply let v8 handle that. +function utf16End(buf) { + var r = buf && buf.length ? this.write(buf) : ''; + if (this.lastNeed) { + var end = this.lastTotal - this.lastNeed; + return r + this.lastChar.toString('utf16le', 0, end); + } + return r; +} + +function base64Text(buf, i) { + var n = (buf.length - i) % 3; + if (n === 0) return buf.toString('base64', i); + this.lastNeed = 3 - n; + this.lastTotal = 3; + if (n === 1) { + this.lastChar[0] = buf[buf.length - 1]; + } else { + this.lastChar[0] = buf[buf.length - 2]; + this.lastChar[1] = buf[buf.length - 1]; + } + return buf.toString('base64', i, buf.length - n); +} + +function base64End(buf) { + var r = buf && buf.length ? this.write(buf) : ''; + if (this.lastNeed) return r + this.lastChar.toString('base64', 0, 3 - this.lastNeed); + return r; +} + +// Pass bytes on through for single-byte encodings (e.g. ascii, latin1, hex) +function simpleWrite(buf) { + return buf.toString(this.encoding); +} + +function simpleEnd(buf) { + return buf && buf.length ? this.write(buf) : ''; +} \ No newline at end of file diff --git a/tests/corpus/readable-stream-3/node_modules/string_decoder/package.json b/tests/corpus/readable-stream-3/node_modules/string_decoder/package.json new file mode 100644 index 000000000..b2bb14116 --- /dev/null +++ b/tests/corpus/readable-stream-3/node_modules/string_decoder/package.json @@ -0,0 +1,34 @@ +{ + "name": "string_decoder", + "version": "1.3.0", + "description": "The string_decoder module from Node core", + "main": "lib/string_decoder.js", + "files": [ + "lib" + ], + "dependencies": { + "safe-buffer": "~5.2.0" + }, + "devDependencies": { + "babel-polyfill": "^6.23.0", + "core-util-is": "^1.0.2", + "inherits": "^2.0.3", + "tap": "~0.4.8" + }, + "scripts": { + "test": "tap test/parallel/*.js && node test/verify-dependencies", + "ci": "tap test/parallel/*.js test/ours/*.js --tap | tee test.tap && node test/verify-dependencies.js" + }, + "repository": { + "type": "git", + "url": "git://github.com/nodejs/string_decoder.git" + }, + "homepage": "https://github.com/nodejs/string_decoder", + "keywords": [ + "string", + "decoder", + "browser", + "browserify" + ], + "license": "MIT" +} diff --git a/tests/corpus/readable-stream-3/node_modules/util-deprecate/History.md b/tests/corpus/readable-stream-3/node_modules/util-deprecate/History.md new file mode 100644 index 000000000..acc867537 --- /dev/null +++ b/tests/corpus/readable-stream-3/node_modules/util-deprecate/History.md @@ -0,0 +1,16 @@ + +1.0.2 / 2015-10-07 +================== + + * use try/catch when checking `localStorage` (#3, @kumavis) + +1.0.1 / 2014-11-25 +================== + + * browser: use `console.warn()` for deprecation calls + * browser: more jsdocs + +1.0.0 / 2014-04-30 +================== + + * initial commit diff --git a/tests/corpus/readable-stream-3/node_modules/util-deprecate/LICENSE b/tests/corpus/readable-stream-3/node_modules/util-deprecate/LICENSE new file mode 100644 index 000000000..6a60e8c22 --- /dev/null +++ b/tests/corpus/readable-stream-3/node_modules/util-deprecate/LICENSE @@ -0,0 +1,24 @@ +(The MIT License) + +Copyright (c) 2014 Nathan Rajlich + +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/readable-stream-3/node_modules/util-deprecate/README.md b/tests/corpus/readable-stream-3/node_modules/util-deprecate/README.md new file mode 100644 index 000000000..75622fa7c --- /dev/null +++ b/tests/corpus/readable-stream-3/node_modules/util-deprecate/README.md @@ -0,0 +1,53 @@ +util-deprecate +============== +### The Node.js `util.deprecate()` function with browser support + +In Node.js, this module simply re-exports the `util.deprecate()` function. + +In the web browser (i.e. via browserify), a browser-specific implementation +of the `util.deprecate()` function is used. + + +## API + +A `deprecate()` function is the only thing exposed by this module. + +``` javascript +// setup: +exports.foo = deprecate(foo, 'foo() is deprecated, use bar() instead'); + + +// users see: +foo(); +// foo() is deprecated, use bar() instead +foo(); +foo(); +``` + + +## License + +(The MIT License) + +Copyright (c) 2014 Nathan Rajlich + +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/readable-stream-3/node_modules/util-deprecate/browser.js b/tests/corpus/readable-stream-3/node_modules/util-deprecate/browser.js new file mode 100644 index 000000000..549ae2f06 --- /dev/null +++ b/tests/corpus/readable-stream-3/node_modules/util-deprecate/browser.js @@ -0,0 +1,67 @@ + +/** + * Module exports. + */ + +module.exports = deprecate; + +/** + * Mark that a method should not be used. + * Returns a modified function which warns once by default. + * + * If `localStorage.noDeprecation = true` is set, then it is a no-op. + * + * If `localStorage.throwDeprecation = true` is set, then deprecated functions + * will throw an Error when invoked. + * + * If `localStorage.traceDeprecation = true` is set, then deprecated functions + * will invoke `console.trace()` instead of `console.error()`. + * + * @param {Function} fn - the function to deprecate + * @param {String} msg - the string to print to the console when `fn` is invoked + * @returns {Function} a new "deprecated" version of `fn` + * @api public + */ + +function deprecate (fn, msg) { + if (config('noDeprecation')) { + return fn; + } + + var warned = false; + function deprecated() { + if (!warned) { + if (config('throwDeprecation')) { + throw new Error(msg); + } else if (config('traceDeprecation')) { + console.trace(msg); + } else { + console.warn(msg); + } + warned = true; + } + return fn.apply(this, arguments); + } + + return deprecated; +} + +/** + * Checks `localStorage` for boolean values for the given `name`. + * + * @param {String} name + * @returns {Boolean} + * @api private + */ + +function config (name) { + // accessing global.localStorage can trigger a DOMException in sandboxed iframes + try { + if (!global.localStorage) return false; + } catch (_) { + return false; + } + var val = global.localStorage[name]; + if (null == val) return false; + return String(val).toLowerCase() === 'true'; +} diff --git a/tests/corpus/readable-stream-3/node_modules/util-deprecate/node.js b/tests/corpus/readable-stream-3/node_modules/util-deprecate/node.js new file mode 100644 index 000000000..5e6fcff5d --- /dev/null +++ b/tests/corpus/readable-stream-3/node_modules/util-deprecate/node.js @@ -0,0 +1,6 @@ + +/** + * For Node.js, simply re-export the core `util.deprecate` function. + */ + +module.exports = require('util').deprecate; diff --git a/tests/corpus/readable-stream-3/node_modules/util-deprecate/package.json b/tests/corpus/readable-stream-3/node_modules/util-deprecate/package.json new file mode 100644 index 000000000..2e79f89a9 --- /dev/null +++ b/tests/corpus/readable-stream-3/node_modules/util-deprecate/package.json @@ -0,0 +1,27 @@ +{ + "name": "util-deprecate", + "version": "1.0.2", + "description": "The Node.js `util.deprecate()` function with browser support", + "main": "node.js", + "browser": "browser.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "git://github.com/TooTallNate/util-deprecate.git" + }, + "keywords": [ + "util", + "deprecate", + "browserify", + "browser", + "node" + ], + "author": "Nathan Rajlich (http://n8.io/)", + "license": "MIT", + "bugs": { + "url": "https://github.com/TooTallNate/util-deprecate/issues" + }, + "homepage": "https://github.com/TooTallNate/util-deprecate" +} diff --git a/tests/corpus/readable-stream-disable/main.cjs b/tests/corpus/readable-stream-disable/main.cjs new file mode 100644 index 000000000..ba3f220cc --- /dev/null +++ b/tests/corpus/readable-stream-disable/main.cjs @@ -0,0 +1,49 @@ +// readable-stream 2.x compiles as Node's builtin stream module: the +// package's own READABLE_STREAM=disable behavior, which this program +// selects before its first require so Node takes the same branch. (Run +// without the variable, Node loads the package's JavaScript streams, whose +// output can differ: class identity, `instanceof` against the builtin +// classes, and error messages.) node_modules vendors readable-stream +// 2.3.8 and its dependencies, unmodified (MIT). +"use strict"; +process.env.READABLE_STREAM = "disable"; +var stream = require("stream"); +var rs = require("readable-stream"); +var Transform = require("readable-stream/transform"); +var PassThrough = require("readable-stream/passthrough"); +var Writable = require("readable-stream/writable"); + +console.log(rs.Readable === stream.Readable, rs.Writable === stream.Writable); +console.log(Transform === stream.Transform, PassThrough === stream.PassThrough); +console.log(Writable === stream.Writable, typeof rs.Stream); + +function shout(words) { + const source = new rs.Readable({ read: function () {} }); + const seen = []; + const sink = new Writable({ + write: function (chunk, encoding, callback) { + seen.push(String(chunk)); + callback(); + }, + }); + sink.on("finish", function () { + console.log("piped", seen.join("|")); + }); + source + .pipe( + new Transform({ + transform: function (chunk, encoding, callback) { + callback(null, String(chunk).toUpperCase()); + }, + }), + ) + .pipe(new PassThrough()) + .pipe(sink); + words.forEach(function (w) { + source.push(w); + }); + source.push(null); +} + +shout(["a", "bc", "def"]); +console.log("sync done"); diff --git a/tests/corpus/readable-stream-disable/node_modules/core-util-is/LICENSE b/tests/corpus/readable-stream-disable/node_modules/core-util-is/LICENSE new file mode 100644 index 000000000..d8d7f9437 --- /dev/null +++ b/tests/corpus/readable-stream-disable/node_modules/core-util-is/LICENSE @@ -0,0 +1,19 @@ +Copyright Node.js contributors. 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. diff --git a/tests/corpus/readable-stream-disable/node_modules/core-util-is/lib/util.js b/tests/corpus/readable-stream-disable/node_modules/core-util-is/lib/util.js new file mode 100644 index 000000000..6e5a20d7d --- /dev/null +++ b/tests/corpus/readable-stream-disable/node_modules/core-util-is/lib/util.js @@ -0,0 +1,107 @@ +// Copyright Joyent, Inc. and other Node 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. + +// NOTE: These type checking functions intentionally don't use `instanceof` +// because it is fragile and can be easily faked with `Object.create()`. + +function isArray(arg) { + if (Array.isArray) { + return Array.isArray(arg); + } + return objectToString(arg) === '[object Array]'; +} +exports.isArray = isArray; + +function isBoolean(arg) { + return typeof arg === 'boolean'; +} +exports.isBoolean = isBoolean; + +function isNull(arg) { + return arg === null; +} +exports.isNull = isNull; + +function isNullOrUndefined(arg) { + return arg == null; +} +exports.isNullOrUndefined = isNullOrUndefined; + +function isNumber(arg) { + return typeof arg === 'number'; +} +exports.isNumber = isNumber; + +function isString(arg) { + return typeof arg === 'string'; +} +exports.isString = isString; + +function isSymbol(arg) { + return typeof arg === 'symbol'; +} +exports.isSymbol = isSymbol; + +function isUndefined(arg) { + return arg === void 0; +} +exports.isUndefined = isUndefined; + +function isRegExp(re) { + return objectToString(re) === '[object RegExp]'; +} +exports.isRegExp = isRegExp; + +function isObject(arg) { + return typeof arg === 'object' && arg !== null; +} +exports.isObject = isObject; + +function isDate(d) { + return objectToString(d) === '[object Date]'; +} +exports.isDate = isDate; + +function isError(e) { + return (objectToString(e) === '[object Error]' || e instanceof Error); +} +exports.isError = isError; + +function isFunction(arg) { + return typeof arg === 'function'; +} +exports.isFunction = isFunction; + +function isPrimitive(arg) { + return arg === null || + typeof arg === 'boolean' || + typeof arg === 'number' || + typeof arg === 'string' || + typeof arg === 'symbol' || // ES6 symbol + typeof arg === 'undefined'; +} +exports.isPrimitive = isPrimitive; + +exports.isBuffer = require('buffer').Buffer.isBuffer; + +function objectToString(o) { + return Object.prototype.toString.call(o); +} diff --git a/tests/corpus/readable-stream-disable/node_modules/core-util-is/package.json b/tests/corpus/readable-stream-disable/node_modules/core-util-is/package.json new file mode 100644 index 000000000..b0c51f580 --- /dev/null +++ b/tests/corpus/readable-stream-disable/node_modules/core-util-is/package.json @@ -0,0 +1,38 @@ +{ + "name": "core-util-is", + "version": "1.0.3", + "description": "The `util.is*` functions introduced in Node v0.12.", + "main": "lib/util.js", + "files": [ + "lib" + ], + "repository": { + "type": "git", + "url": "git://github.com/isaacs/core-util-is" + }, + "keywords": [ + "util", + "isBuffer", + "isArray", + "isNumber", + "isString", + "isRegExp", + "isThis", + "isThat", + "polyfill" + ], + "author": "Isaac Z. Schlueter (http://blog.izs.me/)", + "license": "MIT", + "bugs": { + "url": "https://github.com/isaacs/core-util-is/issues" + }, + "scripts": { + "test": "tap test.js", + "preversion": "npm test", + "postversion": "npm publish", + "prepublishOnly": "git push origin --follow-tags" + }, + "devDependencies": { + "tap": "^15.0.9" + } +} diff --git a/tests/corpus/readable-stream-disable/node_modules/inherits/LICENSE b/tests/corpus/readable-stream-disable/node_modules/inherits/LICENSE new file mode 100644 index 000000000..dea3013d6 --- /dev/null +++ b/tests/corpus/readable-stream-disable/node_modules/inherits/LICENSE @@ -0,0 +1,16 @@ +The ISC License + +Copyright (c) Isaac Z. Schlueter + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. + diff --git a/tests/corpus/readable-stream-disable/node_modules/inherits/inherits.js b/tests/corpus/readable-stream-disable/node_modules/inherits/inherits.js new file mode 100644 index 000000000..f71f2d932 --- /dev/null +++ b/tests/corpus/readable-stream-disable/node_modules/inherits/inherits.js @@ -0,0 +1,9 @@ +try { + var util = require('util'); + /* istanbul ignore next */ + if (typeof util.inherits !== 'function') throw ''; + module.exports = util.inherits; +} catch (e) { + /* istanbul ignore next */ + module.exports = require('./inherits_browser.js'); +} diff --git a/tests/corpus/readable-stream-disable/node_modules/inherits/inherits_browser.js b/tests/corpus/readable-stream-disable/node_modules/inherits/inherits_browser.js new file mode 100644 index 000000000..86bbb3dc2 --- /dev/null +++ b/tests/corpus/readable-stream-disable/node_modules/inherits/inherits_browser.js @@ -0,0 +1,27 @@ +if (typeof Object.create === 'function') { + // implementation from standard node.js 'util' module + module.exports = function inherits(ctor, superCtor) { + if (superCtor) { + ctor.super_ = superCtor + ctor.prototype = Object.create(superCtor.prototype, { + constructor: { + value: ctor, + enumerable: false, + writable: true, + configurable: true + } + }) + } + }; +} else { + // old school shim for old browsers + module.exports = function inherits(ctor, superCtor) { + if (superCtor) { + ctor.super_ = superCtor + var TempCtor = function () {} + TempCtor.prototype = superCtor.prototype + ctor.prototype = new TempCtor() + ctor.prototype.constructor = ctor + } + } +} diff --git a/tests/corpus/readable-stream-disable/node_modules/inherits/package.json b/tests/corpus/readable-stream-disable/node_modules/inherits/package.json new file mode 100644 index 000000000..37b4366b8 --- /dev/null +++ b/tests/corpus/readable-stream-disable/node_modules/inherits/package.json @@ -0,0 +1,29 @@ +{ + "name": "inherits", + "description": "Browser-friendly inheritance fully compatible with standard node.js inherits()", + "version": "2.0.4", + "keywords": [ + "inheritance", + "class", + "klass", + "oop", + "object-oriented", + "inherits", + "browser", + "browserify" + ], + "main": "./inherits.js", + "browser": "./inherits_browser.js", + "repository": "git://github.com/isaacs/inherits", + "license": "ISC", + "scripts": { + "test": "tap" + }, + "devDependencies": { + "tap": "^14.2.4" + }, + "files": [ + "inherits.js", + "inherits_browser.js" + ] +} diff --git a/tests/corpus/readable-stream-disable/node_modules/isarray/index.js b/tests/corpus/readable-stream-disable/node_modules/isarray/index.js new file mode 100644 index 000000000..a57f63495 --- /dev/null +++ b/tests/corpus/readable-stream-disable/node_modules/isarray/index.js @@ -0,0 +1,5 @@ +var toString = {}.toString; + +module.exports = Array.isArray || function (arr) { + return toString.call(arr) == '[object Array]'; +}; diff --git a/tests/corpus/readable-stream-disable/node_modules/isarray/package.json b/tests/corpus/readable-stream-disable/node_modules/isarray/package.json new file mode 100644 index 000000000..1a4317a9c --- /dev/null +++ b/tests/corpus/readable-stream-disable/node_modules/isarray/package.json @@ -0,0 +1,45 @@ +{ + "name": "isarray", + "description": "Array#isArray for older browsers", + "version": "1.0.0", + "repository": { + "type": "git", + "url": "git://github.com/juliangruber/isarray.git" + }, + "homepage": "https://github.com/juliangruber/isarray", + "main": "index.js", + "dependencies": {}, + "devDependencies": { + "tape": "~2.13.4" + }, + "keywords": [ + "browser", + "isarray", + "array" + ], + "author": { + "name": "Julian Gruber", + "email": "mail@juliangruber.com", + "url": "http://juliangruber.com" + }, + "license": "MIT", + "testling": { + "files": "test.js", + "browsers": [ + "ie/8..latest", + "firefox/17..latest", + "firefox/nightly", + "chrome/22..latest", + "chrome/canary", + "opera/12..latest", + "opera/next", + "safari/5.1..latest", + "ipad/6.0..latest", + "iphone/6.0..latest", + "android-browser/4.2..latest" + ] + }, + "scripts": { + "test": "tape test.js" + } +} diff --git a/tests/corpus/readable-stream-disable/node_modules/process-nextick-args/index.js b/tests/corpus/readable-stream-disable/node_modules/process-nextick-args/index.js new file mode 100644 index 000000000..3eecf1148 --- /dev/null +++ b/tests/corpus/readable-stream-disable/node_modules/process-nextick-args/index.js @@ -0,0 +1,45 @@ +'use strict'; + +if (typeof process === 'undefined' || + !process.version || + process.version.indexOf('v0.') === 0 || + process.version.indexOf('v1.') === 0 && process.version.indexOf('v1.8.') !== 0) { + module.exports = { nextTick: nextTick }; +} else { + module.exports = process +} + +function nextTick(fn, arg1, arg2, arg3) { + if (typeof fn !== 'function') { + throw new TypeError('"callback" argument must be a function'); + } + var len = arguments.length; + var args, i; + switch (len) { + case 0: + case 1: + return process.nextTick(fn); + case 2: + return process.nextTick(function afterTickOne() { + fn.call(null, arg1); + }); + case 3: + return process.nextTick(function afterTickTwo() { + fn.call(null, arg1, arg2); + }); + case 4: + return process.nextTick(function afterTickThree() { + fn.call(null, arg1, arg2, arg3); + }); + default: + args = new Array(len - 1); + i = 0; + while (i < args.length) { + args[i++] = arguments[i]; + } + return process.nextTick(function afterTick() { + fn.apply(null, args); + }); + } +} + diff --git a/tests/corpus/readable-stream-disable/node_modules/process-nextick-args/license.md b/tests/corpus/readable-stream-disable/node_modules/process-nextick-args/license.md new file mode 100644 index 000000000..c67e3532b --- /dev/null +++ b/tests/corpus/readable-stream-disable/node_modules/process-nextick-args/license.md @@ -0,0 +1,19 @@ +# Copyright (c) 2015 Calvin Metcalf + +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/readable-stream-disable/node_modules/process-nextick-args/package.json b/tests/corpus/readable-stream-disable/node_modules/process-nextick-args/package.json new file mode 100644 index 000000000..6070b723f --- /dev/null +++ b/tests/corpus/readable-stream-disable/node_modules/process-nextick-args/package.json @@ -0,0 +1,25 @@ +{ + "name": "process-nextick-args", + "version": "2.0.1", + "description": "process.nextTick but always with args", + "main": "index.js", + "files": [ + "index.js" + ], + "scripts": { + "test": "node test.js" + }, + "repository": { + "type": "git", + "url": "https://github.com/calvinmetcalf/process-nextick-args.git" + }, + "author": "", + "license": "MIT", + "bugs": { + "url": "https://github.com/calvinmetcalf/process-nextick-args/issues" + }, + "homepage": "https://github.com/calvinmetcalf/process-nextick-args", + "devDependencies": { + "tap": "~0.2.6" + } +} diff --git a/tests/corpus/readable-stream-disable/node_modules/readable-stream/LICENSE b/tests/corpus/readable-stream-disable/node_modules/readable-stream/LICENSE new file mode 100644 index 000000000..2873b3b2e --- /dev/null +++ b/tests/corpus/readable-stream-disable/node_modules/readable-stream/LICENSE @@ -0,0 +1,47 @@ +Node.js is licensed for use as follows: + +""" +Copyright Node.js contributors. 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. +""" + +This license applies to parts of Node.js originating from the +https://github.com/joyent/node repository: + +""" +Copyright Joyent, Inc. and other Node contributors. 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. +""" diff --git a/tests/corpus/readable-stream-disable/node_modules/readable-stream/duplex-browser.js b/tests/corpus/readable-stream-disable/node_modules/readable-stream/duplex-browser.js new file mode 100644 index 000000000..f8b2db83d --- /dev/null +++ b/tests/corpus/readable-stream-disable/node_modules/readable-stream/duplex-browser.js @@ -0,0 +1 @@ +module.exports = require('./lib/_stream_duplex.js'); diff --git a/tests/corpus/readable-stream-disable/node_modules/readable-stream/duplex.js b/tests/corpus/readable-stream-disable/node_modules/readable-stream/duplex.js new file mode 100644 index 000000000..46924cbfd --- /dev/null +++ b/tests/corpus/readable-stream-disable/node_modules/readable-stream/duplex.js @@ -0,0 +1 @@ +module.exports = require('./readable').Duplex diff --git a/tests/corpus/readable-stream-disable/node_modules/readable-stream/lib/_stream_duplex.js b/tests/corpus/readable-stream-disable/node_modules/readable-stream/lib/_stream_duplex.js new file mode 100644 index 000000000..57003c32d --- /dev/null +++ b/tests/corpus/readable-stream-disable/node_modules/readable-stream/lib/_stream_duplex.js @@ -0,0 +1,131 @@ +// Copyright Joyent, Inc. and other Node 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. + +// a duplex stream is just a stream that is both readable and writable. +// Since JS doesn't have multiple prototypal inheritance, this class +// prototypally inherits from Readable, and then parasitically from +// Writable. + +'use strict'; + +/**/ + +var pna = require('process-nextick-args'); +/**/ + +/**/ +var objectKeys = Object.keys || function (obj) { + var keys = []; + for (var key in obj) { + keys.push(key); + }return keys; +}; +/**/ + +module.exports = Duplex; + +/**/ +var util = Object.create(require('core-util-is')); +util.inherits = require('inherits'); +/**/ + +var Readable = require('./_stream_readable'); +var Writable = require('./_stream_writable'); + +util.inherits(Duplex, Readable); + +{ + // avoid scope creep, the keys array can then be collected + var keys = objectKeys(Writable.prototype); + for (var v = 0; v < keys.length; v++) { + var method = keys[v]; + if (!Duplex.prototype[method]) Duplex.prototype[method] = Writable.prototype[method]; + } +} + +function Duplex(options) { + if (!(this instanceof Duplex)) return new Duplex(options); + + Readable.call(this, options); + Writable.call(this, options); + + if (options && options.readable === false) this.readable = false; + + if (options && options.writable === false) this.writable = false; + + this.allowHalfOpen = true; + if (options && options.allowHalfOpen === false) this.allowHalfOpen = false; + + this.once('end', onend); +} + +Object.defineProperty(Duplex.prototype, 'writableHighWaterMark', { + // making it explicit this property is not enumerable + // because otherwise some prototype manipulation in + // userland will fail + enumerable: false, + get: function () { + return this._writableState.highWaterMark; + } +}); + +// the no-half-open enforcer +function onend() { + // if we allow half-open state, or if the writable side ended, + // then we're ok. + if (this.allowHalfOpen || this._writableState.ended) return; + + // no more data can be written. + // But allow more writes to happen in this tick. + pna.nextTick(onEndNT, this); +} + +function onEndNT(self) { + self.end(); +} + +Object.defineProperty(Duplex.prototype, 'destroyed', { + get: function () { + if (this._readableState === undefined || this._writableState === undefined) { + return false; + } + return this._readableState.destroyed && this._writableState.destroyed; + }, + set: function (value) { + // we ignore the value if the stream + // has not been initialized yet + if (this._readableState === undefined || this._writableState === undefined) { + return; + } + + // backward compatibility, the user is explicitly + // managing destroyed + this._readableState.destroyed = value; + this._writableState.destroyed = value; + } +}); + +Duplex.prototype._destroy = function (err, cb) { + this.push(null); + this.end(); + + pna.nextTick(cb, err); +}; \ No newline at end of file diff --git a/tests/corpus/readable-stream-disable/node_modules/readable-stream/lib/_stream_passthrough.js b/tests/corpus/readable-stream-disable/node_modules/readable-stream/lib/_stream_passthrough.js new file mode 100644 index 000000000..612edb4d8 --- /dev/null +++ b/tests/corpus/readable-stream-disable/node_modules/readable-stream/lib/_stream_passthrough.js @@ -0,0 +1,47 @@ +// Copyright Joyent, Inc. and other Node 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. + +// a passthrough stream. +// basically just the most minimal sort of Transform stream. +// Every written chunk gets output as-is. + +'use strict'; + +module.exports = PassThrough; + +var Transform = require('./_stream_transform'); + +/**/ +var util = Object.create(require('core-util-is')); +util.inherits = require('inherits'); +/**/ + +util.inherits(PassThrough, Transform); + +function PassThrough(options) { + if (!(this instanceof PassThrough)) return new PassThrough(options); + + Transform.call(this, options); +} + +PassThrough.prototype._transform = function (chunk, encoding, cb) { + cb(null, chunk); +}; \ No newline at end of file diff --git a/tests/corpus/readable-stream-disable/node_modules/readable-stream/lib/_stream_readable.js b/tests/corpus/readable-stream-disable/node_modules/readable-stream/lib/_stream_readable.js new file mode 100644 index 000000000..3af95cb2d --- /dev/null +++ b/tests/corpus/readable-stream-disable/node_modules/readable-stream/lib/_stream_readable.js @@ -0,0 +1,1019 @@ +// Copyright Joyent, Inc. and other Node 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. + +'use strict'; + +/**/ + +var pna = require('process-nextick-args'); +/**/ + +module.exports = Readable; + +/**/ +var isArray = require('isarray'); +/**/ + +/**/ +var Duplex; +/**/ + +Readable.ReadableState = ReadableState; + +/**/ +var EE = require('events').EventEmitter; + +var EElistenerCount = function (emitter, type) { + return emitter.listeners(type).length; +}; +/**/ + +/**/ +var Stream = require('./internal/streams/stream'); +/**/ + +/**/ + +var Buffer = require('safe-buffer').Buffer; +var OurUint8Array = (typeof global !== 'undefined' ? global : typeof window !== 'undefined' ? window : typeof self !== 'undefined' ? self : {}).Uint8Array || function () {}; +function _uint8ArrayToBuffer(chunk) { + return Buffer.from(chunk); +} +function _isUint8Array(obj) { + return Buffer.isBuffer(obj) || obj instanceof OurUint8Array; +} + +/**/ + +/**/ +var util = Object.create(require('core-util-is')); +util.inherits = require('inherits'); +/**/ + +/**/ +var debugUtil = require('util'); +var debug = void 0; +if (debugUtil && debugUtil.debuglog) { + debug = debugUtil.debuglog('stream'); +} else { + debug = function () {}; +} +/**/ + +var BufferList = require('./internal/streams/BufferList'); +var destroyImpl = require('./internal/streams/destroy'); +var StringDecoder; + +util.inherits(Readable, Stream); + +var kProxyEvents = ['error', 'close', 'destroy', 'pause', 'resume']; + +function prependListener(emitter, event, fn) { + // Sadly this is not cacheable as some libraries bundle their own + // event emitter implementation with them. + if (typeof emitter.prependListener === 'function') return emitter.prependListener(event, fn); + + // This is a hack to make sure that our error handler is attached before any + // userland ones. NEVER DO THIS. This is here only because this code needs + // to continue to work with older versions of Node.js that do not include + // the prependListener() method. The goal is to eventually remove this hack. + if (!emitter._events || !emitter._events[event]) emitter.on(event, fn);else if (isArray(emitter._events[event])) emitter._events[event].unshift(fn);else emitter._events[event] = [fn, emitter._events[event]]; +} + +function ReadableState(options, stream) { + Duplex = Duplex || require('./_stream_duplex'); + + options = options || {}; + + // Duplex streams are both readable and writable, but share + // the same options object. + // However, some cases require setting options to different + // values for the readable and the writable sides of the duplex stream. + // These options can be provided separately as readableXXX and writableXXX. + var isDuplex = stream instanceof Duplex; + + // object stream flag. Used to make read(n) ignore n and to + // make all the buffer merging and length checks go away + this.objectMode = !!options.objectMode; + + if (isDuplex) this.objectMode = this.objectMode || !!options.readableObjectMode; + + // the point at which it stops calling _read() to fill the buffer + // Note: 0 is a valid value, means "don't call _read preemptively ever" + var hwm = options.highWaterMark; + var readableHwm = options.readableHighWaterMark; + var defaultHwm = this.objectMode ? 16 : 16 * 1024; + + if (hwm || hwm === 0) this.highWaterMark = hwm;else if (isDuplex && (readableHwm || readableHwm === 0)) this.highWaterMark = readableHwm;else this.highWaterMark = defaultHwm; + + // cast to ints. + this.highWaterMark = Math.floor(this.highWaterMark); + + // A linked list is used to store data chunks instead of an array because the + // linked list can remove elements from the beginning faster than + // array.shift() + this.buffer = new BufferList(); + this.length = 0; + this.pipes = null; + this.pipesCount = 0; + this.flowing = null; + this.ended = false; + this.endEmitted = false; + this.reading = false; + + // a flag to be able to tell if the event 'readable'/'data' is emitted + // immediately, or on a later tick. We set this to true at first, because + // any actions that shouldn't happen until "later" should generally also + // not happen before the first read call. + this.sync = true; + + // whenever we return null, then we set a flag to say + // that we're awaiting a 'readable' event emission. + this.needReadable = false; + this.emittedReadable = false; + this.readableListening = false; + this.resumeScheduled = false; + + // has it been destroyed + this.destroyed = false; + + // Crypto is kind of old and crusty. Historically, its default string + // encoding is 'binary' so we have to make this configurable. + // Everything else in the universe uses 'utf8', though. + this.defaultEncoding = options.defaultEncoding || 'utf8'; + + // the number of writers that are awaiting a drain event in .pipe()s + this.awaitDrain = 0; + + // if true, a maybeReadMore has been scheduled + this.readingMore = false; + + this.decoder = null; + this.encoding = null; + if (options.encoding) { + if (!StringDecoder) StringDecoder = require('string_decoder/').StringDecoder; + this.decoder = new StringDecoder(options.encoding); + this.encoding = options.encoding; + } +} + +function Readable(options) { + Duplex = Duplex || require('./_stream_duplex'); + + if (!(this instanceof Readable)) return new Readable(options); + + this._readableState = new ReadableState(options, this); + + // legacy + this.readable = true; + + if (options) { + if (typeof options.read === 'function') this._read = options.read; + + if (typeof options.destroy === 'function') this._destroy = options.destroy; + } + + Stream.call(this); +} + +Object.defineProperty(Readable.prototype, 'destroyed', { + get: function () { + if (this._readableState === undefined) { + return false; + } + return this._readableState.destroyed; + }, + set: function (value) { + // we ignore the value if the stream + // has not been initialized yet + if (!this._readableState) { + return; + } + + // backward compatibility, the user is explicitly + // managing destroyed + this._readableState.destroyed = value; + } +}); + +Readable.prototype.destroy = destroyImpl.destroy; +Readable.prototype._undestroy = destroyImpl.undestroy; +Readable.prototype._destroy = function (err, cb) { + this.push(null); + cb(err); +}; + +// Manually shove something into the read() buffer. +// This returns true if the highWaterMark has not been hit yet, +// similar to how Writable.write() returns true if you should +// write() some more. +Readable.prototype.push = function (chunk, encoding) { + var state = this._readableState; + var skipChunkCheck; + + if (!state.objectMode) { + if (typeof chunk === 'string') { + encoding = encoding || state.defaultEncoding; + if (encoding !== state.encoding) { + chunk = Buffer.from(chunk, encoding); + encoding = ''; + } + skipChunkCheck = true; + } + } else { + skipChunkCheck = true; + } + + return readableAddChunk(this, chunk, encoding, false, skipChunkCheck); +}; + +// Unshift should *always* be something directly out of read() +Readable.prototype.unshift = function (chunk) { + return readableAddChunk(this, chunk, null, true, false); +}; + +function readableAddChunk(stream, chunk, encoding, addToFront, skipChunkCheck) { + var state = stream._readableState; + if (chunk === null) { + state.reading = false; + onEofChunk(stream, state); + } else { + var er; + if (!skipChunkCheck) er = chunkInvalid(state, chunk); + if (er) { + stream.emit('error', er); + } else if (state.objectMode || chunk && chunk.length > 0) { + if (typeof chunk !== 'string' && !state.objectMode && Object.getPrototypeOf(chunk) !== Buffer.prototype) { + chunk = _uint8ArrayToBuffer(chunk); + } + + if (addToFront) { + if (state.endEmitted) stream.emit('error', new Error('stream.unshift() after end event'));else addChunk(stream, state, chunk, true); + } else if (state.ended) { + stream.emit('error', new Error('stream.push() after EOF')); + } else { + state.reading = false; + if (state.decoder && !encoding) { + chunk = state.decoder.write(chunk); + if (state.objectMode || chunk.length !== 0) addChunk(stream, state, chunk, false);else maybeReadMore(stream, state); + } else { + addChunk(stream, state, chunk, false); + } + } + } else if (!addToFront) { + state.reading = false; + } + } + + return needMoreData(state); +} + +function addChunk(stream, state, chunk, addToFront) { + if (state.flowing && state.length === 0 && !state.sync) { + stream.emit('data', chunk); + stream.read(0); + } else { + // update the buffer info. + state.length += state.objectMode ? 1 : chunk.length; + if (addToFront) state.buffer.unshift(chunk);else state.buffer.push(chunk); + + if (state.needReadable) emitReadable(stream); + } + maybeReadMore(stream, state); +} + +function chunkInvalid(state, chunk) { + var er; + if (!_isUint8Array(chunk) && typeof chunk !== 'string' && chunk !== undefined && !state.objectMode) { + er = new TypeError('Invalid non-string/buffer chunk'); + } + return er; +} + +// if it's past the high water mark, we can push in some more. +// Also, if we have no data yet, we can stand some +// more bytes. This is to work around cases where hwm=0, +// such as the repl. Also, if the push() triggered a +// readable event, and the user called read(largeNumber) such that +// needReadable was set, then we ought to push more, so that another +// 'readable' event will be triggered. +function needMoreData(state) { + return !state.ended && (state.needReadable || state.length < state.highWaterMark || state.length === 0); +} + +Readable.prototype.isPaused = function () { + return this._readableState.flowing === false; +}; + +// backwards compatibility. +Readable.prototype.setEncoding = function (enc) { + if (!StringDecoder) StringDecoder = require('string_decoder/').StringDecoder; + this._readableState.decoder = new StringDecoder(enc); + this._readableState.encoding = enc; + return this; +}; + +// Don't raise the hwm > 8MB +var MAX_HWM = 0x800000; +function computeNewHighWaterMark(n) { + if (n >= MAX_HWM) { + n = MAX_HWM; + } else { + // Get the next highest power of 2 to prevent increasing hwm excessively in + // tiny amounts + n--; + n |= n >>> 1; + n |= n >>> 2; + n |= n >>> 4; + n |= n >>> 8; + n |= n >>> 16; + n++; + } + return n; +} + +// This function is designed to be inlinable, so please take care when making +// changes to the function body. +function howMuchToRead(n, state) { + if (n <= 0 || state.length === 0 && state.ended) return 0; + if (state.objectMode) return 1; + if (n !== n) { + // Only flow one buffer at a time + if (state.flowing && state.length) return state.buffer.head.data.length;else return state.length; + } + // If we're asking for more than the current hwm, then raise the hwm. + if (n > state.highWaterMark) state.highWaterMark = computeNewHighWaterMark(n); + if (n <= state.length) return n; + // Don't have enough + if (!state.ended) { + state.needReadable = true; + return 0; + } + return state.length; +} + +// you can override either this method, or the async _read(n) below. +Readable.prototype.read = function (n) { + debug('read', n); + n = parseInt(n, 10); + var state = this._readableState; + var nOrig = n; + + if (n !== 0) state.emittedReadable = false; + + // if we're doing read(0) to trigger a readable event, but we + // already have a bunch of data in the buffer, then just trigger + // the 'readable' event and move on. + if (n === 0 && state.needReadable && (state.length >= state.highWaterMark || state.ended)) { + debug('read: emitReadable', state.length, state.ended); + if (state.length === 0 && state.ended) endReadable(this);else emitReadable(this); + return null; + } + + n = howMuchToRead(n, state); + + // if we've ended, and we're now clear, then finish it up. + if (n === 0 && state.ended) { + if (state.length === 0) endReadable(this); + return null; + } + + // All the actual chunk generation logic needs to be + // *below* the call to _read. The reason is that in certain + // synthetic stream cases, such as passthrough streams, _read + // may be a completely synchronous operation which may change + // the state of the read buffer, providing enough data when + // before there was *not* enough. + // + // So, the steps are: + // 1. Figure out what the state of things will be after we do + // a read from the buffer. + // + // 2. If that resulting state will trigger a _read, then call _read. + // Note that this may be asynchronous, or synchronous. Yes, it is + // deeply ugly to write APIs this way, but that still doesn't mean + // that the Readable class should behave improperly, as streams are + // designed to be sync/async agnostic. + // Take note if the _read call is sync or async (ie, if the read call + // has returned yet), so that we know whether or not it's safe to emit + // 'readable' etc. + // + // 3. Actually pull the requested chunks out of the buffer and return. + + // if we need a readable event, then we need to do some reading. + var doRead = state.needReadable; + debug('need readable', doRead); + + // if we currently have less than the highWaterMark, then also read some + if (state.length === 0 || state.length - n < state.highWaterMark) { + doRead = true; + debug('length less than watermark', doRead); + } + + // however, if we've ended, then there's no point, and if we're already + // reading, then it's unnecessary. + if (state.ended || state.reading) { + doRead = false; + debug('reading or ended', doRead); + } else if (doRead) { + debug('do read'); + state.reading = true; + state.sync = true; + // if the length is currently zero, then we *need* a readable event. + if (state.length === 0) state.needReadable = true; + // call internal read method + this._read(state.highWaterMark); + state.sync = false; + // If _read pushed data synchronously, then `reading` will be false, + // and we need to re-evaluate how much data we can return to the user. + if (!state.reading) n = howMuchToRead(nOrig, state); + } + + var ret; + if (n > 0) ret = fromList(n, state);else ret = null; + + if (ret === null) { + state.needReadable = true; + n = 0; + } else { + state.length -= n; + } + + if (state.length === 0) { + // If we have nothing in the buffer, then we want to know + // as soon as we *do* get something into the buffer. + if (!state.ended) state.needReadable = true; + + // If we tried to read() past the EOF, then emit end on the next tick. + if (nOrig !== n && state.ended) endReadable(this); + } + + if (ret !== null) this.emit('data', ret); + + return ret; +}; + +function onEofChunk(stream, state) { + if (state.ended) return; + if (state.decoder) { + var chunk = state.decoder.end(); + if (chunk && chunk.length) { + state.buffer.push(chunk); + state.length += state.objectMode ? 1 : chunk.length; + } + } + state.ended = true; + + // emit 'readable' now to make sure it gets picked up. + emitReadable(stream); +} + +// Don't emit readable right away in sync mode, because this can trigger +// another read() call => stack overflow. This way, it might trigger +// a nextTick recursion warning, but that's not so bad. +function emitReadable(stream) { + var state = stream._readableState; + state.needReadable = false; + if (!state.emittedReadable) { + debug('emitReadable', state.flowing); + state.emittedReadable = true; + if (state.sync) pna.nextTick(emitReadable_, stream);else emitReadable_(stream); + } +} + +function emitReadable_(stream) { + debug('emit readable'); + stream.emit('readable'); + flow(stream); +} + +// at this point, the user has presumably seen the 'readable' event, +// and called read() to consume some data. that may have triggered +// in turn another _read(n) call, in which case reading = true if +// it's in progress. +// However, if we're not ended, or reading, and the length < hwm, +// then go ahead and try to read some more preemptively. +function maybeReadMore(stream, state) { + if (!state.readingMore) { + state.readingMore = true; + pna.nextTick(maybeReadMore_, stream, state); + } +} + +function maybeReadMore_(stream, state) { + var len = state.length; + while (!state.reading && !state.flowing && !state.ended && state.length < state.highWaterMark) { + debug('maybeReadMore read 0'); + stream.read(0); + if (len === state.length) + // didn't get any data, stop spinning. + break;else len = state.length; + } + state.readingMore = false; +} + +// abstract method. to be overridden in specific implementation classes. +// call cb(er, data) where data is <= n in length. +// for virtual (non-string, non-buffer) streams, "length" is somewhat +// arbitrary, and perhaps not very meaningful. +Readable.prototype._read = function (n) { + this.emit('error', new Error('_read() is not implemented')); +}; + +Readable.prototype.pipe = function (dest, pipeOpts) { + var src = this; + var state = this._readableState; + + switch (state.pipesCount) { + case 0: + state.pipes = dest; + break; + case 1: + state.pipes = [state.pipes, dest]; + break; + default: + state.pipes.push(dest); + break; + } + state.pipesCount += 1; + debug('pipe count=%d opts=%j', state.pipesCount, pipeOpts); + + var doEnd = (!pipeOpts || pipeOpts.end !== false) && dest !== process.stdout && dest !== process.stderr; + + var endFn = doEnd ? onend : unpipe; + if (state.endEmitted) pna.nextTick(endFn);else src.once('end', endFn); + + dest.on('unpipe', onunpipe); + function onunpipe(readable, unpipeInfo) { + debug('onunpipe'); + if (readable === src) { + if (unpipeInfo && unpipeInfo.hasUnpiped === false) { + unpipeInfo.hasUnpiped = true; + cleanup(); + } + } + } + + function onend() { + debug('onend'); + dest.end(); + } + + // when the dest drains, it reduces the awaitDrain counter + // on the source. This would be more elegant with a .once() + // handler in flow(), but adding and removing repeatedly is + // too slow. + var ondrain = pipeOnDrain(src); + dest.on('drain', ondrain); + + var cleanedUp = false; + function cleanup() { + debug('cleanup'); + // cleanup event handlers once the pipe is broken + dest.removeListener('close', onclose); + dest.removeListener('finish', onfinish); + dest.removeListener('drain', ondrain); + dest.removeListener('error', onerror); + dest.removeListener('unpipe', onunpipe); + src.removeListener('end', onend); + src.removeListener('end', unpipe); + src.removeListener('data', ondata); + + cleanedUp = true; + + // if the reader is waiting for a drain event from this + // specific writer, then it would cause it to never start + // flowing again. + // So, if this is awaiting a drain, then we just call it now. + // If we don't know, then assume that we are waiting for one. + if (state.awaitDrain && (!dest._writableState || dest._writableState.needDrain)) ondrain(); + } + + // If the user pushes more data while we're writing to dest then we'll end up + // in ondata again. However, we only want to increase awaitDrain once because + // dest will only emit one 'drain' event for the multiple writes. + // => Introduce a guard on increasing awaitDrain. + var increasedAwaitDrain = false; + src.on('data', ondata); + function ondata(chunk) { + debug('ondata'); + increasedAwaitDrain = false; + var ret = dest.write(chunk); + if (false === ret && !increasedAwaitDrain) { + // If the user unpiped during `dest.write()`, it is possible + // to get stuck in a permanently paused state if that write + // also returned false. + // => Check whether `dest` is still a piping destination. + if ((state.pipesCount === 1 && state.pipes === dest || state.pipesCount > 1 && indexOf(state.pipes, dest) !== -1) && !cleanedUp) { + debug('false write response, pause', state.awaitDrain); + state.awaitDrain++; + increasedAwaitDrain = true; + } + src.pause(); + } + } + + // if the dest has an error, then stop piping into it. + // however, don't suppress the throwing behavior for this. + function onerror(er) { + debug('onerror', er); + unpipe(); + dest.removeListener('error', onerror); + if (EElistenerCount(dest, 'error') === 0) dest.emit('error', er); + } + + // Make sure our error handler is attached before userland ones. + prependListener(dest, 'error', onerror); + + // Both close and finish should trigger unpipe, but only once. + function onclose() { + dest.removeListener('finish', onfinish); + unpipe(); + } + dest.once('close', onclose); + function onfinish() { + debug('onfinish'); + dest.removeListener('close', onclose); + unpipe(); + } + dest.once('finish', onfinish); + + function unpipe() { + debug('unpipe'); + src.unpipe(dest); + } + + // tell the dest that it's being piped to + dest.emit('pipe', src); + + // start the flow if it hasn't been started already. + if (!state.flowing) { + debug('pipe resume'); + src.resume(); + } + + return dest; +}; + +function pipeOnDrain(src) { + return function () { + var state = src._readableState; + debug('pipeOnDrain', state.awaitDrain); + if (state.awaitDrain) state.awaitDrain--; + if (state.awaitDrain === 0 && EElistenerCount(src, 'data')) { + state.flowing = true; + flow(src); + } + }; +} + +Readable.prototype.unpipe = function (dest) { + var state = this._readableState; + var unpipeInfo = { hasUnpiped: false }; + + // if we're not piping anywhere, then do nothing. + if (state.pipesCount === 0) return this; + + // just one destination. most common case. + if (state.pipesCount === 1) { + // passed in one, but it's not the right one. + if (dest && dest !== state.pipes) return this; + + if (!dest) dest = state.pipes; + + // got a match. + state.pipes = null; + state.pipesCount = 0; + state.flowing = false; + if (dest) dest.emit('unpipe', this, unpipeInfo); + return this; + } + + // slow case. multiple pipe destinations. + + if (!dest) { + // remove all. + var dests = state.pipes; + var len = state.pipesCount; + state.pipes = null; + state.pipesCount = 0; + state.flowing = false; + + for (var i = 0; i < len; i++) { + dests[i].emit('unpipe', this, { hasUnpiped: false }); + }return this; + } + + // try to find the right one. + var index = indexOf(state.pipes, dest); + if (index === -1) return this; + + state.pipes.splice(index, 1); + state.pipesCount -= 1; + if (state.pipesCount === 1) state.pipes = state.pipes[0]; + + dest.emit('unpipe', this, unpipeInfo); + + return this; +}; + +// set up data events if they are asked for +// Ensure readable listeners eventually get something +Readable.prototype.on = function (ev, fn) { + var res = Stream.prototype.on.call(this, ev, fn); + + if (ev === 'data') { + // Start flowing on next tick if stream isn't explicitly paused + if (this._readableState.flowing !== false) this.resume(); + } else if (ev === 'readable') { + var state = this._readableState; + if (!state.endEmitted && !state.readableListening) { + state.readableListening = state.needReadable = true; + state.emittedReadable = false; + if (!state.reading) { + pna.nextTick(nReadingNextTick, this); + } else if (state.length) { + emitReadable(this); + } + } + } + + return res; +}; +Readable.prototype.addListener = Readable.prototype.on; + +function nReadingNextTick(self) { + debug('readable nexttick read 0'); + self.read(0); +} + +// pause() and resume() are remnants of the legacy readable stream API +// If the user uses them, then switch into old mode. +Readable.prototype.resume = function () { + var state = this._readableState; + if (!state.flowing) { + debug('resume'); + state.flowing = true; + resume(this, state); + } + return this; +}; + +function resume(stream, state) { + if (!state.resumeScheduled) { + state.resumeScheduled = true; + pna.nextTick(resume_, stream, state); + } +} + +function resume_(stream, state) { + if (!state.reading) { + debug('resume read 0'); + stream.read(0); + } + + state.resumeScheduled = false; + state.awaitDrain = 0; + stream.emit('resume'); + flow(stream); + if (state.flowing && !state.reading) stream.read(0); +} + +Readable.prototype.pause = function () { + debug('call pause flowing=%j', this._readableState.flowing); + if (false !== this._readableState.flowing) { + debug('pause'); + this._readableState.flowing = false; + this.emit('pause'); + } + return this; +}; + +function flow(stream) { + var state = stream._readableState; + debug('flow', state.flowing); + while (state.flowing && stream.read() !== null) {} +} + +// wrap an old-style stream as the async data source. +// This is *not* part of the readable stream interface. +// It is an ugly unfortunate mess of history. +Readable.prototype.wrap = function (stream) { + var _this = this; + + var state = this._readableState; + var paused = false; + + stream.on('end', function () { + debug('wrapped end'); + if (state.decoder && !state.ended) { + var chunk = state.decoder.end(); + if (chunk && chunk.length) _this.push(chunk); + } + + _this.push(null); + }); + + stream.on('data', function (chunk) { + debug('wrapped data'); + if (state.decoder) chunk = state.decoder.write(chunk); + + // don't skip over falsy values in objectMode + if (state.objectMode && (chunk === null || chunk === undefined)) return;else if (!state.objectMode && (!chunk || !chunk.length)) return; + + var ret = _this.push(chunk); + if (!ret) { + paused = true; + stream.pause(); + } + }); + + // proxy all the other methods. + // important when wrapping filters and duplexes. + for (var i in stream) { + if (this[i] === undefined && typeof stream[i] === 'function') { + this[i] = function (method) { + return function () { + return stream[method].apply(stream, arguments); + }; + }(i); + } + } + + // proxy certain important events. + for (var n = 0; n < kProxyEvents.length; n++) { + stream.on(kProxyEvents[n], this.emit.bind(this, kProxyEvents[n])); + } + + // when we try to consume some more bytes, simply unpause the + // underlying stream. + this._read = function (n) { + debug('wrapped _read', n); + if (paused) { + paused = false; + stream.resume(); + } + }; + + return this; +}; + +Object.defineProperty(Readable.prototype, 'readableHighWaterMark', { + // making it explicit this property is not enumerable + // because otherwise some prototype manipulation in + // userland will fail + enumerable: false, + get: function () { + return this._readableState.highWaterMark; + } +}); + +// exposed for testing purposes only. +Readable._fromList = fromList; + +// Pluck off n bytes from an array of buffers. +// Length is the combined lengths of all the buffers in the list. +// This function is designed to be inlinable, so please take care when making +// changes to the function body. +function fromList(n, state) { + // nothing buffered + if (state.length === 0) return null; + + var ret; + if (state.objectMode) ret = state.buffer.shift();else if (!n || n >= state.length) { + // read it all, truncate the list + if (state.decoder) ret = state.buffer.join('');else if (state.buffer.length === 1) ret = state.buffer.head.data;else ret = state.buffer.concat(state.length); + state.buffer.clear(); + } else { + // read part of list + ret = fromListPartial(n, state.buffer, state.decoder); + } + + return ret; +} + +// Extracts only enough buffered data to satisfy the amount requested. +// This function is designed to be inlinable, so please take care when making +// changes to the function body. +function fromListPartial(n, list, hasStrings) { + var ret; + if (n < list.head.data.length) { + // slice is the same for buffers and strings + ret = list.head.data.slice(0, n); + list.head.data = list.head.data.slice(n); + } else if (n === list.head.data.length) { + // first chunk is a perfect match + ret = list.shift(); + } else { + // result spans more than one buffer + ret = hasStrings ? copyFromBufferString(n, list) : copyFromBuffer(n, list); + } + return ret; +} + +// Copies a specified amount of characters from the list of buffered data +// chunks. +// This function is designed to be inlinable, so please take care when making +// changes to the function body. +function copyFromBufferString(n, list) { + var p = list.head; + var c = 1; + var ret = p.data; + n -= ret.length; + while (p = p.next) { + var str = p.data; + var nb = n > str.length ? str.length : n; + if (nb === str.length) ret += str;else ret += str.slice(0, n); + n -= nb; + if (n === 0) { + if (nb === str.length) { + ++c; + if (p.next) list.head = p.next;else list.head = list.tail = null; + } else { + list.head = p; + p.data = str.slice(nb); + } + break; + } + ++c; + } + list.length -= c; + return ret; +} + +// Copies a specified amount of bytes from the list of buffered data chunks. +// This function is designed to be inlinable, so please take care when making +// changes to the function body. +function copyFromBuffer(n, list) { + var ret = Buffer.allocUnsafe(n); + var p = list.head; + var c = 1; + p.data.copy(ret); + n -= p.data.length; + while (p = p.next) { + var buf = p.data; + var nb = n > buf.length ? buf.length : n; + buf.copy(ret, ret.length - n, 0, nb); + n -= nb; + if (n === 0) { + if (nb === buf.length) { + ++c; + if (p.next) list.head = p.next;else list.head = list.tail = null; + } else { + list.head = p; + p.data = buf.slice(nb); + } + break; + } + ++c; + } + list.length -= c; + return ret; +} + +function endReadable(stream) { + var state = stream._readableState; + + // If we get here before consuming all the bytes, then that is a + // bug in node. Should never happen. + if (state.length > 0) throw new Error('"endReadable()" called on non-empty stream'); + + if (!state.endEmitted) { + state.ended = true; + pna.nextTick(endReadableNT, state, stream); + } +} + +function endReadableNT(state, stream) { + // Check that we didn't get one last unshift. + if (!state.endEmitted && state.length === 0) { + state.endEmitted = true; + stream.readable = false; + stream.emit('end'); + } +} + +function indexOf(xs, x) { + for (var i = 0, l = xs.length; i < l; i++) { + if (xs[i] === x) return i; + } + return -1; +} \ No newline at end of file diff --git a/tests/corpus/readable-stream-disable/node_modules/readable-stream/lib/_stream_transform.js b/tests/corpus/readable-stream-disable/node_modules/readable-stream/lib/_stream_transform.js new file mode 100644 index 000000000..fcfc105af --- /dev/null +++ b/tests/corpus/readable-stream-disable/node_modules/readable-stream/lib/_stream_transform.js @@ -0,0 +1,214 @@ +// Copyright Joyent, Inc. and other Node 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. + +// a transform stream is a readable/writable stream where you do +// something with the data. Sometimes it's called a "filter", +// but that's not a great name for it, since that implies a thing where +// some bits pass through, and others are simply ignored. (That would +// be a valid example of a transform, of course.) +// +// While the output is causally related to the input, it's not a +// necessarily symmetric or synchronous transformation. For example, +// a zlib stream might take multiple plain-text writes(), and then +// emit a single compressed chunk some time in the future. +// +// Here's how this works: +// +// The Transform stream has all the aspects of the readable and writable +// stream classes. When you write(chunk), that calls _write(chunk,cb) +// internally, and returns false if there's a lot of pending writes +// buffered up. When you call read(), that calls _read(n) until +// there's enough pending readable data buffered up. +// +// In a transform stream, the written data is placed in a buffer. When +// _read(n) is called, it transforms the queued up data, calling the +// buffered _write cb's as it consumes chunks. If consuming a single +// written chunk would result in multiple output chunks, then the first +// outputted bit calls the readcb, and subsequent chunks just go into +// the read buffer, and will cause it to emit 'readable' if necessary. +// +// This way, back-pressure is actually determined by the reading side, +// since _read has to be called to start processing a new chunk. However, +// a pathological inflate type of transform can cause excessive buffering +// here. For example, imagine a stream where every byte of input is +// interpreted as an integer from 0-255, and then results in that many +// bytes of output. Writing the 4 bytes {ff,ff,ff,ff} would result in +// 1kb of data being output. In this case, you could write a very small +// amount of input, and end up with a very large amount of output. In +// such a pathological inflating mechanism, there'd be no way to tell +// the system to stop doing the transform. A single 4MB write could +// cause the system to run out of memory. +// +// However, even in such a pathological case, only a single written chunk +// would be consumed, and then the rest would wait (un-transformed) until +// the results of the previous transformed chunk were consumed. + +'use strict'; + +module.exports = Transform; + +var Duplex = require('./_stream_duplex'); + +/**/ +var util = Object.create(require('core-util-is')); +util.inherits = require('inherits'); +/**/ + +util.inherits(Transform, Duplex); + +function afterTransform(er, data) { + var ts = this._transformState; + ts.transforming = false; + + var cb = ts.writecb; + + if (!cb) { + return this.emit('error', new Error('write callback called multiple times')); + } + + ts.writechunk = null; + ts.writecb = null; + + if (data != null) // single equals check for both `null` and `undefined` + this.push(data); + + cb(er); + + var rs = this._readableState; + rs.reading = false; + if (rs.needReadable || rs.length < rs.highWaterMark) { + this._read(rs.highWaterMark); + } +} + +function Transform(options) { + if (!(this instanceof Transform)) return new Transform(options); + + Duplex.call(this, options); + + this._transformState = { + afterTransform: afterTransform.bind(this), + needTransform: false, + transforming: false, + writecb: null, + writechunk: null, + writeencoding: null + }; + + // start out asking for a readable event once data is transformed. + this._readableState.needReadable = true; + + // we have implemented the _read method, and done the other things + // that Readable wants before the first _read call, so unset the + // sync guard flag. + this._readableState.sync = false; + + if (options) { + if (typeof options.transform === 'function') this._transform = options.transform; + + if (typeof options.flush === 'function') this._flush = options.flush; + } + + // When the writable side finishes, then flush out anything remaining. + this.on('prefinish', prefinish); +} + +function prefinish() { + var _this = this; + + if (typeof this._flush === 'function') { + this._flush(function (er, data) { + done(_this, er, data); + }); + } else { + done(this, null, null); + } +} + +Transform.prototype.push = function (chunk, encoding) { + this._transformState.needTransform = false; + return Duplex.prototype.push.call(this, chunk, encoding); +}; + +// This is the part where you do stuff! +// override this function in implementation classes. +// 'chunk' is an input chunk. +// +// Call `push(newChunk)` to pass along transformed output +// to the readable side. You may call 'push' zero or more times. +// +// Call `cb(err)` when you are done with this chunk. If you pass +// an error, then that'll put the hurt on the whole operation. If you +// never call cb(), then you'll never get another chunk. +Transform.prototype._transform = function (chunk, encoding, cb) { + throw new Error('_transform() is not implemented'); +}; + +Transform.prototype._write = function (chunk, encoding, cb) { + var ts = this._transformState; + ts.writecb = cb; + ts.writechunk = chunk; + ts.writeencoding = encoding; + if (!ts.transforming) { + var rs = this._readableState; + if (ts.needTransform || rs.needReadable || rs.length < rs.highWaterMark) this._read(rs.highWaterMark); + } +}; + +// Doesn't matter what the args are here. +// _transform does all the work. +// That we got here means that the readable side wants more data. +Transform.prototype._read = function (n) { + var ts = this._transformState; + + if (ts.writechunk !== null && ts.writecb && !ts.transforming) { + ts.transforming = true; + this._transform(ts.writechunk, ts.writeencoding, ts.afterTransform); + } else { + // mark that we need a transform, so that any data that comes in + // will get processed, now that we've asked for it. + ts.needTransform = true; + } +}; + +Transform.prototype._destroy = function (err, cb) { + var _this2 = this; + + Duplex.prototype._destroy.call(this, err, function (err2) { + cb(err2); + _this2.emit('close'); + }); +}; + +function done(stream, er, data) { + if (er) return stream.emit('error', er); + + if (data != null) // single equals check for both `null` and `undefined` + stream.push(data); + + // if there's nothing in the write buffer, then that means + // that nothing more will ever be provided + if (stream._writableState.length) throw new Error('Calling transform done when ws.length != 0'); + + if (stream._transformState.transforming) throw new Error('Calling transform done when still transforming'); + + return stream.push(null); +} \ No newline at end of file diff --git a/tests/corpus/readable-stream-disable/node_modules/readable-stream/lib/_stream_writable.js b/tests/corpus/readable-stream-disable/node_modules/readable-stream/lib/_stream_writable.js new file mode 100644 index 000000000..e1e897ff3 --- /dev/null +++ b/tests/corpus/readable-stream-disable/node_modules/readable-stream/lib/_stream_writable.js @@ -0,0 +1,685 @@ +// Copyright Joyent, Inc. and other Node 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. + +// A bit simpler than readable streams. +// Implement an async ._write(chunk, encoding, cb), and it'll handle all +// the drain event emission and buffering. + +'use strict'; + +/**/ + +var pna = require('process-nextick-args'); +/**/ + +module.exports = Writable; + +/* */ +function WriteReq(chunk, encoding, cb) { + this.chunk = chunk; + this.encoding = encoding; + this.callback = cb; + this.next = null; +} + +// It seems a linked list but it is not +// there will be only 2 of these for each stream +function CorkedRequest(state) { + var _this = this; + + this.next = null; + this.entry = null; + this.finish = function () { + onCorkedFinish(_this, state); + }; +} +/* */ + +/**/ +var asyncWrite = !process.browser && ['v0.10', 'v0.9.'].indexOf(process.version.slice(0, 5)) > -1 ? setImmediate : pna.nextTick; +/**/ + +/**/ +var Duplex; +/**/ + +Writable.WritableState = WritableState; + +/**/ +var util = Object.create(require('core-util-is')); +util.inherits = require('inherits'); +/**/ + +/**/ +var internalUtil = { + deprecate: require('util-deprecate') +}; +/**/ + +/**/ +var Stream = require('./internal/streams/stream'); +/**/ + +/**/ + +var Buffer = require('safe-buffer').Buffer; +var OurUint8Array = (typeof global !== 'undefined' ? global : typeof window !== 'undefined' ? window : typeof self !== 'undefined' ? self : {}).Uint8Array || function () {}; +function _uint8ArrayToBuffer(chunk) { + return Buffer.from(chunk); +} +function _isUint8Array(obj) { + return Buffer.isBuffer(obj) || obj instanceof OurUint8Array; +} + +/**/ + +var destroyImpl = require('./internal/streams/destroy'); + +util.inherits(Writable, Stream); + +function nop() {} + +function WritableState(options, stream) { + Duplex = Duplex || require('./_stream_duplex'); + + options = options || {}; + + // Duplex streams are both readable and writable, but share + // the same options object. + // However, some cases require setting options to different + // values for the readable and the writable sides of the duplex stream. + // These options can be provided separately as readableXXX and writableXXX. + var isDuplex = stream instanceof Duplex; + + // object stream flag to indicate whether or not this stream + // contains buffers or objects. + this.objectMode = !!options.objectMode; + + if (isDuplex) this.objectMode = this.objectMode || !!options.writableObjectMode; + + // the point at which write() starts returning false + // Note: 0 is a valid value, means that we always return false if + // the entire buffer is not flushed immediately on write() + var hwm = options.highWaterMark; + var writableHwm = options.writableHighWaterMark; + var defaultHwm = this.objectMode ? 16 : 16 * 1024; + + if (hwm || hwm === 0) this.highWaterMark = hwm;else if (isDuplex && (writableHwm || writableHwm === 0)) this.highWaterMark = writableHwm;else this.highWaterMark = defaultHwm; + + // cast to ints. + this.highWaterMark = Math.floor(this.highWaterMark); + + // if _final has been called + this.finalCalled = false; + + // drain event flag. + this.needDrain = false; + // at the start of calling end() + this.ending = false; + // when end() has been called, and returned + this.ended = false; + // when 'finish' is emitted + this.finished = false; + + // has it been destroyed + this.destroyed = false; + + // should we decode strings into buffers before passing to _write? + // this is here so that some node-core streams can optimize string + // handling at a lower level. + var noDecode = options.decodeStrings === false; + this.decodeStrings = !noDecode; + + // Crypto is kind of old and crusty. Historically, its default string + // encoding is 'binary' so we have to make this configurable. + // Everything else in the universe uses 'utf8', though. + this.defaultEncoding = options.defaultEncoding || 'utf8'; + + // not an actual buffer we keep track of, but a measurement + // of how much we're waiting to get pushed to some underlying + // socket or file. + this.length = 0; + + // a flag to see when we're in the middle of a write. + this.writing = false; + + // when true all writes will be buffered until .uncork() call + this.corked = 0; + + // a flag to be able to tell if the onwrite cb is called immediately, + // or on a later tick. We set this to true at first, because any + // actions that shouldn't happen until "later" should generally also + // not happen before the first write call. + this.sync = true; + + // a flag to know if we're processing previously buffered items, which + // may call the _write() callback in the same tick, so that we don't + // end up in an overlapped onwrite situation. + this.bufferProcessing = false; + + // the callback that's passed to _write(chunk,cb) + this.onwrite = function (er) { + onwrite(stream, er); + }; + + // the callback that the user supplies to write(chunk,encoding,cb) + this.writecb = null; + + // the amount that is being written when _write is called. + this.writelen = 0; + + this.bufferedRequest = null; + this.lastBufferedRequest = null; + + // number of pending user-supplied write callbacks + // this must be 0 before 'finish' can be emitted + this.pendingcb = 0; + + // emit prefinish if the only thing we're waiting for is _write cbs + // This is relevant for synchronous Transform streams + this.prefinished = false; + + // True if the error was already emitted and should not be thrown again + this.errorEmitted = false; + + // count buffered requests + this.bufferedRequestCount = 0; + + // allocate the first CorkedRequest, there is always + // one allocated and free to use, and we maintain at most two + this.corkedRequestsFree = new CorkedRequest(this); +} + +WritableState.prototype.getBuffer = function getBuffer() { + var current = this.bufferedRequest; + var out = []; + while (current) { + out.push(current); + current = current.next; + } + return out; +}; + +(function () { + try { + Object.defineProperty(WritableState.prototype, 'buffer', { + get: internalUtil.deprecate(function () { + return this.getBuffer(); + }, '_writableState.buffer is deprecated. Use _writableState.getBuffer ' + 'instead.', 'DEP0003') + }); + } catch (_) {} +})(); + +// Test _writableState for inheritance to account for Duplex streams, +// whose prototype chain only points to Readable. +var realHasInstance; +if (typeof Symbol === 'function' && Symbol.hasInstance && typeof Function.prototype[Symbol.hasInstance] === 'function') { + realHasInstance = Function.prototype[Symbol.hasInstance]; + Object.defineProperty(Writable, Symbol.hasInstance, { + value: function (object) { + if (realHasInstance.call(this, object)) return true; + if (this !== Writable) return false; + + return object && object._writableState instanceof WritableState; + } + }); +} else { + realHasInstance = function (object) { + return object instanceof this; + }; +} + +function Writable(options) { + Duplex = Duplex || require('./_stream_duplex'); + + // Writable ctor is applied to Duplexes, too. + // `realHasInstance` is necessary because using plain `instanceof` + // would return false, as no `_writableState` property is attached. + + // Trying to use the custom `instanceof` for Writable here will also break the + // Node.js LazyTransform implementation, which has a non-trivial getter for + // `_writableState` that would lead to infinite recursion. + if (!realHasInstance.call(Writable, this) && !(this instanceof Duplex)) { + return new Writable(options); + } + + this._writableState = new WritableState(options, this); + + // legacy. + this.writable = true; + + if (options) { + if (typeof options.write === 'function') this._write = options.write; + + if (typeof options.writev === 'function') this._writev = options.writev; + + if (typeof options.destroy === 'function') this._destroy = options.destroy; + + if (typeof options.final === 'function') this._final = options.final; + } + + Stream.call(this); +} + +// Otherwise people can pipe Writable streams, which is just wrong. +Writable.prototype.pipe = function () { + this.emit('error', new Error('Cannot pipe, not readable')); +}; + +function writeAfterEnd(stream, cb) { + var er = new Error('write after end'); + // TODO: defer error events consistently everywhere, not just the cb + stream.emit('error', er); + pna.nextTick(cb, er); +} + +// Checks that a user-supplied chunk is valid, especially for the particular +// mode the stream is in. Currently this means that `null` is never accepted +// and undefined/non-string values are only allowed in object mode. +function validChunk(stream, state, chunk, cb) { + var valid = true; + var er = false; + + if (chunk === null) { + er = new TypeError('May not write null values to stream'); + } else if (typeof chunk !== 'string' && chunk !== undefined && !state.objectMode) { + er = new TypeError('Invalid non-string/buffer chunk'); + } + if (er) { + stream.emit('error', er); + pna.nextTick(cb, er); + valid = false; + } + return valid; +} + +Writable.prototype.write = function (chunk, encoding, cb) { + var state = this._writableState; + var ret = false; + var isBuf = !state.objectMode && _isUint8Array(chunk); + + if (isBuf && !Buffer.isBuffer(chunk)) { + chunk = _uint8ArrayToBuffer(chunk); + } + + if (typeof encoding === 'function') { + cb = encoding; + encoding = null; + } + + if (isBuf) encoding = 'buffer';else if (!encoding) encoding = state.defaultEncoding; + + if (typeof cb !== 'function') cb = nop; + + if (state.ended) writeAfterEnd(this, cb);else if (isBuf || validChunk(this, state, chunk, cb)) { + state.pendingcb++; + ret = writeOrBuffer(this, state, isBuf, chunk, encoding, cb); + } + + return ret; +}; + +Writable.prototype.cork = function () { + var state = this._writableState; + + state.corked++; +}; + +Writable.prototype.uncork = function () { + var state = this._writableState; + + if (state.corked) { + state.corked--; + + if (!state.writing && !state.corked && !state.bufferProcessing && state.bufferedRequest) clearBuffer(this, state); + } +}; + +Writable.prototype.setDefaultEncoding = function setDefaultEncoding(encoding) { + // node::ParseEncoding() requires lower case. + if (typeof encoding === 'string') encoding = encoding.toLowerCase(); + if (!(['hex', 'utf8', 'utf-8', 'ascii', 'binary', 'base64', 'ucs2', 'ucs-2', 'utf16le', 'utf-16le', 'raw'].indexOf((encoding + '').toLowerCase()) > -1)) throw new TypeError('Unknown encoding: ' + encoding); + this._writableState.defaultEncoding = encoding; + return this; +}; + +function decodeChunk(state, chunk, encoding) { + if (!state.objectMode && state.decodeStrings !== false && typeof chunk === 'string') { + chunk = Buffer.from(chunk, encoding); + } + return chunk; +} + +Object.defineProperty(Writable.prototype, 'writableHighWaterMark', { + // making it explicit this property is not enumerable + // because otherwise some prototype manipulation in + // userland will fail + enumerable: false, + get: function () { + return this._writableState.highWaterMark; + } +}); + +// if we're already writing something, then just put this +// in the queue, and wait our turn. Otherwise, call _write +// If we return false, then we need a drain event, so set that flag. +function writeOrBuffer(stream, state, isBuf, chunk, encoding, cb) { + if (!isBuf) { + var newChunk = decodeChunk(state, chunk, encoding); + if (chunk !== newChunk) { + isBuf = true; + encoding = 'buffer'; + chunk = newChunk; + } + } + var len = state.objectMode ? 1 : chunk.length; + + state.length += len; + + var ret = state.length < state.highWaterMark; + // we must ensure that previous needDrain will not be reset to false. + if (!ret) state.needDrain = true; + + if (state.writing || state.corked) { + var last = state.lastBufferedRequest; + state.lastBufferedRequest = { + chunk: chunk, + encoding: encoding, + isBuf: isBuf, + callback: cb, + next: null + }; + if (last) { + last.next = state.lastBufferedRequest; + } else { + state.bufferedRequest = state.lastBufferedRequest; + } + state.bufferedRequestCount += 1; + } else { + doWrite(stream, state, false, len, chunk, encoding, cb); + } + + return ret; +} + +function doWrite(stream, state, writev, len, chunk, encoding, cb) { + state.writelen = len; + state.writecb = cb; + state.writing = true; + state.sync = true; + if (writev) stream._writev(chunk, state.onwrite);else stream._write(chunk, encoding, state.onwrite); + state.sync = false; +} + +function onwriteError(stream, state, sync, er, cb) { + --state.pendingcb; + + if (sync) { + // defer the callback if we are being called synchronously + // to avoid piling up things on the stack + pna.nextTick(cb, er); + // this can emit finish, and it will always happen + // after error + pna.nextTick(finishMaybe, stream, state); + stream._writableState.errorEmitted = true; + stream.emit('error', er); + } else { + // the caller expect this to happen before if + // it is async + cb(er); + stream._writableState.errorEmitted = true; + stream.emit('error', er); + // this can emit finish, but finish must + // always follow error + finishMaybe(stream, state); + } +} + +function onwriteStateUpdate(state) { + state.writing = false; + state.writecb = null; + state.length -= state.writelen; + state.writelen = 0; +} + +function onwrite(stream, er) { + var state = stream._writableState; + var sync = state.sync; + var cb = state.writecb; + + onwriteStateUpdate(state); + + if (er) onwriteError(stream, state, sync, er, cb);else { + // Check if we're actually ready to finish, but don't emit yet + var finished = needFinish(state); + + if (!finished && !state.corked && !state.bufferProcessing && state.bufferedRequest) { + clearBuffer(stream, state); + } + + if (sync) { + /**/ + asyncWrite(afterWrite, stream, state, finished, cb); + /**/ + } else { + afterWrite(stream, state, finished, cb); + } + } +} + +function afterWrite(stream, state, finished, cb) { + if (!finished) onwriteDrain(stream, state); + state.pendingcb--; + cb(); + finishMaybe(stream, state); +} + +// Must force callback to be called on nextTick, so that we don't +// emit 'drain' before the write() consumer gets the 'false' return +// value, and has a chance to attach a 'drain' listener. +function onwriteDrain(stream, state) { + if (state.length === 0 && state.needDrain) { + state.needDrain = false; + stream.emit('drain'); + } +} + +// if there's something in the buffer waiting, then process it +function clearBuffer(stream, state) { + state.bufferProcessing = true; + var entry = state.bufferedRequest; + + if (stream._writev && entry && entry.next) { + // Fast case, write everything using _writev() + var l = state.bufferedRequestCount; + var buffer = new Array(l); + var holder = state.corkedRequestsFree; + holder.entry = entry; + + var count = 0; + var allBuffers = true; + while (entry) { + buffer[count] = entry; + if (!entry.isBuf) allBuffers = false; + entry = entry.next; + count += 1; + } + buffer.allBuffers = allBuffers; + + doWrite(stream, state, true, state.length, buffer, '', holder.finish); + + // doWrite is almost always async, defer these to save a bit of time + // as the hot path ends with doWrite + state.pendingcb++; + state.lastBufferedRequest = null; + if (holder.next) { + state.corkedRequestsFree = holder.next; + holder.next = null; + } else { + state.corkedRequestsFree = new CorkedRequest(state); + } + state.bufferedRequestCount = 0; + } else { + // Slow case, write chunks one-by-one + while (entry) { + var chunk = entry.chunk; + var encoding = entry.encoding; + var cb = entry.callback; + var len = state.objectMode ? 1 : chunk.length; + + doWrite(stream, state, false, len, chunk, encoding, cb); + entry = entry.next; + state.bufferedRequestCount--; + // if we didn't call the onwrite immediately, then + // it means that we need to wait until it does. + // also, that means that the chunk and cb are currently + // being processed, so move the buffer counter past them. + if (state.writing) { + break; + } + } + + if (entry === null) state.lastBufferedRequest = null; + } + + state.bufferedRequest = entry; + state.bufferProcessing = false; +} + +Writable.prototype._write = function (chunk, encoding, cb) { + cb(new Error('_write() is not implemented')); +}; + +Writable.prototype._writev = null; + +Writable.prototype.end = function (chunk, encoding, cb) { + var state = this._writableState; + + if (typeof chunk === 'function') { + cb = chunk; + chunk = null; + encoding = null; + } else if (typeof encoding === 'function') { + cb = encoding; + encoding = null; + } + + if (chunk !== null && chunk !== undefined) this.write(chunk, encoding); + + // .end() fully uncorks + if (state.corked) { + state.corked = 1; + this.uncork(); + } + + // ignore unnecessary end() calls. + if (!state.ending) endWritable(this, state, cb); +}; + +function needFinish(state) { + return state.ending && state.length === 0 && state.bufferedRequest === null && !state.finished && !state.writing; +} +function callFinal(stream, state) { + stream._final(function (err) { + state.pendingcb--; + if (err) { + stream.emit('error', err); + } + state.prefinished = true; + stream.emit('prefinish'); + finishMaybe(stream, state); + }); +} +function prefinish(stream, state) { + if (!state.prefinished && !state.finalCalled) { + if (typeof stream._final === 'function') { + state.pendingcb++; + state.finalCalled = true; + pna.nextTick(callFinal, stream, state); + } else { + state.prefinished = true; + stream.emit('prefinish'); + } + } +} + +function finishMaybe(stream, state) { + var need = needFinish(state); + if (need) { + prefinish(stream, state); + if (state.pendingcb === 0) { + state.finished = true; + stream.emit('finish'); + } + } + return need; +} + +function endWritable(stream, state, cb) { + state.ending = true; + finishMaybe(stream, state); + if (cb) { + if (state.finished) pna.nextTick(cb);else stream.once('finish', cb); + } + state.ended = true; + stream.writable = false; +} + +function onCorkedFinish(corkReq, state, err) { + var entry = corkReq.entry; + corkReq.entry = null; + while (entry) { + var cb = entry.callback; + state.pendingcb--; + cb(err); + entry = entry.next; + } + + // reuse the free corkReq. + state.corkedRequestsFree.next = corkReq; +} + +Object.defineProperty(Writable.prototype, 'destroyed', { + get: function () { + if (this._writableState === undefined) { + return false; + } + return this._writableState.destroyed; + }, + set: function (value) { + // we ignore the value if the stream + // has not been initialized yet + if (!this._writableState) { + return; + } + + // backward compatibility, the user is explicitly + // managing destroyed + this._writableState.destroyed = value; + } +}); + +Writable.prototype.destroy = destroyImpl.destroy; +Writable.prototype._undestroy = destroyImpl.undestroy; +Writable.prototype._destroy = function (err, cb) { + this.end(); + cb(err); +}; \ No newline at end of file diff --git a/tests/corpus/readable-stream-disable/node_modules/readable-stream/lib/internal/streams/BufferList.js b/tests/corpus/readable-stream-disable/node_modules/readable-stream/lib/internal/streams/BufferList.js new file mode 100644 index 000000000..5e080976c --- /dev/null +++ b/tests/corpus/readable-stream-disable/node_modules/readable-stream/lib/internal/streams/BufferList.js @@ -0,0 +1,78 @@ +'use strict'; + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +var Buffer = require('safe-buffer').Buffer; +var util = require('util'); + +function copyBuffer(src, target, offset) { + src.copy(target, offset); +} + +module.exports = function () { + function BufferList() { + _classCallCheck(this, BufferList); + + this.head = null; + this.tail = null; + this.length = 0; + } + + BufferList.prototype.push = function push(v) { + var entry = { data: v, next: null }; + if (this.length > 0) this.tail.next = entry;else this.head = entry; + this.tail = entry; + ++this.length; + }; + + BufferList.prototype.unshift = function unshift(v) { + var entry = { data: v, next: this.head }; + if (this.length === 0) this.tail = entry; + this.head = entry; + ++this.length; + }; + + BufferList.prototype.shift = function shift() { + if (this.length === 0) return; + var ret = this.head.data; + if (this.length === 1) this.head = this.tail = null;else this.head = this.head.next; + --this.length; + return ret; + }; + + BufferList.prototype.clear = function clear() { + this.head = this.tail = null; + this.length = 0; + }; + + BufferList.prototype.join = function join(s) { + if (this.length === 0) return ''; + var p = this.head; + var ret = '' + p.data; + while (p = p.next) { + ret += s + p.data; + }return ret; + }; + + BufferList.prototype.concat = function concat(n) { + if (this.length === 0) return Buffer.alloc(0); + var ret = Buffer.allocUnsafe(n >>> 0); + var p = this.head; + var i = 0; + while (p) { + copyBuffer(p.data, ret, i); + i += p.data.length; + p = p.next; + } + return ret; + }; + + return BufferList; +}(); + +if (util && util.inspect && util.inspect.custom) { + module.exports.prototype[util.inspect.custom] = function () { + var obj = util.inspect({ length: this.length }); + return this.constructor.name + ' ' + obj; + }; +} \ No newline at end of file diff --git a/tests/corpus/readable-stream-disable/node_modules/readable-stream/lib/internal/streams/destroy.js b/tests/corpus/readable-stream-disable/node_modules/readable-stream/lib/internal/streams/destroy.js new file mode 100644 index 000000000..85a821407 --- /dev/null +++ b/tests/corpus/readable-stream-disable/node_modules/readable-stream/lib/internal/streams/destroy.js @@ -0,0 +1,84 @@ +'use strict'; + +/**/ + +var pna = require('process-nextick-args'); +/**/ + +// undocumented cb() API, needed for core, not for public API +function destroy(err, cb) { + var _this = this; + + var readableDestroyed = this._readableState && this._readableState.destroyed; + var writableDestroyed = this._writableState && this._writableState.destroyed; + + if (readableDestroyed || writableDestroyed) { + if (cb) { + cb(err); + } else if (err) { + if (!this._writableState) { + pna.nextTick(emitErrorNT, this, err); + } else if (!this._writableState.errorEmitted) { + this._writableState.errorEmitted = true; + pna.nextTick(emitErrorNT, this, err); + } + } + + return this; + } + + // we set destroyed to true before firing error callbacks in order + // to make it re-entrance safe in case destroy() is called within callbacks + + if (this._readableState) { + this._readableState.destroyed = true; + } + + // if this is a duplex stream mark the writable part as destroyed as well + if (this._writableState) { + this._writableState.destroyed = true; + } + + this._destroy(err || null, function (err) { + if (!cb && err) { + if (!_this._writableState) { + pna.nextTick(emitErrorNT, _this, err); + } else if (!_this._writableState.errorEmitted) { + _this._writableState.errorEmitted = true; + pna.nextTick(emitErrorNT, _this, err); + } + } else if (cb) { + cb(err); + } + }); + + return this; +} + +function undestroy() { + if (this._readableState) { + this._readableState.destroyed = false; + this._readableState.reading = false; + this._readableState.ended = false; + this._readableState.endEmitted = false; + } + + if (this._writableState) { + this._writableState.destroyed = false; + this._writableState.ended = false; + this._writableState.ending = false; + this._writableState.finalCalled = false; + this._writableState.prefinished = false; + this._writableState.finished = false; + this._writableState.errorEmitted = false; + } +} + +function emitErrorNT(self, err) { + self.emit('error', err); +} + +module.exports = { + destroy: destroy, + undestroy: undestroy +}; \ No newline at end of file diff --git a/tests/corpus/readable-stream-disable/node_modules/readable-stream/lib/internal/streams/stream-browser.js b/tests/corpus/readable-stream-disable/node_modules/readable-stream/lib/internal/streams/stream-browser.js new file mode 100644 index 000000000..9332a3fda --- /dev/null +++ b/tests/corpus/readable-stream-disable/node_modules/readable-stream/lib/internal/streams/stream-browser.js @@ -0,0 +1 @@ +module.exports = require('events').EventEmitter; diff --git a/tests/corpus/readable-stream-disable/node_modules/readable-stream/lib/internal/streams/stream.js b/tests/corpus/readable-stream-disable/node_modules/readable-stream/lib/internal/streams/stream.js new file mode 100644 index 000000000..ce2ad5b6e --- /dev/null +++ b/tests/corpus/readable-stream-disable/node_modules/readable-stream/lib/internal/streams/stream.js @@ -0,0 +1 @@ +module.exports = require('stream'); diff --git a/tests/corpus/readable-stream-disable/node_modules/readable-stream/package.json b/tests/corpus/readable-stream-disable/node_modules/readable-stream/package.json new file mode 100644 index 000000000..514c178e9 --- /dev/null +++ b/tests/corpus/readable-stream-disable/node_modules/readable-stream/package.json @@ -0,0 +1,52 @@ +{ + "name": "readable-stream", + "version": "2.3.8", + "description": "Streams3, a user-land copy of the stream library from Node.js", + "main": "readable.js", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + }, + "devDependencies": { + "assert": "^1.4.0", + "babel-polyfill": "^6.9.1", + "buffer": "^4.9.0", + "lolex": "^2.3.2", + "nyc": "^6.4.0", + "tap": "^0.7.0", + "tape": "^4.8.0" + }, + "scripts": { + "test": "tap test/parallel/*.js test/ours/*.js && node test/verify-dependencies.js", + "ci": "tap test/parallel/*.js test/ours/*.js --tap | tee test.tap && node test/verify-dependencies.js", + "cover": "nyc npm test", + "report": "nyc report --reporter=lcov" + }, + "repository": { + "type": "git", + "url": "git://github.com/nodejs/readable-stream" + }, + "keywords": [ + "readable", + "stream", + "pipe" + ], + "browser": { + "util": false, + "./readable.js": "./readable-browser.js", + "./writable.js": "./writable-browser.js", + "./duplex.js": "./duplex-browser.js", + "./lib/internal/streams/stream.js": "./lib/internal/streams/stream-browser.js" + }, + "nyc": { + "include": [ + "lib/**.js" + ] + }, + "license": "MIT" +} diff --git a/tests/corpus/readable-stream-disable/node_modules/readable-stream/passthrough.js b/tests/corpus/readable-stream-disable/node_modules/readable-stream/passthrough.js new file mode 100644 index 000000000..ffd791d7f --- /dev/null +++ b/tests/corpus/readable-stream-disable/node_modules/readable-stream/passthrough.js @@ -0,0 +1 @@ +module.exports = require('./readable').PassThrough diff --git a/tests/corpus/readable-stream-disable/node_modules/readable-stream/readable-browser.js b/tests/corpus/readable-stream-disable/node_modules/readable-stream/readable-browser.js new file mode 100644 index 000000000..e50372592 --- /dev/null +++ b/tests/corpus/readable-stream-disable/node_modules/readable-stream/readable-browser.js @@ -0,0 +1,7 @@ +exports = module.exports = require('./lib/_stream_readable.js'); +exports.Stream = exports; +exports.Readable = exports; +exports.Writable = require('./lib/_stream_writable.js'); +exports.Duplex = require('./lib/_stream_duplex.js'); +exports.Transform = require('./lib/_stream_transform.js'); +exports.PassThrough = require('./lib/_stream_passthrough.js'); diff --git a/tests/corpus/readable-stream-disable/node_modules/readable-stream/readable.js b/tests/corpus/readable-stream-disable/node_modules/readable-stream/readable.js new file mode 100644 index 000000000..ec89ec533 --- /dev/null +++ b/tests/corpus/readable-stream-disable/node_modules/readable-stream/readable.js @@ -0,0 +1,19 @@ +var Stream = require('stream'); +if (process.env.READABLE_STREAM === 'disable' && Stream) { + module.exports = Stream; + exports = module.exports = Stream.Readable; + exports.Readable = Stream.Readable; + exports.Writable = Stream.Writable; + exports.Duplex = Stream.Duplex; + exports.Transform = Stream.Transform; + exports.PassThrough = Stream.PassThrough; + exports.Stream = Stream; +} else { + exports = module.exports = require('./lib/_stream_readable.js'); + exports.Stream = Stream || exports; + exports.Readable = exports; + exports.Writable = require('./lib/_stream_writable.js'); + exports.Duplex = require('./lib/_stream_duplex.js'); + exports.Transform = require('./lib/_stream_transform.js'); + exports.PassThrough = require('./lib/_stream_passthrough.js'); +} diff --git a/tests/corpus/readable-stream-disable/node_modules/readable-stream/transform.js b/tests/corpus/readable-stream-disable/node_modules/readable-stream/transform.js new file mode 100644 index 000000000..b1baba26d --- /dev/null +++ b/tests/corpus/readable-stream-disable/node_modules/readable-stream/transform.js @@ -0,0 +1 @@ +module.exports = require('./readable').Transform diff --git a/tests/corpus/readable-stream-disable/node_modules/readable-stream/writable-browser.js b/tests/corpus/readable-stream-disable/node_modules/readable-stream/writable-browser.js new file mode 100644 index 000000000..ebdde6a85 --- /dev/null +++ b/tests/corpus/readable-stream-disable/node_modules/readable-stream/writable-browser.js @@ -0,0 +1 @@ +module.exports = require('./lib/_stream_writable.js'); diff --git a/tests/corpus/readable-stream-disable/node_modules/readable-stream/writable.js b/tests/corpus/readable-stream-disable/node_modules/readable-stream/writable.js new file mode 100644 index 000000000..3211a6f80 --- /dev/null +++ b/tests/corpus/readable-stream-disable/node_modules/readable-stream/writable.js @@ -0,0 +1,8 @@ +var Stream = require("stream") +var Writable = require("./lib/_stream_writable.js") + +if (process.env.READABLE_STREAM === 'disable') { + module.exports = Stream && Stream.Writable || Writable +} else { + module.exports = Writable +} diff --git a/tests/corpus/readable-stream-disable/node_modules/safe-buffer/LICENSE b/tests/corpus/readable-stream-disable/node_modules/safe-buffer/LICENSE new file mode 100644 index 000000000..0c068ceec --- /dev/null +++ b/tests/corpus/readable-stream-disable/node_modules/safe-buffer/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) Feross Aboukhadijeh + +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/readable-stream-disable/node_modules/safe-buffer/index.js b/tests/corpus/readable-stream-disable/node_modules/safe-buffer/index.js new file mode 100644 index 000000000..22438dabb --- /dev/null +++ b/tests/corpus/readable-stream-disable/node_modules/safe-buffer/index.js @@ -0,0 +1,62 @@ +/* eslint-disable node/no-deprecated-api */ +var buffer = require('buffer') +var Buffer = buffer.Buffer + +// alternative to using Object.keys for old browsers +function copyProps (src, dst) { + for (var key in src) { + dst[key] = src[key] + } +} +if (Buffer.from && Buffer.alloc && Buffer.allocUnsafe && Buffer.allocUnsafeSlow) { + module.exports = buffer +} else { + // Copy properties from require('buffer') + copyProps(buffer, exports) + exports.Buffer = SafeBuffer +} + +function SafeBuffer (arg, encodingOrOffset, length) { + return Buffer(arg, encodingOrOffset, length) +} + +// Copy static methods from Buffer +copyProps(Buffer, SafeBuffer) + +SafeBuffer.from = function (arg, encodingOrOffset, length) { + if (typeof arg === 'number') { + throw new TypeError('Argument must not be a number') + } + return Buffer(arg, encodingOrOffset, length) +} + +SafeBuffer.alloc = function (size, fill, encoding) { + if (typeof size !== 'number') { + throw new TypeError('Argument must be a number') + } + var buf = Buffer(size) + if (fill !== undefined) { + if (typeof encoding === 'string') { + buf.fill(fill, encoding) + } else { + buf.fill(fill) + } + } else { + buf.fill(0) + } + return buf +} + +SafeBuffer.allocUnsafe = function (size) { + if (typeof size !== 'number') { + throw new TypeError('Argument must be a number') + } + return Buffer(size) +} + +SafeBuffer.allocUnsafeSlow = function (size) { + if (typeof size !== 'number') { + throw new TypeError('Argument must be a number') + } + return buffer.SlowBuffer(size) +} diff --git a/tests/corpus/readable-stream-disable/node_modules/safe-buffer/package.json b/tests/corpus/readable-stream-disable/node_modules/safe-buffer/package.json new file mode 100644 index 000000000..623fbc3f6 --- /dev/null +++ b/tests/corpus/readable-stream-disable/node_modules/safe-buffer/package.json @@ -0,0 +1,37 @@ +{ + "name": "safe-buffer", + "description": "Safer Node.js Buffer API", + "version": "5.1.2", + "author": { + "name": "Feross Aboukhadijeh", + "email": "feross@feross.org", + "url": "http://feross.org" + }, + "bugs": { + "url": "https://github.com/feross/safe-buffer/issues" + }, + "devDependencies": { + "standard": "*", + "tape": "^4.0.0" + }, + "homepage": "https://github.com/feross/safe-buffer", + "keywords": [ + "buffer", + "buffer allocate", + "node security", + "safe", + "safe-buffer", + "security", + "uninitialized" + ], + "license": "MIT", + "main": "index.js", + "types": "index.d.ts", + "repository": { + "type": "git", + "url": "git://github.com/feross/safe-buffer.git" + }, + "scripts": { + "test": "standard && tape test/*.js" + } +} diff --git a/tests/corpus/readable-stream-disable/node_modules/string_decoder/LICENSE b/tests/corpus/readable-stream-disable/node_modules/string_decoder/LICENSE new file mode 100644 index 000000000..778edb207 --- /dev/null +++ b/tests/corpus/readable-stream-disable/node_modules/string_decoder/LICENSE @@ -0,0 +1,48 @@ +Node.js is licensed for use as follows: + +""" +Copyright Node.js contributors. 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. +""" + +This license applies to parts of Node.js originating from the +https://github.com/joyent/node repository: + +""" +Copyright Joyent, Inc. and other Node contributors. 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. +""" + diff --git a/tests/corpus/readable-stream-disable/node_modules/string_decoder/lib/string_decoder.js b/tests/corpus/readable-stream-disable/node_modules/string_decoder/lib/string_decoder.js new file mode 100644 index 000000000..2e89e63f7 --- /dev/null +++ b/tests/corpus/readable-stream-disable/node_modules/string_decoder/lib/string_decoder.js @@ -0,0 +1,296 @@ +// Copyright Joyent, Inc. and other Node 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. + +'use strict'; + +/**/ + +var Buffer = require('safe-buffer').Buffer; +/**/ + +var isEncoding = Buffer.isEncoding || function (encoding) { + encoding = '' + encoding; + switch (encoding && encoding.toLowerCase()) { + case 'hex':case 'utf8':case 'utf-8':case 'ascii':case 'binary':case 'base64':case 'ucs2':case 'ucs-2':case 'utf16le':case 'utf-16le':case 'raw': + return true; + default: + return false; + } +}; + +function _normalizeEncoding(enc) { + if (!enc) return 'utf8'; + var retried; + while (true) { + switch (enc) { + case 'utf8': + case 'utf-8': + return 'utf8'; + case 'ucs2': + case 'ucs-2': + case 'utf16le': + case 'utf-16le': + return 'utf16le'; + case 'latin1': + case 'binary': + return 'latin1'; + case 'base64': + case 'ascii': + case 'hex': + return enc; + default: + if (retried) return; // undefined + enc = ('' + enc).toLowerCase(); + retried = true; + } + } +}; + +// Do not cache `Buffer.isEncoding` when checking encoding names as some +// modules monkey-patch it to support additional encodings +function normalizeEncoding(enc) { + var nenc = _normalizeEncoding(enc); + if (typeof nenc !== 'string' && (Buffer.isEncoding === isEncoding || !isEncoding(enc))) throw new Error('Unknown encoding: ' + enc); + return nenc || enc; +} + +// StringDecoder provides an interface for efficiently splitting a series of +// buffers into a series of JS strings without breaking apart multi-byte +// characters. +exports.StringDecoder = StringDecoder; +function StringDecoder(encoding) { + this.encoding = normalizeEncoding(encoding); + var nb; + switch (this.encoding) { + case 'utf16le': + this.text = utf16Text; + this.end = utf16End; + nb = 4; + break; + case 'utf8': + this.fillLast = utf8FillLast; + nb = 4; + break; + case 'base64': + this.text = base64Text; + this.end = base64End; + nb = 3; + break; + default: + this.write = simpleWrite; + this.end = simpleEnd; + return; + } + this.lastNeed = 0; + this.lastTotal = 0; + this.lastChar = Buffer.allocUnsafe(nb); +} + +StringDecoder.prototype.write = function (buf) { + if (buf.length === 0) return ''; + var r; + var i; + if (this.lastNeed) { + r = this.fillLast(buf); + if (r === undefined) return ''; + i = this.lastNeed; + this.lastNeed = 0; + } else { + i = 0; + } + if (i < buf.length) return r ? r + this.text(buf, i) : this.text(buf, i); + return r || ''; +}; + +StringDecoder.prototype.end = utf8End; + +// Returns only complete characters in a Buffer +StringDecoder.prototype.text = utf8Text; + +// Attempts to complete a partial non-UTF-8 character using bytes from a Buffer +StringDecoder.prototype.fillLast = function (buf) { + if (this.lastNeed <= buf.length) { + buf.copy(this.lastChar, this.lastTotal - this.lastNeed, 0, this.lastNeed); + return this.lastChar.toString(this.encoding, 0, this.lastTotal); + } + buf.copy(this.lastChar, this.lastTotal - this.lastNeed, 0, buf.length); + this.lastNeed -= buf.length; +}; + +// Checks the type of a UTF-8 byte, whether it's ASCII, a leading byte, or a +// continuation byte. If an invalid byte is detected, -2 is returned. +function utf8CheckByte(byte) { + if (byte <= 0x7F) return 0;else if (byte >> 5 === 0x06) return 2;else if (byte >> 4 === 0x0E) return 3;else if (byte >> 3 === 0x1E) return 4; + return byte >> 6 === 0x02 ? -1 : -2; +} + +// Checks at most 3 bytes at the end of a Buffer in order to detect an +// incomplete multi-byte UTF-8 character. The total number of bytes (2, 3, or 4) +// needed to complete the UTF-8 character (if applicable) are returned. +function utf8CheckIncomplete(self, buf, i) { + var j = buf.length - 1; + if (j < i) return 0; + var nb = utf8CheckByte(buf[j]); + if (nb >= 0) { + if (nb > 0) self.lastNeed = nb - 1; + return nb; + } + if (--j < i || nb === -2) return 0; + nb = utf8CheckByte(buf[j]); + if (nb >= 0) { + if (nb > 0) self.lastNeed = nb - 2; + return nb; + } + if (--j < i || nb === -2) return 0; + nb = utf8CheckByte(buf[j]); + if (nb >= 0) { + if (nb > 0) { + if (nb === 2) nb = 0;else self.lastNeed = nb - 3; + } + return nb; + } + return 0; +} + +// Validates as many continuation bytes for a multi-byte UTF-8 character as +// needed or are available. If we see a non-continuation byte where we expect +// one, we "replace" the validated continuation bytes we've seen so far with +// a single UTF-8 replacement character ('\ufffd'), to match v8's UTF-8 decoding +// behavior. The continuation byte check is included three times in the case +// where all of the continuation bytes for a character exist in the same buffer. +// It is also done this way as a slight performance increase instead of using a +// loop. +function utf8CheckExtraBytes(self, buf, p) { + if ((buf[0] & 0xC0) !== 0x80) { + self.lastNeed = 0; + return '\ufffd'; + } + if (self.lastNeed > 1 && buf.length > 1) { + if ((buf[1] & 0xC0) !== 0x80) { + self.lastNeed = 1; + return '\ufffd'; + } + if (self.lastNeed > 2 && buf.length > 2) { + if ((buf[2] & 0xC0) !== 0x80) { + self.lastNeed = 2; + return '\ufffd'; + } + } + } +} + +// Attempts to complete a multi-byte UTF-8 character using bytes from a Buffer. +function utf8FillLast(buf) { + var p = this.lastTotal - this.lastNeed; + var r = utf8CheckExtraBytes(this, buf, p); + if (r !== undefined) return r; + if (this.lastNeed <= buf.length) { + buf.copy(this.lastChar, p, 0, this.lastNeed); + return this.lastChar.toString(this.encoding, 0, this.lastTotal); + } + buf.copy(this.lastChar, p, 0, buf.length); + this.lastNeed -= buf.length; +} + +// Returns all complete UTF-8 characters in a Buffer. If the Buffer ended on a +// partial character, the character's bytes are buffered until the required +// number of bytes are available. +function utf8Text(buf, i) { + var total = utf8CheckIncomplete(this, buf, i); + if (!this.lastNeed) return buf.toString('utf8', i); + this.lastTotal = total; + var end = buf.length - (total - this.lastNeed); + buf.copy(this.lastChar, 0, end); + return buf.toString('utf8', i, end); +} + +// For UTF-8, a replacement character is added when ending on a partial +// character. +function utf8End(buf) { + var r = buf && buf.length ? this.write(buf) : ''; + if (this.lastNeed) return r + '\ufffd'; + return r; +} + +// UTF-16LE typically needs two bytes per character, but even if we have an even +// number of bytes available, we need to check if we end on a leading/high +// surrogate. In that case, we need to wait for the next two bytes in order to +// decode the last character properly. +function utf16Text(buf, i) { + if ((buf.length - i) % 2 === 0) { + var r = buf.toString('utf16le', i); + if (r) { + var c = r.charCodeAt(r.length - 1); + if (c >= 0xD800 && c <= 0xDBFF) { + this.lastNeed = 2; + this.lastTotal = 4; + this.lastChar[0] = buf[buf.length - 2]; + this.lastChar[1] = buf[buf.length - 1]; + return r.slice(0, -1); + } + } + return r; + } + this.lastNeed = 1; + this.lastTotal = 2; + this.lastChar[0] = buf[buf.length - 1]; + return buf.toString('utf16le', i, buf.length - 1); +} + +// For UTF-16LE we do not explicitly append special replacement characters if we +// end on a partial character, we simply let v8 handle that. +function utf16End(buf) { + var r = buf && buf.length ? this.write(buf) : ''; + if (this.lastNeed) { + var end = this.lastTotal - this.lastNeed; + return r + this.lastChar.toString('utf16le', 0, end); + } + return r; +} + +function base64Text(buf, i) { + var n = (buf.length - i) % 3; + if (n === 0) return buf.toString('base64', i); + this.lastNeed = 3 - n; + this.lastTotal = 3; + if (n === 1) { + this.lastChar[0] = buf[buf.length - 1]; + } else { + this.lastChar[0] = buf[buf.length - 2]; + this.lastChar[1] = buf[buf.length - 1]; + } + return buf.toString('base64', i, buf.length - n); +} + +function base64End(buf) { + var r = buf && buf.length ? this.write(buf) : ''; + if (this.lastNeed) return r + this.lastChar.toString('base64', 0, 3 - this.lastNeed); + return r; +} + +// Pass bytes on through for single-byte encodings (e.g. ascii, latin1, hex) +function simpleWrite(buf) { + return buf.toString(this.encoding); +} + +function simpleEnd(buf) { + return buf && buf.length ? this.write(buf) : ''; +} \ No newline at end of file diff --git a/tests/corpus/readable-stream-disable/node_modules/string_decoder/package.json b/tests/corpus/readable-stream-disable/node_modules/string_decoder/package.json new file mode 100644 index 000000000..518c3eb9f --- /dev/null +++ b/tests/corpus/readable-stream-disable/node_modules/string_decoder/package.json @@ -0,0 +1,31 @@ +{ + "name": "string_decoder", + "version": "1.1.1", + "description": "The string_decoder module from Node core", + "main": "lib/string_decoder.js", + "dependencies": { + "safe-buffer": "~5.1.0" + }, + "devDependencies": { + "babel-polyfill": "^6.23.0", + "core-util-is": "^1.0.2", + "inherits": "^2.0.3", + "tap": "~0.4.8" + }, + "scripts": { + "test": "tap test/parallel/*.js && node test/verify-dependencies", + "ci": "tap test/parallel/*.js test/ours/*.js --tap | tee test.tap && node test/verify-dependencies.js" + }, + "repository": { + "type": "git", + "url": "git://github.com/nodejs/string_decoder.git" + }, + "homepage": "https://github.com/nodejs/string_decoder", + "keywords": [ + "string", + "decoder", + "browser", + "browserify" + ], + "license": "MIT" +} diff --git a/tests/corpus/readable-stream-disable/node_modules/util-deprecate/LICENSE b/tests/corpus/readable-stream-disable/node_modules/util-deprecate/LICENSE new file mode 100644 index 000000000..6a60e8c22 --- /dev/null +++ b/tests/corpus/readable-stream-disable/node_modules/util-deprecate/LICENSE @@ -0,0 +1,24 @@ +(The MIT License) + +Copyright (c) 2014 Nathan Rajlich + +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/readable-stream-disable/node_modules/util-deprecate/browser.js b/tests/corpus/readable-stream-disable/node_modules/util-deprecate/browser.js new file mode 100644 index 000000000..549ae2f06 --- /dev/null +++ b/tests/corpus/readable-stream-disable/node_modules/util-deprecate/browser.js @@ -0,0 +1,67 @@ + +/** + * Module exports. + */ + +module.exports = deprecate; + +/** + * Mark that a method should not be used. + * Returns a modified function which warns once by default. + * + * If `localStorage.noDeprecation = true` is set, then it is a no-op. + * + * If `localStorage.throwDeprecation = true` is set, then deprecated functions + * will throw an Error when invoked. + * + * If `localStorage.traceDeprecation = true` is set, then deprecated functions + * will invoke `console.trace()` instead of `console.error()`. + * + * @param {Function} fn - the function to deprecate + * @param {String} msg - the string to print to the console when `fn` is invoked + * @returns {Function} a new "deprecated" version of `fn` + * @api public + */ + +function deprecate (fn, msg) { + if (config('noDeprecation')) { + return fn; + } + + var warned = false; + function deprecated() { + if (!warned) { + if (config('throwDeprecation')) { + throw new Error(msg); + } else if (config('traceDeprecation')) { + console.trace(msg); + } else { + console.warn(msg); + } + warned = true; + } + return fn.apply(this, arguments); + } + + return deprecated; +} + +/** + * Checks `localStorage` for boolean values for the given `name`. + * + * @param {String} name + * @returns {Boolean} + * @api private + */ + +function config (name) { + // accessing global.localStorage can trigger a DOMException in sandboxed iframes + try { + if (!global.localStorage) return false; + } catch (_) { + return false; + } + var val = global.localStorage[name]; + if (null == val) return false; + return String(val).toLowerCase() === 'true'; +} diff --git a/tests/corpus/readable-stream-disable/node_modules/util-deprecate/node.js b/tests/corpus/readable-stream-disable/node_modules/util-deprecate/node.js new file mode 100644 index 000000000..5e6fcff5d --- /dev/null +++ b/tests/corpus/readable-stream-disable/node_modules/util-deprecate/node.js @@ -0,0 +1,6 @@ + +/** + * For Node.js, simply re-export the core `util.deprecate` function. + */ + +module.exports = require('util').deprecate; diff --git a/tests/corpus/readable-stream-disable/node_modules/util-deprecate/package.json b/tests/corpus/readable-stream-disable/node_modules/util-deprecate/package.json new file mode 100644 index 000000000..2e79f89a9 --- /dev/null +++ b/tests/corpus/readable-stream-disable/node_modules/util-deprecate/package.json @@ -0,0 +1,27 @@ +{ + "name": "util-deprecate", + "version": "1.0.2", + "description": "The Node.js `util.deprecate()` function with browser support", + "main": "node.js", + "browser": "browser.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "git://github.com/TooTallNate/util-deprecate.git" + }, + "keywords": [ + "util", + "deprecate", + "browserify", + "browser", + "node" + ], + "author": "Nathan Rajlich (http://n8.io/)", + "license": "MIT", + "bugs": { + "url": "https://github.com/TooTallNate/util-deprecate/issues" + }, + "homepage": "https://github.com/TooTallNate/util-deprecate" +} diff --git a/tests/diagnostics/error-fields-cause-fences.ts b/tests/diagnostics/error-fields-cause-fences.ts new file mode 100644 index 000000000..072271379 --- /dev/null +++ b/tests/diagnostics/error-fields-cause-fences.ts @@ -0,0 +1,11 @@ +// Node prints an error's own properties in creation order, which puts a +// declared `cause` among the class's other fields. Inspecting an error +// class that declares `cause` beside other fields has no lowering. +import * as util from "node:util"; + +class Retried extends Error { + override cause: unknown = "first attempt"; + retries = 2; +} + +console.log(util.inspect(new Retried("failed"))); diff --git a/tests/diagnostics/error-header-accessor-fences.ts b/tests/diagnostics/error-header-accessor-fences.ts new file mode 100644 index 000000000..b9e37df2d --- /dev/null +++ b/tests/diagnostics/error-header-accessor-fences.ts @@ -0,0 +1,15 @@ +// Inspecting an Error subclass instance renders its header from the +// instance's own name and message. A program class that declares a `name`, +// `message` or `stack` accessor changes what Node reads, so inspecting the +// hierarchy has no lowering. +import * as util from "node:util"; + +class Named extends Error { + override get name(): string { + return "Custom"; + } +} +class Deeper extends Named {} + +console.log(util.inspect(new Named("accessor"))); +console.log(new Deeper("inherited accessor")); diff --git a/tests/diagnostics/error-header-prototype-fences.ts b/tests/diagnostics/error-header-prototype-fences.ts new file mode 100644 index 000000000..33bec4748 --- /dev/null +++ b/tests/diagnostics/error-header-prototype-fences.ts @@ -0,0 +1,9 @@ +// A write to a prototype's `name` changes the header Node prints for an +// Error subclass instance, which renders from the instance's own name, so +// inspecting a program-declared Error subclass has no lowering. +import * as util from "node:util"; + +class Prototyped extends Error {} +Prototyped.prototype.name = "PrototypedError"; + +console.log(util.inspect(new Prototyped("prototype name"))); diff --git a/tests/diagnostics/fresh-spread-exported/items.ts b/tests/diagnostics/fresh-spread-exported/items.ts new file mode 100644 index 000000000..dd1574ab1 --- /dev/null +++ b/tests/diagnostics/fresh-spread-exported/items.ts @@ -0,0 +1,10 @@ +// Exported consts: the importing module reaches their elements, so the +// spreads here keep the element-layout refusal. Copying the elements +// would hide main.ts's write from `results`. +export type Result = { message: string; file: { path: string } | undefined; noisy: boolean }; +export const results: Result[] = []; +export const shared = [{ message: "shared", file: undefined, noisy: false }]; +results.push(...shared); +const listed = [{ message: "listed", file: undefined, noisy: true }]; +export { listed }; +results.unshift(...listed); diff --git a/tests/diagnostics/fresh-spread-exported/main.ts b/tests/diagnostics/fresh-spread-exported/main.ts new file mode 100644 index 000000000..dd2090acd --- /dev/null +++ b/tests/diagnostics/fresh-spread-exported/main.ts @@ -0,0 +1,6 @@ +// An exported const array spread once in its own module: in Node, writes +// through the importer's binding show in the array the spread filled. +import { listed, results, shared } from "./items.js"; +shared[0].message = "changed"; +listed[0].noisy = false; +console.log(results.map((r) => `${r.message}:${r.noisy}`).join(" ")); diff --git a/tests/diagnostics/fresh-spread-repeated.ts b/tests/diagnostics/fresh-spread-repeated.ts new file mode 100644 index 000000000..4b1a6d341 --- /dev/null +++ b/tests/diagnostics/fresh-spread-repeated.ts @@ -0,0 +1,16 @@ +// A const array spread that runs more than once per evaluation of its +// initializer inserts the same elements each time: in a loop body or in a +// function, `results[1] === results[2]` holds in Node. Converting the +// elements into the destination's layout would copy them on each run, so +// these spreads keep the element-layout refusal. +type Result = { message: string; file: { path: string } | undefined; noisy: boolean }; +const results: Result[] = []; +const looped = [{ message: "loop", file: undefined, noisy: false }]; +for (let i = 0; i < 2; i++) results.push(...looped); +const called = [{ message: "call", file: undefined, noisy: true }]; +function addCalled(): void { + results.unshift(...called); +} +addCalled(); +addCalled(); +console.log(results.length, results[0] === results[1]); diff --git a/tests/diagnostics/nested-member-blocker-path.ts b/tests/diagnostics/nested-member-blocker-path.ts new file mode 100644 index 000000000..60edd053c --- /dev/null +++ b/tests/diagnostics/nested-member-blocker-path.ts @@ -0,0 +1,19 @@ +// A record blocked by a member deep inside it names the member's path: +// through a function's result (`()`), the promise and union around it, +// record members (`.name`) and array elements (`[]`). +interface Job { + id: string; + handle: Promise & { cancel(): void }; +} +interface Order { + id: string; + jobs: Job[]; +} +interface Provider { + name: string; + poll: () => Promise; +} +function makeProvider(): Provider { + return { name: "p", poll: async () => undefined }; +} +console.log(makeProvider().name); diff --git a/tests/diagnostics/pick-view-generic-methods.ts b/tests/diagnostics/pick-view-generic-methods.ts new file mode 100644 index 000000000..912867dc6 --- /dev/null +++ b/tests/diagnostics/pick-view-generic-methods.ts @@ -0,0 +1,16 @@ +// A generic method called through a `Pick` view of a class compiles +// against that class. A view that holds an instance of a structurally +// compatible, unrelated class is refused: Node calls that class's own +// method. +class Store { + load(key: string, map: (text: string) => T): T { + return map(key); + } +} +class Mirror { + load(key: string, map: (text: string) => T): T { + return map(`${key}!`); + } +} +const view: Pick = new Mirror(); +console.log(view.load("k", (text) => text.length)); diff --git a/tests/fixtures/node-types/lookup-and-header-entries.ts b/tests/fixtures/node-types/lookup-and-header-entries.ts index a87f612d7..dff99b529 100644 --- a/tests/fixtures/node-types/lookup-and-header-entries.ts +++ b/tests/fixtures/node-types/lookup-and-header-entries.ts @@ -18,8 +18,23 @@ function createLookup(): ( ) => void { return function lookup(hostname, opts, callback) { dns.lookup(hostname, opts as dns.LookupOneOptions, (err, address, family) => { - if (err) return callback(err, "", 0); - callback(null, address, family); + if (err) { + callback(err, address as string, family); + return; + } + // With `all: true` Node answers an address list whatever the + // overload the cast selected says. + if (Array.isArray(address)) { + const v4: dns.LookupAddress[] = []; + for (const entry of address) if (entry.family === 4) v4.push(entry); + // The same list read through casts to the declared element type. + const viaCast = (address as dns.LookupAddress[]).map((entry) => entry.address); + const viaShape = address.map((entry) => (entry as { address: string }).address); + console.log("casts", viaCast.join(","), viaShape.join(",")); + callback(null, v4); + return; + } + callback(null, address as string, family); }); }; } @@ -40,8 +55,11 @@ function flatten(input: HeadersLike | undefined): string[] { const lookup = createLookup(); lookup("localhost", { family: 4 }, (err, address, family) => { console.log("one", err, address, family); - lookup("127.0.0.1", 4, (err3, address3, family3) => { - console.log("family", err3, address3, family3); - console.log(flatten({ accept: "text/plain", "x-list": ["a", "b"], "x-skip": undefined }).join(",")); + lookup("localhost", { all: true }, (err2, addresses) => { + console.log("all", err2, JSON.stringify(addresses)); + lookup("127.0.0.1", 4, (err3, address3, family3) => { + console.log("family", err3, address3, family3); + console.log(flatten({ accept: "text/plain", "x-list": ["a", "b"], "x-skip": undefined }).join(",")); + }); }); }); diff --git a/tests/fixtures/node-types/process-versions-absent.ts b/tests/fixtures/node-types/process-versions-absent.ts new file mode 100644 index 000000000..8a59f7a58 --- /dev/null +++ b/tests/fixtures/node-types/process-versions-absent.ts @@ -0,0 +1,35 @@ +/* @types/node declares http_parser, v8, uv, zlib, ares and modules as + * strings. A compiled binary provides only node and openssl, and Node 24 + * has no http_parser either, so those entries read undefined and compare + * like it. Checks on the others print agreement between equality, typeof + * and `in`, which holds whichever entries the running process has. */ +const versions = process.versions; + +console.log(process.versions.http_parser === undefined, process.versions['http_parser'] === undefined); +console.log(versions.http_parser == null, typeof versions.http_parser, versions.http_parser ?? 'none'); +const parser = versions.http_parser; +console.log(parser === undefined ? 'no http_parser' : parser); +if (versions.http_parser) console.log('http_parser truthy'); +else console.log('http_parser falsy'); + +console.log( + (process.versions.v8 === undefined) === !('v8' in process.versions), + (process.versions['uv'] !== undefined) === 'uv' in versions, + (versions.zlib == null) === (typeof versions.zlib === 'undefined'), + (versions.ares === undefined) === (typeof versions.ares !== 'string'), +); +const names = ['v8', 'uv', 'zlib', 'ares', 'modules', 'node', 'openssl'] as const; +for (const name of names) { + const value = versions[name]; + const absent = value === undefined; + console.log( + name, + absent === !(name in versions), + absent === (typeof value === 'undefined'), + ((value ?? '') === '') === absent, + ); +} +const v8 = versions.v8; +console.log(v8 === undefined || v8.length > 0); +console.log(typeof (versions.modules || 'fallback')); +console.log(typeof versions.node, versions.openssl !== undefined, versions.node === process.version.slice(1)); diff --git a/tests/fixtures/npm/cases/2585-builtin-polyfill-aliases/main.ts b/tests/fixtures/npm/cases/2585-builtin-polyfill-aliases/main.ts new file mode 100644 index 000000000..e309dbd9d --- /dev/null +++ b/tests/fixtures/npm/cases/2585-builtin-polyfill-aliases/main.ts @@ -0,0 +1,12 @@ +// Polyfill packages whose module IS a Node builtin on Node 24, installed as +// their published releases: isarray (Array.isArray), safe-buffer (buffer), +// process-nextick-args (process), inherits (util.inherits) and +// util-deprecate (util.deprecate) compile as the builtins. +import { encode, kinds, later, oldArea, square } from "gtpolyfills"; + +console.log(kinds([[1], "a", 2, [], { length: 0 }])); +console.log(encode("héllo")); +console.log(square(4)); +console.log(oldArea(3)); +later("tick", (message) => console.log(message)); +console.log("sync done"); diff --git a/tests/fixtures/npm/cases/2586-readable-stream-builtin/main.ts b/tests/fixtures/npm/cases/2586-readable-stream-builtin/main.ts new file mode 100644 index 000000000..d787da55b --- /dev/null +++ b/tests/fixtures/npm/cases/2586-readable-stream-builtin/main.ts @@ -0,0 +1,10 @@ +// readable-stream 2.x and 4.x compile as Node's builtin stream module: +// the package's own READABLE_STREAM=disable behavior. The Node side of this +// case runs with READABLE_STREAM=disable; without it Node runs the +// package's JavaScript streams, whose output can differ. +import { kinds, shout } from "gtrs2"; +import { count } from "gtrs4"; + +console.log(kinds()); +shout(["a", "bc", "def"], (result) => console.log("2.x", result)); +count(["one", "two", "three"], (total) => console.log("4.x", total)); diff --git a/tests/fixtures/npm/cases/2587-bundled-polyfill-aliases/main.ts b/tests/fixtures/npm/cases/2587-bundled-polyfill-aliases/main.ts new file mode 100644 index 000000000..808c0f9d6 --- /dev/null +++ b/tests/fixtures/npm/cases/2587-bundled-polyfill-aliases/main.ts @@ -0,0 +1,14 @@ +// esbuild-bundled polyfill packages: __commonJS chunks whose keys name +// isarray, safe-buffer, process-nextick-args, inherits, util-deprecate and +// readable-stream 2.x at their versions compile as the Node builtins the +// modules are on Node 24 (readable-stream as its READABLE_STREAM=disable +// behavior: the Node side of this case runs with that variable set). +import { encode, kinds, later, oldArea, shout, square } from "gtpolybundle"; + +console.log(kinds([[1], "a", 2, []])); +console.log(encode("héllo")); +console.log(square(4)); +console.log(oldArea(3)); +later("tick", (message) => console.log(message)); +shout(["a", "bc"], (result) => console.log(result)); +console.log("sync done"); diff --git a/tests/fixtures/npm/cases/2588-bundled-esm-classes/main.ts b/tests/fixtures/npm/cases/2588-bundled-esm-classes/main.ts new file mode 100644 index 000000000..e61e4b654 --- /dev/null +++ b/tests/fixtures/npm/cases/2588-bundled-esm-classes/main.ts @@ -0,0 +1,8 @@ +// Classes a bundled ES module defines inside its lazy __esm initializer, +// one extending stream.Readable and one extending that class, compile as +// class declarations: the stored class values have no static +// representation for a runtime-provided base. +import { wrap } from "gtesmclass"; + +console.log("start"); +wrap("body", (result) => console.log(result)); diff --git a/tests/fixtures/npm/cases/2589-bundled-error-subclass/main.ts b/tests/fixtures/npm/cases/2589-bundled-error-subclass/main.ts new file mode 100644 index 000000000..c81d26bb5 --- /dev/null +++ b/tests/fixtures/npm/cases/2589-bundled-error-subclass/main.ts @@ -0,0 +1,24 @@ +// A program class extending an error class that a bundled package defines +// inside a lazy __esm initializer (build-utils' NowBuildError shape): `new` +// on the subclass, `instanceof` against both classes, and errors the +// package itself throws. +import { AppError, fail, lazyCode } from "gtesmerror"; + +class TaggedError extends AppError { + tag: string; + constructor(code: string) { + super({ message: `tagged ${code}`, code }); + this.tag = "t"; + } +} + +const e = new TaggedError("X1"); +console.log(e.message, e.code, e.tag, e.hideStackTrace); +console.log(e instanceof AppError, e instanceof Error, e instanceof TaggedError); +try { + fail("BOOM"); +} catch (err) { + console.log(err instanceof AppError, err instanceof TaggedError); + if (err instanceof AppError) console.log(err.code, err.message); +} +console.log(lazyCode()); diff --git a/tests/fixtures/npm/cases/2590-bundled-fs-extra/main.ts b/tests/fixtures/npm/cases/2590-bundled-fs-extra/main.ts new file mode 100644 index 000000000..6b6772f34 --- /dev/null +++ b/tests/fixtures/npm/cases/2590-bundled-fs-extra/main.ts @@ -0,0 +1,9 @@ +// A bundle that embeds fs-extra 9.0.0 (as build-utils does): its fs-extra +// chunk compiles as scriptc's built-in fs-extra, and the package's own +// chunks (graceful-fs, universalify, jsonfile) are never loaded. +import { mkdtempSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { demo } from "gtfsxbundle"; + +demo(mkdtempSync(join(tmpdir(), "scriptc-bundled-fs-extra-"))).then((result) => console.log(result)); diff --git a/tests/fixtures/npm/cases/opentelemetry-api/main.ts b/tests/fixtures/npm/cases/opentelemetry-api/main.ts new file mode 100644 index 000000000..66a86183b --- /dev/null +++ b/tests/fixtures/npm/cases/opentelemetry-api/main.ts @@ -0,0 +1,26 @@ +// The real @opentelemetry/api (1.9.0, its CommonJS build) with no SDK +// registered: the metrics and trace entry points answer their no-op +// implementations. The package's tsc output opens each module with an +// `exports.X = void 0` preamble and builds its enums through +// `exports.E || (exports.E = {})`, and its index adds a default-export +// object next to the named re-exports. +import { metrics, trace, DiagLogLevel, SpanKind, type Counter } from "@opentelemetry/api"; + +let counter: Counter | undefined; +function getCounter(): Counter { + if (!counter) { + counter = metrics.getMeter("example-meter").createCounter("example.count", { + description: "counts examples", + }); + } + return counter; +} + +getCounter().add(1, { reason: "first" }); +getCounter().add(2); +console.log("meter provider", typeof metrics.getMeterProvider().getMeter); +console.log("levels", DiagLogLevel.ERROR, DiagLogLevel.DEBUG, DiagLogLevel[DiagLogLevel.WARN]); +console.log("span kinds", SpanKind.SERVER, SpanKind[SpanKind.CLIENT]); +const span = trace.getTracer("example-tracer").startSpan("work"); +console.log("span recording", span.isRecording()); +span.end(); 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":"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"} \ 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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AQtBwQEhKgsDQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJA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UGoON+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..6c9b6d0dc --- /dev/null +++ b/tests/fixtures/npm/cases/undici7/main.ts @@ -0,0 +1,368 @@ +// 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 hostAgent = new Agent(); + try { + await request(`${origin}/hello`, { dispatcher: hostAgent, headers: { host: "a\r\nx-injected: 1" } }); + } catch (err) { + console.log("host header:", describe(err)); + } + await hostAgent.close(); + 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/h82AhAgA0EGNgIMDNEBC0H4ACECDLcBCyADQZABNgIcIAMgATYCFCADIAA2AgxBACECDM8BC0EAIQACQCADKAI4IgJFDQAgAigCQCICRQ0AIAMgAhEAACEACyAARQ0AIABBFUYNASADQQA2AhwgAyABNgIUIANBgg82AhAgA0EgNgIMQQAhAgzOAQtB9wAhAgy0AQsgA0GPATYCHCADIAE2AhQgA0HsGzYCECADQRU2AgxBACECDMwBCyABIARGBEBBjwEhAgzMAQsCQCABLQAAQSBGBEAgAUEBaiEBDAELIANBADYCHCADIAE2AhQgA0GbHzYCECADQQY2AgxBACECDMwBC0ECIQIMsgELA0AgAS0AAEEgRw0CIAQgAUEBaiIBRw0AC0GOASECDMoBCyABIARGBEBBjQEhAgzKAQsCQCABLQAAQQlrDgRKAABKAAtB9QAhAgywAQsgAy0AKUEFRgRAQfYAIQIMsAELQfQAIQIMrwELIAEgBEYEQEGMASECDMgBCyADQRA2AgggAyABNgIEDAoLIAEgBEYEQEGLASECDMcBCwJAIAEtAABBCWsOBEcAAEcAC0HzACECDK0BCyABIARHBEAgA0EQNgIIIAMgATYCBEHxACECDK0BC0GKASECDMUBCwJAIAEgBEcEQANAIAEtAABBoNAAai0AACIAQQNHBEACQCAAQQFrDgJJAAQLQfAAIQIMrwELIAQgAUEBaiIBRw0AC0GIASECDMYBC0GIASECDMUBCyADQQA2AhwgAyABNgIUIANB2yA2AhAgA0EHNgIMQQAhAgzEAQsgASAERgRAQYkBIQIMxAELAkACQAJAIAEtAABBoNIAai0AAEEBaw4DRgIAAQtB8gAhAgysAQsgA0EANgIcIAMgATYCFCADQbQSNgIQIANBBzYCDEEAIQIMxAELQeoAIQIMqgELIAEgBEcEQCABQQFqIQFB7wAhAgyqAQtBhwEhAgzCAQsgBCABIgBGBEBBhgEhAgzCAQsgAC0AACIBQS9GBEAgAEEBaiEBQe4AIQIMqQELIAFBCWsiAkEXSw0BIAAhAUEBIAJ0QZuAgARxDUEMAQsgBCABIgBGBEBBhQEhAgzBAQsgAC0AAEEvRw0AIABBAWohAQwDC0EAIQIgA0EANgIcIAMgADYCFCADQdsgNgIQIANBBzYCDAy/AQsCQAJAAkACQAJAA0AgAS0AAEGgzgBqLQAAIgBBBUcEQAJAAkAgAEEBaw4IRwUGBwgABAEIC0HrACECDK0BCyABQQFqIQFB7QAhAgysAQsgBCABQQFqIgFHDQALQYQBIQIMwwELIAFBAWoMFAsgAygCBCEAIANBADYCBCADIAAgARAsIgBFDR4gA0HbADYCHCADIAE2AhQgAyAANgIMQQAhAgzBAQsgAygCBCEAIANBADYCBCADIAAgARAsIgBFDR4gA0HdADYCHCADIAE2AhQgAyAANgIMQQAhAgzAAQsgAygCBCEAIANBADYCBCADIAAgARAsIgBFDR4gA0H6ADYCHCADIAE2AhQgAyAANgIMQQAhAgy/AQsgA0EANgIcIAMgATYCFCADQfkPNgIQIANBBzYCDEEAIQIMvgELIAEgBEYEQEGDASECDL4BCwJAIAEtAABBoM4Aai0AAEEBaw4IPgQFBgAIAgMHCyABQQFqIQELQQMhAgyjAQsgAUEBagwNC0EAIQIgA0EANgIcIANB0RI2AhAgA0EHNgIMIAMgAUEBajYCFAy6AQsgAygCBCEAIANBADYCBCADIAAgARAsIgBFDRYgA0HbADYCHCADIAE2AhQgAyAANgIMQQAhAgy5AQsgAygCBCEAIANBADYCBCADIAAgARAsIgBFDRYgA0HdADYCHCADIAE2AhQgAyAANgIMQQAhAgy4AQsgAygCBCEAIANBADYCBCADIAAgARAsIgBFDRYgA0H6ADYCHCADIAE2AhQgAyAANgIMQQAhAgy3AQsgA0EANgIcIAMgATYCFCADQfkPNgIQIANBBzYCDEEAIQIMtgELQewAIQIMnAELIAEgBEYEQEGCASECDLUBCyABQQFqDAILIAEgBEYEQEGBASECDLQBCyABQQFqDAELIAEgBEYNASABQQFqCyEBQQQhAgyYAQtBgAEhAgywAQsDQCABLQAAQaDMAGotAAAiAEECRwRAIABBAUcEQEHpACECDJkBCwwxCyAEIAFBAWoiAUcNAAtB/wAhAgyvAQsgASAERgRAQf4AIQIMrwELAkAgAS0AAEEJaw43LwMGLwQGBgYGBgYGBgYGBgYGBgYGBgYFBgYCBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGAAYLIAFBAWoLIQFBBSECDJQBCyABQQFqDAYLIAMoAgQhACADQQA2AgQgAyAAIAEQLCIARQ0IIANB2wA2AhwgAyABNgIUIAMgADYCDEEAIQIMqwELIAMoAgQhACADQQA2AgQgAyAAIAEQLCIARQ0IIANB3QA2AhwgAyABNgIUIAMgADYCDEEAIQIMqgELIAMoAgQhACADQQA2AgQgAyAAIAEQLCIARQ0IIANB+gA2AhwgAyABNgIUIAMgADYCDEEAIQIMqQELIANBADYCHCADIAE2AhQgA0GNFDYCECADQQc2AgxBACECDKgBCwJAAkACQAJAA0AgAS0AAEGgygBqLQAAIgBBBUcEQAJAIABBAWsOBi4DBAUGAAYLQegAIQIMlAELIAQgAUEBaiIBRw0AC0H9ACECDKsBCyADKAIEIQAgA0EANgIEIAMgACABECwiAEUNByADQdsANgIcIAMgATYCFCADIAA2AgxBACECDKoBCyADKAIEIQAgA0EANgIEIAMgACABECwiAEUNByADQd0ANgIcIAMgATYCFCADIAA2AgxBACECDKkBCyADKAIEIQAgA0EANgIEIAMgACABECwiAEUNByADQfoANgIcIAMgATYCFCADIAA2AgxBACECDKgBCyADQQA2AhwgAyABNgIUIANB5Ag2AhAgA0EHNgIMQQAhAgynAQsgASAERg0BIAFBAWoLIQFBBiECDIwBC0H8ACECDKQBCwJAAkACQAJAA0AgAS0AAEGgyABqLQAAIgBBBUcEQCAAQQFrDgQpAgMEBQsgBCABQQFqIgFHDQALQfsAIQIMpwELIAMoAgQhACADQQA2AgQgAyAAIAEQLCIARQ0DIANB2wA2AhwgAyABNgIUIAMgADYCDEEAIQIMpgELIAMoAgQhACADQQA2AgQgAyAAIAEQLCIARQ0DIANB3QA2AhwgAyABNgIUIAMgADYCDEEAIQIMpQELIAMoAgQhACADQQA2AgQgAyAAIAEQLCIARQ0DIANB+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". + * + * ::= | " " + * ::=