From 992f578589283af8b90cf7a4193eb7b22703a742 Mon Sep 17 00:00:00 2001 From: "Node.js GitHub Bot" Date: Wed, 14 Oct 2026 09:25:59 +0000 Subject: [PATCH] deps: update undici to 8.12.1 --- deps/undici/src/CONTRIBUTING.md | 90 +- deps/undici/src/README.md | 28 +- deps/undici/src/build/{wasm.js => llhttp.js} | 2 +- deps/undici/src/build/milo.js | 53 + deps/undici/src/deps/milo/CODE_OF_CONDUCT.md | 76 + deps/undici/src/deps/milo/Dockerfile | 36 + deps/undici/src/deps/milo/LICENSE.md | 7 + deps/undici/src/deps/milo/Makefile.toml | 140 + deps/undici/src/deps/milo/README.md | 349 + deps/undici/src/deps/milo/macros/Cargo.toml | 30 + deps/undici/src/deps/milo/macros/README.md | 34 + .../deps/milo/macros/constants/callbacks.yml | 29 + .../src/deps/milo/macros/constants/errors.yml | 19 + .../deps/milo/macros/constants/methods.yml | 19 + .../src/deps/milo/macros/constants/states.yml | 16 + .../src/deps/milo/macros/src/actions.rs | 172 + .../src/deps/milo/macros/src/generators.rs | 443 + deps/undici/src/deps/milo/macros/src/lib.rs | 39 + .../src/deps/milo/macros/src/matchers.rs | 16 + .../undici/src/deps/milo/macros/src/native.rs | 90 + .../src/deps/milo/macros/src/parser_fields.rs | 122 + .../src/deps/milo/macros/src/structs.rs | 65 + deps/undici/src/deps/milo/macros/src/wasm.rs | 115 + deps/undici/src/deps/milo/parser/Cargo.toml | 45 + .../undici/src/deps/milo/parser/Makefile.toml | 64 + deps/undici/src/deps/milo/parser/README.md | 26 + .../undici/src/deps/milo/parser/cbindgen.toml | 21 + deps/undici/src/deps/milo/parser/src/lib.rs | 359 + deps/undici/src/deps/milo/parser/src/main.rs | 271 + .../src/deps/milo/parser/src/matchers.rs | 489 ++ .../undici/src/deps/milo/parser/src/native.rs | 220 + deps/undici/src/deps/milo/parser/src/parse.rs | 1578 ++++ deps/undici/src/deps/milo/parser/src/wasm.rs | 363 + .../deps/milo/parser/wasm/src/package.json | 38 + .../src/deps/milo/parser/wasm/src/template.js | 129 + deps/undici/src/deps/milo/rust-toolchain.toml | 2 + deps/undici/src/deps/milo/scripts/Cargo.toml | 19 + .../deps/milo/scripts/src/bin/buildinfo.rs | 8 + .../scripts/src/bin/import-llhttp-tests.rs | 549 ++ .../milo/scripts/src/bin/postbuild-cpp.rs | 66 + .../milo/scripts/src/bin/postbuild-wasm.rs | 391 + .../milo/scripts/src/bin/sync-versions.rs | 39 + deps/undici/src/deps/milo/scripts/src/lib.rs | 109 + deps/undici/src/docs/docs/api/CacheStore.md | 33 +- deps/undici/src/docs/docs/api/Client.md | 8 +- deps/undici/src/docs/docs/api/Dispatcher.md | 3 + deps/undici/src/docs/docs/api/Errors.md | 77 +- deps/undici/src/docs/docs/api/H2CClient.md | 5 +- deps/undici/src/docs/docs/api/Interceptors.md | 19 +- .../src/docs/docs/api/RedirectHandler.md | 23 +- deps/undici/src/docs/docs/index.md | 24 +- deps/undici/src/docs/docs/type-map.json | 132 +- deps/undici/src/eslint.config.js | 2 + .../src/lib/cache/memory-cache-store.js | 61 +- .../src/lib/cache/sqlite-cache-store.js | 88 +- deps/undici/src/lib/core/connect.js | 14 +- deps/undici/src/lib/core/constants.js | 13 +- deps/undici/src/lib/core/errors.js | 39 +- deps/undici/src/lib/core/request.js | 17 +- deps/undici/src/lib/core/symbols.js | 1 + deps/undici/src/lib/core/util.js | 27 +- deps/undici/src/lib/dispatcher/client-h1.js | 194 +- deps/undici/src/lib/dispatcher/client-h2.js | 56 +- deps/undici/src/lib/dispatcher/client.js | 5 +- .../src/lib/dispatcher/dispatcher-base.js | 5 +- .../src/lib/dispatcher/dispatcher1-wrapper.js | 23 +- deps/undici/src/lib/dispatcher/parser-h1.js | 1064 +++ deps/undici/src/lib/dispatcher/pool-base.js | 93 +- deps/undici/src/lib/dispatcher/pool.js | 19 +- deps/undici/src/lib/dispatcher/proxy-agent.js | 26 +- deps/undici/src/lib/dispatcher/retry-agent.js | 10 + .../src/lib/dispatcher/round-robin-pool.js | 20 +- deps/undici/src/lib/handler/cache-handler.js | 110 +- .../src/lib/handler/deduplication-handler.js | 93 +- .../src/lib/handler/redirect-handler.js | 20 +- deps/undici/src/lib/handler/retry-handler.js | 55 +- deps/undici/src/lib/interceptor/cache.js | 14 +- deps/undici/src/lib/interceptor/decompress.js | 11 +- deps/undici/src/lib/interceptor/dns.js | 11 + .../src/lib/interceptor/response-error.js | 100 +- deps/undici/src/lib/llhttp/wasm_build_env.txt | 2 +- deps/undici/src/lib/milo/LICENSE.md | 7 + deps/undici/src/lib/milo/no-simd.js | 1002 +++ deps/undici/src/lib/milo/simd.js | 1002 +++ deps/undici/src/lib/mock/mock-agent.js | 11 + deps/undici/src/lib/mock/snapshot-recorder.js | 5 +- deps/undici/src/lib/mock/snapshot-utils.js | 5 +- deps/undici/src/lib/util/cache.js | 16 +- deps/undici/src/lib/web/cookies/util.js | 21 +- .../lib/web/eventsource/eventsource-stream.js | 32 + .../src/lib/web/eventsource/eventsource.js | 27 +- deps/undici/src/lib/web/fetch/body.js | 85 +- .../src/lib/web/fetch/formdata-parser.js | 16 +- deps/undici/src/lib/web/fetch/headers.js | 4 +- deps/undici/src/lib/web/fetch/index.js | 6 +- deps/undici/src/lib/web/websocket/receiver.js | 16 +- deps/undici/src/package-lock.json | 76 +- deps/undici/src/package.json | 12 +- deps/undici/src/pnpm-workspace.yaml | 5 + deps/undici/src/scripts/release.js | 7 +- deps/undici/src/types/cache-interceptor.d.ts | 10 + deps/undici/src/types/client.d.ts | 6 +- deps/undici/src/types/errors.d.ts | 17 +- deps/undici/src/types/h2c-client.d.ts | 2 +- deps/undici/src/types/interceptors.d.ts | 7 +- deps/undici/undici.js | 7157 +++++++++-------- src/undici_version.h | 2 +- 107 files changed, 14876 insertions(+), 3973 deletions(-) rename deps/undici/src/build/{wasm.js => llhttp.js} (98%) create mode 100644 deps/undici/src/build/milo.js create mode 100644 deps/undici/src/deps/milo/CODE_OF_CONDUCT.md create mode 100644 deps/undici/src/deps/milo/Dockerfile create mode 100644 deps/undici/src/deps/milo/LICENSE.md create mode 100644 deps/undici/src/deps/milo/Makefile.toml create mode 100644 deps/undici/src/deps/milo/README.md create mode 100644 deps/undici/src/deps/milo/macros/Cargo.toml create mode 100644 deps/undici/src/deps/milo/macros/README.md create mode 100644 deps/undici/src/deps/milo/macros/constants/callbacks.yml create mode 100644 deps/undici/src/deps/milo/macros/constants/errors.yml create mode 100644 deps/undici/src/deps/milo/macros/constants/methods.yml create mode 100644 deps/undici/src/deps/milo/macros/constants/states.yml create mode 100644 deps/undici/src/deps/milo/macros/src/actions.rs create mode 100644 deps/undici/src/deps/milo/macros/src/generators.rs create mode 100644 deps/undici/src/deps/milo/macros/src/lib.rs create mode 100644 deps/undici/src/deps/milo/macros/src/matchers.rs create mode 100644 deps/undici/src/deps/milo/macros/src/native.rs create mode 100644 deps/undici/src/deps/milo/macros/src/parser_fields.rs create mode 100644 deps/undici/src/deps/milo/macros/src/structs.rs create mode 100644 deps/undici/src/deps/milo/macros/src/wasm.rs create mode 100644 deps/undici/src/deps/milo/parser/Cargo.toml create mode 100644 deps/undici/src/deps/milo/parser/Makefile.toml create mode 100644 deps/undici/src/deps/milo/parser/README.md create mode 100644 deps/undici/src/deps/milo/parser/cbindgen.toml create mode 100644 deps/undici/src/deps/milo/parser/src/lib.rs create mode 100644 deps/undici/src/deps/milo/parser/src/main.rs create mode 100644 deps/undici/src/deps/milo/parser/src/matchers.rs create mode 100644 deps/undici/src/deps/milo/parser/src/native.rs create mode 100644 deps/undici/src/deps/milo/parser/src/parse.rs create mode 100644 deps/undici/src/deps/milo/parser/src/wasm.rs create mode 100644 deps/undici/src/deps/milo/parser/wasm/src/package.json create mode 100644 deps/undici/src/deps/milo/parser/wasm/src/template.js create mode 100644 deps/undici/src/deps/milo/rust-toolchain.toml create mode 100644 deps/undici/src/deps/milo/scripts/Cargo.toml create mode 100644 deps/undici/src/deps/milo/scripts/src/bin/buildinfo.rs create mode 100644 deps/undici/src/deps/milo/scripts/src/bin/import-llhttp-tests.rs create mode 100644 deps/undici/src/deps/milo/scripts/src/bin/postbuild-cpp.rs create mode 100644 deps/undici/src/deps/milo/scripts/src/bin/postbuild-wasm.rs create mode 100644 deps/undici/src/deps/milo/scripts/src/bin/sync-versions.rs create mode 100644 deps/undici/src/deps/milo/scripts/src/lib.rs create mode 100644 deps/undici/src/lib/dispatcher/parser-h1.js create mode 100644 deps/undici/src/lib/milo/LICENSE.md create mode 100644 deps/undici/src/lib/milo/no-simd.js create mode 100644 deps/undici/src/lib/milo/simd.js create mode 100644 deps/undici/src/pnpm-workspace.yaml diff --git a/deps/undici/src/CONTRIBUTING.md b/deps/undici/src/CONTRIBUTING.md index 35f2773e6354..9dd6b011b5a3 100644 --- a/deps/undici/src/CONTRIBUTING.md +++ b/deps/undici/src/CONTRIBUTING.md @@ -2,6 +2,7 @@ * [Guides](#guides) * [Update `llhttp`](#update-llhttp) + * [Update `milo`](#update-milo) * [Lint](#lint) * [Test](#test) * [Coverage](#coverage) @@ -59,7 +60,7 @@ npm i > This requires [docker](https://www.docker.com/) installed on your machine. ```bash -npm run build:wasm +npm run build-wasm ``` #### Copy the sources to `undici` @@ -83,13 +84,94 @@ cp src/native/api.h build/llhttp.h /deps/llhttp/include/ ```bash cd -npm run build:wasm +npm run build:llhttp ``` #### Commit the contents of lib/llhttp Create a commit which includes all of the updated files in lib/llhttp. + +### Update `milo` + +Milo is built from the upstream sources vendored in `deps/milo`, using its own +Dockerfile rather than a prebuilt npm package. Updating it is manual, like llhttp. +The standalone release CommonJS modules and their license are saved in `lib/milo`. + +The current vendored release is 0.8.5 at commit +`2ec4642184f20cd1803e30650becc54bbd575c6c`. + +#### Clone milo and checkout a release + +```sh +git clone https://github.com/ShogunPanda/milo.git +cd milo +git checkout v0.8.5 +``` + +#### Copy the sources into Undici + +Replace the previous contents of `deps/milo` with the following files and +directories from the selected release, preserving their layout: + +```sh +cp Dockerfile Makefile.toml rust-toolchain.toml LICENSE.md README.md CODE_OF_CONDUCT.md /deps/milo/ +cp -R macros scripts /deps/milo/ +mkdir -p /deps/milo/parser +cp parser/*.toml parser/*.lock parser/*.md /deps/milo/parser/ +cp -R parser/src /deps/milo/parser/ +``` + +Keep the Cargo manifests and all three lockfiles (`macros/Cargo.lock`, +`parser/Cargo.lock` and `scripts/Cargo.lock`), Rust binding generators and JavaScript +template. Preserve Undici's build adjustments: the Docker image digest, pinned +cargo-make version, Binaryen checksums and `--locked` Cargo builds. Update the +checksums for both Linux architectures whenever Binaryen is updated. +Do not copy `parser/tests`, `parser/examples`, `.git`, upstream workflows, +`target`, `dist`, or prebuilt npm packages. Use a clean checkout so no local build +outputs are included. When selecting a newer release, update the version and +full commit recorded above. + +#### Build and copy the release artifacts + +Docker is the only build tool required on the host. The upstream image installs +Rust, cargo-make and Binaryen, then runs `makers wasm` in an internal copy of the +read-only source checkout. See [milo's Docker build instructions](https://github.com/ShogunPanda/milo#building-webassembly-with-docker). + +From Undici, build the vendored sources: + +```sh +cd +npm run build:milo +``` + +`build/milo.js` follows those instructions, mounts `deps/milo` read-only, and +copies only these release artifacts: + +```text +release/package-cjs/src/simd/index.js -> lib/milo/simd.js +release/package-cjs/src/no-simd/index.js -> lib/milo/no-simd.js +release/package-cjs/LICENSE.md -> lib/milo/LICENSE.md +``` + +Each JavaScript module already embeds its WASM binary in base64. Separate `.wasm` +files, unbundled modules, debug packages and package metadata are not copied. +Temporary Docker outputs are removed when the script finishes. The script does +not download or update milo's sources; Docker still downloads the image and build +dependencies as needed. + +The shared-builtin CI job removes the bundled Milo modules and rebuilds them +before compiling Node.js, so that it cannot silently reuse prebuilt artifacts. +The npm package contains only the license and the two bundled JavaScript modules; +`lib/milo/binary`, `lib/milo/src` and separate WASM files are excluded. + +#### Submit the update + +Include the updated sources in `deps/milo`, generated files in `lib/milo` and +the documentation update in a manually prepared PR. This step prepares milo's +build inputs; it does not modify +the runtime parser, llhttp's build script or any workflows. + ### Update `WPTs` @@ -176,9 +258,9 @@ Use the "Create release PR" github action to generate a release PR. If you are packaging `undici` for a distro, this might help if you would like to use an unbundled version instead of bundling one in `libnode.so`. -To enable this, pass `EXTERNAL_PATH=/path/to/global/node_modules/undici` to `build/wasm.js`. +To enable this, pass `EXTERNAL_PATH=/path/to/global/node_modules/undici` to `build/llhttp.js`. Pass this path with `loader.js` appended to `--shared-builtin-undici/undici-path` in Node.js's `configure.py`. -If building on a non-Alpine Linux distribution, you may need to also set the `WASM_CC`, `WASM_CFLAGS`, `WASM_LDFLAGS` and `WASM_LDLIBS` environment variables before running `build/wasm.js`. +If building on a non-Alpine Linux distribution, you may need to also set the `WASM_CC`, `WASM_CFLAGS`, `WASM_LDFLAGS` and `WASM_LDLIBS` environment variables before running `build/llhttp.js`. Similarly, you can set the `WASM_OPT` environment variable to utilize your own `wasm-opt` optimizer. diff --git a/deps/undici/src/README.md b/deps/undici/src/README.md index 169d0cab13db..479059fbc1ab 100644 --- a/deps/undici/src/README.md +++ b/deps/undici/src/README.md @@ -86,6 +86,24 @@ Using [benchmark-http2.js](https://github.com/nodejs/undici/blob/main/benchmarks Node.js includes a built-in `fetch()` implementation powered by undici starting from Node.js v18. However, there are important differences between using the built-in fetch and installing undici as a separate module. +### WebAssembly requirement + +Undici's HTTP/1 parser requires WebAssembly. There is no alternative parser when +WebAssembly is unavailable, for example in Node.js configurations where +`--jitless` disables it. The existence of `fetch` does not imply that it can make +HTTP/1 requests in that environment. + +When the HTTP/1 parser is needed without WebAssembly, Client and dispatcher +requests fail with an `Error` whose code is `ERR_WEBASSEMBLY_NOT_SUPPORTED`. +`fetch` rejects with a `TypeError` carrying that error in `error.cause`. Importing +Undici, accessing `fetch`, and using `Headers`, `FormData`, `Request`, or `Response` +do not themselves require the HTTP/1 parser. + +The error is defined locally in Undici and does not depend on Node.js providing +that error code. For built-in fetch, this behavior depends on the bundled Undici +version. Node.js receives upstream fixes through its normal vendored Undici +update, rather than changes to the generated bundle alone. + ### Built-in Fetch (Node.js v18+) Node.js's built-in fetch is powered by a bundled version of undici: @@ -773,14 +791,18 @@ and `undici.Agent`) which will enable the family autoselection algorithm when es ## Collaborators -* [__Daniele Belardi__](https://github.com/dnlup), -* [__Ethan Arrowood__](https://github.com/ethan-arrowood), +* [__Aras Abbasi__](https://github.com/Uzlopak) +* [__Carlos Fuentes__](https://github.com/metcoder95), +* [__Ethan Arrowood__](https://github.com/Ethan-Arrowood), * [__Matteo Collina__](https://github.com/mcollina), * [__Matthew Aitken__](https://github.com/KhafraDev), +* [__Mert Can Altin__](https://github.com/mertcanaltin), * [__Robert Nagy__](https://github.com/ronag), -* [__Szymon Marczak__](https://github.com/szmarczak), +* [__Trivikram Kamat__](https://github.com/trivikr), +* [__tsctx__](https://github.com/tsctx) ## Past Collaborators +* [__Szymon Marczak__](https://github.com/szmarczak), * [__Tomas Della Vedova__](https://github.com/delvedor), ### Releasers diff --git a/deps/undici/src/build/wasm.js b/deps/undici/src/build/llhttp.js similarity index 98% rename from deps/undici/src/build/wasm.js rename to deps/undici/src/build/llhttp.js index 5357177d12e8..7862fb2f1edc 100644 --- a/deps/undici/src/build/wasm.js +++ b/deps/undici/src/build/llhttp.js @@ -62,7 +62,7 @@ if (process.argv[2] === '--docker') { cmd += ` --mount type=bind,source=${ROOT}/lib/llhttp,target=/home/node/build/lib/llhttp \ --mount type=bind,source=${ROOT}/build,target=/home/node/build/build \ --mount type=bind,source=${ROOT}/deps,target=/home/node/build/deps \ - -t ${WASM_BUILDER_CONTAINER} node build/wasm.js` + -t ${WASM_BUILDER_CONTAINER} node build/llhttp.js` console.log(`> ${cmd}\n\n`) execSync(cmd, { stdio: 'inherit' }) process.exit(0) // eslint-disable-line n/no-process-exit diff --git a/deps/undici/src/build/milo.js b/deps/undici/src/build/milo.js new file mode 100644 index 000000000000..9c6b57fd78f0 --- /dev/null +++ b/deps/undici/src/build/milo.js @@ -0,0 +1,53 @@ +'use strict' + +const { execFileSync } = require('node:child_process') +const { chmod, mkdtemp, readFile, cp, rm, mkdir } = require('node:fs/promises') +const { resolve } = require('node:path') + +const artifacts = { + 'release/package-cjs/src/no-simd/index.js': 'no-simd.js', + 'release/package-cjs/src/simd/index.js': 'simd.js', + 'release/package-cjs/LICENSE.md': 'LICENSE.md' +} + +function docker (...args) { + return execFileSync('docker', args, { stdio: 'inherit' }) +} + +async function buildMilo () { + // Setup some folders + const source = resolve(__dirname, '../deps/milo') + const destination = resolve(__dirname, '../lib/milo') + const temporary = await mkdtemp(resolve(__dirname, '../deps/.build-')) + const imageFile = resolve(temporary, 'id') + + try { + await chmod(temporary, 0o777) + docker('build', '--iidfile', imageFile, source) + + const image = (await readFile(imageFile, 'utf8')).trim() + if (!/^sha256:[a-f0-9]{64}$/.test(image)) { + throw new Error('Docker did not return a valid milo build image ID') + } + + // Follow upstream's README: sources are read-only; only results are mounted writable. + const sourceMount = `type=bind,source=${source},target=/src,readonly` + const outputMount = `type=bind,source=${temporary},target=/output` + docker('run', '--rm', '--mount', sourceMount, '--mount', outputMount, image) + + // Clean the destination directory and recreate it before copying artifacts. + await rm(destination, { recursive: true, force: true }) + await mkdir(destination, { recursive: true }) + + // Copy artifacts from the temporary build output to the destination directory. + for (const [from, to] of Object.entries(artifacts)) { + await cp(resolve(temporary, from), resolve(destination, to)) + } + + console.log(`Milo release artifacts saved in ${destination}`) + } finally { + await rm(temporary, { recursive: true, force: true }) + } +} + +buildMilo() diff --git a/deps/undici/src/deps/milo/CODE_OF_CONDUCT.md b/deps/undici/src/deps/milo/CODE_OF_CONDUCT.md new file mode 100644 index 000000000000..c42431d657da --- /dev/null +++ b/deps/undici/src/deps/milo/CODE_OF_CONDUCT.md @@ -0,0 +1,76 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to make participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, sex characteristics, gender identity and expression, +level of experience, education, socio-economic status, nationality, personal +appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +- Using welcoming and inclusive language +- Being respectful of differing viewpoints and experiences +- Gracefully accepting constructive criticism +- Focusing on what is best for the community +- Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +- The use of sexualized language or imagery and unwelcome sexual attention or + advances +- Trolling, insulting/derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or electronic + address, without explicit permission +- Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies within all project spaces, and it also applies when +an individual is representing the project or its community in public spaces. +Examples of representing a project or community include using an official +project e-mail address, posting via an official social media account, or acting +as an appointed representative at an online or offline event. Representation of +a project may be further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at [INSERT EMAIL ADDRESS]. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see +https://www.contributor-covenant.org/faq diff --git a/deps/undici/src/deps/milo/Dockerfile b/deps/undici/src/deps/milo/Dockerfile new file mode 100644 index 000000000000..4bd55672acdd --- /dev/null +++ b/deps/undici/src/deps/milo/Dockerfile @@ -0,0 +1,36 @@ +FROM ghcr.io/nodejs/wasm-builder@sha256:542fdbef9fa6eb003cc1eb89a9f3e6b967359471fdb29db4910c54474e70911d + +USER root + +ARG BINARYEN_VERSION=123 +ARG RUST_TOOLCHAIN=nightly-2026-07-29 +ARG CARGO_MAKE_VERSION=0.37.24 + +RUN set -eux; \ + case "$(uname -m)" in \ + x86_64) binaryen_arch=x86_64; binaryen_sha256=e959f2170af4c20c552e9de3a0253704d6a9d2766e8fdb88e4d6ac4bae9388fe ;; \ + aarch64) binaryen_arch=aarch64; binaryen_sha256=4b6bd61ba6cd3b18c993b4657d93426c782f9b91b74be0d38018cd8be1319376 ;; \ + *) echo "Unsupported architecture: $(uname -m)" >&2; exit 1 ;; \ + esac; \ + wget -q "https://github.com/WebAssembly/binaryen/releases/download/version_${BINARYEN_VERSION}/binaryen-version_${BINARYEN_VERSION}-${binaryen_arch}-linux.tar.gz" -O /tmp/binaryen.tar.gz; \ + echo "${binaryen_sha256} /tmp/binaryen.tar.gz" | sha256sum -c -; \ + tar -xzf /tmp/binaryen.tar.gz -C /tmp; \ + install -m 0755 "/tmp/binaryen-version_${BINARYEN_VERSION}/bin/wasm-opt" /usr/local/bin/wasm-opt; \ + rm -rf /tmp/binaryen.tar.gz "/tmp/binaryen-version_${BINARYEN_VERSION}" + +USER node + +RUN set -eux; \ + rustup toolchain install "${RUST_TOOLCHAIN}" --profile minimal; \ + rustup target add wasm32-unknown-unknown --toolchain "${RUST_TOOLCHAIN}"; \ + rustup component add rust-src --toolchain "${RUST_TOOLCHAIN}"; \ + cargo +"${RUST_TOOLCHAIN}" install cargo-make --version "${CARGO_MAKE_VERSION}" --locked + +ENV PATH="/home/node/.cargo/bin:${PATH}" +ENV RUSTUP_TOOLCHAIN=${RUST_TOOLCHAIN} + +WORKDIR /workspace/parser + +# Build from an internal copy so Cargo and the build tasks cannot modify the +# read-only source mount. Only the generated WASM artifacts are written out. +CMD ["sh", "-ec", "cp -a /src /tmp/milo && cd /tmp/milo/parser && makers wasm && mkdir -p /output && cp -a /tmp/milo/dist/wasm/. /output/"] diff --git a/deps/undici/src/deps/milo/LICENSE.md b/deps/undici/src/deps/milo/LICENSE.md new file mode 100644 index 000000000000..c485e26f7647 --- /dev/null +++ b/deps/undici/src/deps/milo/LICENSE.md @@ -0,0 +1,7 @@ +ISC License + +Copyright (c) 2023, and above Paolo Insogna, NearForm and other 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/deps/undici/src/deps/milo/Makefile.toml b/deps/undici/src/deps/milo/Makefile.toml new file mode 100644 index 000000000000..aa5527b36e97 --- /dev/null +++ b/deps/undici/src/deps/milo/Makefile.toml @@ -0,0 +1,140 @@ +[config] + skip_core_tasks = true + +[tasks.default] + dependencies = ["build"] + +[tasks.build] + script = ["cd parser", "makers build"] + +[tasks.test] + dependencies = ["test:rust", "test:wasm"] + +[tasks."test:rust"] + script = ["cd parser", "cargo test"] + +[tasks."test:wasm"] + script = ["cd parser", "makers test:wasm"] + +[tasks.format] + dependencies = ["format:rust", "format:js", "format:cpp"] + +[tasks.lint] + dependencies = ["lint:rust", "lint:js"] + +[tasks.clean] + dependencies = ["clean:rust", "clean:dist", "clean:tmp"] + +[tasks.benchmarks] + dependencies = ["benchmarks:cpp", "benchmarks:wasm"] + +[tasks.references] + dependencies = ["references:rust", "references:cpp", "references:wasm"] + +[tasks."llhttp:import"] + script = ["cd parser", "makers scripts:build", "../scripts/target/release/import-llhttp-tests ${1} ."] + +[tasks."llhttp:review"] + script = ["cd parser", "cargo run --example llhttp -- --review ${1}"] + +[tasks."llhttp:show"] + script = ["cd parser", "cargo run --example llhttp -- --show ${1}"] + +[tasks."llhttp:update"] + script = ["cd parser", "cargo run --example llhttp -- --update ${1}"] + +[tasks."clean:rust"] + script = [ + "cd macros", + "cargo clean", + "cd ../parser", + "cargo clean", + "cd ../references/rust", + "cargo clean", + "cd ../../scripts", + "cargo clean", + ] + +[tasks."clean:dist"] + command = "rm" + args = ["-rf", "dist"] + +[tasks."clean:tmp"] + command = "rm" + args = ["-rf", "tmp"] + +[tasks."lint:fix"] + dependencies = ["lint:rust:fix", "lint:js:fix"] + +[tasks."format:rust"] + script = [ + "cd macros", + "cargo fmt", + "cd ../parser", + "cargo fmt", + "cd ../references/rust", + "cargo fmt", + "cd ../../scripts", + "cargo fmt", + ] + +[tasks."format:cpp"] + command = "clang-format" + args = [ + "-style=file:clang-format.yml", + "-i", + "benchmarks/cpp/src/main.cc", + "references/cpp/src/reference.cc", + "references/cpp/src/readme.cc", + ] + +[tasks."format:js"] + command = "pnpm" + args = ["run", "format"] + +[tasks."lint:rust"] + script = [ + "cd macros", + "cargo clippy", + "cd ../parser", + "cargo clippy", + "cd ../references/rust", + "cargo clippy", + "cd ../../scripts", + "cargo clippy", + ] + +[tasks."lint:rust:fix"] + script = [ + "cd macros", + "cargo clippy --fix --allow-dirty", + "cd ../parser", + "cargo clippy --fix --allow-dirty", + "cd ../references/rust", + "cargo clippy --fix --allow-dirty", + "cd ../../scripts", + "cargo clippy --fix --allow-dirty", + ] + +[tasks."lint:js"] + command = "pnpm" + args = ["run", "lint"] + +[tasks."lint:js:fix"] + command = "pnpm" + args = ["run", "lint", "--fix"] + +[tasks."benchmarks:cpp"] + script = ["cd benchmarks/cpp", "makers benchmarks"] + +[tasks."benchmarks:wasm"] + script = ["cd benchmarks/wasm", "makers benchmarks"] + +[tasks."references:rust"] + script = ["cd references/rust", "makers test"] + +[tasks."references:cpp"] + script = ["cd references/cpp", "makers test"] + +[tasks."references:wasm"] + script = ["cd references/wasm", "makers test"] diff --git a/deps/undici/src/deps/milo/README.md b/deps/undici/src/deps/milo/README.md new file mode 100644 index 000000000000..86a4ae446c2d --- /dev/null +++ b/deps/undici/src/deps/milo/README.md @@ -0,0 +1,349 @@ +# @perseveranza-pets/milo + +Milo is a fast and embeddable HTTP/1.1 parser written in [Rust][rust]. + +Milo can report parser activity through callbacks or through parser-owned event buffers. See the language-specific API docs for event decoding details: [JavaScript](./docs/js.md), [Rust](./docs/rust.md), and [C++](./docs/cpp.md). + +## Support Matrix + +Milo supports strict HTTP/1.1 message parsing for Rust, C++, and JavaScript via WebAssembly. + +| Target | Status | Artifact | +| ---------- | --------- | --------------------------------------------- | +| Rust | Supported | `milo` crate | +| C++ | Supported | `milo.h` and `libmilo.a` | +| JavaScript | Supported | `@perseveranza-pets/milo` WebAssembly package | +| CLI | Supported | `milo-parser` binary | + +Milo intentionally rejects HTTP/0.9, HTTP/1.0, RTSP, obs-fold, bare LF, and bare CR. It does not parse HTTP/2 messages, except for strict `PRI * HTTP/2.0` switch-over detection. + +For security and ambiguity reduction, Milo rejects request bodies on `GET` and `HEAD`. Responses to `HEAD` are application context: callers must use `skip_body` when they know a response has no body because it belongs to a `HEAD` request. + +Use `max_body_payload` when an integration needs to cap body payload consumed by a single parse call. `0` means unlimited. Reaching the limit returns normally with unconsumed input left for the next parse call. + +Use `suspend_after_headers` when an integration needs to inspect headers before body parsing starts. Parsing returns after consuming the header terminator and can continue with the next parse call. + +For full parser scope, strictness, and protocol behavior, see [Milo Design](./docs/design.md). + +## How to use it (JavaScript via WebAssembly) + +Install it from npm: + +``` +npm install @perseveranza-pets/milo +``` + +For CommonJS projects, install the CJS-only package instead: + +``` +npm install @perseveranza-pets/milo-cjs +``` + +Then create a sample source file: + +```javascript +import { setup } from '@perseveranza-pets/milo' + +/* + Milo works using callbacks. + + All callbacks have the same signature, which characterizes the payload: + + * The current parser + * from: The payload offset. + * size: The payload length. + + The payload parameters above are relative to the last data sent to the milo.parse method. + + If the current callback has no payload, both values are set to 0. + + The callbacks must be provided using setup and are named in snake case. +*/ +const milo = setup({ + on_data (p, from, size) { + console.log(`Pos=${from} Body: ${message.slice(from, from + size).toString()}`) + } +}) + +// Prepare a message to parse. +const message = Buffer.from('HTTP/1.1 200 OK\r\nContent-Length: 3\r\n\r\nabc') + +// Allocate a memory in the WebAssembly space. This speeds up data copying to the WebAssembly layer. +const ptr = milo.alloc(message.length) + +// Create a buffer we can use normally. +const buffer = Buffer.from(milo.memory.buffer, ptr, message.length) + +// Create the parser. +const parser = milo.create() + +// Toggle on the callbacks you want to receive +milo.setActiveCallbacks(parser, milo.CALLBACK_ACTIVE_ON_DATA) + +// Now perform the main parsing using milo.parse. The method returns the number of consumed characters. +buffer.set(message, 0) +milo.parse(parser, ptr, message.length) + +// Cleanup used resources. +milo.destroy(parser) +milo.dealloc(ptr, message.length) +``` + +The default JavaScript entry point uses the SIMD WebAssembly build. Use `@perseveranza-pets/milo/no-simd` when SIMD is not available, and add `/unbundled` to either entry point to load the external `.wasm` file instead of the bundled JavaScript module. + +The WebAssembly release build uses immediate-abort panics: panics trap without unwinding or rich panic messages. The debug build also enables the `on_state_change` callback and provides more detailed WebAssembly errors. + +CommonJS projects can use the same entry points from `@perseveranza-pets/milo-cjs`: + +```javascript +const { setup } = require('@perseveranza-pets/milo-cjs') +``` + +Finally build and execute it using `node`: + +```bash +node index.js +# Pos=38 Body: abc +``` + +## How to use it (Rust) + +Add `milo-parser` to your `Cargo.toml`: + +```toml +[package] +name = "milo-example" +version = "0.1.0" +edition = "2024" +publish = false + +[dependencies] +milo-parser = "0.4.0" +``` + +Create a sample source file: + +```rust +use core::ffi::c_void; +use core::slice; + +use milo_parser::{Parser, CALLBACK_ACTIVE_ON_DATA}; + +fn main() { + // Create the parser. + let mut parser = Parser::new(); + + // Prepare a message to parse. + let message = String::from("HTTP/1.1 200 OK\r\nContent-Length: 3\r\n\r\nabc"); + parser.context = message.as_ptr() as *mut c_void; + + // Milo works using callbacks. + // + // All callbacks have the same signature, which characterizes the payload: + // + // p: The current parser. + // from: The payload offset. + // size: The payload length. + // + // The payload parameters above are relative to the last data sent to the parse + // method. + // + // If the current callback has no payload, both values are set to 0. + parser.callbacks.on_data = |p: &mut Parser, from: usize, size: usize| { + let message = + unsafe { std::str::from_utf8_unchecked(slice::from_raw_parts(p.context.add(from) as *const u8, size)) }; + + // Use the callback data. + println!("Pos={} Body: {}", from, message); + }; + + // Toggle on the callbacks you want to receive + parser.active_callbacks |= CALLBACK_ACTIVE_ON_DATA; + + // Now perform the main parsing using milo.parse. The method returns the number + // of consumed characters. + parser.parse(message.as_ptr(), message.len()); +} + +``` + +Finally build and execute it using `cargo`: + +```bash +cargo run +# Pos=38 Body: abc +``` + +## How to use it (C++) + +First, let's download Milo release from GitHub. + +You will need a static library file (for Linux/Unix/MacOS is `libmilo.a`) and a header file (`milo.h`). + +Create a sample source file: + +```cpp +#include "milo.h" +#include +#include +#include + +int main() { + // Create the parser. + milo_parser::Parser* parser = milo_parser::milo_create(); + + // Prepare a message to parse. + const char* message = "HTTP/1.1 200 OK\r\nContent-Length: 3\r\n\r\nabc"; + + parser->context = (char*) message; + + /* + Milo works using callbacks. + + All callbacks have the same signature, which characterizes the payload: + + * p: The current parser. + * from: The payload offset. + * size: The payload length. + + The payload parameters above are relative to the last data sent to the milo_parse method. + + If the current callback has no payload, both values are set to 0. + */ + parser->callbacks.on_data = [](milo_parser::Parser* p, uintptr_t from, uintptr_t size) { + const char* payload = reinterpret_cast(p->context) + from; + + printf("Pos=%" PRIuPTR " Body: %.*s\n", from, static_cast(size), payload); + }; + + // Toggle on the callbacks you want to receive + parser->active_callbacks |= milo_parser::CALLBACK_ACTIVE_ON_DATA; + + // Now perform the main parsing using milo.parse. The method returns the number of consumed characters. + milo_parser::milo_parse(parser, reinterpret_cast(message), strlen(message)); + + // Cleanup used resources. + milo_parser::milo_destroy(parser); +} +``` + +And then you can compile using your preferred build system. For instance, let's try with [Clang]: + +```bash +clang++ -std=c++11 -o example main.cc libmilo.a +./example +# Pos=38 Body: abc +``` + +## How to use it (CLI) + +Install it from crates.io: + +```bash +cargo install milo-parser +``` + +The `milo-parser` binary reads an HTTP/1.1 message from standard input by default and prints the callbacks emitted by Milo: + +```bash +printf 'GET / HTTP/1.1\r\nHost: example.com\r\n\r\n' | milo-parser +# offset=0 size=0 event=request +# offset=0 size=0 event=message_start +# offset=0 size=0 event=state_change +# offset=0 size=3 event=method +``` + +Each output line uses this format: + +```text +offset=$OFFSET size=$SIZE event=$EVENT +``` + +Parser errors are emitted as callback lines too: + +```text +offset=$OFFSET size=$SIZE event=error error=$ERROR description="$DESCRIPTION" +``` + +Use `-f` or `--file` to parse a file: + +```bash +milo-parser --file request.http +``` + +By default Milo autodetects request or response input. Use `-o` or `--request` to force request parsing, and `-i` or `--response` to force response parsing: + +```bash +milo-parser --request --file request.http +milo-parser --response --file response.http +``` + +## API + +See the following files, according to the language you are using: + +- [JavaScript via WebAssembly API](./docs/js.md) +- [Rust API](./docs/rust.md) +- [C++ API](./docs/cpp.md) + +## Strictness Differences From llhttp + +Milo does not aim for byte-for-byte llhttp compatibility when lenient behavior would conflict with Milo's strict parser policy. + +- Milo rejects HTTP/0.9, HTTP/1.0, RTSP, obs-fold, bare LF, and bare CR. +- Milo rejects normal HTTP/2 request and response messages. +- Milo rejects request bodies on `GET` and `HEAD`. +- Milo requires `skip_body` for application-known no-body response contexts such as responses to `HEAD`. +- Milo validates protocol framing and syntax, but leaves application semantics to callers. + +## Security Boundaries + +Milo validates HTTP/1.1 syntax, message framing, protocol switching, connection management, and data-after-close behavior. Milo does not validate routing, authorization, representation semantics, URI normalization, `Host` policy, method-specific request-target forms, CONNECT authority-form, or full header field semantics unless they affect safe framing. + +## How it works? + +Milo is a simple state machine which copies data in only one optional case: automatically handling the unconsumed portion of the input data. + +In all other cases, no data is copied and the memory footprint is very small as only a few dozen `bool`, `uintptr_t`, or `uint64_t` fields can represent the entire parser state. + +## Why? + +The scope of Milo is to replace [llhttp] as [Node.js] main HTTP parser. + +This project aims to: + +- Make it maintainable and verifiable using easy to read Rust code. +- Be performant by avoiding any unnecessary data copy. +- Be self-contained and dependency-free. + +To see the rationale behind the replacement of llhttp, check Paolo's talk at [Vancouver's Node Collab Summit][vancouver-talk] in January 2023 ([slides][vancouver-slides]). + +To see the initial disclosure of milo, check Paolo's talk at [NodeConf EU 2023][nodeconf-talk] in November 2023 ([slides][slides]). + +## Sponsored by + +[![NearForm](https://raw.githubusercontent.com/ShogunPanda/milo/main/docs/nearform.jpg)][nearform] + +## Contributing to milo + +See [AGENTS.md](./AGENTS.md) for development setup, local and Docker builds, tests, and contribution guidelines. + +## Copyright + +Copyright (C) 2023 and above Paolo Insogna (paolo@cowtech.it) and NearForm (https://nearform.com). + +Licensed under the ISC license, which can be found at https://choosealicense.com/licenses/isc or in the [LICENSE.md](./LICENSE.md) file. + +[rust]: https://www.rust-lang.org/ +[webassembly]: https://webassembly.org/ +[nearform]: https://nearform.com +[llhttp]: https://github.com/nodejs/llhttp +[Node.js]: https://nodejs.org +[vancouver-talk]: https://youtube.com/watch?v=L-VONzXQ944 +[vancouver-slides]: https://talks.cowtech.it/http-parser +[nodeconf-talk]: https://youtube.com/watch?v=dcHbAeO_ccY +[slides]: https://talks.paoloinsogna.dev/milo +[isc]: https://choosealicense.com/licenses/isc +[match]: https://doc.rust-lang.org/rust-by-example/flow_control/match.html +[match-slice]: https://doc.rust-lang.org/rust-by-example/flow_control/match/destructuring/destructure_slice.html +[Clang]: https://clang.llvm.org/ diff --git a/deps/undici/src/deps/milo/macros/Cargo.toml b/deps/undici/src/deps/milo/macros/Cargo.toml new file mode 100644 index 000000000000..22bdc39224a8 --- /dev/null +++ b/deps/undici/src/deps/milo/macros/Cargo.toml @@ -0,0 +1,30 @@ +[package] + name = "milo-macros" + version = "0.9.0" + edition = "2024" + description = "Macros for milo-parser, a fast and embeddable HTTP/1.1 parser." + homepage = "https://sw.cowtech.it/milo" + repository = "https://github.com/ShogunPanda/milo" + license = "ISC" + keywords = ["http", "parser", "http-parser"] + categories = ["network-programming", "parser-implementations"] + include = ["Cargo.toml", "src/**/*.rs", "constants/*.yml"] + +[lib] + proc-macro = true + +[[bin]] + name = "milo-parser-fields" + path = "src/parser_fields.rs" + +[dependencies] + indexmap = { version = "2.13.0", features = ["serde"] } + proc-macro2 = "1.0.106" + quote = "1.0.45" + regex = "1.12.3" + semver = "1.0.27" + serde = { version = "1.0.228", features = ["derive"] } + serde_json = "1.0.149" + serde_yaml = "0.9.34" + syn = { version = "2.0.117", features = ["full", "extra-traits"] } + toml = "1.0.7" diff --git a/deps/undici/src/deps/milo/macros/README.md b/deps/undici/src/deps/milo/macros/README.md new file mode 100644 index 000000000000..2053fc1a6a32 --- /dev/null +++ b/deps/undici/src/deps/milo/macros/README.md @@ -0,0 +1,34 @@ +# milo-macros + +milo-macros is an internal crate which represents the core of Milo. + +It leverages Rust's [procedural macro], [syn] and [quote] crates to allow an easy definition of states and matchers for the parser. + +To define a new macro, you have to define a new method which the following signature: + +```rust +#[proc_macro] +pub fn new_macro(input: TokenStream) -> TokenStream { + TokenStream::new( /* ... */) +} +``` + +Everything that is not included **within** the return value will not be present in the executed code. Macro performance is not important for runtime because macros are only evaluated at compile time. + +The `generate!` macro also emits parser constants and internal byte lookup tables used by syntax validators. These tables are generated at compile time and are not public API. + +To return a new code, leverage the `quote!` macro, while for parsing use the `parse_macro_input!` macro. Here's an example: + +```rust +#[proc_macro] +pub fn char(input: TokenStream) -> TokenStream { + let character = parse_macro_input!(input as LitChar); + let byte = LitByte::new(c_uchar::try_from(character.value()).unwrap(), character.span()); + + TokenStream::from(quote! { #byte }) +} +``` + +[procedural macro]: https://doc.rust-lang.org/reference/procedural-macros.html +[syn]: https://crates.io/crates/syn +[quote]: https://crates.io/crates/quote diff --git a/deps/undici/src/deps/milo/macros/constants/callbacks.yml b/deps/undici/src/deps/milo/macros/constants/callbacks.yml new file mode 100644 index 000000000000..9313d6d18a78 --- /dev/null +++ b/deps/undici/src/deps/milo/macros/constants/callbacks.yml @@ -0,0 +1,29 @@ +--- +- on_error +- on_finish +- on_message_start +- on_message_complete +- on_request +- on_response +- on_reset +- on_method +- on_url +- on_protocol +- on_version +- on_status +- on_reason +- on_header_name +- on_header_value +- on_headers +- on_connect +- on_upgrade +- on_chunk_length +- on_chunk_extension_name +- on_chunk_extension_value +- on_chunk +- on_body +- on_data +- on_trailer_name +- on_trailer_value +- on_trailers +- on_state_change diff --git a/deps/undici/src/deps/milo/macros/constants/errors.yml b/deps/undici/src/deps/milo/macros/constants/errors.yml new file mode 100644 index 000000000000..dd064151aebc --- /dev/null +++ b/deps/undici/src/deps/milo/macros/constants/errors.yml @@ -0,0 +1,19 @@ +--- +- NONE +- USER +- CALLBACK_ERROR +- UNEXPECTED_STATE +- UNEXPECTED_DATA +- UNEXPECTED_EOF +- UNEXPECTED_CHARACTER +- UNEXPECTED_CONTENT_LENGTH +- UNEXPECTED_TRANSFER_ENCODING +- UNEXPECTED_CONTENT +- UNEXPECTED_TRAILERS +- INVALID_VERSION +- INVALID_STATUS +- INVALID_CONTENT_LENGTH +- INVALID_TRANSFER_ENCODING +- INVALID_CHUNK_SIZE +- MISSING_CONNECTION_UPGRADE +- UNSUPPORTED_HTTP_VERSION diff --git a/deps/undici/src/deps/milo/macros/constants/methods.yml b/deps/undici/src/deps/milo/macros/constants/methods.yml new file mode 100644 index 000000000000..faf0a788f7ca --- /dev/null +++ b/deps/undici/src/deps/milo/macros/constants/methods.yml @@ -0,0 +1,19 @@ +--- +# Milo doesn't validate the method, that is left to the application +- OTHER + +# RFC 9110 section 9 (HTTP Semantics — Methods) +- GET +- HEAD +- POST +- PUT +- DELETE +- CONNECT +- OPTIONS +- TRACE + +# RFC 5789 section 2 (PATCH Method) - This is included as widely used +- PATCH + +# RFC 7540 section 3.5 (HTTP/2 connection preface) +- PRI diff --git a/deps/undici/src/deps/milo/macros/constants/states.yml b/deps/undici/src/deps/milo/macros/constants/states.yml new file mode 100644 index 000000000000..8d0505c49fe5 --- /dev/null +++ b/deps/undici/src/deps/milo/macros/constants/states.yml @@ -0,0 +1,16 @@ +--- +- start +- finish +- error +- request_line +- status_line +- http2_preface +- header +- body_decision +- body_via_content_length +- body_with_no_length +- chunk_header +- chunk_extensions +- chunk_data +- trailer +- tunnel diff --git a/deps/undici/src/deps/milo/macros/src/actions.rs b/deps/undici/src/deps/milo/macros/src/actions.rs new file mode 100644 index 000000000000..9ef3f8b01c6f --- /dev/null +++ b/deps/undici/src/deps/milo/macros/src/actions.rs @@ -0,0 +1,172 @@ +use proc_macro::TokenStream; +use quote::{format_ident, quote}; +use syn::{Expr, Ident, parse_macro_input}; + +use crate::structs::{EventRequest, FailureRequest}; + +/// Emits an event carrying an input range. +pub fn event_with_range(input: TokenStream) -> TokenStream { + let definition = parse_macro_input!(input as EventRequest); + let callback = &definition.identifier; + let callback_const = format_ident!("CALLBACK_{}", callback.to_string().to_uppercase()); + let event_type = quote! { #callback_const + 1 }; + let bitmask = format_ident!("EVENT_ACTIVE_{}", definition.identifier.to_string().to_uppercase()); + let offset = definition.offset.as_ref().expect("event_with_range requires offset"); + let length = definition.length.as_ref().expect("event_with_range requires length"); + let needed = quote! { EVENT_RANGE_SIZE }; + let emit = quote! { + let at = (self.position + #offset) as u32; + let len = (#length) as u32; + unsafe { + *self.events.add(event_cursor) = #event_type; + core::ptr::write_unaligned( + self.events.add(event_cursor + 1) as *mut u32, + at.to_le(), + ); + core::ptr::write_unaligned( + self.events.add(event_cursor + 5) as *mut u32, + len.to_le(), + ); + } + }; + + TokenStream::from(quote! { + if active_events & #bitmask != 0 { + // Reserve space for a later error event and the batch terminator. + if event_cursor + #needed + EVENT_ERROR_RESERVE <= EVENTS_BUFFER_SIZE { + #emit + event_cursor += #needed; + } else { + suspend!(); + } + } + }) +} + +/// Emits an error event. +pub fn event_with_error(input: TokenStream) -> TokenStream { + let definition = parse_macro_input!(input as EventRequest); + let callback = &definition.identifier; + let callback_const = format_ident!("CALLBACK_{}", callback.to_string().to_uppercase()); + let event_type = quote! { #callback_const + 1 }; + let bitmask = format_ident!("EVENT_ACTIVE_{}", definition.identifier.to_string().to_uppercase()); + let needed = quote! { EVENT_ERROR_SIZE }; + let emit = quote! { + let at = self.position as u32; + unsafe { + *self.events.add(event_cursor) = #event_type; + core::ptr::write_unaligned( + self.events.add(event_cursor + 1) as *mut u32, + at.to_le(), + ); + *self.events.add(event_cursor + 5) = self.error_code; + } + }; + + TokenStream::from(quote! { + if active_events & #bitmask != 0 { + if event_cursor + #needed + EVENT_END_SIZE <= EVENTS_BUFFER_SIZE { + #emit + event_cursor += #needed; + } else { + suspend!(); + } + } + }) +} + +/// Emits an event carrying parsed metadata. +pub fn event_with_metadata(input: TokenStream) -> TokenStream { + let definition = parse_macro_input!(input as EventRequest); + let callback = &definition.identifier; + let callback_const = format_ident!("CALLBACK_{}", callback.to_string().to_uppercase()); + let event_type = quote! { #callback_const + 1 }; + let bitmask = format_ident!("EVENT_ACTIVE_{}", definition.identifier.to_string().to_uppercase()); + let offset = definition.offset.unwrap_or_else(|| syn::parse_quote! { 0 }); + let needed = quote! { EVENT_METADATA_SIZE }; + let emit = quote! { + let at = (self.position + #offset) as u32; + let status_or_method = if self.is_request { self.method as u16 } else { self.status as u16 }; + let body_kind = if self.has_content_length { 0u8 } else if self.has_chunked_transfer_encoding { 1u8 } else { 2u8 }; + let should_keep_alive = (!self.has_connection_close) as u8; + // Responses switch protocols only with status 101; requests can propose an upgrade. + let should_upgrade = (self.has_upgrade && self.has_connection_upgrade && (self.is_request || self.status == 101)) as u8; + let has_trailers = self.has_trailers as u8; + let content_length = if self.has_content_length { self.content_length } else { 0 }; + + unsafe { + *self.events.add(event_cursor) = #event_type; + core::ptr::write_unaligned( + self.events.add(event_cursor + 1) as *mut u32, + at.to_le(), + ); + core::ptr::write_unaligned( + self.events.add(event_cursor + 5) as *mut u16, + status_or_method.to_le(), + ); + *self.events.add(event_cursor + 7) = should_keep_alive; + *self.events.add(event_cursor + 8) = should_upgrade; + *self.events.add(event_cursor + 9) = has_trailers; + *self.events.add(event_cursor + 10) = body_kind; + core::ptr::write_unaligned( + self.events.add(event_cursor + 11) as *mut u64, + content_length.to_le(), + ); + } + }; + + TokenStream::from(quote! { + if active_events & #bitmask != 0 { + // Reserve space for a later error event and the batch terminator. + if event_cursor + #needed + EVENT_ERROR_RESERVE <= EVENTS_BUFFER_SIZE { + #emit + event_cursor += #needed; + } else { + suspend!(); + } + } + }) +} + +// Marks a certain number of characters as used. +pub fn advance(input: TokenStream) -> TokenStream { + let len = parse_macro_input!(input as Expr); + + TokenStream::from(quote! { advanced += #len; }) +} + +/// Moves the parser to a new state. +pub fn move_to(input: TokenStream) -> TokenStream { + let definition = parse_macro_input!(input as Ident); + let state = format_ident!("STATE_{}", definition.to_string().to_uppercase()); + + TokenStream::from(quote! { + if self.state != STATE_ERROR { + self.state = #state; + } + }) +} + +/// Go to the next iteration of the parser +pub fn next() -> TokenStream { TokenStream::from(quote! { break 'state; }) } + +/// Marks the parser as suspended, waiting for more data. +pub fn suspend() -> TokenStream { + TokenStream::from(quote! { + parsing = false; + break 'state; + }) +} + +/// Marks the parsing a failed, setting a error code and and error message. +pub fn fail(input: TokenStream) -> TokenStream { + let definition = parse_macro_input!(input as FailureRequest); + let error = format_ident!("ERROR_{}", definition.error); + let message = definition.message; + + TokenStream::from(quote! { + self.set_error(#error, #message); + failed = true; + break 'parser; + }) +} diff --git a/deps/undici/src/deps/milo/macros/src/generators.rs b/deps/undici/src/deps/milo/macros/src/generators.rs new file mode 100644 index 000000000000..f5111106b402 --- /dev/null +++ b/deps/undici/src/deps/milo/macros/src/generators.rs @@ -0,0 +1,443 @@ +use proc_macro::TokenStream; +use quote::{format_ident, quote}; +use regex::{Captures, Regex}; +use syn::{Arm, ItemConst, parse_str}; + +use crate::{native, parser_fields, wasm}; + +fn init_constants() -> (Vec, Vec, Vec, Vec) { + let methods = serde_yaml::from_str(include_str!("../constants/methods.yml")).unwrap(); + let errors = serde_yaml::from_str(include_str!("../constants/errors.yml")).unwrap(); + let callbacks = serde_yaml::from_str(include_str!("../constants/callbacks.yml")).unwrap(); + let states = serde_yaml::from_str(include_str!("../constants/states.yml")).unwrap(); + + (methods, errors, callbacks, states) +} + +fn generate_constants_internal( + items: &[String], + prefix: &str, + start: usize, + strip_prefix: Option<&str>, +) -> Vec { + let mut consts: Vec = Vec::new(); + let mut bytes: Vec<&[u8]> = vec![]; + + for (i, x) in items.iter().enumerate() { + let value = strip_prefix.and_then(|prefix| x.strip_prefix(prefix)).unwrap_or(x); + let uppercased = value.to_uppercase(); + let name = uppercased.replace('-', "_"); + bytes.push(x.as_bytes()); + + consts.push(parse_str::(&format!("pub const {}_{}: u8 = {};", prefix, name, i + start)).unwrap()); + } + + consts +} + +fn generate_bitmask(items: &[String], prefix: &str) -> Vec { + let mut consts: Vec = Vec::new(); + let mut all = 0u64; + + consts.push(parse_str::(&format!("pub const {}_NONE: u64 = 0;", prefix)).unwrap()); + + for (i, x) in items.iter().enumerate() { + let uppercased = x.to_uppercase(); + let name = uppercased.replace('-', "_"); + let bit = 1 << i; + all |= bit; + + consts.push(parse_str::(&format!("pub const {}_{}: u64 = {};", prefix, name, bit)).unwrap()); + } + + consts.push(parse_str::(&format!("pub const {}_ALL: u64 = {};", prefix, all)).unwrap()); + + consts +} + +fn generate_table(validator: F) -> Vec +where + F: Fn(u16) -> bool, +{ + // Precompute byte validators so the parser can use table lookups in hot paths. + (0..=255).map(validator).collect() +} + +/// Generates all parser constants. +fn generate_constants(methods: &[String], errors: &[String], callbacks: &[String], states: &[String]) -> TokenStream { + let methods_consts = generate_constants_internal(methods, "METHOD", 0, None); + let states_consts = generate_constants_internal(states, "STATE", 0, None); + let errors_consts = generate_constants_internal(errors, "ERROR", 0, None); + let callbacks_consts = generate_constants_internal(callbacks, "CALLBACK", 0, None); + let callbacks_bitmask = generate_bitmask(callbacks, "CALLBACK_ACTIVE"); + let event_bitmask = generate_bitmask(callbacks, "EVENT_ACTIVE"); + let event_consts = generate_constants_internal(callbacks, "EVENT", 1, Some("on_")); + let parser_field_offsets = parser_fields::generate_constants(); + let token_table = generate_table(|byte| { + (0x30..=0x39).contains(&byte) + || (0x41..=0x5a).contains(&byte) + || (0x61..=0x7a).contains(&byte) + || matches!( + byte as u8, + b'!' | b'#' | b'$' | b'%' | b'&' | b'\'' | b'*' | b'+' | b'-' | b'.' | b'^' | b'_' | b'`' | b'|' | b'~' + ) + }); + let url_table = generate_table(|byte| { + (0x30..=0x39).contains(&byte) + || (0x41..=0x5a).contains(&byte) + || (0x61..=0x7a).contains(&byte) + || matches!( + byte as u8, + b'-' + | b'.' + | b'_' + | b'~' + | b':' + | b'/' + | b'?' + | b'[' + | b']' + | b'@' + | b'!' + | b'$' + | b'&' + | b'\'' + | b'(' + | b')' + | b'*' + | b'+' + | b',' + | b';' + | b'=' + | b'%' + ) + }); + let quoted_string_table = generate_table(|byte| { + byte == 0x09 + || byte == 0x20 + || byte == 0x21 + || (0x23..=0x5b).contains(&byte) + || (0x5d..=0x7e).contains(&byte) + || byte >= 0x80 + }); + let quoted_pair_table = + generate_table(|byte| byte == 0x09 || byte == 0x20 || (0x21..=0x7e).contains(&byte) || byte >= 0x80); + + TokenStream::from(quote! { + pub type StateHandler = fn (parser: &mut Parser, data: &[c_uchar], available: usize); + + #[unsafe(no_mangle)] + pub type Callback = fn (&mut Parser, usize, usize); + + #(#methods_consts)* + #(#errors_consts)* + #(#callbacks_consts)* + pub const EVENT_END: u8 = 0; + #(#event_consts)* + #(#callbacks_bitmask)* + #(#event_bitmask)* + #(#states_consts)* + #parser_field_offsets + + const EVENTS_BUFFER_SIZE: usize = 64 * 1024; + const EVENT_RANGE_SIZE: usize = 9; + const EVENT_METADATA_SIZE: usize = 19; + const EVENT_ERROR_SIZE: usize = 6; + const EVENT_END_SIZE: usize = 1; + const EVENT_ERROR_RESERVE: usize = EVENT_ERROR_SIZE + EVENT_END_SIZE; + // Reserve MESSAGE_COMPLETE, RESET and FINISH, regardless of which events are enabled. + const EVENT_COMPLETION_SIZE: usize = 3 * EVENT_RANGE_SIZE; + // Include DATA and BODY before the completion group. + const EVENT_BODY_COMPLETION_SIZE: usize = 2 * EVENT_RANGE_SIZE + EVENT_COMPLETION_SIZE; + // Include TRAILERS before the completion group. + const EVENT_TRAILER_COMPLETION_SIZE: usize = EVENT_RANGE_SIZE + EVENT_COMPLETION_SIZE; + + /// cbindgen:ignore + static TOKEN_TABLE: [bool; 256] = [#(#token_table),*]; + + /// cbindgen:ignore + static URL_TABLE: [bool; 256] = [#(#url_table),*]; + + /// cbindgen:ignore + static QUOTED_STRING_TABLE: [bool; 256] = [#(#quoted_string_table),*]; + + /// cbindgen:ignore + static QUOTED_PAIR_TABLE: [bool; 256] = [#(#quoted_pair_table),*]; + }) +} + +/// Generates all parser enums. +fn generate_enums(methods: &[String], errors: &[String], callbacks: &[String], states: &[String]) -> TokenStream { + let snake_matcher = Regex::new(r"_([a-z])").unwrap(); + + let methods_ref = methods; + let errors_ref = errors; + let callbacks_ref = callbacks; + let events_ref: Vec = core::iter::once("END".to_string()) + .chain( + callbacks_ref + .iter() + .map(|x| x.strip_prefix("on_").unwrap_or(x).to_string()), + ) + .collect(); + let states_ref = states; + + let methods: Vec<_> = methods_ref + .iter() + .map(|x| format_ident!("{}", x.replace('-', "_"))) + .collect(); + + let errors: Vec<_> = errors_ref.iter().map(|x| format_ident!("{}", x)).collect(); + + let callbacks: Vec<_> = callbacks_ref + .iter() + .map(|x| { + let lowercase = x.to_lowercase(); + + format_ident!( + "{}", + snake_matcher.replace_all(lowercase.as_str(), |captures: &Captures| captures[1].to_uppercase()) + ) + }) + .collect(); + + let events: Vec<_> = events_ref + .iter() + .map(|x| format_ident!("{}", x.to_uppercase())) + .collect(); + + let states: Vec<_> = states_ref + .iter() + .map(|x| format_ident!("{}", x.to_uppercase())) + .collect(); + + let methods_from: Vec<_> = methods_ref + .iter() + .enumerate() + .map(|(x, i)| parse_str::(&format!("{} => Ok(Methods::{})", x, i.replace('-', "_"))).unwrap()) + .collect(); + + let errors_from: Vec<_> = errors_ref + .iter() + .enumerate() + .map(|(x, i)| parse_str::(&format!("{} => Ok(Errors::{})", x, i)).unwrap()) + .collect(); + + let callbacks_from: Vec<_> = callbacks + .iter() + .enumerate() + .map(|(x, i)| parse_str::(&format!("{} => Ok(Callbacks::{})", x, i)).unwrap()) + .collect(); + + let events_from: Vec<_> = events + .iter() + .enumerate() + .map(|(x, i)| parse_str::(&format!("{} => Ok(Events::{})", x, i)).unwrap()) + .collect(); + + let states_from: Vec<_> = states + .iter() + .enumerate() + .map(|(x, i)| parse_str::(&format!("{} => Ok(States::{})", x, i)).unwrap()) + .collect(); + + let methods_into: Vec<_> = methods_ref + .iter() + .map(|x| parse_str::(&format!("Methods::{} => \"{}\"", x.replace('-', "_"), x)).unwrap()) + .collect(); + + let errors_into: Vec<_> = errors_ref + .iter() + .map(|x| parse_str::(&format!("Errors::{} => \"{}\"", x, x)).unwrap()) + .collect(); + + let callbacks_into: Vec<_> = callbacks + .iter() + .map(|x| parse_str::(&format!("Callbacks::{} => \"{}\"", x, x)).unwrap()) + .collect(); + + let events_into: Vec<_> = events_ref + .iter() + .map(|x| parse_str::(&format!("Events::{} => \"{}\"", x.to_uppercase(), x.to_uppercase())).unwrap()) + .collect(); + + let states_into: Vec<_> = states + .iter() + .map(|x| parse_str::(&format!("States::{} => \"{}\"", x, x)).unwrap()) + .collect(); + + TokenStream::from(quote! { + #[repr(u8)] + #[derive(Copy, Clone, Debug)] + pub enum Methods { + #(#methods),* + } + + #[repr(u8)] + #[derive(Copy, Clone, Debug)] + pub enum Errors { + #(#errors),* + } + + #[repr(u8)] + #[derive(Copy, Clone, Debug)] + pub enum Callbacks { + #(#callbacks),* + } + + #[repr(u8)] + #[derive(Copy, Clone, Debug)] + pub enum Events { + #(#events),* + } + + #[repr(u8)] + #[derive(Copy, Clone, Debug)] + pub enum States { + #(#states),* + } + + impl TryFrom for Methods { + type Error = (); + + fn try_from(value: u8) -> Result { + match value { + #(#methods_from),*, + _ => Err(()) + } + } + } + + impl TryFrom for Errors { + type Error = (); + + fn try_from(value: u8) -> Result { + match value { + #(#errors_from),*, + _ => Err(()) + } + } + } + + impl TryFrom for Callbacks { + type Error = (); + + fn try_from(value: u8) -> Result { + match value { + #(#callbacks_from),*, + _ => Err(()) + } + } + } + + impl TryFrom for Events { + type Error = (); + + fn try_from(value: u8) -> Result { + match value { + #(#events_from),*, + _ => Err(()) + } + } + } + + impl TryFrom for States { + type Error = (); + + fn try_from(value: u8) -> Result { + match value { + #(#states_from),*, + _ => Err(()) + } + } + } + + impl From for &str { + fn from(value: Methods) -> Self { + match value { + #(#methods_into),* + } + } + } + + impl From for &str { + fn from(value: Errors) -> Self { + match value { + #(#errors_into),* + } + } + } + + impl From for &str { + fn from(value: Callbacks) -> Self { + match value { + #(#callbacks_into),* + } + } + } + + impl From for &str { + fn from(value: Events) -> Self { + match value { + #(#events_into),* + } + } + } + + impl From for &str { + fn from(value: States) -> Self { + match value { + #(#states_into),* + } + } + } + + impl Methods { + pub fn as_str(self) -> &'static str { + self.into() + } + } + + impl Errors { + pub fn as_str(self) -> &'static str { + self.into() + } + } + + impl Callbacks { + pub fn as_str(self) -> &'static str { + self.into() + } + } + + impl Events { + pub fn as_str(self) -> &'static str { + self.into() + } + } + + impl States { + pub fn as_str(self) -> &'static str { + self.into() + } + } + }) +} + +fn generate_callbacks(callbacks: &[String]) -> TokenStream { + let native = native::generate_callbacks(callbacks); + let wasm = wasm::generate_callbacks(callbacks); + + TokenStream::from_iter([native, wasm]) +} + +/// Generates the complete parser. +pub fn generate() -> TokenStream { + let (methods, errors, callbacks, states) = init_constants(); + + let constants_code = generate_constants(&methods, &errors, &callbacks, &states); + let enums_code = generate_enums(&methods, &errors, &callbacks, &states); + let callbacks_code = generate_callbacks(&callbacks); + + TokenStream::from_iter([constants_code, enums_code, callbacks_code]) +} diff --git a/deps/undici/src/deps/milo/macros/src/lib.rs b/deps/undici/src/deps/milo/macros/src/lib.rs new file mode 100644 index 000000000000..ea68cd5e8ccc --- /dev/null +++ b/deps/undici/src/deps/milo/macros/src/lib.rs @@ -0,0 +1,39 @@ +use proc_macro::TokenStream; + +mod actions; +mod generators; +mod matchers; +mod native; +mod parser_fields; +mod structs; +mod wasm; + +#[proc_macro] +pub fn generate(_: TokenStream) -> TokenStream { generators::generate() } + +#[proc_macro] +pub fn case_insensitive_string(input: TokenStream) -> TokenStream { matchers::case_insensitive_string(input) } + +#[proc_macro] +pub fn event_with_range(input: TokenStream) -> TokenStream { actions::event_with_range(input) } + +#[proc_macro] +pub fn event_with_error(input: TokenStream) -> TokenStream { actions::event_with_error(input) } + +#[proc_macro] +pub fn event_with_metadata(input: TokenStream) -> TokenStream { actions::event_with_metadata(input) } + +#[proc_macro] +pub fn advance(input: TokenStream) -> TokenStream { actions::advance(input) } + +#[proc_macro] +pub fn move_to(input: TokenStream) -> TokenStream { actions::move_to(input) } + +#[proc_macro] +pub fn next(_: TokenStream) -> TokenStream { actions::next() } + +#[proc_macro] +pub fn suspend(_: TokenStream) -> TokenStream { actions::suspend() } + +#[proc_macro] +pub fn fail(input: TokenStream) -> TokenStream { actions::fail(input) } diff --git a/deps/undici/src/deps/milo/macros/src/matchers.rs b/deps/undici/src/deps/milo/macros/src/matchers.rs new file mode 100644 index 000000000000..43da6e23e798 --- /dev/null +++ b/deps/undici/src/deps/milo/macros/src/matchers.rs @@ -0,0 +1,16 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::{Expr, LitStr, parse_macro_input, parse_str}; + +/// Matches a string in case insensitive way. +pub fn case_insensitive_string(input: TokenStream) -> TokenStream { + let definition = parse_macro_input!(input as LitStr); + + let bytes: Vec<_> = definition + .value() + .bytes() + .map(|b| parse_str::(&format!("{} | {}", b.to_ascii_uppercase(), b.to_ascii_lowercase())).unwrap()) + .collect(); + + TokenStream::from(quote! { [#(#bytes),*] }) +} diff --git a/deps/undici/src/deps/milo/macros/src/native.rs b/deps/undici/src/deps/milo/macros/src/native.rs new file mode 100644 index 000000000000..a10f57c678c3 --- /dev/null +++ b/deps/undici/src/deps/milo/macros/src/native.rs @@ -0,0 +1,90 @@ +use proc_macro::TokenStream; +use quote::{format_ident, quote}; + +/// Generates all parser callbacks. +pub fn generate_callbacks(callbacks: &[String]) -> TokenStream { + let callbacks: Vec<_> = callbacks.iter().map(|x| format_ident!("{}", x)).collect(); + let replay_arms = callbacks.iter().map(|callback| { + let callback_name = callback.to_string(); + let event_const = format_ident!( + "EVENT_{}", + callback_name + .strip_prefix("on_") + .unwrap_or(&callback_name) + .to_uppercase() + ); + let active_const = format_ident!("CALLBACK_ACTIVE_{}", callback_name.to_uppercase()); + + if callback_name == "on_error" { + quote! { + #event_const => { + let at = unsafe { core::ptr::read_unaligned(self.events.add(cursor + 1) as *const u32) }.to_le() as usize; + if self.active_callbacks & #active_const != 0 { + (self.callbacks.#callback)(self, at, 0); + } + cursor += EVENT_ERROR_SIZE; + } + } + } else if callback_name == "on_headers" { + quote! { + #event_const => { + let at = unsafe { core::ptr::read_unaligned(self.events.add(cursor + 1) as *const u32) }.to_le() as usize; + if self.active_callbacks & #active_const != 0 { + (self.callbacks.#callback)(self, at, 0); + } + cursor += EVENT_METADATA_SIZE; + } + } + } else { + quote! { + #event_const => { + let at = unsafe { core::ptr::read_unaligned(self.events.add(cursor + 1) as *const u32) }.to_le() as usize; + let len = unsafe { core::ptr::read_unaligned(self.events.add(cursor + 5) as *const u32) }.to_le() as usize; + if self.active_callbacks & #active_const != 0 { + (self.callbacks.#callback)(self, at, len); + } + cursor += EVENT_RANGE_SIZE; + } + } + } + }); + + TokenStream::from(quote! { + #[cfg(not(target_family = "wasm"))] + fn noop_internal(_parser: &mut Parser, _at: usize, _len: usize) {} + + #[cfg(not(target_family = "wasm"))] + #[repr(C)] + #[derive(Clone, Debug)] + pub struct ParserCallbacks { + #( pub #callbacks: Callback),* + } + + #[cfg(not(target_family = "wasm"))] + impl ParserCallbacks { + fn new() -> ParserCallbacks { + ParserCallbacks { + #( #callbacks: noop_internal ),* + } + } + } + + #[cfg(not(target_family = "wasm"))] + impl Parser { + #[inline] + fn invoke_callbacks(&mut self) { + let mut cursor = 0usize; + + loop { + let event_type = unsafe { *self.events.add(cursor) }; + + match event_type { + EVENT_END => break, + #(#replay_arms)* + _ => break, + } + } + } + } + }) +} diff --git a/deps/undici/src/deps/milo/macros/src/parser_fields.rs b/deps/undici/src/deps/milo/macros/src/parser_fields.rs new file mode 100644 index 000000000000..c547583bc502 --- /dev/null +++ b/deps/undici/src/deps/milo/macros/src/parser_fields.rs @@ -0,0 +1,122 @@ +use core::mem::offset_of; + +use proc_macro2::TokenStream; +use quote::{format_ident, quote}; + +type WasmPointer = u32; +type WasmUsize = u32; + +// Keep this in sync with parser::Parser when compiled with target_family = +// "wasm". +#[repr(C)] +struct ParserStub { + max_start_line_length: WasmUsize, + max_header_length: WasmUsize, + max_body_payload: u64, + autodetect: bool, + is_request: bool, + suspend_after_headers: bool, + manage_unconsumed: bool, + continue_without_data: bool, + is_connect: bool, + skip_body: bool, + debug: bool, + parsed: u64, + position: WasmUsize, + state: u8, + paused: bool, + error_code: u8, + content_length: u64, + chunk_size: u64, + remaining_content_length: u64, + remaining_chunk_size: u64, + status: u32, + method: u8, + has_content_length: bool, + has_transfer_encoding: bool, + has_chunked_transfer_encoding: bool, + has_connection_close: bool, + has_connection_upgrade: bool, + has_upgrade: bool, + has_trailers: bool, + active_callbacks: u64, + active_events: u64, + ptr: WasmPointer, + error_description: [u8; 255], + unconsumed: WasmPointer, + unconsumed_len: WasmUsize, + error_description_len: u8, + events: WasmPointer, +} + +pub const FIELDS: &[(&str, usize)] = &[ + ("MAX_START_LINE_LENGTH", offset_of!(ParserStub, max_start_line_length)), + ("MAX_HEADER_LENGTH", offset_of!(ParserStub, max_header_length)), + ("MAX_BODY_PAYLOAD", offset_of!(ParserStub, max_body_payload)), + ("AUTODETECT", offset_of!(ParserStub, autodetect)), + ("IS_REQUEST", offset_of!(ParserStub, is_request)), + ("SUSPEND_AFTER_HEADERS", offset_of!(ParserStub, suspend_after_headers)), + ("MANAGE_UNCONSUMED", offset_of!(ParserStub, manage_unconsumed)), + ("CONTINUE_WITHOUT_DATA", offset_of!(ParserStub, continue_without_data)), + ("IS_CONNECT", offset_of!(ParserStub, is_connect)), + ("SKIP_BODY", offset_of!(ParserStub, skip_body)), + ("DEBUG", offset_of!(ParserStub, debug)), + ("PARSED", offset_of!(ParserStub, parsed)), + ("POSITION", offset_of!(ParserStub, position)), + ("STATE", offset_of!(ParserStub, state)), + ("PAUSED", offset_of!(ParserStub, paused)), + ("ERROR_CODE", offset_of!(ParserStub, error_code)), + ("CONTENT_LENGTH", offset_of!(ParserStub, content_length)), + ("CHUNK_SIZE", offset_of!(ParserStub, chunk_size)), + ( + "REMAINING_CONTENT_LENGTH", + offset_of!(ParserStub, remaining_content_length), + ), + ("REMAINING_CHUNK_SIZE", offset_of!(ParserStub, remaining_chunk_size)), + ("STATUS", offset_of!(ParserStub, status)), + ("METHOD", offset_of!(ParserStub, method)), + ("HAS_CONTENT_LENGTH", offset_of!(ParserStub, has_content_length)), + ("HAS_TRANSFER_ENCODING", offset_of!(ParserStub, has_transfer_encoding)), + ( + "HAS_CHUNKED_TRANSFER_ENCODING", + offset_of!(ParserStub, has_chunked_transfer_encoding), + ), + ("HAS_CONNECTION_CLOSE", offset_of!(ParserStub, has_connection_close)), + ("HAS_CONNECTION_UPGRADE", offset_of!(ParserStub, has_connection_upgrade)), + ("HAS_UPGRADE", offset_of!(ParserStub, has_upgrade)), + ("HAS_TRAILERS", offset_of!(ParserStub, has_trailers)), + ("ACTIVE_CALLBACKS", offset_of!(ParserStub, active_callbacks)), + ("ACTIVE_EVENTS", offset_of!(ParserStub, active_events)), + ("PTR", offset_of!(ParserStub, ptr)), + ("ERROR_DESCRIPTION", offset_of!(ParserStub, error_description)), + ("UNCONSUMED", offset_of!(ParserStub, unconsumed)), + ("UNCONSUMED_LEN", offset_of!(ParserStub, unconsumed_len)), + ("ERROR_DESCRIPTION_LEN", offset_of!(ParserStub, error_description_len)), + ("EVENTS", offset_of!(ParserStub, events)), +]; + +pub fn generate_constants() -> TokenStream { + let constants = FIELDS.iter().map(|(name, offset)| { + let ident = format_ident!("PARSER_FIELD_{}", name); + + quote! { + #[cfg(target_family = "wasm")] + pub const #ident: usize = #offset; + } + }); + + quote! { + #(#constants)* + } +} + +#[allow(dead_code)] +fn main() { + let constants = FIELDS + .iter() + .map(|(name, offset)| format!("\"PARSER_FIELD_{name}\":{offset}")) + .collect::>() + .join(","); + + println!("{{{}}}", constants); +} diff --git a/deps/undici/src/deps/milo/macros/src/structs.rs b/deps/undici/src/deps/milo/macros/src/structs.rs new file mode 100644 index 000000000000..2252fe81a29c --- /dev/null +++ b/deps/undici/src/deps/milo/macros/src/structs.rs @@ -0,0 +1,65 @@ +use syn::parse::{Parse, ParseStream}; +use syn::{Expr, Ident, Result, Token}; + +/// An identifier associated to a message, typically a string - An example of +/// this is used in `fail!`. +pub struct FailureRequest { + pub error: Ident, + pub message: Expr, +} + +/// An identifier associated to an expression - An example of this is used in +/// `event_with_range!`. +pub struct EventRequest { + pub identifier: Ident, + pub offset: Option, + pub length: Option, +} + +impl Parse for FailureRequest { + // Parses a failure definition + fn parse(input: ParseStream) -> Result { + // Get the code + let error = input.parse()?; + + // Discard the comma + input.parse::()?; + + // Get the message + let message = input.parse()?; + + Ok(FailureRequest { error, message }) + } +} + +impl Parse for EventRequest { + // Parses a identifier and its optional expression + fn parse(input: ParseStream) -> Result { + let identifier = input.parse()?; + let mut offset = None; + let mut length = None; + + // If there is more input + if !input.is_empty() { + // Discard the comma + input.parse::()?; + + // Parse the expression + offset = Some(input.parse::()?); + + if !input.is_empty() { + // Discard the comma + input.parse::()?; + + // Parse the expression + length = Some(input.parse::()?); + } + } + + Ok(EventRequest { + identifier, + offset, + length, + }) + } +} diff --git a/deps/undici/src/deps/milo/macros/src/wasm.rs b/deps/undici/src/deps/milo/macros/src/wasm.rs new file mode 100644 index 000000000000..8b8cc3ef3c05 --- /dev/null +++ b/deps/undici/src/deps/milo/macros/src/wasm.rs @@ -0,0 +1,115 @@ +use proc_macro::TokenStream; +use quote::{format_ident, quote}; + +/// Generates all parser callbacks. +pub fn generate_callbacks(callbacks: &[String]) -> TokenStream { + let callbacks: Vec<_> = callbacks + .iter() + .filter(|x| x.as_str() != "on_headers") + .map(|x| format_ident!("{}", x)) + .collect(); + let replay_arms = callbacks + .iter() + .filter(|callback| callback.to_string() != "on_error") + .map(|callback| { + let callback_name = callback.to_string(); + let event_const = format_ident!( + "EVENT_{}", + callback_name + .strip_prefix("on_") + .unwrap_or(&callback_name) + .to_uppercase() + ); + let active_const = format_ident!("CALLBACK_ACTIVE_{}", callback_name.to_uppercase()); + + quote! { + #event_const => { + let at = unsafe { core::ptr::read_unaligned(self.events.add(cursor + 1) as *const u32) }.to_le() as usize; + let len = unsafe { core::ptr::read_unaligned(self.events.add(cursor + 5) as *const u32) }.to_le() as usize; + if self.active_callbacks & #active_const != 0 { + unsafe { #callback(self.ptr, at, len); } + } + cursor += EVENT_RANGE_SIZE; + } + } + }); + + TokenStream::from(quote! { + #[cfg(target_family = "wasm")] + #[link(wasm_import_module = "env")] + unsafe extern "C" { + #(fn #callbacks(parser: *mut c_void, _at: usize, _len: usize);)* + fn on_headers( + parser: *mut c_void, + at: usize, + method_or_status: u32, + should_keep_alive: bool, + should_upgrade: bool, + has_trailers: bool, + body_kind: u8, + content_length: f64, + ); + + #[cfg(any(debug_assertions, feature = "debug"))] + fn logger(message: u64); + } + + #[cfg(all(any(debug_assertions, feature = "debug"), target_family = "wasm"))] + #[unsafe(no_mangle)] + pub fn __start() { + std::panic::set_hook(Box::new(|panic_info| { + debug(format!("WebAssembly panicked: {:#?}", panic_info)); + })); + } + + #[cfg(target_family = "wasm")] + impl Parser { + #[inline] + fn invoke_callbacks(&mut self) { + let mut cursor = 0usize; + + loop { + let event_type = unsafe { *self.events.add(cursor) }; + + match event_type { + EVENT_END => break, + EVENT_ERROR => { + let at = unsafe { core::ptr::read_unaligned(self.events.add(cursor + 1) as *const u32) }.to_le() as usize; + if self.active_callbacks & CALLBACK_ACTIVE_ON_ERROR != 0 { + unsafe { on_error(self.ptr, at, 0); } + } + cursor += EVENT_ERROR_SIZE; + } + EVENT_HEADERS => { + let at = unsafe { core::ptr::read_unaligned(self.events.add(cursor + 1) as *const u32) }.to_le() as usize; + let method_or_status = unsafe { core::ptr::read_unaligned(self.events.add(cursor + 5) as *const u16) }.to_le() as u32; + let should_keep_alive = unsafe { *self.events.add(cursor + 7) } != 0; + let should_upgrade = unsafe { *self.events.add(cursor + 8) } != 0; + let has_trailers = unsafe { *self.events.add(cursor + 9) } != 0; + let body_kind = unsafe { *self.events.add(cursor + 10) }; + let content_length = unsafe { core::ptr::read_unaligned(self.events.add(cursor + 11) as *const u64) }.to_le() as f64; + + if self.active_callbacks & CALLBACK_ACTIVE_ON_HEADERS != 0 { + unsafe { + on_headers( + self.ptr, + at, + method_or_status, + should_keep_alive, + should_upgrade, + has_trailers, + body_kind, + content_length, + ); + } + } + cursor += EVENT_METADATA_SIZE; + } + #(#replay_arms)* + _ => break, + } + } + } + } + }) +} diff --git a/deps/undici/src/deps/milo/parser/Cargo.toml b/deps/undici/src/deps/milo/parser/Cargo.toml new file mode 100644 index 000000000000..c5b3abed8acd --- /dev/null +++ b/deps/undici/src/deps/milo/parser/Cargo.toml @@ -0,0 +1,45 @@ +[package] + name = "milo-parser" + version = "0.9.0" + edition = "2024" + description = "A fast and embeddable HTTP/1.1 parser." + homepage = "https://sw.cowtech.it/milo" + repository = "https://github.com/ShogunPanda/milo" + license = "ISC" + keywords = ["http", "parser", "http-parser"] + categories = ["network-programming", "parser-implementations"] + +[lib] + crate-type = ["cdylib", "staticlib", "lib"] + +[[bin]] + name = "milo-parser" + path = "src/main.rs" + +[features] + debug = [] + +[dependencies] + milo-macros = { version = "0.9.0", path = "../macros" } + memchr = "2.8.0" + +[dev-dependencies] + comfy-table = { version = "7.2.2" } + libc = "0.2" + regex = "1.12.3" + serde = { version = "1.0.228", features = ["derive"] } + serde_json = "1.0.145" + serde_yaml = "0.9.34" + similar = "3.0.0" + +[profile.release] + opt-level = 3 + debug = false + debug-assertions = false + overflow-checks = false + lto = true + panic = 'abort' + incremental = false + codegen-units = 1 + rpath = false + strip = true diff --git a/deps/undici/src/deps/milo/parser/Makefile.toml b/deps/undici/src/deps/milo/parser/Makefile.toml new file mode 100644 index 000000000000..d77d77452417 --- /dev/null +++ b/deps/undici/src/deps/milo/parser/Makefile.toml @@ -0,0 +1,64 @@ +[config] + skip_core_tasks = true + +[env] + TARGET = { script = ["rustc -vV | sed -n 's|host: ||p'"] } + +[tasks.default] + dependencies = ["build"] + +[tasks.build] + dependencies = ["cpp", "wasm"] + +[tasks.test] + dependencies = ["build", "test:rust", "test:wasm"] + +[tasks."test:rust"] + command = "cargo" + args = ["test"] + +[tasks."test:wasm"] + dependencies = ["wasm:release"] + command = "node" + args = ["--test", "--test-reporter=cleaner-spec-reporter", "wasm/test/*.test.js"] + +[tasks.cpp] + dependencies = ["cpp:headers", "cpp:libs"] + +[tasks.wasm] + dependencies = ["wasm:release", "wasm:debug"] + +[tasks."cpp:libs"] + dependencies = ["cpp:release", "cpp:debug"] + +[tasks."cpp:release"] + env = { CONFIGURATION = "release", "CARGO_OPTIONS" = "--release" } + run_task = "cpp:lib" + +[tasks."cpp:debug"] + env = { CONFIGURATION = "debug", "CARGO_OPTIONS" = "--release --features debug" } + run_task = "cpp:lib" + +[tasks."cpp:lib"] + dependencies = ["scripts:build"] + script = ["mkdir -p ../dist/cpp/${CONFIGURATION}", "rm -rf ../dist/cpp/${CONFIGURATION}/*", "cargo build --locked -Z unstable-options --target ${TARGET} ${CARGO_OPTIONS} --artifact-dir target/cpp/${CONFIGURATION}", "cp target/cpp/${CONFIGURATION}/libmilo_parser.a ../dist/cpp/${CONFIGURATION}/libmilo.a", "cp target/headers/milo.h ../dist/cpp/${CONFIGURATION}", "../scripts/target/release/postbuild-cpp ${CONFIGURATION}"] + +[tasks."scripts:build"] + command = "cargo" + args = ["build", "--locked", "--release", "--manifest-path", "../scripts/Cargo.toml", "--target-dir", "../scripts/target", "--bins"] + +[tasks."cpp:headers"] + command = "cbindgen" + args = ["--quiet", "--output", "target/headers/milo.h"] + +[tasks."wasm:release"] + env = { CONFIGURATION = "release", CARGO_BUILD_OPTIONS = "-Z unstable-options -Z build-std=std,panic_abort", CARGO_OPTIONS = "--release", RUSTFLAGS_BASE = "-Z unstable-options -C panic=immediate-abort", WASMPACK_OPTIONS = "--release" } + run_task = "wasm:module" + +[tasks."wasm:debug"] + env = { CONFIGURATION = "debug", CARGO_BUILD_OPTIONS = "-Z unstable-options", CARGO_OPTIONS = "--release --features debug", RUSTFLAGS_BASE = "", WASMPACK_OPTIONS = "--release" } + run_task = "wasm:module" + +[tasks."wasm:module"] + dependencies = ["scripts:build"] + script = ["mkdir -p ../dist/wasm", "rm -rf ../dist/wasm/${CONFIGURATION}", "mkdir -p ../dist/wasm/${CONFIGURATION}/binary", "RUSTFLAGS=\"${RUSTFLAGS_BASE} -C target-feature=+simd128\" cargo build --locked --lib ${CARGO_BUILD_OPTIONS} --target wasm32-unknown-unknown ${CARGO_OPTIONS}", "cp -a target/wasm32-unknown-unknown/release/milo_parser.wasm ../dist/wasm/${CONFIGURATION}/binary/simd.wasm", "wasm-opt -O3 --enable-bulk-memory-opt --enable-simd -o ../dist/wasm/${CONFIGURATION}/binary/simd.wasm ../dist/wasm/${CONFIGURATION}/binary/simd.wasm", "RUSTFLAGS=\"${RUSTFLAGS_BASE}\" cargo build --locked --lib ${CARGO_BUILD_OPTIONS} --target wasm32-unknown-unknown ${CARGO_OPTIONS}", "cp -a target/wasm32-unknown-unknown/release/milo_parser.wasm ../dist/wasm/${CONFIGURATION}/binary/no-simd.wasm", "wasm-opt -O3 --enable-bulk-memory-opt -o ../dist/wasm/${CONFIGURATION}/binary/no-simd.wasm ../dist/wasm/${CONFIGURATION}/binary/no-simd.wasm", "../scripts/target/release/postbuild-wasm ${CONFIGURATION}"] diff --git a/deps/undici/src/deps/milo/parser/README.md b/deps/undici/src/deps/milo/parser/README.md new file mode 100644 index 000000000000..7e99f16ff41d --- /dev/null +++ b/deps/undici/src/deps/milo/parser/README.md @@ -0,0 +1,26 @@ +# Milo Parser + +This folder contains the milo main parser. + +To build milo, simply run: + +```bash +makers +``` + +this will generate `cpp` and `wasm` build in `dist` folder. +These files can be copied in the source code to embed milo. + +## How to use the API + +See the main [README.md](../README.md) in the parent folder. + +## Verify compiled code and macro substitution + +Once [cargo-expand](https://github.com/dtolnay/cargo-expand), is installed, run: + +```bash +cargo expand > compiled.rs +``` + +This will generate a file which can be analyzed to see what is the final compiled code after all macros have been evaluated. diff --git a/deps/undici/src/deps/milo/parser/cbindgen.toml b/deps/undici/src/deps/milo/parser/cbindgen.toml new file mode 100644 index 000000000000..844779ada29a --- /dev/null +++ b/deps/undici/src/deps/milo/parser/cbindgen.toml @@ -0,0 +1,21 @@ +language = "C++" +include_guard = "MILO_H" +namespace = "milo_parser" + +[parse.expand] + crates = ["milo-parser"] + +[export] + include = [ + "Parser", + "ParserCallbacks", + "CStringWithLength", + "Callback", + "MessageTypes", + "Connections", + "Methods", + "States", + "Errors", + "Callbacks", + "Events", + ] diff --git a/deps/undici/src/deps/milo/parser/src/lib.rs b/deps/undici/src/deps/milo/parser/src/lib.rs new file mode 100644 index 000000000000..f5f0b50d6f9a --- /dev/null +++ b/deps/undici/src/deps/milo/parser/src/lib.rs @@ -0,0 +1,359 @@ +#![allow(unused_imports)] + +extern crate alloc; + +use alloc::ffi::CString; +use alloc::vec::Vec; +use alloc::{boxed::Box, format}; +use core::cell::{Cell, RefCell}; +use core::ffi::{c_char, c_uchar, c_void}; +use core::fmt::Debug; +use core::str; +use core::{mem, ptr}; +use core::{slice, slice::from_raw_parts}; + +use milo_macros::generate; + +#[repr(C)] +#[derive(Debug)] +pub struct Parser { + // User writable + #[cfg(not(target_family = "wasm"))] + pub context: *mut c_void, + pub max_start_line_length: usize, + pub max_header_length: usize, + pub max_body_payload: u64, + pub autodetect: bool, + pub is_request: bool, + pub suspend_after_headers: bool, + pub manage_unconsumed: bool, + pub continue_without_data: bool, + pub is_connect: bool, + pub skip_body: bool, + pub debug: bool, + + // Generic state + pub parsed: u64, + pub position: usize, + pub state: u8, + pub paused: bool, + pub error_code: u8, + + // Current message flags + pub content_length: u64, + pub chunk_size: u64, + pub remaining_content_length: u64, + pub remaining_chunk_size: u64, + pub status: u32, + pub method: u8, + pub has_content_length: bool, + pub has_transfer_encoding: bool, + pub has_chunked_transfer_encoding: bool, + pub has_connection_close: bool, + pub has_connection_upgrade: bool, + pub has_upgrade: bool, + pub has_trailers: bool, + + // Callback handling + pub active_callbacks: u64, + pub active_events: u64, + #[cfg(not(target_family = "wasm"))] + pub callbacks: ParserCallbacks, + + // WASM Specific + #[cfg(target_family = "wasm")] + pub ptr: *mut c_void, + + // Complex data types - We need to split them in order to be exportable to C++ + pub error_description: [u8; 255], + pub unconsumed: *const c_uchar, + pub unconsumed_len: usize, + pub error_description_len: u8, + + // Event buffer. Keep this at the end of the struct for external readers. + pub events: *mut c_uchar, +} + +#[cfg(not(target_family = "wasm"))] +mod native; + +#[cfg(not(target_family = "wasm"))] +pub use crate::native::*; + +#[cfg(target_family = "wasm")] +mod wasm; + +#[cfg(target_family = "wasm")] +pub use crate::wasm::*; + +generate!(); + +impl Parser { + /// Creates a new parser + pub fn new() -> Parser { + let mut events = Box::new([0u8; 65536]); + let events_ptr = events.as_mut_ptr(); + mem::forget(events); + + Parser { + // User writable + #[cfg(not(target_family = "wasm"))] + context: ptr::null_mut(), + max_start_line_length: 8192, + max_header_length: 8192, + max_body_payload: 0, + autodetect: true, + is_request: false, + suspend_after_headers: false, + manage_unconsumed: false, + continue_without_data: false, + is_connect: false, + skip_body: false, + debug: false, + // Generic state + parsed: 0, + position: 0, + state: STATE_START, + paused: false, + error_code: ERROR_NONE, + // Current message flags + content_length: 0, + chunk_size: 0, + remaining_content_length: 0, + remaining_chunk_size: 0, + status: 0, + method: 0, + has_content_length: false, + has_transfer_encoding: false, + has_chunked_transfer_encoding: false, + has_connection_close: false, + has_connection_upgrade: false, + has_upgrade: false, + has_trailers: false, + // Callbacks handling + active_callbacks: 0, + active_events: 0, + #[cfg(not(target_family = "wasm"))] + callbacks: ParserCallbacks::new(), + // WASM Specific + #[cfg(target_family = "wasm")] + ptr: ptr::null_mut(), + // Complex data types + error_description: [0; 255], + unconsumed: ptr::null(), + unconsumed_len: 0, + error_description_len: 0, + events: events_ptr, + } + } + + /// Resets a parser. The second parameters specifies if to also reset the + /// parsed counter. + /// + /// The following fields are not modified: + /// * position + /// * context + /// * autodetect + /// * is_request + /// * suspend_after_headers + /// * manage_unconsumed + /// * continue_without_data + /// * debug + /// * context + pub fn reset(&mut self, keep_parsed: bool) { + self.state = STATE_START; + self.paused = false; + + if !keep_parsed { + self.parsed = 0; + } + + self.error_code = ERROR_NONE; + + self.error_description[0] = 0; + self.error_description_len = 0; + + self.clear_unconsumed(); + + self.clear(); + self.skip_body = false; + unsafe { + *self.events = EVENT_END; + } + } + + pub(crate) fn clear_unconsumed(&mut self) { + if self.unconsumed_len > 0 { + // SAFETY: Retained input is allocated as a boxed slice with exactly this + // length, and the parser owns it until replacement, reset, or destruction. + unsafe { + drop(Box::from_raw(ptr::slice_from_raw_parts_mut( + self.unconsumed as *mut c_uchar, + self.unconsumed_len, + ))); + } + } + self.unconsumed = ptr::null(); + self.unconsumed_len = 0; + } + + /// Clears all values about the message in the parser. + pub fn clear(&mut self) { + self.is_connect = false; + self.method = 0; + self.status = 0; + self.has_content_length = false; + self.has_transfer_encoding = false; + self.has_chunked_transfer_encoding = false; + self.has_connection_close = false; + self.has_connection_upgrade = false; + self.has_upgrade = false; + self.has_trailers = false; + self.content_length = 0; + self.chunk_size = 0; + self.remaining_content_length = 0; + self.remaining_chunk_size = 0; + } + + #[inline(always)] + pub(crate) fn try_emit_event_range( + &mut self, + event_cursor: &mut usize, + event_type: u8, + at: usize, + len: usize, + ) -> bool { + // Reserve six bytes for a later error event and one for the terminator. + if *event_cursor + EVENT_RANGE_SIZE + EVENT_ERROR_RESERVE > EVENTS_BUFFER_SIZE { + return false; + } + + unsafe { + *self.events.add(*event_cursor) = event_type; + core::ptr::write_unaligned(self.events.add(*event_cursor + 1) as *mut u32, (at as u32).to_le()); + core::ptr::write_unaligned(self.events.add(*event_cursor + 5) as *mut u32, (len as u32).to_le()); + } + *event_cursor += EVENT_RANGE_SIZE; + true + } + + #[inline(always)] + pub(crate) fn try_emit_event_error(&mut self, event_cursor: &mut usize) -> bool { + if *event_cursor + EVENT_ERROR_SIZE + EVENT_END_SIZE > EVENTS_BUFFER_SIZE { + return false; + } + + unsafe { + *self.events.add(*event_cursor) = EVENT_ERROR; + core::ptr::write_unaligned( + self.events.add(*event_cursor + 1) as *mut u32, + (self.position as u32).to_le(), + ); + *self.events.add(*event_cursor + 5) = self.error_code; + } + *event_cursor += EVENT_ERROR_SIZE; + true + } + + /// Pauses the parser. It will have to be resumed via `resume`. + pub fn pause(&mut self) { self.paused = true; } + + /// Resumes the parser. + pub fn resume(&mut self) { self.paused = false; } + + /// Marks the parser as finished. Any new data received via `parse` will + /// put the parser in the error state. + pub fn finish(&mut self) { + match self.state { + // If the parser is one of the initial states, simply jump to finish + STATE_START | STATE_REQUEST_LINE | STATE_STATUS_LINE | STATE_FINISH => { + self.state = STATE_FINISH; + } + STATE_BODY_WITH_NO_LENGTH => { + // Notify that the message has been completed + let active_events = self.active_events | self.active_callbacks; + let mut event_cursor = 0usize; + if active_events & EVENT_ACTIVE_ON_MESSAGE_COMPLETE != 0 { + self.try_emit_event_range(&mut event_cursor, EVENT_MESSAGE_COMPLETE, self.position, 0); + } + unsafe { + *self.events.add(event_cursor) = EVENT_END; + } + + // Set the state to be finished + self.state = STATE_FINISH; + } + STATE_ERROR => (), + // In another other state, this is an error + _ => { + self.fail(ERROR_UNEXPECTED_EOF, "Unexpected end of data"); + } + } + } + + /// Marks parsing as failed and starts a standalone error event batch. + #[inline(always)] + pub fn fail(&mut self, code: u8, description: &str) { + self.set_error(code, description); + let mut event_cursor = 0usize; + if (self.active_events | self.active_callbacks) & EVENT_ACTIVE_ON_ERROR != 0 { + self.try_emit_event_error(&mut event_cursor); + } + // SAFETY: The cursor follows at most one error event in the owned event buffer. + unsafe { + *self.events.add(event_cursor) = EVENT_END; + } + } + + /// Records an error without changing the current event batch. + #[inline(always)] + pub(crate) fn set_error(&mut self, code: u8, description: &str) { + let bytes = description.as_bytes(); + let len = bytes.len().min(254); + + self.state = STATE_ERROR; + self.error_code = code; + self.error_description[..len].copy_from_slice(&bytes[..len]); + self.error_description[len] = 0; + self.error_description_len = len as u8; + } + + /// Returns the current parser's state as string. + pub fn state_str(&self) -> &str { States::try_from(self.state).unwrap().as_str() } + + /// Returns the current parser's error state as string. + pub fn error_code_str(&self) -> &str { Errors::try_from(self.error_code).unwrap().as_str() } + + /// Returns the current parser's error description as string. + pub fn error_description_str(&self) -> &str { + unsafe { + if self.error_description_len > 0 { + str::from_utf8_unchecked(from_raw_parts( + self.error_description.as_ptr(), + self.error_description_len as usize, + )) + } else { + "" + } + } + } +} + +impl Drop for Parser { + fn drop(&mut self) { + self.clear_unconsumed(); + if !self.events.is_null() { + unsafe { + let _ = Box::from_raw(self.events as *mut [u8; 65536]); + } + self.events = ptr::null_mut(); + } + } +} + +impl Default for Parser { + fn default() -> Self { Self::new() } +} + +mod matchers; +mod parse; diff --git a/deps/undici/src/deps/milo/parser/src/main.rs b/deps/undici/src/deps/milo/parser/src/main.rs new file mode 100644 index 000000000000..46ae7fc795d9 --- /dev/null +++ b/deps/undici/src/deps/milo/parser/src/main.rs @@ -0,0 +1,271 @@ +use std::env; +use std::ffi::c_void; +use std::fs; +use std::io::{self, Read}; +use std::path::PathBuf; +use std::process::ExitCode; + +use milo_parser::{CALLBACK_ACTIVE_ALL, Parser, STATE_ERROR}; + +struct Options { + file: Option, + mode: Mode, +} + +#[derive(Clone, Copy, PartialEq)] +enum Mode { + Autodetect, + Request, + Response, +} + +struct Context { + output: Vec, +} + +enum RunError { + Usage(String), + Io(String), +} + +fn print_usage() { + eprintln!("Usage: milo-parser [-f|--file PATH] [-o|--request | -i|--response]"); +} + +fn parse_args() -> Result { + let mut args = env::args().skip(1); + let mut options = Options { + file: None, + mode: Mode::Autodetect, + }; + + while let Some(arg) = args.next() { + match arg.as_str() { + "-f" | "--file" => { + let path = args.next().ok_or_else(|| format!("Missing value for {arg}"))?; + options.file = Some(PathBuf::from(path)); + } + "-o" | "--request" => { + if options.mode != Mode::Autodetect { + return Err("Cannot use both request and response mode".into()); + } + + options.mode = Mode::Request; + } + "-i" | "--response" => { + if options.mode != Mode::Autodetect { + return Err("Cannot use both request and response mode".into()); + } + + options.mode = Mode::Response; + } + "-h" | "--help" => { + print_usage(); + std::process::exit(0); + } + _ => return Err(format!("Unknown argument: {arg}")), + } + } + + Ok(options) +} + +fn read_input(options: &Options) -> io::Result> { + match &options.file { + Some(path) => fs::read(path), + None => { + let mut input = Vec::new(); + io::stdin().read_to_end(&mut input)?; + Ok(input) + } + } +} + +fn quote(value: &str) -> String { + let mut quoted = String::with_capacity(value.len() + 2); + quoted.push('"'); + + for c in value.chars() { + match c { + '"' => quoted.push_str("\\\""), + '\\' => quoted.push_str("\\\\"), + '\n' => quoted.push_str("\\n"), + '\r' => quoted.push_str("\\r"), + '\t' => quoted.push_str("\\t"), + _ => quoted.push(c), + } + } + + quoted.push('"'); + quoted +} + +fn append_output(parser: &mut Parser, line: String) { + let mut context = unsafe { Box::from_raw(parser.context as *mut Context) }; + context.output.push(line); + let _ = Box::into_raw(context); +} + +fn event(parser: &mut Parser, offset: usize, size: usize, name: &str) { + append_output(parser, format!("offset={offset} size={size} event={name}")); +} + +fn on_error(parser: &mut Parser, offset: usize, size: usize) { + append_output( + parser, + format!( + "offset={offset} size={size} event=error error={} description={}", + parser.error_code_str(), + quote(parser.error_description_str()) + ), + ); +} + +fn on_finish(parser: &mut Parser, offset: usize, size: usize) { event(parser, offset, size, "finish"); } + +fn on_message_start(parser: &mut Parser, offset: usize, size: usize) { event(parser, offset, size, "message_start"); } + +fn on_message_complete(parser: &mut Parser, offset: usize, size: usize) { + event(parser, offset, size, "message_complete"); +} + +fn on_request(parser: &mut Parser, offset: usize, size: usize) { event(parser, offset, size, "request"); } + +fn on_response(parser: &mut Parser, offset: usize, size: usize) { event(parser, offset, size, "response"); } + +fn on_reset(parser: &mut Parser, offset: usize, size: usize) { event(parser, offset, size, "reset"); } + +fn on_method(parser: &mut Parser, offset: usize, size: usize) { event(parser, offset, size, "method"); } + +fn on_url(parser: &mut Parser, offset: usize, size: usize) { event(parser, offset, size, "url"); } + +fn on_protocol(parser: &mut Parser, offset: usize, size: usize) { event(parser, offset, size, "protocol"); } + +fn on_version(parser: &mut Parser, offset: usize, size: usize) { event(parser, offset, size, "version"); } + +fn on_status(parser: &mut Parser, offset: usize, size: usize) { event(parser, offset, size, "status"); } + +fn on_reason(parser: &mut Parser, offset: usize, size: usize) { event(parser, offset, size, "reason"); } + +fn on_header_name(parser: &mut Parser, offset: usize, size: usize) { event(parser, offset, size, "header_name"); } + +fn on_header_value(parser: &mut Parser, offset: usize, size: usize) { event(parser, offset, size, "header_value"); } + +fn on_headers(parser: &mut Parser, offset: usize, size: usize) { event(parser, offset, size, "headers"); } + +fn on_connect(parser: &mut Parser, offset: usize, size: usize) { event(parser, offset, size, "connect"); } + +fn on_upgrade(parser: &mut Parser, offset: usize, size: usize) { event(parser, offset, size, "upgrade"); } + +fn on_chunk_length(parser: &mut Parser, offset: usize, size: usize) { event(parser, offset, size, "chunk_length"); } + +fn on_chunk_extension_name(parser: &mut Parser, offset: usize, size: usize) { + event(parser, offset, size, "chunk_extension_name"); +} + +fn on_chunk_extension_value(parser: &mut Parser, offset: usize, size: usize) { + event(parser, offset, size, "chunk_extension_value"); +} + +fn on_chunk(parser: &mut Parser, offset: usize, size: usize) { event(parser, offset, size, "chunk"); } + +fn on_body(parser: &mut Parser, offset: usize, size: usize) { event(parser, offset, size, "body"); } + +fn on_data(parser: &mut Parser, offset: usize, size: usize) { event(parser, offset, size, "data"); } + +fn on_trailer_name(parser: &mut Parser, offset: usize, size: usize) { event(parser, offset, size, "trailer_name"); } + +fn on_trailer_value(parser: &mut Parser, offset: usize, size: usize) { event(parser, offset, size, "trailer_value"); } + +fn on_trailers(parser: &mut Parser, offset: usize, size: usize) { event(parser, offset, size, "trailers"); } + +fn on_state_change(parser: &mut Parser, offset: usize, size: usize) { event(parser, offset, size, "state_change"); } + +fn create_parser(mode: Mode) -> Parser { + let mut parser = Parser::new(); + let context = Box::new(Context { output: Vec::new() }); + parser.context = Box::into_raw(context) as *mut c_void; + parser.active_callbacks = CALLBACK_ACTIVE_ALL; + + match mode { + Mode::Autodetect => { + parser.autodetect = true; + } + Mode::Request => { + parser.autodetect = false; + parser.is_request = true; + } + Mode::Response => { + parser.autodetect = false; + parser.is_request = false; + } + } + + parser.callbacks.on_error = on_error; + parser.callbacks.on_finish = on_finish; + parser.callbacks.on_message_start = on_message_start; + parser.callbacks.on_message_complete = on_message_complete; + parser.callbacks.on_request = on_request; + parser.callbacks.on_response = on_response; + parser.callbacks.on_reset = on_reset; + parser.callbacks.on_method = on_method; + parser.callbacks.on_url = on_url; + parser.callbacks.on_protocol = on_protocol; + parser.callbacks.on_version = on_version; + parser.callbacks.on_status = on_status; + parser.callbacks.on_reason = on_reason; + parser.callbacks.on_header_name = on_header_name; + parser.callbacks.on_header_value = on_header_value; + parser.callbacks.on_headers = on_headers; + parser.callbacks.on_connect = on_connect; + parser.callbacks.on_upgrade = on_upgrade; + parser.callbacks.on_chunk_length = on_chunk_length; + parser.callbacks.on_chunk_extension_name = on_chunk_extension_name; + parser.callbacks.on_chunk_extension_value = on_chunk_extension_value; + parser.callbacks.on_chunk = on_chunk; + parser.callbacks.on_body = on_body; + parser.callbacks.on_data = on_data; + parser.callbacks.on_trailer_name = on_trailer_name; + parser.callbacks.on_trailer_value = on_trailer_value; + parser.callbacks.on_trailers = on_trailers; + parser.callbacks.on_state_change = on_state_change; + + parser +} + +fn run() -> Result { + let options = parse_args().map_err(RunError::Usage)?; + let input = read_input(&options).map_err(|e| RunError::Io(e.to_string()))?; + let mut parser = create_parser(options.mode); + + parser.parse(input.as_ptr(), input.len()); + + if parser.state != STATE_ERROR { + parser.finish(); + } + + let failed = parser.state == STATE_ERROR; + let context = unsafe { Box::from_raw(parser.context as *mut Context) }; + + for line in &context.output { + println!("{line}"); + } + + Ok(failed) +} + +fn main() -> ExitCode { + match run() { + Ok(false) => ExitCode::SUCCESS, + Ok(true) => ExitCode::from(1), + Err(RunError::Usage(error)) => { + eprintln!("{error}"); + print_usage(); + ExitCode::from(2) + } + Err(RunError::Io(error)) => { + eprintln!("{error}"); + ExitCode::from(1) + } + } +} diff --git a/deps/undici/src/deps/milo/parser/src/matchers.rs b/deps/undici/src/deps/milo/parser/src/matchers.rs new file mode 100644 index 000000000000..0687d26b394f --- /dev/null +++ b/deps/undici/src/deps/milo/parser/src/matchers.rs @@ -0,0 +1,489 @@ +use crate::{QUOTED_PAIR_TABLE, QUOTED_STRING_TABLE, TOKEN_TABLE, URL_TABLE}; + +pub enum MatchResult { + Continue, + Suspend, + Stop, +} + +pub enum HeaderLineScanResult { + Cr(usize), + Invalid(usize), + Incomplete, +} + +#[inline(always)] +pub fn ensure_valid_line(data: &[u8], cr: usize, available: usize) -> MatchResult { + if cr + 1 == available { + MatchResult::Suspend + } else if data[cr + 1] != b'\n' { + MatchResult::Stop + } else { + MatchResult::Continue + } +} + +#[inline(always)] +pub fn find_cr(data: &[u8], available: usize) -> Option { + if available == 0 { + None + } else { + find_char(data, 0, available - 1, b'\r') + } +} + +#[inline(always)] +pub fn find_char(buf: &[u8], start: usize, end: usize, needle: u8) -> Option { + if start > end || end >= buf.len() { + return None; + } + + memchr::memchr(needle, &buf[start..=end]).map(|i| start + i) +} + +#[inline(always)] +pub fn find_char2(buf: &[u8], start: usize, end: usize, needle1: u8, needle2: u8) -> Option { + if start > end || end >= buf.len() { + return None; + } + + memchr::memchr2(needle1, needle2, &buf[start..=end]).map(|i| start + i) +} + +#[inline(always)] +pub fn is_digit(byte: u8) -> bool { byte.wrapping_sub(b'0') <= 9 } + +#[inline(always)] +pub fn is_ws(byte: u8) -> bool { byte == b' ' || byte == b'\t' } + +#[inline(always)] +pub fn validate_token(data: &[u8], start: usize, end: usize) -> bool { + if start == end { + return false; + } + + let mut i = start; + while i < end { + if !TOKEN_TABLE[data[i] as usize] { + return false; + } + + i += 1; + } + + true +} + +#[inline(always)] +pub fn validate_quoted_string(data: &[u8], start: usize, end: usize) -> bool { + let mut i = start; + while i < end { + let byte = data[i]; + if byte == b'\\' { + i += 1; + if i == end || !QUOTED_PAIR_TABLE[data[i] as usize] { + return false; + } + } else if !QUOTED_STRING_TABLE[byte as usize] { + return false; + } + + i += 1; + } + + true +} + +#[inline(always)] +pub fn validate_url(data: &[u8], start: usize, end: usize) -> bool { + if start == end { + return false; + } + + let mut i = start; + while i < end { + if !URL_TABLE[data[i] as usize] { + return false; + } + + i += 1; + } + + true +} + +#[inline(always)] +pub fn strip_ows_fast(data: &[u8], start_ref: &mut usize, end_ref: &mut usize, allow_empty: bool) -> bool { + let start = *start_ref; + let end = *end_ref; + + // The single-space shortcut is valid only when no further leading OWS follows. + if start + 1 < end && data[start] == b' ' && !is_ws(data[start + 1]) && !is_ws(data[end - 1]) { + *start_ref = start + 1; + return true; + } + + strip_ows(data, start_ref, end_ref, allow_empty) +} + +#[inline(always)] +pub fn strip_ows(data: &[u8], start_ref: &mut usize, end_ref: &mut usize, allow_empty: bool) -> bool { + let mut start = *start_ref; + let mut end = *end_ref; + + while start < end && is_ws(data[start]) { + start += 1; + } + + while end > start && is_ws(data[end - 1]) { + end -= 1; + } + + if start == end && !allow_empty { + return false; + } + + *start_ref = start; + *end_ref = end; + true +} + +#[inline(always)] +fn scan_header_line_scalar(ptr: *const u8, mut i: usize, end: usize) -> HeaderLineScanResult { + while i < end { + let byte = unsafe { *ptr.add(i) }; + + if byte == b'\r' { + return HeaderLineScanResult::Cr(i); + } + + if (byte < 0x20 && byte != b'\t') || byte == 0x7f { + return HeaderLineScanResult::Invalid(i); + } + + i += 1; + } + + HeaderLineScanResult::Incomplete +} + +#[cfg(target_arch = "aarch64")] +#[inline(always)] +pub fn find_header_line_end(ptr: *const u8, len: usize) -> HeaderLineScanResult { + use core::arch::aarch64::*; + + if len < 16 { + return scan_header_line_scalar(ptr, 0, len); + } + + let mut i = 0; + let v_20 = unsafe { vdupq_n_u8(0x20) }; + let v_7f = unsafe { vdupq_n_u8(0x7f) }; + let v_tab = unsafe { vdupq_n_u8(b'\t') }; + let v_cr = unsafe { vdupq_n_u8(b'\r') }; + + while i + 16 <= len { + let x = unsafe { vld1q_u8(ptr.add(i)) }; + let lt_20 = unsafe { vcltq_u8(x, v_20) }; + let eq_tab = unsafe { vceqq_u8(x, v_tab) }; + let eq_cr = unsafe { vceqq_u8(x, v_cr) }; + let eq_7f = unsafe { vceqq_u8(x, v_7f) }; + + // Header lines stop at CR; other control bytes are invalid except HTAB. + let ctrl = unsafe { vbicq_u8(lt_20, eq_tab) }; + let invalid = unsafe { vbicq_u8(vorrq_u8(ctrl, eq_7f), eq_cr) }; + let found = unsafe { vorrq_u8(eq_cr, invalid) }; + + if unsafe { vmaxvq_u8(found) } != 0 { + return scan_header_line_scalar(ptr, i, i + 16); + } + + i += 16; + } + + scan_header_line_scalar(ptr, i, len) +} + +#[cfg(target_arch = "x86_64")] +#[inline(always)] +pub fn find_header_line_end(ptr: *const u8, len: usize) -> HeaderLineScanResult { + use core::arch::x86_64::*; + + if len < 16 { + return scan_header_line_scalar(ptr, 0, len); + } + + let mut i = 0; + let v_20 = unsafe { _mm_set1_epi8(0x20u8 as i8) }; + let v_7f = unsafe { _mm_set1_epi8(0x7fu8 as i8) }; + let v_tab = unsafe { _mm_set1_epi8(b'\t' as i8) }; + let v_cr = unsafe { _mm_set1_epi8(b'\r' as i8) }; + let v_zero = unsafe { _mm_setzero_si128() }; + let v_ones = unsafe { _mm_cmpeq_epi8(v_zero, v_zero) }; + + while i + 32 <= len { + let x = unsafe { _mm_loadu_si128(ptr.add(i) as *const __m128i) }; + let lt_20 = unsafe { _mm_andnot_si128(_mm_cmpeq_epi8(_mm_subs_epu8(v_20, x), v_zero), v_ones) }; + let eq_tab = unsafe { _mm_cmpeq_epi8(x, v_tab) }; + let eq_cr = unsafe { _mm_cmpeq_epi8(x, v_cr) }; + let eq_7f = unsafe { _mm_cmpeq_epi8(x, v_7f) }; + + // Header lines stop at CR; other control bytes are invalid except HTAB. + let ctrl = unsafe { _mm_andnot_si128(eq_tab, lt_20) }; + let invalid = unsafe { _mm_andnot_si128(eq_cr, _mm_or_si128(ctrl, eq_7f)) }; + let cr_mask = unsafe { _mm_movemask_epi8(eq_cr) }; + let invalid_mask = unsafe { _mm_movemask_epi8(invalid) }; + let found_mask = cr_mask | invalid_mask; + + if found_mask != 0 { + let index = i + found_mask.trailing_zeros() as usize; + return if cr_mask & (1 << (index - i)) != 0 { + HeaderLineScanResult::Cr(index) + } else { + HeaderLineScanResult::Invalid(index) + }; + } + + let x = unsafe { _mm_loadu_si128(ptr.add(i + 16) as *const __m128i) }; + let lt_20 = unsafe { _mm_andnot_si128(_mm_cmpeq_epi8(_mm_subs_epu8(v_20, x), v_zero), v_ones) }; + let eq_tab = unsafe { _mm_cmpeq_epi8(x, v_tab) }; + let eq_cr = unsafe { _mm_cmpeq_epi8(x, v_cr) }; + let eq_7f = unsafe { _mm_cmpeq_epi8(x, v_7f) }; + let ctrl = unsafe { _mm_andnot_si128(eq_tab, lt_20) }; + let invalid = unsafe { _mm_andnot_si128(eq_cr, _mm_or_si128(ctrl, eq_7f)) }; + let cr_mask = unsafe { _mm_movemask_epi8(eq_cr) }; + let invalid_mask = unsafe { _mm_movemask_epi8(invalid) }; + let found_mask = cr_mask | invalid_mask; + + if found_mask != 0 { + let lane = found_mask.trailing_zeros() as usize; + let index = i + 16 + lane; + return if cr_mask & (1 << lane) != 0 { + HeaderLineScanResult::Cr(index) + } else { + HeaderLineScanResult::Invalid(index) + }; + } + + i += 32; + } + + while i + 16 <= len { + let x = unsafe { _mm_loadu_si128(ptr.add(i) as *const __m128i) }; + let lt_20 = unsafe { _mm_andnot_si128(_mm_cmpeq_epi8(_mm_subs_epu8(v_20, x), v_zero), v_ones) }; + let eq_tab = unsafe { _mm_cmpeq_epi8(x, v_tab) }; + let eq_cr = unsafe { _mm_cmpeq_epi8(x, v_cr) }; + let eq_7f = unsafe { _mm_cmpeq_epi8(x, v_7f) }; + let ctrl = unsafe { _mm_andnot_si128(eq_tab, lt_20) }; + let invalid = unsafe { _mm_andnot_si128(eq_cr, _mm_or_si128(ctrl, eq_7f)) }; + let cr_mask = unsafe { _mm_movemask_epi8(eq_cr) }; + let invalid_mask = unsafe { _mm_movemask_epi8(invalid) }; + let found_mask = cr_mask | invalid_mask; + + if found_mask != 0 { + let lane = found_mask.trailing_zeros() as usize; + let index = i + lane; + return if cr_mask & (1 << lane) != 0 { + HeaderLineScanResult::Cr(index) + } else { + HeaderLineScanResult::Invalid(index) + }; + } + + i += 16; + } + + scan_header_line_scalar(ptr, i, len) +} + +#[cfg(all(target_arch = "wasm32", target_feature = "simd128"))] +#[target_feature(enable = "simd128")] +pub fn find_header_line_end(ptr: *const u8, len: usize) -> HeaderLineScanResult { + use core::arch::wasm32::*; + + if len < 16 { + return scan_header_line_scalar(ptr, 0, len); + } + + let mut i = 0; + let v_20 = u8x16_splat(0x20); + let v_7f = u8x16_splat(0x7f); + let v_tab = u8x16_splat(b'\t'); + let v_cr = u8x16_splat(b'\r'); + + while i + 16 <= len { + let x = unsafe { v128_load(ptr.add(i) as *const v128) }; + let lt_20 = u8x16_lt(x, v_20); + let eq_tab = u8x16_eq(x, v_tab); + let eq_cr = u8x16_eq(x, v_cr); + let eq_7f = u8x16_eq(x, v_7f); + + // Header lines stop at CR; other control bytes are invalid except HTAB. + // WASM andnot(a, b) computes a & !b, unlike the x86 intrinsic. + let ctrl = v128_andnot(lt_20, eq_tab); + let invalid = v128_andnot(v128_or(ctrl, eq_7f), eq_cr); + let found = v128_or(eq_cr, invalid); + + if v128_any_true(found) { + return scan_header_line_scalar(ptr, i, i + 16); + } + + i += 16; + } + + scan_header_line_scalar(ptr, i, len) +} + +#[cfg(not(any( + target_arch = "aarch64", + target_arch = "x86_64", + all(target_arch = "wasm32", target_feature = "simd128") +)))] +#[inline(always)] +pub fn find_header_line_end(ptr: *const u8, len: usize) -> HeaderLineScanResult { scan_header_line_scalar(ptr, 0, len) } + +#[cfg(target_arch = "aarch64")] +#[target_feature(enable = "neon")] +pub fn validate_token_value(ptr: *const u8, len: usize) -> bool { + use core::arch::aarch64::*; + + let mut i = 0; + + let v_20 = vdupq_n_u8(0x20); + let v_7f = vdupq_n_u8(0x7f); + let v_tab = vdupq_n_u8(9); + + while i + 16 <= len { + let x = unsafe { vld1q_u8(ptr.add(i)) }; + let lt_20 = vcltq_u8(x, v_20); + let eq_tab = vceqq_u8(x, v_tab); + let eq_7f = vceqq_u8(x, v_7f); + + // Field values allow HTAB but reject the remaining C0 controls and DEL. + let ctrl = vbicq_u8(lt_20, eq_tab); // lt_20 & !eq_tab + let invalid = vorrq_u8(ctrl, eq_7f); + + if vmaxvq_u8(invalid) != 0 { + return false; + } + + i += 16; + } + + while i < len { + let b = unsafe { *ptr.add(i) }; + + if (b < 0x20 && b != b'\t') || b == 0x7f { + return false; + } + + i += 1; + } + + true +} + +#[cfg(target_arch = "x86_64")] +#[target_feature(enable = "sse2")] +pub fn validate_token_value(ptr: *const u8, len: usize) -> bool { + use core::arch::x86_64::*; + + let mut i = 0; + let v_20 = _mm_set1_epi8(0x20u8 as i8); + let v_7f = _mm_set1_epi8(0x7fu8 as i8); + let v_tab = _mm_set1_epi8(b'\t' as i8); + let v_zero = _mm_setzero_si128(); + let v_ones = _mm_cmpeq_epi8(v_zero, v_zero); + + while i + 16 <= len { + let x = unsafe { _mm_loadu_si128(ptr.add(i) as *const __m128i) }; + let lt_20 = _mm_andnot_si128(_mm_cmpeq_epi8(_mm_subs_epu8(v_20, x), v_zero), v_ones); + let eq_tab = _mm_cmpeq_epi8(x, v_tab); + let eq_7f = _mm_cmpeq_epi8(x, v_7f); + + // Field values allow HTAB but reject the remaining C0 controls and DEL. + let ctrl = _mm_andnot_si128(eq_tab, lt_20); + let invalid = _mm_or_si128(ctrl, eq_7f); + + if _mm_movemask_epi8(invalid) != 0 { + return false; + } + + i += 16; + } + + while i < len { + let b = unsafe { *ptr.add(i) }; + + if (b < 0x20 && b != b'\t') || b == 0x7f { + return false; + } + + i += 1; + } + + true +} + +#[cfg(all(target_arch = "wasm32", target_feature = "simd128"))] +#[target_feature(enable = "simd128")] +pub fn validate_token_value(ptr: *const u8, len: usize) -> bool { + use core::arch::wasm32::*; + + let mut i = 0; + let v_20 = u8x16_splat(0x20); + let v_7f = u8x16_splat(0x7f); + let v_tab = u8x16_splat(b'\t'); + + while i + 16 <= len { + let x = unsafe { v128_load(ptr.add(i) as *const v128) }; + let lt_20 = u8x16_lt(x, v_20); + let eq_tab = u8x16_eq(x, v_tab); + let eq_7f = u8x16_eq(x, v_7f); + + // Field values allow HTAB but reject the remaining C0 controls and DEL. + // WASM andnot(a, b) computes a & !b, unlike the x86 intrinsic. + let ctrl = v128_andnot(lt_20, eq_tab); + let invalid = v128_or(ctrl, eq_7f); + + if v128_any_true(invalid) { + return false; + } + + i += 16; + } + + while i < len { + let b = unsafe { *ptr.add(i) }; + + if (b < 0x20 && b != b'\t') || b == 0x7f { + return false; + } + + i += 1; + } + + true +} + +#[cfg(not(any( + target_arch = "aarch64", + target_arch = "x86_64", + all(target_arch = "wasm32", target_feature = "simd128") +)))] +#[inline(always)] +pub fn validate_token_value(ptr: *const u8, len: usize) -> bool { + let mut i = 0; + + while i < len { + let b = unsafe { *ptr.add(i) }; + + if (b < 0x20 && b != b'\t') || b == 0x7f { + return false; + } + + i += 1; + } + + true +} diff --git a/deps/undici/src/deps/milo/parser/src/native.rs b/deps/undici/src/deps/milo/parser/src/native.rs new file mode 100644 index 000000000000..071802e7eed2 --- /dev/null +++ b/deps/undici/src/deps/milo/parser/src/native.rs @@ -0,0 +1,220 @@ +#![allow(clippy::not_unsafe_ptr_arg_deref)] + +use core::ptr; +use core::str; +use core::{slice, slice::from_raw_parts}; +use std::ffi::{CString, c_char, c_uchar}; + +use crate::parse; +use crate::{Callbacks, Errors, Events, Methods, Parser, States}; + +#[repr(C)] +pub struct CStringWithLength { + pub ptr: *const c_uchar, + pub len: usize, +} + +impl CStringWithLength { + fn new(value: &str) -> CStringWithLength { + let cstring = CString::new(value).unwrap(); + + CStringWithLength { + ptr: cstring.into_raw() as *const c_uchar, + len: value.len(), + } + } +} + +impl From<&str> for CStringWithLength { + fn from(value: &str) -> Self { CStringWithLength::new(value) } +} + +impl From for &str { + fn from(value: CStringWithLength) -> Self { + unsafe { str::from_utf8_unchecked(slice::from_raw_parts(value.ptr, value.len)) } + } +} + +/// Returns if debug informations are available in this build. +#[unsafe(no_mangle)] +pub extern "C" fn milo_has_debug() -> bool { cfg!(any(debug_assertions, feature = "debug")) } + +/// Returns if debug tracing is enabled for this parser. +#[unsafe(no_mangle)] +pub extern "C" fn milo_is_debug(parser: *const Parser) -> bool { unsafe { (*parser).debug } } + +/// A callback that simply returns `0`. +/// +/// Use this callback as pointer when you want to remove a callback from the +/// parser. +#[unsafe(no_mangle)] +pub extern "C" fn milo_noop(_parser: &mut Parser, _at: usize, _len: usize) {} + +/// Cleans up memory used by a string previously returned by one of the milo's C +/// public interface. +#[unsafe(no_mangle)] +pub extern "C" fn milo_free_string(s: CStringWithLength) { + unsafe { + let _ = CString::from_raw(s.ptr as *mut c_char); + } +} + +/// Creates a new parser. +#[unsafe(no_mangle)] +pub extern "C" fn milo_create() -> *mut Parser { Box::into_raw(Box::new(Parser::new())) } + +/// Destroys a parser. +#[unsafe(no_mangle)] +pub extern "C" fn milo_destroy(parser: *mut Parser) { + if parser.is_null() { + return; + } + + unsafe { + let _ = Box::from_raw(parser); + } +} + +/// Parses a slice of characters. It returns the number of consumed characters. +#[unsafe(no_mangle)] +pub extern "C" fn milo_parse(parser: *mut Parser, data: *const c_uchar, limit: usize) -> usize { + unsafe { (*parser).parse(data, limit) } +} + +/// Sets the parser event bitmask. +#[unsafe(no_mangle)] +pub extern "C" fn milo_set_active_events(parser: *mut Parser, value: u64) { + unsafe { + (*parser).active_events = value; + } +} + +/// Sets the maximum body payload consumed by a single parse invocation. +#[unsafe(no_mangle)] +pub extern "C" fn milo_set_max_body_payload(parser: *mut Parser, value: u64) { + unsafe { + (*parser).max_body_payload = value; + } +} + +/// Sets whether parsing should stop after headers have completed. +#[unsafe(no_mangle)] +pub extern "C" fn milo_set_suspend_after_headers(parser: *mut Parser, value: bool) { + unsafe { + (*parser).suspend_after_headers = value; + } +} + +/// Resets a parser. The second parameters specifies if to also reset the +/// parsed counter. +/// +/// The following fields are not modified: +/// * position +/// * context +/// * autodetect +/// * is_request +/// * suspend_after_headers +/// * max_body_payload +/// * manage_unconsumed +/// * continue_without_data +/// * debug +/// * context +#[unsafe(no_mangle)] +pub extern "C" fn milo_reset(parser: *mut Parser, keep_parsed: bool) { unsafe { (*parser).reset(keep_parsed) } } + +/// Clears all values about the message in the parser. +/// +/// The autodetect and is_request fields are not cleared. +#[unsafe(no_mangle)] +pub extern "C" fn milo_clear(parser: *mut Parser) { unsafe { (*parser).clear() } } + +/// Pauses the parser. It will have to be resumed via `milo_resume`. +#[unsafe(no_mangle)] +pub extern "C" fn milo_pause(parser: *mut Parser) { unsafe { (*parser).pause() } } + +/// Resumes the parser. +#[unsafe(no_mangle)] +pub extern "C" fn milo_resume(parser: *mut Parser) { unsafe { (*parser).resume() } } + +/// Completes the current message without consuming more input. +#[unsafe(no_mangle)] +pub extern "C" fn milo_complete(parser: *mut Parser) { unsafe { (*parser).complete() } } + +/// Marks the parser as finished. Any new data received via `milo_parse` will +/// put the parser in the error state. +#[unsafe(no_mangle)] +pub extern "C" fn milo_finish(parser: *mut Parser) { unsafe { (*parser).finish() } } + +/// Marks the parsing a failed, setting a error code and and error message. +#[unsafe(no_mangle)] +pub extern "C" fn milo_fail(parser: *mut Parser, code: u8, description: CStringWithLength) { + unsafe { (*parser).fail(code, description.into()) }; +} + +/// Returns the current parser's state as string. +/// +/// The returned value must be freed using `free_string`. +#[unsafe(no_mangle)] +pub extern "C" fn milo_state_string(parser: *mut Parser) -> CStringWithLength { + unsafe { (*parser).state_str().into() } +} + +/// Returns a parser method as string. +/// +/// The returned value must be freed using `free_string`. +#[unsafe(no_mangle)] +pub extern "C" fn milo_method_to_string(method: u8) -> CStringWithLength { + Methods::try_from(method) + .map_or("UNKNOWN", |method| method.as_str()) + .into() +} + +/// Returns a parser error as string. +/// +/// The returned value must be freed using `free_string`. +#[unsafe(no_mangle)] +pub extern "C" fn milo_error_to_string(error: u8) -> CStringWithLength { + Errors::try_from(error).map_or("UNKNOWN", |error| error.as_str()).into() +} + +/// Returns a parser callback as string. +/// +/// The returned value must be freed using `free_string`. +#[unsafe(no_mangle)] +pub extern "C" fn milo_callback_to_string(callback: u8) -> CStringWithLength { + Callbacks::try_from(callback) + .map_or("UNKNOWN", |callback| callback.as_str()) + .into() +} + +/// Returns a parser state as string. +/// +/// The returned value must be freed using `free_string`. +#[unsafe(no_mangle)] +pub extern "C" fn milo_state_to_string(state: u8) -> CStringWithLength { + States::try_from(state).map_or("UNKNOWN", |state| state.as_str()).into() +} + +/// Returns a parser event as string. +/// +/// The returned value must be freed using `free_string`. +#[unsafe(no_mangle)] +pub extern "C" fn milo_event_to_string(event: u8) -> CStringWithLength { + Events::try_from(event).map_or("UNKNOWN", |event| event.as_str()).into() +} + +/// Returns the current parser's error state as string. +/// +/// The returned value must be freed using `free_string`. +#[unsafe(no_mangle)] +pub extern "C" fn milo_error_code_string(parser: *mut Parser) -> CStringWithLength { + unsafe { (*parser).error_code_str().into() } +} + +/// Returns the current parser's error descrition. +/// +/// The returned value must be freed using `free_string`. +#[unsafe(no_mangle)] +pub extern "C" fn milo_error_description_string(parser: *mut Parser) -> CStringWithLength { + unsafe { (*parser).error_description_str().into() } +} diff --git a/deps/undici/src/deps/milo/parser/src/parse.rs b/deps/undici/src/deps/milo/parser/src/parse.rs new file mode 100644 index 000000000000..80105a0a20c7 --- /dev/null +++ b/deps/undici/src/deps/milo/parser/src/parse.rs @@ -0,0 +1,1578 @@ +#![allow(clippy::not_unsafe_ptr_arg_deref)] + +extern crate alloc; + +use alloc::vec::Vec; +use alloc::{boxed::Box, format}; +use core::cell::{Cell, RefCell}; +use core::ffi::{c_char, c_uchar, c_void}; +use core::fmt::Debug; +use core::ptr; +use core::str; +use core::{slice, slice::from_raw_parts}; +use std::string; +#[cfg(all(not(target_family = "wasm"), any(debug_assertions, feature = "debug")))] +use std::time::Instant; + +use milo_macros::*; + +use crate::Methods::CONNECT; +use crate::matchers::*; +use crate::*; + +impl Parser { + /// Parses a slice of characters. + /// + /// It returns the number of consumed characters. + pub fn parse(&mut self, input: *const c_uchar, limit: usize) -> usize { + let mut event_cursor = 0usize; + let mut failed = false; + let active_events = self.active_events | self.active_callbacks; + + // Error states cannot resume without reset. Do not aggregate or retain new + // input on retries, which would otherwise grow the pending buffer forever. + if self.state == STATE_ERROR { + self.position = 0; + } + + // Paused and failed parsers consume nothing and produce an empty event batch. + if self.paused || self.state == STATE_ERROR { + if active_events != 0 { + // SAFETY: The live parser owns the event buffer and its first byte. + unsafe { + *self.events = EVENT_END; + } + } + + return 0; + } + + let input = unsafe { from_raw_parts(input, limit) }; + + // Set the data to analyze, prepending unconsumed data from previous iteration + // if needed + let mut limit = limit; + let aggregate: Vec; + let unconsumed_len = self.unconsumed_len; + + let mut data = if self.manage_unconsumed && unconsumed_len > 0 { + unsafe { + limit += unconsumed_len; + let unconsumed = from_raw_parts(self.unconsumed, unconsumed_len); + + // Rebuild a contiguous view so state handlers can scan across parse-call + // boundaries. + aggregate = [unconsumed, input].concat(); + &aggregate[..] + } + } else { + input + }; + + // Limit the data that is currently analyzed + data = &data[..limit]; + + let mut available = data.len(); + + #[cfg(all(not(target_family = "wasm"), any(debug_assertions, feature = "debug")))] + let mut last = Instant::now(); + + #[cfg(all(not(target_family = "wasm"), any(debug_assertions, feature = "debug")))] + let start = Instant::now(); + + #[cfg(any(debug_assertions, feature = "debug"))] + let mut previous_state = self.state; + + #[cfg(any(debug_assertions, feature = "debug"))] + let previous_position = self.position; + + // States will advance position manually, the parser has to explicitly + // track it + self.position = 0; + let mut advanced: usize; + let mut parsing = true; + let max_body_payload = self.max_body_payload; + let mut body_payload_read = 0u64; + let has_active_events = active_events != 0; + let ( + has_request_start_events, + has_response_start_events, + has_request_line_events, + has_response_line_events, + has_metadata_event, + has_header_name_event, + has_header_value_event, + has_trailer_name_event, + has_trailer_value_event, + has_complete_events, + has_finish_event, + ) = if has_active_events { + ( + active_events & (EVENT_ACTIVE_ON_REQUEST | EVENT_ACTIVE_ON_MESSAGE_START) != 0, + active_events & (EVENT_ACTIVE_ON_RESPONSE | EVENT_ACTIVE_ON_MESSAGE_START) != 0, + active_events + & (EVENT_ACTIVE_ON_METHOD | EVENT_ACTIVE_ON_URL | EVENT_ACTIVE_ON_PROTOCOL | EVENT_ACTIVE_ON_VERSION) + != 0, + active_events + & (EVENT_ACTIVE_ON_PROTOCOL | EVENT_ACTIVE_ON_VERSION | EVENT_ACTIVE_ON_STATUS | EVENT_ACTIVE_ON_REASON) + != 0, + active_events & EVENT_ACTIVE_ON_HEADERS != 0, + active_events & EVENT_ACTIVE_ON_HEADER_NAME != 0, + active_events & EVENT_ACTIVE_ON_HEADER_VALUE != 0, + active_events & EVENT_ACTIVE_ON_TRAILER_NAME != 0, + active_events & EVENT_ACTIVE_ON_TRAILER_VALUE != 0, + active_events & (EVENT_ACTIVE_ON_MESSAGE_COMPLETE | EVENT_ACTIVE_ON_RESET) != 0, + active_events & EVENT_ACTIVE_ON_FINISH != 0, + ) + } else { + ( + false, false, false, false, false, false, false, false, false, false, false, + ) + }; + + #[cfg(any(debug_assertions, feature = "debug"))] + if self.debug { + eprintln!("[milo_parser::debug] loop enter"); + } + + // Until there is data or there is a request to continue + 'parser: while parsing && (!self.paused) && (available != 0 || self.continue_without_data) { + #[cfg(any(debug_assertions, feature = "debug"))] + if self.debug { + eprintln!( + "[milo_parser::debug] loop before processing: previous_position={}, position={}, available={}, \ + continue_without_data={}", + previous_position, self.position, available, self.continue_without_data + ); + } + + // Reset the flag before processing; states set it again when they need another + // zero-byte turn. + self.continue_without_data = false; + advanced = 0; + + 'state: { + match self.state { + // If the parser has finished and it receives more data, error + STATE_FINISH => { + fail!(UNEXPECTED_CHARACTER, "Unexpected data"); + } + + // The error state is a no-op + STATE_ERROR => { + suspend!(); + } + + // Choose the initial state depending on the configured message type. + STATE_START => { + if !self.autodetect && self.is_request { + if has_request_start_events { + event_with_range!(on_request, 0, 0); + event_with_range!(on_message_start, 0, 0); + } + move_to!(request_line); + } else if !self.autodetect { + if has_response_start_events { + event_with_range!(on_response, 0, 0); + event_with_range!(on_message_start, 0, 0); + } + move_to!(status_line); + } else if data.len() >= 5 && data[4] == b'/' && data.starts_with(b"HTTP") { + self.is_request = false; + if has_response_start_events { + event_with_range!(on_response, 0, 0); + event_with_range!(on_message_start, 0, 0); + } + move_to!(status_line); + } else if data.len() >= 2 && data.starts_with(b"\r\n") { + // RFC 9112 section 2.2 + advance!(2); + } else { + // For performance reason, we assume it's a request so we don't lookup the + // method twice + self.is_request = true; + if has_request_start_events { + event_with_range!(on_request, 0, 0); + event_with_range!(on_message_start, 0, 0); + } + move_to!(request_line); + } + } + + STATE_REQUEST_LINE => { + match find_cr(data, available) { + // // RFC 9112 section 3 + Some(cr) => { + match ensure_valid_line(data, cr, available) { + MatchResult::Continue => {} + MatchResult::Suspend => { + suspend!(); + } + MatchResult::Stop => { + fail!(UNEXPECTED_CHARACTER, "Expected CRLF"); + } + } + + // RFC 9112 section 2.2 - Repeated + if cr == 0 { + advance!(2); + next!(); + } else if cr < 14 + // Length of "GET / HTTP/1.1" + { + fail!(UNEXPECTED_CHARACTER, "Request line too short"); + } + + // The line is potentially valid, clear the parser + self.clear(); + + // RFC 9112 section 3.1 + let method_start = 0; + let method_end = match find_char(data, method_start, cr, b' ') { + Some(index) if index > method_start => index, + _ => { + fail!(UNEXPECTED_CHARACTER, "Expected space after method"); + } + }; + + // RFC 9112 section 3.2 + let url_start = method_end + 1; + let url_end = match find_char(data, url_start, cr, b' ') { + Some(index) if index > url_start => index, + _ => { + fail!(UNEXPECTED_CHARACTER, "Expected space after URL"); + } + }; + + // RFC 9112 section 2.3 + let protocol_start = url_end + 1; + let protocol_end = match find_char(data, protocol_start, cr, b'/') { + Some(index) if index > protocol_start => index, + _ => { + fail!(UNEXPECTED_CHARACTER, "Expected / after the protocol name"); + } + }; + + let method_slice = &data[method_start..method_end]; + self.method = match method_slice.len() { + 3 => { + match method_slice { + b"GET" => METHOD_GET, + b"PUT" => METHOD_PUT, + b"PRI" => METHOD_PRI, + _ => METHOD_OTHER, + } + } + 4 => { + match method_slice { + b"HEAD" => METHOD_HEAD, + b"POST" => METHOD_POST, + _ => METHOD_OTHER, + } + } + 5 => { + match method_slice { + b"PATCH" => METHOD_PATCH, + b"TRACE" => METHOD_TRACE, + _ => METHOD_OTHER, + } + } + 6 => { + match method_slice { + b"DELETE" => METHOD_DELETE, + _ => METHOD_OTHER, + } + } + 7 => { + match method_slice { + b"CONNECT" => { + self.is_connect = true; + METHOD_CONNECT + } + b"OPTIONS" => METHOD_OPTIONS, + _ => METHOD_OTHER, + } + } + _ => METHOD_OTHER, + }; + + if self.method == METHOD_OTHER && !validate_token(data, method_start, method_end) { + fail!(UNEXPECTED_CHARACTER, "Invalid method character"); + } + + if !validate_url(data, url_start, url_end) { + fail!(UNEXPECTED_CHARACTER, "Invalid URL character"); + } + + let version_start = protocol_end + 1; + if cr != protocol_start + 8 { + fail!(UNEXPECTED_CHARACTER, "Invalid protocol name"); + } + + if &data[protocol_start..cr] == b"HTTP/1.1" { + if self.method == METHOD_PRI { + fail!(UNSUPPORTED_HTTP_VERSION, "PRI is only valid with HTTP/2.0"); + } + } else if &data[protocol_start..cr] == b"HTTP/2.0" { + if self.method != METHOD_PRI { + fail!(UNSUPPORTED_HTTP_VERSION, "Unsupported HTTP version"); + } + } else { + fail!(UNEXPECTED_CHARACTER, "Invalid protocol"); + } + + if has_request_line_events { + event_with_range!(on_method, method_start, method_end - method_start); + event_with_range!(on_url, url_start, url_end - url_start); + event_with_range!(on_protocol, protocol_start, protocol_end - protocol_start); + event_with_range!(on_version, version_start, 3); + } + + advance!(cr + 2); + + if self.method == METHOD_PRI { + move_to!(http2_preface); + } else { + move_to!(header); + } + } + None => { + if available >= self.max_start_line_length { + fail!(UNEXPECTED_CHARACTER, "Request line too long"); + } else { + suspend!(); + } + } + } + } + + // RFC 9112 section 4 + STATE_STATUS_LINE => { + match find_cr(data, available) { + Some(cr) => { + match ensure_valid_line(data, cr, available) { + MatchResult::Continue => {} + MatchResult::Suspend => { + suspend!(); + } + MatchResult::Stop => { + fail!(UNEXPECTED_CHARACTER, "Expected CRLF"); + } + } + + // RFC 9112 section 2.2 - Repeated + if cr == 0 { + advance!(2); + next!(); + } else if cr < 13 + // Length of "HTTP/1.1 200 " + { + fail!(UNEXPECTED_CHARACTER, "Status line too short"); + } + + // The line is potentially valid, clear the parser + self.clear(); + + let protocol_start = 0; + let protocol_end = 4; + let version_start = protocol_end + 1; + let version_end = protocol_start + 8; + + if cr < version_end || data[version_end] != b' ' { + fail!(UNEXPECTED_CHARACTER, "Expected space after protocol"); + } + + match &data[protocol_start..version_end] { + b"HTTP/1.1" => {} + [b'H', b'T', b'T', b'P', b'/', ..] => { + fail!(UNSUPPORTED_HTTP_VERSION, "Unsupported HTTP version"); + } + _ => { + fail!(UNEXPECTED_CHARACTER, "Invalid protocol"); + } + } + + let status_start = version_end + 1; + // Even if the reason is empty, there must be at least a space after the status + // code. So three digits plus a space + let status_end = version_end + 5; + if status_end > cr { + fail!(INVALID_STATUS, "Expected HTTP response status"); + } + + if !is_digit(data[status_start]) + || !is_digit(data[status_start + 1]) + || !is_digit(data[status_start + 2]) + { + fail!(INVALID_STATUS, "Invalid HTTP response status"); + } + + if data[status_start + 3] != b' ' { + fail!(INVALID_STATUS, "Expected a space after HTTP response status"); + } + + let reason_start = status_start + 4; + let reason_end = cr; + if reason_start != reason_end + && unsafe { !validate_token_value(data.as_ptr().add(reason_start), reason_end - reason_start) } + { + fail!(UNEXPECTED_CHARACTER, "Invalid status reason character"); + } + + self.status = ((data[status_start] - b'0') as u32) * 100 + + ((data[status_start + 1] - b'0') as u32) * 10 + + (data[status_start + 2] - b'0') as u32; + + if has_response_line_events { + event_with_range!(on_protocol, protocol_start, 4); + event_with_range!(on_version, version_start, 3); + event_with_range!(on_status, status_start, 3); + if reason_end > reason_start { + event_with_range!(on_reason, reason_start, reason_end - reason_start); + } + } + + advance!(cr + 2); + move_to!(header); + } + None => { + if available >= self.max_start_line_length { + fail!(UNEXPECTED_CHARACTER, "Status line too long"); + } else { + suspend!(); + } + } + } + } + + STATE_HTTP2_PREFACE => { + if available < 8 { + suspend!(); + } else if &data[..8] == b"\r\nSM\r\n\r\n" { + advance!(8); + move_to!(tunnel); + } else { + fail!(UNEXPECTED_CHARACTER, "Malformed HTTP/2.0 preface"); + } + } + + STATE_HEADER => { + match find_header_line_end(data.as_ptr(), available) { + HeaderLineScanResult::Cr(cr) => { + match ensure_valid_line(data, cr, available) { + MatchResult::Continue => {} + MatchResult::Suspend => { + suspend!(); + } + MatchResult::Stop => { + fail!(UNEXPECTED_CHARACTER, "Expected CRLF"); + } + } + + // No more headers or no headers at all, move to the headers state + if cr == 0 { + advance!(2); + if has_metadata_event { + event_with_metadata!(on_headers, 2); + } + move_to!(body_decision); + self.continue_without_data = true; + + if self.suspend_after_headers { + suspend!(); + } else { + next!(); + } + } + + // RFC 9112 section.4 + // RFC 9110 section 5.5 and 5.6 + let header_name_start = 0; + let header_name_end = match find_char(data, header_name_start, cr, b':') { + Some(index) if index > header_name_start => index, + _ => { + fail!(UNEXPECTED_CHARACTER, "Invalid header field name character"); + } + }; + let mut header_value_start = header_name_end + 1; + let mut header_value_end = cr; + + let status = self.status; + let first_header_byte = data[header_name_start]; + if !matches!(first_header_byte, b'c' | b'C' | b't' | b'T' | b'u' | b'U') { + if !validate_token(data, header_name_start, header_name_end) { + fail!(UNEXPECTED_CHARACTER, "Invalid header field name character"); + } + + if has_header_value_event { + strip_ows_fast(data, &mut header_value_start, &mut header_value_end, true); + } + } else { + let header_name_len = header_name_end - header_name_start; + match (header_name_len, &data[header_name_start..header_name_end]) { + // RFC 9112 section 6.2 + (14, case_insensitive_string!("content-length")) => { + if self.has_transfer_encoding { + fail!( + UNEXPECTED_CONTENT_LENGTH, + "Unexpected Content-Length header when Transfer-Encoding header is present" + ); + } else if status == 205 || status == 204 || status / 100 == 1 { + fail!( + UNEXPECTED_CONTENT_LENGTH, + "Unexpected Content-Length header for a response without body" + ); + } else if self.has_content_length { + fail!(INVALID_CONTENT_LENGTH, "Invalid duplicate Content-Length header"); + } + + if header_value_start < cr && !is_ws(data[cr - 1]) { + let value_start = if data[header_value_start] == b' ' { + header_value_start + 1 + } else { + header_value_start + }; + + if value_start < cr && !is_ws(data[value_start]) { + header_value_start = value_start; + } else if !strip_ows_fast(data, &mut header_value_start, &mut header_value_end, false) { + fail!(UNEXPECTED_CHARACTER, "Expected Content-Length header value"); + } + } else if !strip_ows_fast(data, &mut header_value_start, &mut header_value_end, false) { + fail!(UNEXPECTED_CHARACTER, "Expected Content-Length header value"); + } + + let mut i = header_value_start; + let mut content_length = 0u64; + + if header_value_end - header_value_start > 19 { + // Milo caps Content-Length at 19 digits as a practical limit. This keeps + // parsing overflow-safe while allowing values far + // beyond realistic message sizes. + fail!(INVALID_CONTENT_LENGTH, "Invalid Content-Length header"); + } + + while i < header_value_end { + let current = data[i]; + if !is_digit(current) { + fail!(INVALID_CONTENT_LENGTH, "Invalid Content-Length header"); + } + + content_length = content_length * 10 + (current - b'0') as u64; + i += 1; + } + + self.has_content_length = true; + self.content_length = content_length; + self.remaining_content_length = content_length; + } + // RFC 9112 section 6.1 + (17, case_insensitive_string!("transfer-encoding")) => { + if self.has_content_length { + fail!( + UNEXPECTED_TRANSFER_ENCODING, + "Unexpected Transfer-Encoding header when Content-Length header is present" + ); + } else if status == 304 || status == 205 || status == 204 || status / 100 == 1 { + fail!( + UNEXPECTED_TRANSFER_ENCODING, + "Unexpected Transfer-Encoding header for a response without body" + ); + } + + if !strip_ows_fast(data, &mut header_value_start, &mut header_value_end, false) { + fail!(UNEXPECTED_CHARACTER, "Expected Transfer-Encoding header value"); + } + + self.has_transfer_encoding = true; + + if &data[header_value_start..header_value_end] == b"chunked" { + // If this is true, it means the Transfer-Encoding header was specified more + // than once. This is the second repetition and therefore, the previous one is + // no longer the last one, making it invalid. + if self.has_chunked_transfer_encoding { + fail!( + INVALID_TRANSFER_ENCODING, + "The value \"chunked\" in the Transfer-Encoding header must be the last provided and can \ + be provided only once" + ); + } + + self.has_chunked_transfer_encoding = true; + } else { + let mut token_start = header_value_start; + loop { + while token_start < header_value_end && is_ws(data[token_start]) { + token_start += 1; + } + + if token_start == header_value_end { + break; + } + + let token_end_raw = match find_char(data, token_start, header_value_end, b',') { + Some(comma) => comma, + None => header_value_end, + }; + let mut token_end = token_end_raw; + + if !strip_ows_fast(data, &mut token_start, &mut token_end, false) { + fail!(UNEXPECTED_CHARACTER, "Expected Transfer-Encoding header value"); + } + + self.has_transfer_encoding = true; + + if let case_insensitive_string!("chunked") = data[token_start..token_end] { + // If this is true, it means the Transfer-Encoding header was specified more + // than once. This is the second repetition and therefore, the previous one is + // no longer the last one, making it invalid. + if self.has_chunked_transfer_encoding { + fail!( + INVALID_TRANSFER_ENCODING, + "The value \"chunked\" in the Transfer-Encoding header must be the last provided and \ + can be provided only once" + ); + } + + self.has_chunked_transfer_encoding = true; + } else { + if self.has_chunked_transfer_encoding { + // Any other value when chunked was already specified is invalid as the previous + // chunked would not be the last one anymore + fail!( + INVALID_TRANSFER_ENCODING, + "The value \"chunked\" in the Transfer-Encoding header must be the last provided" + ); + } + } + + if token_end_raw == header_value_end { + break; + } else { + token_start = token_end_raw + 1; + } + } + } + } + // RFC 9112 section 9.6 + (10, case_insensitive_string!("connection")) => { + if !strip_ows_fast(data, &mut header_value_start, &mut header_value_end, false) { + fail!(UNEXPECTED_CHARACTER, "Expected Connection header value"); + } + + match data[header_value_start..header_value_end] { + case_insensitive_string!("close") => { + self.has_connection_close = true; + } + case_insensitive_string!("keep-alive") => { + // Keep-alive is implicit unless Connection: close is + // present. + } + case_insensitive_string!("upgrade") => { + self.has_connection_upgrade = true; + } + _ => { + // Comma separated values + let mut token_start = header_value_start; + loop { + while token_start < header_value_end && is_ws(data[token_start]) { + token_start += 1; + } + + if token_start == header_value_end { + break; + } + + let token_end_raw = match find_char(data, token_start, header_value_end, b',') { + Some(comma) => comma, + None => header_value_end, + }; + let mut token_end = token_end_raw; + + if !strip_ows_fast(data, &mut token_start, &mut token_end, false) { + fail!(UNEXPECTED_CHARACTER, "Expected Connection header value"); + } + + match data[token_start..token_end] { + case_insensitive_string!("close") => { + self.has_connection_close = true; + } + case_insensitive_string!("upgrade") => { + self.has_connection_upgrade = true; + } + case_insensitive_string!("keep-alive") => {} + _ => { + if !validate_token(data, token_start, token_end) { + fail!(UNEXPECTED_CHARACTER, "Invalid Connection header value"); + } + } + } + + if token_end_raw == header_value_end { + break; + } else { + token_start = token_end_raw + 1; + } + } + } + } + } + (7, case_insensitive_string!("trailer")) => { + self.has_trailers = true; + + if !strip_ows_fast(data, &mut header_value_start, &mut header_value_end, false) { + fail!(UNEXPECTED_CHARACTER, "Expected Trailer header value"); + } + } + (7, case_insensitive_string!("upgrade")) => { + if !strip_ows_fast(data, &mut header_value_start, &mut header_value_end, false) { + fail!(UNEXPECTED_CHARACTER, "Expected Upgrade header value"); + } + + let mut token_start = header_value_start; + loop { + while token_start < header_value_end && is_ws(data[token_start]) { + token_start += 1; + } + + if token_start == header_value_end { + break; + } + + let token_end_raw = match find_char(data, token_start, header_value_end, b',') { + Some(comma) => comma, + None => header_value_end, + }; + let mut token_end = token_end_raw; + + if !strip_ows_fast(data, &mut token_start, &mut token_end, false) { + fail!(UNEXPECTED_CHARACTER, "Expected Upgrade header value"); + } + + let protocol_name_end = find_char(data, token_start, token_end, b'/').unwrap_or(token_end); + if !validate_token(data, token_start, protocol_name_end) { + fail!(UNEXPECTED_CHARACTER, "Invalid Upgrade header value"); + } + + if protocol_name_end < token_end { + let protocol_version_start = protocol_name_end + 1; + if find_char(data, protocol_version_start, token_end, b'/').is_some() + || !validate_token(data, protocol_version_start, token_end) + { + fail!(UNEXPECTED_CHARACTER, "Invalid Upgrade header value"); + } + } + + if token_end_raw == header_value_end { + break; + } else { + token_start = token_end_raw + 1; + } + } + + self.has_upgrade = true; + } + _ => { + if !validate_token(data, header_name_start, header_name_end) { + fail!(UNEXPECTED_CHARACTER, "Invalid header field name character"); + } + + if has_header_value_event { + strip_ows_fast(data, &mut header_value_start, &mut header_value_end, true); + } + } + } + } + + if has_header_name_event { + event_with_range!(on_header_name, header_name_start, header_name_end - header_name_start); + } + + if has_header_value_event { + event_with_range!( + on_header_value, + header_value_start, + header_value_end - header_value_start + ); + } + + advance!(cr + 2); + } + HeaderLineScanResult::Invalid(invalid) => { + match find_char(data, 0, invalid, b':') { + Some(_) => { + fail!(UNEXPECTED_CHARACTER, "Invalid header field value character"); + } + None => { + fail!(UNEXPECTED_CHARACTER, "Invalid header field name character"); + } + } + } + HeaderLineScanResult::Incomplete => { + if available >= self.max_header_length { + fail!(UNEXPECTED_CHARACTER, "Header line too long"); + } else { + suspend!(); + } + } + } + } + + // RFC 9110 section 9.3.6 and 7.8 - Headers have finished, check if the + // connection must be upgraded or a body is expected. + STATE_BODY_DECISION => { + let method = self.method; + let status = self.status; + + if self.has_upgrade && !self.has_connection_upgrade { + fail!( + MISSING_CONNECTION_UPGRADE, + "Missing Connection header set to \"upgrade\" when using the Upgrade header" + ); + } + + if self.has_trailers && !self.has_chunked_transfer_encoding { + fail!( + UNEXPECTED_TRAILERS, + "Trailers are not allowed when not using chunked transfer encoding" + ); + } else if self.is_request && (method == METHOD_GET || method == METHOD_HEAD) && self.content_length > 0 { + fail!(UNEXPECTED_CONTENT, "Unexpected content for the request (GET or HEAD)"); + } + + // In case of Connection: Upgrade or a CONNECT method + if self.is_connect { + // In case of CONNECT method + event_with_range!(on_connect, 0, 0); + move_to!(tunnel); + } else if self.has_upgrade && !self.is_request && status == 101 { + event_with_range!(on_upgrade, 0, 0); + move_to!(tunnel); + } else if self.is_request { + if self.has_transfer_encoding && !self.has_chunked_transfer_encoding { + fail!( + UNEXPECTED_CONTENT_LENGTH, + "Transfer-Encoding last header value must be \"chunked\" if the header is present" + ); + } else if self.skip_body { + self.continue_without_data = true; + if !self.complete_message( + 0, + active_events, + has_complete_events, + has_finish_event, + &mut event_cursor, + ) { + suspend!(); + } + } else if self.has_content_length { + // RFC 9110 section 6.3 + if self.content_length == 0 { + self.continue_without_data = true; + if !self.complete_message( + 0, + active_events, + has_complete_events, + has_finish_event, + &mut event_cursor, + ) { + suspend!(); + } + } else { + move_to!(body_via_content_length); + } + } else if !self.has_chunked_transfer_encoding { + self.continue_without_data = true; + if !self.complete_message( + 0, + active_events, + has_complete_events, + has_finish_event, + &mut event_cursor, + ) { + suspend!(); + } + } else { + move_to!(chunk_header); + } + } else { + // Response + // RFC 9110 section 15.4.5 + if self.skip_body || (status < 200 && status != 101) || status == 204 || status == 205 || status == 304 { + self.continue_without_data = true; + if !self.complete_message( + 0, + active_events, + has_complete_events, + has_finish_event, + &mut event_cursor, + ) { + suspend!(); + } + } else if self.has_content_length { + if self.content_length == 0 { + self.continue_without_data = true; + if !self.complete_message( + 0, + active_events, + has_complete_events, + has_finish_event, + &mut event_cursor, + ) { + suspend!(); + } + } else { + move_to!(body_via_content_length); + } + } else if self.has_chunked_transfer_encoding { + move_to!(chunk_header); + } else { + move_to!(body_with_no_length); + } + } + } + + // RFC 9112 section 6.2 + STATE_BODY_VIA_CONTENT_LENGTH => { + let expected = self.remaining_content_length; + let mut to_consume = expected.min(available as u64); + + if max_body_payload != 0 { + if body_payload_read >= max_body_payload { + suspend!(); + } + + to_consume = to_consume.min(max_body_payload - body_payload_read); + } + + if to_consume == 0 { + suspend!(); + } + + let to_consume_usize = to_consume as usize; + body_payload_read += to_consume; + + if to_consume < expected { + event_with_range!(on_data, 0, to_consume_usize); + self.remaining_content_length -= to_consume; + advance!(to_consume_usize); + } else { + // Suspend before emitting or consuming any part of the final body segment. + if event_cursor + EVENT_BODY_COMPLETION_SIZE + EVENT_ERROR_RESERVE > EVENTS_BUFFER_SIZE { + suspend!(); + } + event_with_range!(on_data, 0, to_consume_usize); + event_with_range!(on_body, to_consume_usize, 0); + + self.remaining_content_length = 0; + + self.continue_without_data = true; + + if !self.complete_message( + to_consume_usize, + active_events, + has_complete_events, + has_finish_event, + &mut event_cursor, + ) { + suspend!(); + } + advance!(to_consume_usize); + } + } + + // RFC 9110 section 6.3 - Body with no length nor chunked encoding. + // This is only allowed in responses. + // Note that on_body can't and will not be called here as there is no way to + // know when the response finishes. + STATE_BODY_WITH_NO_LENGTH => { + let mut to_consume = available as u64; + + if max_body_payload != 0 { + if body_payload_read >= max_body_payload { + suspend!(); + } + + to_consume = to_consume.min(max_body_payload - body_payload_read); + } + + if to_consume == 0 { + suspend!(); + } + + let to_consume_usize = to_consume as usize; + body_payload_read += to_consume; + + event_with_range!(on_data, 0, to_consume_usize); + advance!(to_consume_usize); + } + + // RFC 9112 section 7.1 + STATE_CHUNK_HEADER => { + match find_cr(data, available) { + Some(cr) => { + match ensure_valid_line(data, cr, available) { + MatchResult::Continue => {} + MatchResult::Suspend => { + suspend!(); + } + MatchResult::Stop => { + fail!(UNEXPECTED_CHARACTER, "Expected CRLF"); + } + } + + let chunk_length_start = 0; + // Note, the character is optional since chunk extensions are not required + let chunk_length_end = match find_char(data, chunk_length_start, cr, b';') { + Some(index) => index, + None => cr, + }; + + if chunk_length_end == 0 { + fail!(UNEXPECTED_CHARACTER, "Invalid chunk length character"); + } else if chunk_length_end - chunk_length_start > 16 { + fail!(INVALID_CHUNK_SIZE, "Invalid chunk length size"); + } + + let mut i = chunk_length_start; + let mut chunk_length = 0u64; + while i < chunk_length_end { + let b = data[i]; + + let hex = if b.is_ascii_digit() { + (b - b'0') as u64 + } else if (b'a'..=b'f').contains(&b) { + (b - b'a' + 10) as u64 + } else if (b'A'..=b'F').contains(&b) { + (b - b'A' + 10) as u64 + } else { + fail!(UNEXPECTED_CHARACTER, "Invalid chunk length character"); + }; + + chunk_length = chunk_length * 16 + hex; + i += 1; + } + + self.chunk_size = chunk_length; + self.remaining_chunk_size = chunk_length; + + event_with_range!( + on_chunk_length, + chunk_length_start, + chunk_length_end - chunk_length_start + ); + + // There are extensions + if chunk_length_end < cr { + advance!(chunk_length_end + 1); + move_to!(chunk_extensions); + } else { + self.continue_without_data = true; + + if self.chunk_size == 0 { + event_with_range!(on_chunk, 3, 0); + event_with_range!(on_body, 3, 0); + advance!(cr + 2); + move_to!(trailer); + } else { + advance!(cr + 2); + move_to!(chunk_data); + } + } + } + None => { + if available >= self.max_header_length { + fail!(UNEXPECTED_CHARACTER, "Chunk header too long"); + } else { + suspend!(); + } + } + } + } + + STATE_CHUNK_EXTENSIONS => { + match find_cr(data, available) { + Some(cr) => { + match ensure_valid_line(data, cr, available) { + MatchResult::Continue => {} + MatchResult::Suspend => { + suspend!(); + } + MatchResult::Stop => { + fail!(UNEXPECTED_CHARACTER, "Expected CRLF"); + } + } + + let mut name_start = 0; + // Find the first between = or ; + let name_end_raw = find_char2(data, name_start, cr, b'=', b';').unwrap_or(cr); + let mut name_end = name_end_raw; + + if !strip_ows(data, &mut name_start, &mut name_end, false) { + fail!(UNEXPECTED_CHARACTER, "Expected chunk extension name"); + } + + if !validate_token(data, name_start, name_end) { + fail!(UNEXPECTED_CHARACTER, "Invalid chunk extension name character"); + } + + // No value + if name_end == cr || data[name_end_raw] == b';' { + event_with_range!(on_chunk_extension_name, name_start, name_end - name_start); + + if name_end_raw == cr { + if self.chunk_size == 0 { + event_with_range!(on_body, 0, 0); + advance!(cr + 2); + move_to!(trailer); + } else { + advance!(cr + 2); + move_to!(chunk_data); + } + } else { + advance!(name_end_raw + 1); + move_to!(chunk_extensions); + } + } else { + // Get the value + let mut value_start = name_end_raw + 1; + let mut value_end: usize; + let next_extension: usize; + + // Strip OWS before the value + while value_start < cr && is_ws(data[value_start]) { + value_start += 1; + } + + if value_start == cr { + fail!(UNEXPECTED_CHARACTER, "Expected chunk extension value"); + } + + // Quoted string + // RFC 9110 section 5.6.4 + let mut quoted = false; + let quote_start = value_start; + if data[value_start] == b'"' { + quoted = true; + value_start += 1; + let mut quote_start = value_start; + + loop { + match find_char(data, quote_start, cr, b'"') { + Some(index) => { + // Count consecutive backslashes immediately before the quote + let mut backslash_count = 0usize; + let mut i = index; + + while i > quote_start && data[i - 1] == b'\\' { + backslash_count += 1; + i -= 1; + } + + if backslash_count.is_multiple_of(2) { + // quote is not escaped + value_end = index; + break; + } else { + // quote is escaped, continue searching after it + quote_start = index + 1; + } + } + None => { + fail!(UNEXPECTED_CHARACTER, "Expected closing quote for chunk extension value"); + } + }; + } + + if !validate_quoted_string(data, value_start, value_end) { + fail!(UNEXPECTED_CHARACTER, "Invalid chunk extension quoted value character"); + } + + next_extension = value_end + 1; + } else { + value_end = find_char(data, value_start, cr, b';').unwrap_or(cr); + next_extension = if value_end == cr { cr } else { value_end }; + + if !strip_ows(data, &mut value_start, &mut value_end, false) { + fail!(UNEXPECTED_CHARACTER, "Expected chunk extension value"); + } + + if value_start != value_end && !validate_token(data, value_start, value_end) { + fail!(UNEXPECTED_CHARACTER, "Invalid chunk extension value character"); + } + } + + event_with_range!(on_chunk_extension_name, name_start, name_end - name_start); + + if quoted { + event_with_range!(on_chunk_extension_value, quote_start, value_end - quote_start + 1); + } else { + event_with_range!(on_chunk_extension_value, value_start, value_end - value_start); + } + + let next_semicolon = find_char(data, next_extension, cr, b';').unwrap_or(cr); + + let mut i = next_extension; + while i < next_semicolon { + if !is_ws(data[i]) { + fail!(UNEXPECTED_CHARACTER, "Invalid chunk extension character after value"); + } + i += 1; + } + + if next_semicolon < cr { + advance!(next_semicolon + 1); + } else { + if self.chunk_size == 0 { + event_with_range!(on_body, 0, 0); + advance!(cr + 2); + move_to!(trailer); + } else { + advance!(cr + 2); + move_to!(chunk_data); + } + } + } + } + None => { + // Given in chunk_header we already validated this, this should not happen. + if available >= self.max_header_length { + fail!(UNEXPECTED_CHARACTER, "Chunk header too long"); + } else { + suspend!(); + } + } + } + } + + STATE_CHUNK_DATA => { + let expected = self.remaining_chunk_size; + + // No more data for this chunk, just wait for the CRLF + if expected == 0 { + if available < 2 { + suspend!(); + } else if data[0] != b'\r' || data[1] != b'\n' { + fail!(UNEXPECTED_CHARACTER, "Expected CRLF after chunk data"); + } else { + advance!(2); + move_to!(chunk_header); + } + } else { + let mut to_consume = expected.min(available as u64); + + if max_body_payload != 0 { + if body_payload_read >= max_body_payload { + suspend!(); + } + + to_consume = to_consume.min(max_body_payload - body_payload_read); + } + + if to_consume == 0 { + suspend!(); + } + + let to_consume_usize = to_consume as usize; + body_payload_read += to_consume; + + event_with_range!(on_chunk, 0, to_consume_usize); + event_with_range!(on_data, 0, to_consume_usize); + + if to_consume < expected { + self.remaining_chunk_size -= to_consume; + } else { + self.remaining_chunk_size = 0; + } + + advance!(to_consume_usize); + } + } + + // RFC 9112 section 7.1.2 + STATE_TRAILER => { + match find_header_line_end(data.as_ptr(), available) { + HeaderLineScanResult::Cr(cr) => { + match ensure_valid_line(data, cr, available) { + MatchResult::Continue => {} + MatchResult::Suspend => { + suspend!(); + } + MatchResult::Stop => { + fail!(UNEXPECTED_CHARACTER, "Expected CRLF"); + } + } + + // No more trailers or no trailers at all, message completed + if cr == 0 { + // Keep the trailer notification and completion in the same batch. + if event_cursor + EVENT_TRAILER_COMPLETION_SIZE + EVENT_ERROR_RESERVE > EVENTS_BUFFER_SIZE { + suspend!(); + } + event_with_range!(on_trailers, 2, 0); + self.continue_without_data = true; + if !self.complete_message( + 2, + active_events, + has_complete_events, + has_finish_event, + &mut event_cursor, + ) { + suspend!(); + } + advance!(2); + next!(); + } + + let trailer_name_start = 0; + let trailer_name_end = match find_char(data, trailer_name_start, cr, b':') { + Some(index) if index > trailer_name_start => index, + _ => { + fail!(UNEXPECTED_CHARACTER, "Invalid trailer field name character"); + } + }; + + let mut trailer_value_start = trailer_name_end + 1; + let mut trailer_value_end = cr; + if has_trailer_value_event { + strip_ows_fast(data, &mut trailer_value_start, &mut trailer_value_end, true); + } + + // Validate + if !validate_token(data, trailer_name_start, trailer_name_end) { + fail!(UNEXPECTED_CHARACTER, "Invalid trailer field name character"); + } + + if has_trailer_name_event { + event_with_range!( + on_trailer_name, + trailer_name_start, + trailer_name_end - trailer_name_start + ); + } + + if has_trailer_value_event { + event_with_range!( + on_trailer_value, + trailer_value_start, + trailer_value_end - trailer_value_start + ); + } + advance!(cr + 2); + } + HeaderLineScanResult::Invalid(invalid) => { + match find_char(data, 0, invalid, b':') { + Some(_) => { + fail!(UNEXPECTED_CHARACTER, "Invalid trailer field value character"); + } + None => { + fail!(UNEXPECTED_CHARACTER, "Invalid trailer field name character"); + } + } + } + HeaderLineScanResult::Incomplete => { + if available >= self.max_header_length { + fail!(UNEXPECTED_CHARACTER, "Trailer line too long"); + } else { + suspend!(); + } + } + } + } + + // Return PAUSE makes this method idempotent without failing - In this state + // all data is ignored since the connection is not in HTTP anymore + STATE_TUNNEL => { + suspend!(); + } + + _ => { + fail!(UNEXPECTED_STATE, "Invalid state"); + } + } + } + + // Update the parser position + if advanced > 0 { + self.position += advanced; + data = &data[advanced..]; + available -= advanced; + + #[cfg(any(debug_assertions, feature = "debug"))] + if self.debug { + eprintln!( + "[milo_parser::debug] loop before processing: position={}, advanced={}, available={}, \ + continue_without_data={}", + self.position, advanced, available, self.continue_without_data + ); + } + } + + // Notify the status change + #[cfg(any(debug_assertions, feature = "debug"))] + if previous_state != self.state { + if active_events & EVENT_ACTIVE_ON_STATE_CHANGE != 0 + && !self.try_emit_event_range( + &mut event_cursor, + EVENT_STATE_CHANGE, + self.position, + self.state as usize, + ) + { + parsing = false; + continue 'parser; + } + previous_state = self.state; + } + + // Show the duration of the operation + #[cfg(all(not(target_family = "wasm"), any(debug_assertions, feature = "debug")))] + if self.debug { + let duration = Instant::now().duration_since(last).as_nanos(); + + if duration > 0 { + eprintln!( + "[milo_parser::debug] loop iteration ({:?}) completed in {} ns", + self.state_str(), + duration + ); + } + + last = Instant::now(); + } + } + + #[cfg(any(debug_assertions, feature = "debug"))] + if self.debug { + eprintln!("[milo_parser::debug] loop exit"); + } + + let consumed = self.position; + self.parsed += consumed as u64; + + if self.manage_unconsumed { + self.clear_unconsumed(); + + // Use a boxed slice so the length also determines the allocation layout + // when releasing retained input; Vec capacity is not stored in the ABI. + if consumed < limit { + let retained = data.to_vec().into_boxed_slice(); + self.unconsumed_len = retained.len(); + self.unconsumed = Box::into_raw(retained) as *const c_uchar; + } + } + + #[cfg(all(not(target_family = "wasm"), any(debug_assertions, feature = "debug")))] + if self.debug { + let duration = Instant::now().duration_since(start).as_nanos(); + + if duration > 0 { + eprintln!( + "[milo_parser::debug] parse ({:?}, consumed {} of {}) completed in {} ns", + self.state_str(), + consumed, + limit, + duration + ); + } + } + + // Append only errors raised by this call, after all preceding events. + if failed && active_events & EVENT_ACTIVE_ON_ERROR != 0 { + self.try_emit_event_error(&mut event_cursor); + } + + if has_active_events { + unsafe { + *self.events.add(event_cursor) = EVENT_END; + } + } + + if self.active_callbacks != 0 { + self.invoke_callbacks(); + } + + // Return the number of consumed bytes + consumed + } + + /// Completes the current message without consuming more input. + /// + /// This is intended for integrations that consume opaque body bytes outside + /// Milo after headers were parsed, then need Milo to perform the normal + /// completion transition and emit completion events. + pub fn complete(&mut self) { + match self.state { + STATE_BODY_DECISION + | STATE_TUNNEL + | STATE_BODY_VIA_CONTENT_LENGTH + | STATE_BODY_WITH_NO_LENGTH + | STATE_CHUNK_HEADER + | STATE_TRAILER => {} + _ => { + self.fail(ERROR_UNEXPECTED_STATE, "Invalid state"); + return; + } + } + + let active_events = self.active_events | self.active_callbacks; + let has_complete_events = active_events & (EVENT_ACTIVE_ON_MESSAGE_COMPLETE | EVENT_ACTIVE_ON_RESET) != 0; + let has_finish_event = active_events & EVENT_ACTIVE_ON_FINISH != 0; + let mut event_cursor = 0usize; + + if !self.complete_message( + 0, + active_events, + has_complete_events, + has_finish_event, + &mut event_cursor, + ) { + self.paused = true; + } + + unsafe { + *self.events.add(event_cursor) = EVENT_END; + } + } + + // RFC 9110 section 6.4.1 - Message completed. + #[inline(always)] + fn complete_message( + &mut self, + offset: usize, + active_events: u64, + has_complete_events: bool, + has_finish_event: bool, + event_cursor: &mut usize, + ) -> bool { + // Never publish a partial completion group that would be replayed on retry. + if *event_cursor + EVENT_COMPLETION_SIZE + EVENT_ERROR_RESERVE > EVENTS_BUFFER_SIZE { + return false; + } + if has_complete_events { + if (active_events & EVENT_ACTIVE_ON_MESSAGE_COMPLETE != 0 + && !self.try_emit_event_range(event_cursor, EVENT_MESSAGE_COMPLETE, self.position + offset, 0)) + || (active_events & EVENT_ACTIVE_ON_RESET != 0 + && !self.try_emit_event_range(event_cursor, EVENT_RESET, self.position + offset, 0)) + { + return false; + } + } + + self.continue_without_data = false; + self.skip_body = false; + + if self.has_upgrade && self.is_request { + move_to!(tunnel); + } else if self.has_connection_close { + if has_finish_event && !self.try_emit_event_range(event_cursor, EVENT_FINISH, self.position, 0) { + return false; + } + move_to!(finish); + } else { + move_to!(start); + } + + true + } +} diff --git a/deps/undici/src/deps/milo/parser/src/wasm.rs b/deps/undici/src/deps/milo/parser/src/wasm.rs new file mode 100644 index 000000000000..73d380c7ba5f --- /dev/null +++ b/deps/undici/src/deps/milo/parser/src/wasm.rs @@ -0,0 +1,363 @@ +use core::ffi::{c_uchar, c_void}; +use std::slice; + +use crate::Parser; + +#[cfg(any(debug_assertions, feature = "debug"))] +pub fn debug(message: String) { unsafe { crate::logger(((message.as_ptr() as u64) << 32) + message.len() as u64) } } + +/// Returns if debug informations are available in this build. +#[unsafe(no_mangle)] +pub fn milo_has_debug() -> bool { cfg!(any(debug_assertions, feature = "debug")) } + +#[unsafe(no_mangle)] +pub fn alloc(len: usize) -> *mut c_void { + let buffer = Vec::with_capacity(len); + let (ptr, _, _) = { buffer.into_raw_parts() }; + ptr as *mut c_void +} + +#[unsafe(no_mangle)] +pub fn dealloc(ptr: *mut c_void, len: usize) { + if ptr.is_null() { + return; + } + + unsafe { + let _ = Vec::from_raw_parts(ptr, len, len); + } +} + +/// Creates a new parser. +#[unsafe(no_mangle)] +pub fn create() -> *mut c_void { + let ptr = Box::into_raw(Box::new(Parser::new())) as *mut c_void; + + // Recreate the parser from the box to assign the reference to itself + let mut parser = unsafe { Box::from_raw(ptr as *mut Parser) }; + parser.ptr = ptr; + let _ = Box::into_raw(parser); + + ptr +} + +/// Destroys a parser. +#[unsafe(no_mangle)] +pub fn destroy(parser: *mut c_void) { + if parser.is_null() { + return; + } + + unsafe { + let _ = Box::from_raw(parser as *mut Parser); + } +} + +/// Resets a parser. The second parameters specifies if to also reset the +/// parsed counter. +#[unsafe(no_mangle)] +pub fn reset(parser: *mut c_void, keep_parsed: bool) { unsafe { (*(parser as *mut Parser)).reset(keep_parsed) } } + +/// Clears all values in the parser. +/// +/// Persisted fields, unconsumed data and the position are not cleared. +#[unsafe(no_mangle)] +pub fn clear(parser: *mut c_void) { unsafe { (*(parser as *mut Parser)).clear() } } + +// Parses a slice of characters. It returns the number of consumed characters. +#[unsafe(no_mangle)] +pub fn parse(parser: *mut c_void, data: *const c_uchar, limit: usize) -> usize { + unsafe { (*(parser as *mut Parser)).parse(data, limit) } +} + +/// Pauses the parser. It will have to be resumed via `resume`. +#[unsafe(no_mangle)] +pub fn pause(parser: *mut c_void) { unsafe { (*(parser as *mut Parser)).pause() } } + +/// Resumes the parser. +#[unsafe(no_mangle)] +pub fn resume(parser: *mut c_void) { unsafe { (*(parser as *mut Parser)).resume() } } + +/// Completes the current message without consuming more input. +#[unsafe(no_mangle)] +pub fn complete(parser: *mut c_void) { unsafe { (*(parser as *mut Parser)).complete() } } + +/// Marks the parser as finished. Any new data received via `parse` will +/// put the parser in the error state. +#[unsafe(no_mangle)] +pub fn finish(parser: *mut c_void) { unsafe { (*(parser as *mut Parser)).finish() } } + +/// Marks the parser as failed. +#[unsafe(no_mangle)] +pub fn fail(parser: *mut c_void, code: u8, description_ptr: *const c_uchar, description_len: usize) { + unsafe { + (*(parser as *mut Parser)).fail( + code, + std::str::from_utf8_unchecked(slice::from_raw_parts(description_ptr, description_len)), + ); + } +} + +// Generates a getter. +// pub fn wasm_getter(input: TokenStream) -> TokenStream { +// let snake_matcher = Regex::new(r"([A-Z])").unwrap(); +// +// let definition = parse_macro_input!(input as Property); +// let property = definition.property; +// let fn_name = format_ident!( +// "{}", +// snake_matcher.replace_all(&definition.getter.to_string().as_str(), |captures: +// &Captures| { format!("_{}", captures[1].to_lowercase()) +// }) +// ); +// +// let return_type = definition.r#type; +// +// TokenStream::from(quote! { +// Gets the parser #property. +// #[unsafe(no_mangle)] +// pub fn #fn_name(parser: *const c_void) -> #return_type { unsafe { (*(parser +// as *const Parser)).#property } } }) +// } + +// Getters +// Get the parser autodetect property. +#[unsafe(no_mangle)] +pub fn is_autodetect(parser: *const c_void) -> bool { unsafe { (*(parser as *const Parser)).autodetect } } + +// Get the parser is_request property. +#[unsafe(no_mangle)] +pub fn is_request(parser: *const c_void) -> bool { unsafe { (*(parser as *const Parser)).is_request } } + +// Get the parser debug property. +#[unsafe(no_mangle)] +pub fn is_debug(parser: *const c_void) -> bool { unsafe { (*(parser as *const Parser)).debug } } + +// Get the parser paused property. +#[unsafe(no_mangle)] +pub fn is_paused(parser: *const c_void) -> bool { unsafe { (*(parser as *const Parser)).paused } } + +// Get the parser manage_unconsumed property. +#[unsafe(no_mangle)] +pub fn should_manage_unconsumed(parser: *const c_void) -> bool { + unsafe { (*(parser as *const Parser)).manage_unconsumed } +} + +// Get the parser suspend_after_headers property. +#[unsafe(no_mangle)] +pub fn should_suspend_after_headers(parser: *const c_void) -> bool { + unsafe { (*(parser as *const Parser)).suspend_after_headers } +} + +// Get the parser max_start_line_length property. +#[unsafe(no_mangle)] +pub fn get_max_start_line_length(parser: *const c_void) -> usize { + unsafe { (*(parser as *const Parser)).max_start_line_length } +} + +// Get the parser max_header_length property. +#[unsafe(no_mangle)] +pub fn get_max_header_length(parser: *const c_void) -> usize { + unsafe { (*(parser as *const Parser)).max_header_length } +} + +// Get the parser max_body_payload property. +#[unsafe(no_mangle)] +pub fn get_max_body_payload(parser: *const c_void) -> u64 { unsafe { (*(parser as *const Parser)).max_body_payload } } + +// Get the parser continue_without_data property. +#[unsafe(no_mangle)] +pub fn should_continue_without_data(parser: *const c_void) -> bool { + unsafe { (*(parser as *const Parser)).continue_without_data } +} + +// Get the parser is_connect property. +#[unsafe(no_mangle)] +pub fn is_connect(parser: *const c_void) -> bool { unsafe { (*(parser as *const Parser)).is_connect } } + +// Get the parser skip_body property. +#[unsafe(no_mangle)] +pub fn should_skip_body(parser: *const c_void) -> bool { unsafe { (*(parser as *const Parser)).skip_body } } + +// Get the parser state property. +#[unsafe(no_mangle)] +pub fn get_state(parser: *const c_void) -> u8 { unsafe { (*(parser as *const Parser)).state } } + +// Get the parser position property. +#[unsafe(no_mangle)] +pub fn get_position(parser: *const c_void) -> usize { unsafe { (*(parser as *const Parser)).position } } + +// Get the parser parsed property. +#[unsafe(no_mangle)] +pub fn get_parsed(parser: *const c_void) -> u64 { unsafe { (*(parser as *const Parser)).parsed } } + +// Get the parser error_code property. +#[unsafe(no_mangle)] +pub fn get_error_code(parser: *const c_void) -> u8 { unsafe { (*(parser as *const Parser)).error_code } } + +// Get the parser method property. +#[unsafe(no_mangle)] +pub fn get_method(parser: *const c_void) -> u8 { unsafe { (*(parser as *const Parser)).method } } + +// Get the parser status property. +#[unsafe(no_mangle)] +pub fn get_status(parser: *const c_void) -> u32 { unsafe { (*(parser as *const Parser)).status } } + +// Get the parser has_connection_close property. +#[unsafe(no_mangle)] +pub fn has_connection_close(parser: *const c_void) -> bool { + unsafe { (*(parser as *const Parser)).has_connection_close } +} + +// Get the parser has_connection_upgrade property. +#[unsafe(no_mangle)] +pub fn has_connection_upgrade(parser: *const c_void) -> bool { + unsafe { (*(parser as *const Parser)).has_connection_upgrade } +} + +// Get the parser content_length property. +#[unsafe(no_mangle)] +pub fn get_content_length(parser: *const c_void) -> u64 { unsafe { (*(parser as *const Parser)).content_length } } + +// Get the parser chunk_size property. +#[unsafe(no_mangle)] +pub fn get_chunk_size(parser: *const c_void) -> u64 { unsafe { (*(parser as *const Parser)).chunk_size } } + +// Get the parser remaining_content_length property. +#[unsafe(no_mangle)] +pub fn get_remaining_content_length(parser: *const c_void) -> u64 { + unsafe { (*(parser as *const Parser)).remaining_content_length } +} + +// Get the parser remaining_chunk_size property. +#[unsafe(no_mangle)] +pub fn get_remaining_chunk_size(parser: *const c_void) -> u64 { + unsafe { (*(parser as *const Parser)).remaining_chunk_size } +} + +// Get the parser has_content_length property. +#[unsafe(no_mangle)] +pub fn has_content_length(parser: *const c_void) -> bool { unsafe { (*(parser as *const Parser)).has_content_length } } + +// Get the parser has_transfer_encoding property. +#[unsafe(no_mangle)] +pub fn has_transfer_encoding(parser: *const c_void) -> bool { + unsafe { (*(parser as *const Parser)).has_transfer_encoding } +} + +// Get the parser has_chunked_transfer_encoding property. +#[unsafe(no_mangle)] +pub fn has_chunked_transfer_encoding(parser: *const c_void) -> bool { + unsafe { (*(parser as *const Parser)).has_chunked_transfer_encoding } +} + +// Get the parser has_upgrade property. +#[unsafe(no_mangle)] +pub fn has_upgrade(parser: *const c_void) -> bool { unsafe { (*(parser as *const Parser)).has_upgrade } } + +// Get the parser has_trailers property. +#[unsafe(no_mangle)] +pub fn has_trailers(parser: *const c_void) -> bool { unsafe { (*(parser as *const Parser)).has_trailers } } + +/// Gets the parser callback error description, if any. This is meant for +/// internal use. +#[unsafe(no_mangle)] +pub fn get_error_description_raw(parser: *mut c_void) -> u64 { + let parser = unsafe { &(*(parser as *const Parser)) }; + + let ptr = parser.error_description.as_ptr() as u64; + let len = parser.error_description_len as u64; + + (ptr << 32) + len +} + +#[unsafe(no_mangle)] +pub fn set_should_autodetect(parser: *mut c_void, value: bool) { + unsafe { + (*(parser as *mut Parser)).autodetect = value; + } +} + +#[unsafe(no_mangle)] +pub fn set_is_request(parser: *mut c_void, value: bool) { + unsafe { + (*(parser as *mut Parser)).is_request = value; + } +} + +#[unsafe(no_mangle)] +pub fn set_should_manage_unconsumed(parser: *mut c_void, value: bool) { + unsafe { + (*(parser as *mut Parser)).manage_unconsumed = value; + } +} + +#[unsafe(no_mangle)] +pub fn set_should_suspend_after_headers(parser: *mut c_void, value: bool) { + unsafe { + (*(parser as *mut Parser)).suspend_after_headers = value; + } +} + +#[unsafe(no_mangle)] +pub fn set_max_start_line_length(parser: *mut c_void, value: usize) { + unsafe { + (*(parser as *mut Parser)).max_start_line_length = value; + } +} + +#[unsafe(no_mangle)] +pub fn set_max_header_length(parser: *mut c_void, value: usize) { + unsafe { + (*(parser as *mut Parser)).max_header_length = value; + } +} + +#[unsafe(no_mangle)] +pub fn set_max_body_payload(parser: *mut c_void, value: u64) { + unsafe { + (*(parser as *mut Parser)).max_body_payload = value; + } +} + +#[unsafe(no_mangle)] +pub fn set_should_continue_without_data(parser: *mut c_void, value: bool) { + unsafe { + (*(parser as *mut Parser)).continue_without_data = value; + } +} + +#[unsafe(no_mangle)] +pub fn set_should_skip_body(parser: *mut c_void, value: bool) { + unsafe { + (*(parser as *mut Parser)).skip_body = value; + } +} + +#[unsafe(no_mangle)] +pub fn set_is_connect(parser: *mut c_void, value: bool) { + unsafe { + (*(parser as *mut Parser)).is_connect = value; + } +} + +#[unsafe(no_mangle)] +pub fn set_debug(parser: *mut c_void, value: bool) { + unsafe { + (*(parser as *mut Parser)).debug = value; + } +} + +#[unsafe(no_mangle)] +pub fn set_active_callbacks(parser: *mut c_void, value: u64) { + unsafe { + (*(parser as *mut Parser)).active_callbacks = value; + } +} + +#[unsafe(no_mangle)] +pub fn set_active_events(parser: *mut c_void, value: u64) { + unsafe { + (*(parser as *mut Parser)).active_events = value; + } +} diff --git a/deps/undici/src/deps/milo/parser/wasm/src/package.json b/deps/undici/src/deps/milo/parser/wasm/src/package.json new file mode 100644 index 000000000000..ca21cebd75bc --- /dev/null +++ b/deps/undici/src/deps/milo/parser/wasm/src/package.json @@ -0,0 +1,38 @@ +{ + "name": "@perseveranza-pets/milo", + "version": "0.9.0", + "description": "A fast and embeddable HTTP/1.1 parser.", + "homepage": "https://sw.cowtech.it/milo", + "repository": { + "type": "git", + "url": "git+https://github.com/ShogunPanda/milo.git" + }, + "keywords": [ + "http", + "parser" + ], + "bugs": { + "url": "https://github.com/ShogunPanda/milo/issues" + }, + "author": "Shogun ", + "license": "ISC", + "private": false, + "type": "module", + "files": [ + "binary", + "src", + "CODE_OF_CONDUCT.md", + "LICENSE.md", + "README.md" + ], + "main": "src/simd/index.js", + "exports": { + ".": "./src/simd/index.js", + "./unbundled": "./src/simd/unbundled.js", + "./no-simd": "./src/no-simd/index.js", + "./no-simd/unbundled": "./src/no-simd/unbundled.js" + }, + "engines": { + "node": ">= 22.21.0" + } +} diff --git a/deps/undici/src/deps/milo/parser/wasm/src/template.js b/deps/undici/src/deps/milo/parser/wasm/src/template.js new file mode 100644 index 000000000000..8c3d066c7c72 --- /dev/null +++ b/deps/undici/src/deps/milo/parser/wasm/src/template.js @@ -0,0 +1,129 @@ +/* REPLACE: module */ + +const textEncoder = new TextEncoder() +const textDecoder = new TextDecoder() + +function log (logger, raw) { + const len = Number(BigInt.asUintN(32, raw)) + const ptr = Number(raw >> 32n) + + logger(textDecoder.decode(new Uint8Array(this.memory.buffer, ptr, len))) +} + +function alloc (len) { + return this.alloc(len) >>> 0 +} + +function dealloc (ptr, len) { + return this.dealloc(ptr, len) +} + +function create () { + return this.create() >>> 0 +} + +function destroy (parser) { + this.destroy(parser) +} + +function parse (parser, data, limit) { + return this.parse(parser, data, limit) >>> 0 +} + +function fail (parser, code, description) { + const len = description.length + const ptr = this.alloc(len) + const buffer = new Uint8Array(this.memory.buffer, ptr, len) + textEncoder.encodeInto(description, buffer) + + this.fail(parser, code, ptr, len) + this.dealloc(ptr, len) +} + +function hasDebug () { + return this.milo_has_debug() !== 0 +} + +/* REPLACE: enums */ + +/* REPLACE: getters */ + +/* REPLACE: setters */ + +function simpleCreate (spans, create) { + const parser = create() + spans[parser] = [] + this.setActiveCallbacks(parser, this.CALLBACK_ACTIVE_ALL) + return parser +} + +function simpleDestroy (spans, destroy, parser) { + spans[parser] = undefined + destroy(parser) +} + +export function noop () {} + +export function setup (env = {}) { + let { logger: logOption, ...instanceEnvironment } = env + let logger = noop + const context = {} + + if (logOption) { + if (typeof logOption !== 'function') { + logOption = console.log + } + + logger = log.bind(context, logOption) + } + + // Create the WASM instance + /* eslint-disable-next-line no-undef */ + const instance = new WebAssembly.Instance(wasmModule, { + env: { + logger, + /* REPLACE: callbacks:noop */ + ...instanceEnvironment + } + }) + + const wasm = instance.exports + context.memory = wasm.memory + + const milo = { + /* REPLACE: version */ + /* REPLACE: constants */ + /* REPLACE: enums:list */ + /* REPLACE: getters:list */ + /* REPLACE: setters:list */ + memory: wasm.memory, + alloc: alloc.bind(wasm), + dealloc: dealloc.bind(wasm), + create: create.bind(wasm), + destroy: destroy.bind(wasm), + parse: parse.bind(wasm), + fail: fail.bind(wasm), + hasDebug: hasDebug.bind(wasm), + clear: wasm.clear, + complete: wasm.complete, + finish: wasm.finish, + pause: wasm.pause, + reset: wasm.reset, + resume: wasm.resume + } + + /* REPLACE: start */ + return milo +} + +export function simple () { + const spans = {} + + const milo = setup({/* REPLACE: callbacks:simple */}) + + milo.spans = spans + milo.create = simpleCreate.bind(milo, spans, milo.create) + milo.destroy = simpleDestroy.bind(milo, spans, milo.destroy) + + return milo +} diff --git a/deps/undici/src/deps/milo/rust-toolchain.toml b/deps/undici/src/deps/milo/rust-toolchain.toml new file mode 100644 index 000000000000..26f190453c0f --- /dev/null +++ b/deps/undici/src/deps/milo/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "nightly-2026-07-29" diff --git a/deps/undici/src/deps/milo/scripts/Cargo.toml b/deps/undici/src/deps/milo/scripts/Cargo.toml new file mode 100644 index 000000000000..9e2a4c819a9d --- /dev/null +++ b/deps/undici/src/deps/milo/scripts/Cargo.toml @@ -0,0 +1,19 @@ +[package] + name = "milo-build-scripts" + version = "0.1.0" + edition = "2024" + publish = false + +[dependencies] + base64 = "0.22.1" + icu_collator = "2.1" + icu_normalizer = "2.1" + markdown = "1.0.0" + proc-macro2 = "1.0.106" + quote = "1.0.45" + regex = "1.12.3" + semver = "1.0.27" + serde = { version = "1.0.228", features = ["derive"] } + serde_json = { version = "1.0.149", features = ["preserve_order"] } + serde_yaml = "0.9.34" + toml = "1.0.7" diff --git a/deps/undici/src/deps/milo/scripts/src/bin/buildinfo.rs b/deps/undici/src/deps/milo/scripts/src/bin/buildinfo.rs new file mode 100644 index 000000000000..f0274a44a0a3 --- /dev/null +++ b/deps/undici/src/deps/milo/scripts/src/bin/buildinfo.rs @@ -0,0 +1,8 @@ +use std::error::Error; + +use milo_build_scripts::get_build_info; + +fn main() -> Result<(), Box> { + println!("{}", serde_json::to_string_pretty(&get_build_info()?)?); + Ok(()) +} diff --git a/deps/undici/src/deps/milo/scripts/src/bin/import-llhttp-tests.rs b/deps/undici/src/deps/milo/scripts/src/bin/import-llhttp-tests.rs new file mode 100644 index 000000000000..138d18e7d541 --- /dev/null +++ b/deps/undici/src/deps/milo/scripts/src/bin/import-llhttp-tests.rs @@ -0,0 +1,549 @@ +use std::{ + collections::HashSet, + env, + error::Error, + fs, + io::ErrorKind, + path::{Path, PathBuf}, + process::{self, Command}, + sync::LazyLock, +}; + +use icu_collator::{Collator, options::CollatorOptions}; +use icu_normalizer::DecomposingNormalizer; +use markdown::{ParseOptions, mdast::Node, to_mdast}; +use regex::Regex; +use serde_json::{Map, Value, json}; + +const FIXTURE_PREFIX: &str = "tests/fixtures/llhttp"; + +struct Case { + titles: Vec, + source: Value, + meta: Value, + http: Option, + log: Option, +} + +fn fail(message: &str) -> ! { + eprintln!("{message}"); + process::exit(1); +} + +fn clean_heading(title: &str) -> String { title.replace('`', "").trim().to_owned() } + +fn decode_html_entities(value: &str) -> Result> { + static ENTITIES: LazyLock = + LazyLock::new(|| Regex::new(r"&(?:quot|apos|amp|lt|gt|#(\d+)|#x([0-9a-fA-F]+));").unwrap()); + let mut decoded = String::new(); + let mut end = 0; + for captures in ENTITIES.captures_iter(value) { + let matched = captures.get(0).unwrap(); + decoded.push_str(&value[end..matched.start()]); + let character = match matched.as_str() { + """ => '"', + "'" => '\'', + "&" => '&', + "<" => '<', + ">" => '>', + _ => { + let code = if let Some(decimal) = captures.get(1) { + decimal.as_str().parse::()? + } else { + u32::from_str_radix(captures.get(2).unwrap().as_str(), 16)? + }; + char::from_u32(code).ok_or("Invalid HTML character reference")? + } + }; + decoded.push(character); + end = matched.end(); + } + decoded.push_str(&value[end..]); + Ok(decoded) +} + +fn parse_html_meta(value: &str) -> Result, Box> { + static COMMENT: LazyLock = LazyLock::new(|| Regex::new(r"(?s)").unwrap()); + static BARE: LazyLock = LazyLock::new(|| Regex::new(r"(?s)^\s*meta=(.*?)\s*$").unwrap()); + let Some(captures) = COMMENT.captures(value).or_else(|| BARE.captures(value)) else { + return Ok(None); + }; + // llhttp stores JSON metadata in markdown HTML nodes, sometimes entity-escaped + // by the parser. + let meta: Value = serde_json::from_str(&decode_html_entities(captures[1].trim())?)?; + if !has_meta(&meta) { + return Ok(None); + } + Ok(Some(meta)) +} + +fn has_meta(value: &Value) -> bool { value.as_object().is_some_and(|object| !object.is_empty()) } + +fn stringify_fixture(fixture: &Value) -> String { + // Match yaml's QUOTE_SINGLE values, PLAIN keys, and unlimited line width. + // serde_yaml has no option for choosing the scalar style, so emit this JSON + // subset explicitly while retaining the insertion order of object keys. + fn quote(value: &str, indent: usize, key: bool) -> String { + let double = value + .chars() + .any(|c| matches!(c, '\0'..='\u{8}' | '\u{b}'..='\u{1f}' | '\u{7f}'..='\u{9f}')) + || (key && value.contains('\n')) + || value + .as_bytes() + .windows(2) + .any(|pair| matches!(pair, [b' ' | b'\t', b'\n'] | [b'\n', b' ' | b'\t'])); + if double { + let json = serde_json::to_string(value).unwrap(); + let characters = json.chars().collect::>(); + let mut result = String::new(); + let mut i = 0; + while i < characters.len() { + if characters[i] == ' ' && characters.get(i + 1..i + 3) == Some(&['\\', 'n']) { + result.push_str("\\ "); + i += 1; + } + if characters[i] == '\\' { + match characters[i + 1] { + 'u' => { + let code = characters[i + 2..i + 6].iter().collect::(); + match code.as_str() { + "0000" => result.push_str("\\0"), + "0007" => result.push_str("\\a"), + "000b" => result.push_str("\\v"), + "001b" => result.push_str("\\e"), + _ if code.starts_with("00") => result.push_str(&format!("\\x{}", &code[2..])), + _ => result.push_str(&format!("\\u{code}")), + } + i += 6; + continue; + } + 'n' if !key && characters.get(i + 2) != Some(&'"') && json.encode_utf16().count() >= 40 => { + result.push_str("\n\n"); + while characters.get(i + 2..i + 4) == Some(&['\\', 'n']) && characters.get(i + 4) != Some(&'"') { + result.push('\n'); + i += 2; + } + result.push_str(&" ".repeat(indent)); + if characters.get(i + 2) == Some(&' ') { + result.push('\\'); + } + i += 2; + continue; + } + _ => { + result.push(characters[i]); + result.push(characters[i + 1]); + i += 2; + continue; + } + } + } + result.push(characters[i]); + i += 1; + } + return result; + } + let mut result = String::from("'"); + let mut characters = value.chars().peekable(); + while let Some(character) = characters.next() { + result.push(character); + if character == '\'' { + result.push('\''); + } else if character == '\n' && characters.peek() != Some(&'\n') { + result.push('\n'); + result.push_str(&" ".repeat(indent)); + } + } + result.push('\''); + result + } + + fn key(value: &str, indent: usize) -> String { + static SPECIAL: LazyLock = LazyLock::new(|| { + Regex::new(r#"^[\n\t ,\[\]{}#&*!|>'"%@`]|^[?-]$|^[?-][ \t]|[\n:][ \t]|[ \t]\n|[\n\t ]#|[\n\t :]$"#).unwrap() + }); + if value.is_empty() + || SPECIAL.is_match(value) + || !matches!(serde_yaml::from_str::(value), Ok(Value::String(_))) + { + return quote(value, indent, true); + } + value.to_owned() + } + + fn scalar(value: &Value, indent: usize) -> Option { + match value { + Value::String(value) => Some(quote(value, indent, false)), + Value::Array(value) if value.is_empty() => Some("[]".into()), + Value::Object(value) if value.is_empty() => Some("{}".into()), + Value::Array(_) | Value::Object(_) => None, + _ => Some(value.to_string()), + } + } + + fn emit(value: &Value, indent: usize) -> String { + let padding = " ".repeat(indent); + let mut output = String::new(); + match value { + Value::Object(object) => { + for (name, value) in object { + output.push_str(&format!("{padding}{}:", key(name, indent))); + if let Some(value) = scalar(value, indent + 2) { + output.push_str(&format!(" {value}\n")); + } else { + output.push('\n'); + output.push_str(&emit(value, indent + 2)); + } + } + } + Value::Array(array) => { + for value in array { + output.push_str(&format!("{padding}- ")); + if let Some(value) = scalar(value, indent + 2) { + output.push_str(&value); + output.push('\n'); + } else { + let nested = emit(value, indent + 2); + output.push_str(&nested[indent + 2..]); + } + } + } + _ => { + output.push_str(&format!("{padding}{}\n", scalar(value, indent).unwrap())); + } + } + output + } + + format!("---\n{}", emit(fixture, 0)) +} + +fn normalize_fixture_for_comparison(value: &Value) -> Value { + match value { + Value::Array(values) => values.iter().map(normalize_fixture_for_comparison).collect(), + Value::Object(object) => { + // Compare semantic fixture data only; checked is local review state and key + // order is irrelevant. + let mut keys = object.keys().filter(|key| *key != "checked").collect::>(); + keys.sort(); + keys + .into_iter() + .map(|key| (key.clone(), normalize_fixture_for_comparison(&object[key]))) + .collect() + } + _ => value.clone(), + } +} + +// Node's glob excludes dotfiles and does not descend into symlinked +// directories. +fn glob_files(root: &Path, extension: &str) -> Result, Box> { + let entries = match fs::read_dir(root) { + Ok(entries) => entries, + Err(error) if error.kind() == ErrorKind::NotFound => return Ok(Vec::new()), + Err(error) => return Err(error.into()), + }; + let mut entries = entries.collect::, _>>()?; + entries.sort_by_key(|entry| entry.file_name()); + let mut files = Vec::new(); + for entry in entries { + if entry.file_name().to_string_lossy().starts_with('.') { + continue; + } + if entry.file_type()?.is_dir() { + files.extend(glob_files(&entry.path(), extension)?); + } else if entry.path().extension().is_some_and(|value| value == extension) { + files.push(entry.path()); + } + } + Ok(files) +} + +fn process_section( + llhttp_root: &Path, + output_root: &Path, + fixture_root: &Path, + section: &str, + seen_files: &mut HashSet, +) -> Result<(), Box> { + let source = llhttp_root.join("test").join(section); + let mut cases = Vec::new(); + let mut used_names = HashSet::new(); + + // Find markdown files. + let mut files = glob_files(&source, "md")?; + // Use Unicode collation, as localeCompare does, rather than byte ordering. + let collator = Collator::try_new(Default::default(), CollatorOptions::default())?; + files.sort_by(|a, b| collator.compare(&a.to_string_lossy(), &b.to_string_lossy())); + + // Process each file. + for file in files { + let raw = fs::read_to_string(&file)?; + let source_path = file.strip_prefix(llhttp_root)?.to_string_lossy().replace('\\', "/"); + // Parse llhttp markdown sections into test cases. + // markdown-rs exposes the same mdast node types used by remark-parse. + let tree = to_mdast(&raw, &ParseOptions::default()).map_err(|error| error.to_string())?; + let mut parsed = Vec::new(); + let mut current_section: Option = None; + let mut current_case: Option = None; + + let flush_case = |current_case: &mut Option, parsed: &mut Vec| { + // A valid llhttp markdown case is complete only after both input and expected + // log blocks are seen. + if let Some(case) = current_case.take() + && case.http.is_some() + && case.log.is_some() + { + parsed.push(case); + } + }; + + for node in tree.children().into_iter().flatten() { + if let Node::Heading(heading) = node + && (heading.depth == 2 || heading.depth == 3) + { + flush_case(&mut current_case, &mut parsed); + let heading_text = heading + .children + .iter() + .filter_map(|child| { + match child { + Node::Text(text) => Some(text.value.as_str()), + Node::InlineCode(code) => Some(code.value.as_str()), + _ => None, + } + }) + .collect::(); + let cleaned_heading = clean_heading(&heading_text); + let titles = if heading.depth == 2 { + current_section = Some(cleaned_heading.clone()); + vec![cleaned_heading] + } else if let Some(section) = current_section.as_ref().filter(|section| !section.is_empty()) { + vec![section.clone(), cleaned_heading] + } else { + vec![cleaned_heading] + }; + current_case = Some(Case { + titles, + source: json!({ "path": source_path, "line": heading.position.as_ref().map_or(1, |position| position.start.line) }), + meta: Value::Null, + http: None, + log: None, + }); + continue; + } + + if let Node::Html(html) = node + && let Some(case) = current_case.as_mut().filter(|case| case.http.is_none()) + { + if let Some(Value::Object(meta)) = parse_html_meta(&html.value)? { + if !case.meta.is_object() { + case.meta = Value::Object(Map::new()); + } + case.meta.as_object_mut().unwrap().extend(meta); + } + continue; + } + + if let Node::Code(code) = node + && let Some(case) = current_case.as_mut() + { + let code_lang = code.lang.as_deref().unwrap_or("").to_lowercase(); + if code_lang == "http" { + case.http = Some(code.value.clone()); + } else if code_lang == "log" { + case.log = Some(code.value.clone()); + } + } + } + flush_case(&mut current_case, &mut parsed); + + for item in parsed { + let input = item.http.unwrap(); + let log = item.log.unwrap(); + if input.is_empty() || log.is_empty() { + continue; + } + + // Build deterministic fixture file name from titles. + // Keep the original normalization and collision suffixes for fixture names. + static NON_NAME: LazyLock = LazyLock::new(|| Regex::new(r"[^a-z0-9\s-]").unwrap()); + static SPACES: LazyLock = LazyLock::new(|| Regex::new(r"[\s_]+").unwrap()); + static HYPHENS: LazyLock = LazyLock::new(|| Regex::new(r"-+").unwrap()); + let name = item + .titles + .iter() + .map(|part| { + let normalized = DecomposingNormalizer::new_nfkd() + .normalize(&clean_heading(part)) + .to_lowercase(); + let normalized = NON_NAME.replace_all(&normalized, " "); + let normalized = SPACES.replace_all(normalized.trim(), "-"); + HYPHENS.replace_all(&normalized, "-").trim_matches('-').to_owned() + }) + .filter(|part| !part.is_empty()) + .collect::>() + .join("-"); + let mut file_name = format!("{}.yml", if name.is_empty() { "test" } else { &name }); + if !used_names.contains(&file_name) { + used_names.insert(file_name.clone()); + } else { + let mut counter = 2; + while used_names.contains(&format!("{name}-{counter}.yml")) { + counter += 1; + } + file_name = format!("{name}-{counter}.yml"); + used_names.insert(file_name.clone()); + } + + let prefix = &item.titles[0]; + let child = item.titles.get(1).filter(|child| !child.is_empty()); + // Keep display names aligned with existing fixture format. + let title_case = |value: &str| { + value + .split_whitespace() + .map(|part| { + let mut characters = part.chars(); + format!("{}{}", characters.next().unwrap().to_uppercase(), characters.as_str()) + }) + .collect::>() + .join(" ") + }; + let mut fixture = json!({ + "path": format!("{FIXTURE_PREFIX}/{section}/{file_name}"), + "name": if let Some(child) = child { + format!("{} / {}", clean_heading(prefix), title_case(&clean_heading(child))) + } else { + title_case(&clean_heading(prefix)) + }, + "checked": false, + "source": item.source, + }); + if has_meta(&item.meta) { + fixture["meta"] = item.meta; + } + fixture["input"] = input.split('\n').map(|line| Value::String(line.into())).collect(); + fixture["llhttp"] = log.split('\n').map(|line| Value::String(line.into())).collect(); + cases.push((file_name, fixture)); + } + } + + let target_dir = fixture_root.join(format!("{section}s")); + fs::create_dir_all(&target_dir)?; + let temp_fixture_path = target_dir.join(format!(".import-llhttp-{section}-temp.yml")); + for (i, (path, fixture)) in cases.iter().enumerate() { + println!("Processing {section} case {}/{}: {path}", i + 1, cases.len()); + let file_path = target_dir.join(path); + seen_files.insert(format!("{section}s/{path}")); + let mut initial_fixture = fixture.clone(); + initial_fixture["output"] = json!([]); + let initial_content = stringify_fixture(&initial_fixture); + fs::write(&temp_fixture_path, initial_content)?; + + // Run generator using a temporary fixture to compute output. + let file_arg = temp_fixture_path + .strip_prefix(output_root)? + .to_string_lossy() + .replace('\\', "/"); + let result = Command::new("cargo") + .args(["run", "--example", "llhttp", "--", "--generate", &file_arg]) + .current_dir(output_root) + .output(); + let stdout = match result { + Ok(result) + if result.status.success() + && result.stdout.len() <= 10 * 1024 * 1024 + && result.stderr.len() <= 10 * 1024 * 1024 => + { + String::from_utf8_lossy(&result.stdout).into_owned() + } + result => { + let details = result + .map(|result| String::from_utf8_lossy(&result.stderr).trim().to_owned()) + .unwrap_or_default(); + let details = if details.is_empty() { + String::new() + } else { + format!("\n{details}") + }; + fail(&format!("Failed to run cargo generator for {file_arg}{details}")); + } + }; + let Some(marker_index) = stdout.find("---") else { + fail(&format!( + "Failed to parse cargo output for {file_arg}: missing YAML marker" + )); + }; + let yaml_snippet = stdout[marker_index + 3..].trim(); + if yaml_snippet.is_empty() { + fail(&format!( + "Failed to parse cargo output for {file_arg}: empty YAML snippet" + )); + } + let output: Value = serde_yaml::from_str(yaml_snippet)?; + let mut final_fixture = fixture.clone(); + final_fixture["output"] = output; + + // Skip writes when only comments/checked differ semantically. + let skip_overwrite = match fs::read_to_string(&file_path) { + Ok(raw) => { + serde_yaml::from_str::(&raw).is_ok_and(|existing_fixture| { + normalize_fixture_for_comparison(&existing_fixture) == normalize_fixture_for_comparison(&final_fixture) + }) + } + Err(error) if error.kind() == ErrorKind::NotFound => false, + Err(error) => return Err(error.into()), + }; + if skip_overwrite { + continue; + } + let final_content = stringify_fixture(&final_fixture); + fs::write(file_path, final_content)?; + } + match fs::remove_file(temp_fixture_path) { + Ok(()) => {} + Err(error) if error.kind() == ErrorKind::NotFound => {} + Err(error) => return Err(error.into()), + } + Ok(()) +} + +fn main() -> Result<(), Box> { + let args = env::args().collect::>(); + if args.len() < 3 || args[1].is_empty() || args[2].is_empty() { + println!("Usage: import-llhttp-tests "); + return Ok(()); + } + let llhttp_root = std::path::absolute(&args[1])?; + let output_root = std::path::absolute(&args[2])?; + let fixture_root = output_root.join(FIXTURE_PREFIX); + fs::create_dir_all(&fixture_root)?; + + // Snapshot fixtures that existed before import starts. + // Keep discovery order for stale-file messages, as JavaScript's Set does. + let mut existing_files = Vec::new(); + for section in ["requests", "responses"] { + let section_root = fixture_root.join(section); + // Section folder may not exist yet. + if let Ok(files) = glob_files(§ion_root, "yml") { + for file in files { + existing_files.push(file.strip_prefix(&fixture_root)?.to_string_lossy().replace('\\', "/")); + } + } + } + let mut seen_files = HashSet::new(); + process_section(&llhttp_root, &output_root, &fixture_root, "request", &mut seen_files)?; + process_section(&llhttp_root, &output_root, &fixture_root, "response", &mut seen_files)?; + for file in existing_files { + if seen_files.contains(&file) { + continue; + } + match fs::remove_file(fixture_root.join(&file)) { + Ok(()) => {} + Err(error) if error.kind() == ErrorKind::NotFound => {} + Err(error) => return Err(error.into()), + } + println!("Removed stale fixture: {file}"); + } + Ok(()) +} diff --git a/deps/undici/src/deps/milo/scripts/src/bin/postbuild-cpp.rs b/deps/undici/src/deps/milo/scripts/src/bin/postbuild-cpp.rs new file mode 100644 index 000000000000..b45f5597725c --- /dev/null +++ b/deps/undici/src/deps/milo/scripts/src/bin/postbuild-cpp.rs @@ -0,0 +1,66 @@ +use std::{env, error::Error, fs, path::Path}; + +use milo_build_scripts::get_build_info; + +fn prepend_version_and_method_map(header: &str) -> Result> { + let header_matcher = "namespace milo_parser {"; + let info = get_build_info()?; + let version = info.version; + // Create the method map, required by Node.js. + let methods = info + .constants + .iter() + .filter_map(|(key, value)| { + key + .strip_prefix("METHOD_") + .map(|method| format!(" EACH({value}, {method}, {method}) \\")) + }) + .collect::>() + .join("\n"); + let updated_header = format!( + r#"#define MILO_VERSION "{}" +#define MILO_VERSION_MAJOR {} +#define MILO_VERSION_MINOR {} +#define MILO_VERSION_PATCH {} +#define MILO_VERSION_PRERELEASE "{}" + +#define MILO_METHODS_MAP(EACH) \ +{methods} + +namespace milo_parser {{ + +struct Parser;"#, + version.raw, version.major, version.minor, version.patch, version.prerelease + ); + + // Replace the header with the new code. + // Match the JavaScript normalization without changing other whitespace. + let mut normalized = String::with_capacity(header.len()); + let mut newlines = 0; + for character in header.chars() { + if character == '\n' { + newlines += 1; + } else { + newlines = 0; + } + if newlines <= 2 { + normalized.push(character); + } + } + Ok(normalized.replacen(header_matcher, &updated_header, 1)) +} + +fn main() -> Result<(), Box> { + let profile = env::args().nth(1).ok_or("Usage: postbuild-cpp ")?; + // Read the file. + let header_path = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../dist/cpp") + .join(profile) + .join("milo.h"); + let header = fs::read_to_string(&header_path)?; + // Apply modifications. + let header = prepend_version_and_method_map(&header)?; + // Write the updated file. + fs::write(header_path, header)?; + Ok(()) +} diff --git a/deps/undici/src/deps/milo/scripts/src/bin/postbuild-wasm.rs b/deps/undici/src/deps/milo/scripts/src/bin/postbuild-wasm.rs new file mode 100644 index 000000000000..33a248210dbe --- /dev/null +++ b/deps/undici/src/deps/milo/scripts/src/bin/postbuild-wasm.rs @@ -0,0 +1,391 @@ +use std::{env, error::Error, fs, io::ErrorKind, path::Path}; + +use base64::{Engine, engine::general_purpose::STANDARD}; +use milo_build_scripts::{BuildInfo, get_build_info}; +use serde_json::Value; + +const ENUMS: &[(&str, &str)] = &[ + ("ERROR_", "Errors"), + ("METHOD_", "Methods"), + ("CALLBACK_", "Callbacks"), + ("CALLBACK_ACTIVE_", "CallbackActives"), + ("EVENT_", "Events"), + ("EVENT_ACTIVE_", "EventActives"), + ("STATE_", "States"), + ("PARSER_FIELD_", "ParserFields"), +]; + +enum GetterType { + Bool, + Number, + BigInt, + String, +} + +const GETTERS: &[(&str, GetterType, &str)] = &[ + ("isAutodetect", GetterType::Bool, "is_autodetect"), + ("isRequest", GetterType::Bool, "is_request"), + ("isPaused", GetterType::Bool, "is_paused"), + ("shouldManageUnconsumed", GetterType::Bool, "should_manage_unconsumed"), + ( + "shouldSuspendAfterHeaders", + GetterType::Bool, + "should_suspend_after_headers", + ), + ("getMaxStartLineLength", GetterType::Number, "get_max_start_line_length"), + ("getMaxHeaderLength", GetterType::Number, "get_max_header_length"), + ("getMaxBodyPayload", GetterType::BigInt, "get_max_body_payload"), + ( + "shouldContinueWithoutData", + GetterType::Bool, + "should_continue_without_data", + ), + ("isConnect", GetterType::Bool, "is_connect"), + ("isDebug", GetterType::Bool, "is_debug"), + ("shouldSkipBody", GetterType::Bool, "should_skip_body"), + ("getState", GetterType::Number, "get_state"), + ("getPosition", GetterType::Number, "get_position"), + ("getParsed", GetterType::BigInt, "get_parsed"), + ("getErrorCode", GetterType::Number, "get_error_code"), + ("getMethod", GetterType::Number, "get_method"), + ("getStatus", GetterType::Number, "get_status"), + ("hasConnectionClose", GetterType::Bool, "has_connection_close"), + ("hasConnectionUpgrade", GetterType::Bool, "has_connection_upgrade"), + ("getContentLength", GetterType::BigInt, "get_content_length"), + ("getChunkSize", GetterType::BigInt, "get_chunk_size"), + ( + "getRemainingContentLength", + GetterType::BigInt, + "get_remaining_content_length", + ), + ("getRemainingChunkSize", GetterType::BigInt, "get_remaining_chunk_size"), + ("hasContentLength", GetterType::Bool, "has_content_length"), + ("hasTransferEncoding", GetterType::Bool, "has_transfer_encoding"), + ( + "hasChunkedTransferEncoding", + GetterType::Bool, + "has_chunked_transfer_encoding", + ), + ("hasUpgrade", GetterType::Bool, "has_upgrade"), + ("hasTrailers", GetterType::Bool, "has_trailers"), + ("getErrorDescription", GetterType::String, "get_error_description_raw"), +]; + +const SETTERS: &[(&str, &str)] = &[ + ("setShouldAutodetect", "set_should_autodetect"), + ("setShouldContinueWithoutData", "set_should_continue_without_data"), + ("setIsRequest", "set_is_request"), + ("setIsConnect", "set_is_connect"), + ("setDebug", "set_debug"), + ("setShouldManageUnconsumed", "set_should_manage_unconsumed"), + ("setShouldSuspendAfterHeaders", "set_should_suspend_after_headers"), + ("setMaxStartLineLength", "set_max_start_line_length"), + ("setMaxHeaderLength", "set_max_header_length"), + ("setMaxBodyPayload", "set_max_body_payload"), + ("setShouldSkipBody", "set_should_skip_body"), + ("setActiveCallbacks", "set_active_callbacks"), + ("setActiveEvents", "set_active_events"), +]; + +fn get_callbacks(info: &BuildInfo) -> Vec<(&str, &Value)> { + info + .constants + .iter() + .filter(|(key, _)| key.starts_with("CALLBACK_") && !key.starts_with("CALLBACK_ACTIVE")) + .map(|(key, value)| (key.as_str(), value)) + .collect() +} + +fn generate_enums(info: &BuildInfo) -> String { + let mut replacement = String::new(); + for &(selector, name) in ENUMS { + let mut matching = info + .constants + .iter() + .filter(|(key, _)| key.starts_with(selector)) + .collect::>(); + let mut suffix = ""; + if selector == "CALLBACK_" { + matching.retain(|(key, _)| !key.starts_with("CALLBACK_ACTIVE")); + } else if selector == "EVENT_" { + matching.retain(|(key, _)| !key.starts_with("EVENT_ACTIVE_")); + } else if selector == "CALLBACK_ACTIVE_" || selector == "EVENT_ACTIVE_" { + suffix = "n"; + } + let entries = matching + .iter() + .map(|(key, value)| format!(" {}: {value}{suffix}", &key[selector.len()..])) + .chain( + matching + .iter() + .map(|(key, value)| format!(" {value}{suffix}: '{}'", &key[selector.len()..])), + ) + .collect::>() + .join(",\n"); + replacement.push_str(&format!("const {name} = Object.freeze({{\n{entries}\n}})\n\n")); + } + replacement.trim_end().to_owned() +} + +fn generate_enums_lists() -> String { + ENUMS + .iter() + .map(|(_, name)| format!("{name},")) + .collect::>() + .join("\n") +} + +fn generate_constants(info: &BuildInfo) -> String { + info + .constants + .iter() + .map(|(key, value)| { + let mut value = value.to_string(); + if key.starts_with("CALLBACK_ACTIVE_") || key.starts_with("EVENT_ACTIVE_") { + value.push('n'); + } + format!("{key}: {value},") + }) + .collect::>() + .join("\n") +} + +fn generate_getters() -> String { + let mut replacement = String::new(); + for (getter, kind, raw_getter) in GETTERS { + let call = format!("this.{raw_getter}(parser)"); + let body = match kind { + GetterType::Bool => format!(" return {call} !== 0"), + GetterType::Number => format!(" return {call} >>> 0"), + GetterType::BigInt => format!(" return BigInt.asUintN(64, {call})"), + GetterType::String => { + format!( + " const raw = {call}\n const len = Number(BigInt.asUintN(32, raw))\n const ptr = Number(raw >> 32n)\n \ + return textDecoder.decode(new Uint8Array(this.memory.buffer, ptr, len))" + ) + } + }; + replacement.push_str(&format!("function {getter} (parser) {{\n{body}\n}}\n\n")); + } + replacement.trim_end().to_owned() +} + +fn generate_setters() -> String { + let mut replacement = String::new(); + for (setter, raw_setter) in SETTERS { + replacement.push_str(&format!( + "function {setter} (parser, value) {{\n this.{raw_setter}(parser, value)\n}}\n\n" + )); + } + replacement.trim_end().to_owned() +} + +fn generate_getters_list() -> String { + GETTERS + .iter() + .map(|(name, _, _)| format!("{name}: {name}.bind(wasm),")) + .collect::>() + .join("\n") +} + +fn generate_setters_list() -> String { + SETTERS + .iter() + .map(|(name, _)| format!("{name}: {name}.bind(wasm),")) + .collect::>() + .join("\n") +} + +fn generate_noop_callbacks(info: &BuildInfo) -> String { + get_callbacks(info) + .iter() + .map(|(key, _)| format!("{}: noop,", key.replacen("CALLBACK_", "", 1).to_lowercase())) + .collect::>() + .join("\n") +} + +fn generate_simple_callbacks(info: &BuildInfo) -> String { + get_callbacks(info) + .iter() + .map(|(key, value)| { + let name = key.replacen("CALLBACK_", "", 1).to_lowercase(); + format!("{name} (parser, at, len) {{\n spans[parser].push([{value}, at, len])\n}}") + }) + .collect::>() + .join(",\n") +} + +fn generate_module(profile: &str, info: &BuildInfo, loader: &str, commonjs: bool) -> Result> { + let template = fs::read_to_string(Path::new(env!("CARGO_MANIFEST_DIR")).join("../parser/wasm/src/template.js"))?; + let version = &info.version; + let mut output = String::new(); + + // The template is shared by bundled/unbundled and ESM/CJS outputs; + // placeholders keep the runtime code identical. + // Placeholders occupy complete lines. Emit already formatted fragments using + // the template's indentation, so packaging needs no JavaScript formatter. + for line in template.lines() { + let trimmed = line.trim_start(); + if let Some(id) = trimmed.strip_prefix("/* REPLACE: ").and_then(|s| s.strip_suffix(" */")) { + let replacement = match id { + "module" => loader.to_owned(), + "version" => { + format!( + "version: {{\n raw: '{}',\n major: {},\n minor: {},\n patch: {},\n prerelease: '{}'\n}},", + version.raw, version.major, version.minor, version.patch, version.prerelease + ) + } + "enums" => generate_enums(info), + "enums:list" => generate_enums_lists(), + "constants" => generate_constants(info), + "getters" => generate_getters(), + "setters" => generate_setters(), + "getters:list" => generate_getters_list(), + "setters:list" => generate_setters_list(), + "callbacks:noop" => generate_noop_callbacks(info), + "callbacks:simple" => generate_simple_callbacks(info), + "start" => { + if profile == "debug" { + "wasm.__start()".to_owned() + } else { + continue; + } + } + _ => { + eprintln!("Unsupported placeholder type {id}"); + trimmed.to_owned() + } + }; + let indent = &line[..line.len() - trimmed.len()]; + for fragment in replacement.lines() { + if !fragment.is_empty() { + output.push_str(indent); + output.push_str(fragment); + } + output.push('\n'); + } + } else { + output.push_str(line); + output.push('\n'); + } + } + if commonjs { + output = output + .lines() + .map(|line| { + if line.starts_with("export function ") { + &line["export ".len()..] + } else { + line + } + }) + .collect::>() + .join("\n"); + output.push('\n'); + output.push_str("\nmodule.exports = { wasmModule, noop, setup, simple }\n"); + } + Ok(output) +} + +fn generate_commonjs_package_json(package_json: &Value) -> Value { + let mut cjs_package_json = package_json.clone(); + cjs_package_json["name"] = "@perseveranza-pets/milo-cjs".into(); + cjs_package_json["type"] = "commonjs".into(); + cjs_package_json +} + +fn generate_variant( + profile: &str, + info: &BuildInfo, + root_folder: &Path, + variant: &str, + commonjs: bool, +) -> Result<(), Box> { + let wasm_file = format!("{variant}.wasm"); + let source_folder = root_folder.join("src").join(variant); + let profile_root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../dist/wasm").join(profile); + let wasm = STANDARD.encode(fs::read(profile_root.join("binary").join(&wasm_file))?); + let unbundled_loader = if commonjs { + format!( + "const {{ readFileSync }} = require('node:fs')\nconst {{ join }} = require('node:path')\n\nconst wasmModule = \ + new WebAssembly.Module(readFileSync(join(__dirname, '../../binary/{wasm_file}')))" + ) + } else { + format!( + "import {{ readFileSync }} from 'node:fs'\n\nexport const wasmModule = new WebAssembly.Module(readFileSync(new \ + URL('../../binary/{wasm_file}', import.meta.url)))" + ) + }; + let bundled_loader = if commonjs { + format!("const wasmModule = new WebAssembly.Module(\n Buffer.from(\n '{wasm}',\n 'base64'\n )\n)") + } else { + format!( + "export const wasmModule = new WebAssembly.Module(\n Uint8Array.from(\n globalThis.atob(\n '{wasm}'\n ),\n c => c.codePointAt(0)\n )\n)" + ) + }; + let unbundled = generate_module(profile, info, &unbundled_loader, commonjs)?; + let bundled = generate_module(profile, info, &bundled_loader, commonjs)?; + fs::create_dir_all(&source_folder)?; + fs::write(source_folder.join("unbundled.js"), unbundled)?; + fs::write(source_folder.join("index.js"), bundled)?; + Ok(()) +} + +fn copy_directory(source: &Path, destination: &Path) -> Result<(), Box> { + fs::create_dir_all(destination)?; + for entry in fs::read_dir(source)? { + let entry = entry?; + let target = destination.join(entry.file_name()); + if entry.file_type()?.is_dir() { + copy_directory(&entry.path(), &target)?; + } else { + fs::copy(entry.path(), target)?; + } + } + Ok(()) +} + +// TODO@PI: Generate TypeScript declarations. +fn main() -> Result<(), Box> { + let info = get_build_info()?; + let profile = env::args().nth(1).ok_or("Usage: postbuild-wasm ")?; + let root = Path::new(env!("CARGO_MANIFEST_DIR")).join(".."); + // Open the package.json and update the version. + let mut package_json: Value = serde_json::from_str(&fs::read_to_string(root.join("parser/wasm/src/package.json"))?)?; + let profile_root = root.join("dist/wasm").join(&profile); + let root_folder = profile_root.join("package"); + let cjs_root_folder = profile_root.join("package-cjs"); + package_json["version"] = info.version.raw.clone().into(); + let cjs_package_json = generate_commonjs_package_json(&package_json); + + // Write files. + for folder in [&root_folder, &cjs_root_folder] { + match fs::remove_dir_all(folder) { + Ok(()) => {} + Err(error) if error.kind() == ErrorKind::NotFound => {} + Err(error) => return Err(error.into()), + } + } + fs::create_dir_all(&root_folder)?; + fs::create_dir_all(&cjs_root_folder)?; + copy_directory(&profile_root.join("binary"), &root_folder.join("binary"))?; + copy_directory(&profile_root.join("binary"), &cjs_root_folder.join("binary"))?; + generate_variant(&profile, &info, &root_folder, "simd", false)?; + generate_variant(&profile, &info, &root_folder, "no-simd", false)?; + generate_variant(&profile, &info, &cjs_root_folder, "simd", true)?; + generate_variant(&profile, &info, &cjs_root_folder, "no-simd", true)?; + fs::write( + root_folder.join("package.json"), + serde_json::to_string_pretty(&package_json)?, + )?; + fs::write( + cjs_root_folder.join("package.json"), + serde_json::to_string_pretty(&cjs_package_json)?, + )?; + // Copy other Markdown files from root. + for name in ["CODE_OF_CONDUCT.md", "LICENSE.md", "README.md"] { + fs::copy(root.join(name), root_folder.join(name))?; + fs::copy(root.join(name), cjs_root_folder.join(name))?; + } + Ok(()) +} diff --git a/deps/undici/src/deps/milo/scripts/src/bin/sync-versions.rs b/deps/undici/src/deps/milo/scripts/src/bin/sync-versions.rs new file mode 100644 index 000000000000..85c171ca5799 --- /dev/null +++ b/deps/undici/src/deps/milo/scripts/src/bin/sync-versions.rs @@ -0,0 +1,39 @@ +use std::{ + env, + error::Error, + fs, + path::Path, + process::{Command, Stdio}, +}; + +use regex::{Captures, Regex}; + +fn main() -> Result<(), Box> { + let folders = ["macros", "parser", "references/rust", "parser/wasm/src"]; + let version = env::args().nth(1).ok_or("Usage: sync-versions ")?; + let root = Path::new(env!("CARGO_MANIFEST_DIR")).join(".."); + + for folder in folders { + let path = root.join(folder); + env::set_current_dir(path)?; + // Like execFileSync, wait for each update and discard its stdout. + let output = Command::new("cambi") + .args(["update", &version]) + .stderr(Stdio::inherit()) + .output()?; + if !output.status.success() { + return Err(format!("cambi update failed in {folder}: {}", output.status).into()); + } + } + + // Dependency of milo-macros. + // Update it without reformatting the manifest. + let main_cargo = root.join("parser/Cargo.toml"); + let main_cargo_content = fs::read_to_string(&main_cargo)?; + let matcher = Regex::new(r#"(?m)^(\s+milo-macros = \{ version = ")([^"]+)(")"#)?; + let main_cargo_content = matcher.replace(&main_cargo_content, |captures: &Captures<'_>| { + format!("{}{version}{}", &captures[1], &captures[3]) + }); + fs::write(main_cargo, main_cargo_content.as_bytes())?; + Ok(()) +} diff --git a/deps/undici/src/deps/milo/scripts/src/lib.rs b/deps/undici/src/deps/milo/scripts/src/lib.rs new file mode 100644 index 000000000000..666dc6be0d63 --- /dev/null +++ b/deps/undici/src/deps/milo/scripts/src/lib.rs @@ -0,0 +1,109 @@ +use std::{error::Error, fs, path::Path}; + +use serde::{Deserialize, Serialize}; +use serde_json::{Map, Value}; + +// Reuse the layout used by the macros without spawning Cargo at runtime. +// The macro generator and CLI entry point are unused in this crate. +#[allow(dead_code)] +#[path = "../../macros/src/parser_fields.rs"] +mod parser_fields; + +#[derive(Serialize)] +pub struct Version { + pub raw: String, + pub major: u64, + pub minor: u64, + pub patch: u64, + pub prerelease: String, +} + +#[derive(Serialize)] +pub struct BuildInfo { + pub version: Version, + pub constants: Map, +} + +#[derive(Deserialize)] +struct Package { + version: String, +} + +#[derive(Deserialize)] +struct Manifest { + package: Package, +} + +fn read_yaml_list(name: &str) -> Result, Box> { + let root = Path::new(env!("CARGO_MANIFEST_DIR")).join(".."); + let raw = fs::read_to_string(root.join(format!("macros/constants/{name}.yml")))?; + Ok(serde_yaml::from_str(&raw)?) +} + +fn read_version() -> Result> { + let root = Path::new(env!("CARGO_MANIFEST_DIR")).join(".."); + let manifest: Manifest = toml::from_str(&fs::read_to_string(root.join("parser/Cargo.toml"))?)?; + let parsed = semver::Version::parse(&manifest.package.version)?; + Ok(Version { + raw: manifest.package.version, + major: parsed.major, + minor: parsed.minor, + patch: parsed.patch, + prerelease: parsed.pre.to_string(), + }) +} + +fn read_parser_fields() -> Map { + // The JavaScript helper spawned Cargo; Rust can reuse the same offsets + // directly. + parser_fields::FIELDS + .iter() + .map(|&(name, offset)| (format!("PARSER_FIELD_{name}"), offset.into())) + .collect() +} + +/// Reads the current repository inputs, independently of the working directory. +pub fn get_build_info() -> Result> { + let version = read_version()?; + let methods = read_yaml_list("methods")?; + let errors = read_yaml_list("errors")?; + let callbacks = read_yaml_list("callbacks")?; + let states = read_yaml_list("states")?; + let parser_fields = read_parser_fields(); + + // Insertion order is part of the generated JSON and downstream bindings. + let mut constants = Map::new(); + for (i, method) in methods.iter().enumerate() { + constants.insert(format!("METHOD_{}", method.replace('-', "_")), i.into()); + } + for (i, callback) in callbacks.iter().enumerate() { + constants.insert(format!("CALLBACK_{}", callback.to_uppercase()), i.into()); + } + constants.insert("EVENT_END".into(), 0.into()); + for (i, callback) in callbacks.iter().enumerate() { + let name = callback.strip_prefix("on_").unwrap_or(callback).to_uppercase(); + constants.insert(format!("EVENT_{name}"), (i + 1).into()); + } + + let mut all = 0; + constants.insert("CALLBACK_ACTIVE_NONE".into(), 0.into()); + constants.insert("EVENT_ACTIVE_NONE".into(), 0.into()); + for (i, callback) in callbacks.iter().enumerate() { + // Match JavaScript's signed 32-bit shifts, including the masked shift count. + let bit = 1_i32.wrapping_shl((i % 32) as u32); + constants.insert(format!("CALLBACK_ACTIVE_{}", callback.to_uppercase()), bit.into()); + constants.insert(format!("EVENT_ACTIVE_{}", callback.to_uppercase()), bit.into()); + all |= bit; + } + constants.insert("CALLBACK_ACTIVE_ALL".into(), all.into()); + constants.insert("EVENT_ACTIVE_ALL".into(), all.into()); + for (i, error) in errors.iter().enumerate() { + constants.insert(format!("ERROR_{error}"), i.into()); + } + for (i, state) in states.iter().enumerate() { + constants.insert(format!("STATE_{}", state.to_uppercase()), i.into()); + } + constants.extend(parser_fields); + + Ok(BuildInfo { version, constants }) +} diff --git a/deps/undici/src/docs/docs/api/CacheStore.md b/deps/undici/src/docs/docs/api/CacheStore.md index 3e30abd5699b..45c202d89872 100644 --- a/deps/undici/src/docs/docs/api/CacheStore.md +++ b/deps/undici/src/docs/docs/api/CacheStore.md @@ -42,7 +42,8 @@ added: v7.0.0 Stores cached responses in memory. The store enforces upper bounds on the total number of responses, the total size of all responses, and the size of any single response. When a limit is exceeded, the store evicts approximately half of its -entries and emits a [`'maxSizeExceeded'`](#event-maxsizeexceeded) event. +entries, least recently used first, and emits a +[`'maxSizeExceeded'`](#event-maxsizeexceeded) event. ```mjs import { interceptors, cacheStores, Agent, setGlobalDispatcher } from 'undici' @@ -163,7 +164,9 @@ added: v7.0.0 --> Stores cached responses in a SQLite database using the [`node:sqlite`][] API. -The constructor throws when [`node:sqlite`][] is not available. +The constructor throws when [`node:sqlite`][] is not available. Response bodies +larger than `compressThreshold` are stored compressed with zstd and +decompressed when read back, saving database space. ```mjs import { interceptors, cacheStores, Agent, setGlobalDispatcher } from 'undici' @@ -189,9 +192,16 @@ added: v7.0.0 * `maxEntrySize` {number} The maximum size, in bytes, of a single response body. Responses whose body exceeds this value are not cached. Must not exceed `2000000000` (2 GB). **Default:** `2000000000` (2 GB). + * `compressThreshold` {number} The size, in bytes, above which a response + body is stored compressed with zstd. Responses with an easily compressible + Content-Type (e.g. json, xml, plain text) are compressed regardless of + size. Requires a Node version with zstd + support (22.19+); when unavailable, bodies are stored uncompressed. + **Default:** `1048576` (1 MiB). -`maxCount` and `maxEntrySize` must be non-negative integers; a `TypeError` is -thrown otherwise, or if `maxEntrySize` is greater than 2 GB. +`maxCount`, `maxEntrySize`, and `compressThreshold` must be non-negative +integers; a `TypeError` is thrown otherwise, or if `maxEntrySize` is greater +than 2 GB. ### `sqliteCacheStore.close()` @@ -225,7 +235,8 @@ added: v7.0.0 See [`GetResult`](#getresult). Looks up a cached response for `key`, comparing the request method and every -header named in the stored `vary` map. +header named in the stored `vary` map. A body that was stored compressed is +decompressed before being returned. ### `sqliteCacheStore.set(key, value)` @@ -240,9 +251,11 @@ added: v7.0.0 * Returns: {undefined} Writes a response into the database directly, without going through a stream. If -the body exceeds `maxEntrySize`, nothing is stored. When an entry already exists -for `key` it is overwritten; otherwise a new row is inserted and old entries are -pruned to honour `maxCount`. This method is used internally by +the body exceeds `maxEntrySize`, nothing is stored. A body larger than +`compressThreshold`, or with an easily compressible Content-Type (e.g. json, +xml, plain text), is compressed with zstd before being written. When an entry +already exists for `key` it is overwritten; otherwise a new row is inserted and +old entries are pruned to honour `maxCount`. This method is used internally by [`sqliteCacheStore.createWriteStream()`](#sqlitecachestorecreatewritestreamkey-value). ### `sqliteCacheStore.createWriteStream(key, value)` @@ -260,7 +273,9 @@ added: v7.0.0 Returns a {Writable} stream used to write the response body into the store. When the stream finishes, the buffered body is committed via -[`sqliteCacheStore.set()`](#sqlitecachestoresetkey-value). If the body exceeds +[`sqliteCacheStore.set()`](#sqlitecachestoresetkey-value), where bodies larger +than `compressThreshold` or with an easily compressible Content-Type are +compressed with zstd. If the body exceeds `maxEntrySize`, the stream is destroyed and nothing is stored. ### `sqliteCacheStore.delete(key)` diff --git a/deps/undici/src/docs/docs/api/Client.md b/deps/undici/src/docs/docs/api/Client.md index 5f44ae42db86..e72a841df95d 100644 --- a/deps/undici/src/docs/docs/api/Client.md +++ b/deps/undici/src/docs/docs/api/Client.md @@ -116,8 +116,8 @@ added: v1.0.0 * `maxConcurrentStreams` {number} _Deprecated: use h2Options.useH2c instead_ The maximum number of concurrent HTTP/2 streams for a single session. Once h2 is negotiated this — not `pipelining`, which is HTTP/1.1 only — is the ceiling used to dispatch in-flight requests. - It may be overridden by the server's `SETTINGS_MAX_CONCURRENT_STREAMS` - frame. **Default:** `100`. + The server's `SETTINGS_MAX_CONCURRENT_STREAMS` can lower this limit, but + not raise it. **Default:** `100`. * `connectionWindowSize` {number} _Deprecated: use h2Options.connectionWindowSize instead_ The HTTP/2 connection-level flow-control window size set via `ClientHttp2Session.setLocalWindowSize()`. Must be a positive integer. **Default:** `524288`. @@ -130,8 +130,8 @@ added: v1.0.0 * `maxConcurrentStreams` {number} The maximum number of concurrent HTTP/2 streams for a single session. Once h2 is negotiated this — not `pipelining`, which is HTTP/1.1 only — is the ceiling used to dispatch in-flight requests. - It may be overridden by the server's `SETTINGS_MAX_CONCURRENT_STREAMS` - frame. **Default:** `100`. + The server's `SETTINGS_MAX_CONCURRENT_STREAMS` can lower this limit, but + not raise it. **Default:** `100`. * `connectionWindowSize` {number} The HTTP/2 connection-level flow-control window size set via `ClientHttp2Session.setLocalWindowSize()`. Must be a positive integer. **Default:** `524288`. diff --git a/deps/undici/src/docs/docs/api/Dispatcher.md b/deps/undici/src/docs/docs/api/Dispatcher.md index c85705b4c528..4e3c12931309 100644 --- a/deps/undici/src/docs/docs/api/Dispatcher.md +++ b/deps/undici/src/docs/docs/api/Dispatcher.md @@ -209,6 +209,9 @@ added: v4.0.0 * `path` {string} The request path. * `method` {string} The HTTP method, e.g. `'GET'` or `'POST'`. * `body` {string|Buffer|Uint8Array|Readable|FormData|null} The request body. + Undici's network clients require a `FormData` body created by the same + undici package instance. A body from another source, including Node.js's + built-in `FormData`, is rejected with an `InvalidArgumentError`. **Default:** `null`. * `headers` {UndiciHeaders} Request headers. **Default:** `null`. * `query` {Object} Query string parameters embedded into the request URL. Both diff --git a/deps/undici/src/docs/docs/api/Errors.md b/deps/undici/src/docs/docs/api/Errors.md index d390bfaeb000..c71d61d77a5c 100644 --- a/deps/undici/src/docs/docs/api/Errors.md +++ b/deps/undici/src/docs/docs/api/Errors.md @@ -18,7 +18,7 @@ if (err instanceof errors.ConnectTimeoutError) { } ``` -All errors, except [`HTTPParserError`][], extend [`UndiciError`][]. Each error +All errors, except [`HTTPParserError`][] and `WebAssemblyNotSupportedError`, extend [`UndiciError`][]. Each error carries a stable `code` string (for example `UND_ERR_CONNECT_TIMEOUT`) and a `name`. @@ -32,6 +32,37 @@ if (err.code === 'UND_ERR_CONNECT_TIMEOUT') { } ``` +## Timeout-related errors + +The timeout errors below are controlled by dispatcher/client options: + +| Error code | When it is thrown | Related option | +| ---------- | ----------------- | -------------- | +| `UND_ERR_CONNECT_TIMEOUT` | A connection could not be established before the connection timeout elapsed. | [`connect.timeout`](Client.md) | +| `UND_ERR_HEADERS_TIMEOUT` | The full response headers were not received before the headers timeout elapsed. | [`headersTimeout`](Client.md) | +| `UND_ERR_BODY_TIMEOUT` | While reading the response body, no new data arrived before the body timeout elapsed. | [`bodyTimeout`](Client.md) | +| `UND_ERR_SOCKET` | A generic socket-level failure happened. | Inspect `error.socket` for connection details. | + +### Configuring timeouts for `fetch` + +If you are using `fetch` (including Node.js built-in `fetch`), configure timeouts +through the global dispatcher: + +```js +import { Agent, setGlobalDispatcher, fetch } from 'undici' + +setGlobalDispatcher(new Agent({ + connect: { timeout: 10_000 }, // connection phase + headersTimeout: 15_000, // wait for response headers + bodyTimeout: 30_000 // wait between response body chunks +})) + +await fetch('https://example.com/data') +``` + +You can inspect timeout error codes from `error.code` or (for wrapped fetch +errors) from `error.cause?.code`. + ## Class: `UndiciError` + +* Extends: {UndiciError} + +The maximum number of redirects has been reached. + +* `name` {string} Always `'MaxRedirectsError'`. +* `code` {string} Always `'UND_ERR_REDIRECT_MAX'`. + ## Class: `Socks5ProxyError`